refactor(decnet_web/DeckyFleet): extract types + helpers

Foundation for the DeckyFleet split. Types and helpers move to
their own files so the upcoming subcomponent extractions can
import without reaching back through the parent module.

- New DeckyFleet/types.ts (Decky, SwarmDeckyRaw, SwarmMeta,
  Archetype, FilterKey, DeckyStatus). Names exported to match the
  pattern set by AttackerDetail/types.ts.
- New DeckyFleet/helpers.tsx (archetypeIcon, PickIcon, dotFor,
  hitsFor, stateColor). Underscore-prefixed call sites stay via
  import-rename so this commit changes zero behavior.
- DeckyFleet.tsx loses ~110 LOC of inline definitions plus the
  now-unused icon imports (Cpu / Database / Globe / Monitor /
  Shield / Terminal).
This commit is contained in:
2026-05-09 04:52:48 -04:00
parent 6d7c0b6419
commit 8c168c64a8
3 changed files with 133 additions and 110 deletions

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Cpu, Database, Globe, Monitor, Network, PlusCircle, PowerOff,
RefreshCw, Server, Shield, Terminal, Plus, X,
Network, PlusCircle, PowerOff,
RefreshCw, Server, Plus, X,
} from '../icons';
import { useEscapeKey } from '../hooks/useEscapeKey';
import api, { type ApiError } from '../utils/api';
@@ -17,114 +17,19 @@ import ServiceConfigFields, {
compactPayload as svcCompactPayload,
} from './ServiceConfigFields';
import './DeckyFleet.css';
// ─── Types ────────────────────────────────────────────────────────────────
interface SwarmMeta {
host_uuid: string;
host_name: string;
host_address: string;
host_status: string;
state: string;
last_error: string | null;
last_seen: string | null;
}
interface Decky {
name: string;
ip: string;
services: string[];
distro: string;
hostname: string;
archetype: string | null;
service_config: Record<string, Record<string, unknown>>;
mutate_interval: number | null;
last_mutated: number;
swarm?: SwarmMeta;
}
interface SwarmDeckyRaw {
decky_name: string;
decky_ip: string | null;
host_uuid: string;
host_name: string;
host_address: string;
host_status: string;
services: string[];
state: string;
last_error: string | null;
last_seen: string | null;
hostname: string | null;
distro: string | null;
archetype: string | null;
service_config: Record<string, Record<string, unknown>>;
mutate_interval: number | null;
last_mutated: number;
}
interface Archetype {
slug: string;
name: string;
services: string[];
icon: string;
}
type FilterKey = 'all' | 'active' | 'hot' | 'idle';
type DeckyStatus = 'active' | 'hot' | 'idle';
// ─── Helpers ──────────────────────────────────────────────────────────────
const _archetypeIcon = (slug: string): string => {
const s = slug.toLowerCase();
if (s.includes('windows') || s.includes('workstation')) return 'monitor';
if (s.includes('domain')) return 'shield';
if (s.includes('database') || s.includes('sql')) return 'database';
if (s.includes('iot') || s.includes('ot')) return 'cpu';
if (s.includes('web')) return 'globe';
return 'server';
};
// Compact icon resolver for lucide names we use in the wizard.
const PickIcon: React.FC<{ name: string; size?: number; className?: string }> = (
{ name, size = 16, className },
) => {
const map: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
server: Server, monitor: Monitor, shield: Shield, database: Database,
cpu: Cpu, globe: Globe, terminal: Terminal,
};
const Cmp = map[name] ?? Server;
return <Cmp size={size} className={className} />;
};
// Map swarm state -> visual dot status. "active" with no recent hit is idle;
// we don't have per-decky hit counts here, so treat running = active.
const _dotFor = (d: Decky): DeckyStatus => {
if (!d.swarm) return 'active';
switch (d.swarm.state) {
case 'running': return 'active';
case 'failed':
case 'teardown_failed': return 'hot';
case 'pending':
case 'tearing_down':
case 'degraded': return 'idle';
default: return 'idle';
}
};
// Hits placeholder — backend doesn't expose per-decky 24h hit count yet.
const _hitsFor = (_d: Decky): number => 0;
const _stateColor = (state: string): string => {
switch (state) {
case 'running': return 'var(--matrix)';
case 'degraded':
case 'tearing_down':
case 'pending': return 'var(--violet)';
case 'failed':
case 'teardown_failed': return 'var(--alert)';
default: return 'var(--border)';
}
};
import type {
Decky,
SwarmDeckyRaw,
Archetype,
FilterKey,
} from './DeckyFleet/types';
import {
archetypeIcon as _archetypeIcon,
PickIcon,
dotFor as _dotFor,
hitsFor as _hitsFor,
stateColor as _stateColor,
} from './DeckyFleet/helpers';
// ─── Decky inspect panel ─────────────────────────────────────────────────

View File

@@ -0,0 +1,62 @@
import React from 'react';
import {
Cpu, Database, Globe, Monitor, Server, Shield, Terminal,
} from '../../icons';
import type { Decky, DeckyStatus } from './types';
/** Map an archetype slug to a lucide icon name used by PickIcon. */
export const archetypeIcon = (slug: string): string => {
const s = slug.toLowerCase();
if (s.includes('windows') || s.includes('workstation')) return 'monitor';
if (s.includes('domain')) return 'shield';
if (s.includes('database') || s.includes('sql')) return 'database';
if (s.includes('iot') || s.includes('ot')) return 'cpu';
if (s.includes('web')) return 'globe';
return 'server';
};
/** Compact icon resolver for the lucide names DeckyFleet/DeployWizard
* reference by string (data-driven archetype rows). Unknown names
* fall back to the server icon. */
export const PickIcon: React.FC<{ name: string; size?: number; className?: string }> = (
{ name, size = 16, className },
) => {
const map: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
server: Server, monitor: Monitor, shield: Shield, database: Database,
cpu: Cpu, globe: Globe, terminal: Terminal,
};
const Cmp = map[name] ?? Server;
return <Cmp size={size} className={className} />;
};
/** Map swarm state -> visual dot status. "active" with no recent hit
* is idle; we don't have per-decky hit counts here, so treat
* running = active. */
export const dotFor = (d: Decky): DeckyStatus => {
if (!d.swarm) return 'active';
switch (d.swarm.state) {
case 'running': return 'active';
case 'failed':
case 'teardown_failed': return 'hot';
case 'pending':
case 'tearing_down':
case 'degraded': return 'idle';
default: return 'idle';
}
};
/** Hits placeholder — backend doesn't expose per-decky 24h hit count yet. */
export const hitsFor = (_d: Decky): number => 0;
/** CSS variable name for a swarm-state dot color. */
export const stateColor = (state: string): string => {
switch (state) {
case 'running': return 'var(--matrix)';
case 'degraded':
case 'tearing_down':
case 'pending': return 'var(--violet)';
case 'failed':
case 'teardown_failed': return 'var(--alert)';
default: return 'var(--border)';
}
};

View File

@@ -0,0 +1,56 @@
/** Wire + UI types for the DeckyFleet page surface. The canonical
* definitions live here; DeckyFleet.tsx re-exports the public ones
* through this barrel so external siblings can import without
* reaching across the page boundary. */
export interface SwarmMeta {
host_uuid: string;
host_name: string;
host_address: string;
host_status: string;
state: string;
last_error: string | null;
last_seen: string | null;
}
export interface Decky {
name: string;
ip: string;
services: string[];
distro: string;
hostname: string;
archetype: string | null;
service_config: Record<string, Record<string, unknown>>;
mutate_interval: number | null;
last_mutated: number;
swarm?: SwarmMeta;
}
export interface SwarmDeckyRaw {
decky_name: string;
decky_ip: string | null;
host_uuid: string;
host_name: string;
host_address: string;
host_status: string;
services: string[];
state: string;
last_error: string | null;
last_seen: string | null;
hostname: string | null;
distro: string | null;
archetype: string | null;
service_config: Record<string, Record<string, unknown>>;
mutate_interval: number | null;
last_mutated: number;
}
export interface Archetype {
slug: string;
name: string;
services: string[];
icon: string;
}
export type FilterKey = 'all' | 'active' | 'hot' | 'idle';
export type DeckyStatus = 'active' | 'hot' | 'idle';