{decky.hostname}
+ {deckies.length > 0 ? deckies.map(decky => {
+ const tdKey = decky.swarm ? `td:${decky.swarm.host_uuid}:${decky.name}` : '';
+ const tdBusy = tearingDown.has(decky.name) || decky.swarm?.state === 'tearing_down';
+ return (
+
+
+ {decky.name}
+ {decky.ip}
-
-
- DISTRO: {decky.distro}
-
- {decky.archetype && (
-
-
- ARCHETYPE: {decky.archetype}
-
- )}
-
-
- MUTATION:
- {isAdmin ? (
- handleIntervalChange(decky.name, decky.mutate_interval)}
- >
- {decky.mutate_interval ? `EVERY ${decky.mutate_interval}m` : 'DISABLED'}
-
- ) : (
-
- {decky.mutate_interval ? `EVERY ${decky.mutate_interval}m` : 'DISABLED'}
-
- )}
- {isAdmin && (
-
- )}
-
- {decky.last_mutated > 0 && (
-
- Last mutated: {new Date(decky.last_mutated * 1000).toLocaleString()}
-
- )}
-
-
EXPOSED SERVICES:
-
- {decky.services.map(svc => {
- const _config = decky.service_config[svc];
- return (
-
-
- {svc}
-
- {_config && Object.keys(_config).length > 0 && (
-
- {Object.entries(_config).map(([k, v]) => (
-
- {k}:
- {String(v)}
-
- ))}
-
+ {decky.swarm && (
+
+
+
+ {decky.swarm.host_name}
+ @ {decky.swarm.host_address || '—'}
+
+
+ {decky.swarm.state.toUpperCase()}
+
+ {decky.swarm.last_error && (
+
+ ⚠ {decky.swarm.last_error.slice(0, 60)}{decky.swarm.last_error.length > 60 ? '…' : ''}
+
+ )}
+
+ )}
+
+
+
+
+ HOSTNAME: {decky.hostname}
+
+
+
+ DISTRO: {decky.distro}
+
+ {decky.archetype && (
+
+
+ ARCHETYPE: {decky.archetype}
+
+ )}
+ {/* Mutate controls are unihost-only for v1 — swarm-side mutation
+ belongs in a separate ticket (the worker /mutate endpoint
+ still returns 501). */}
+ {!decky.swarm && (
+ <>
+
+
+ MUTATION:
+ {isAdmin ? (
+ handleIntervalChange(decky.name, decky.mutate_interval)}
+ >
+ {decky.mutate_interval ? `EVERY ${decky.mutate_interval}m` : 'DISABLED'}
+
+ ) : (
+
+ {decky.mutate_interval ? `EVERY ${decky.mutate_interval}m` : 'DISABLED'}
+
+ )}
+ {isAdmin && (
+
)}
- );
- })}
+ {decky.last_mutated > 0 && (
+
+ Last mutated: {new Date(decky.last_mutated * 1000).toLocaleString()}
+
+ )}
+ >
+ )}
+
+
+
EXPOSED SERVICES:
+
+ {decky.services.map(svc => {
+ const _config = decky.service_config[svc];
+ return (
+
+
+ {svc}
+
+ {_config && Object.keys(_config).length > 0 && (
+
+ {Object.entries(_config).map(([k, v]) => (
+
+ {k}:
+ {String(v)}
+
+ ))}
+
+ )}
+
+ );
+ })}
+
+
+
+ {decky.swarm && isAdmin && (
+
+
+
+ )}
-
- )) : (
+ );
+ }) : (
NO DECOYS CURRENTLY DEPLOYED IN THIS SECTOR
diff --git a/decnet_web/src/components/Layout.tsx b/decnet_web/src/components/Layout.tsx
index 11c4c42..5ee38f5 100644
--- a/decnet_web/src/components/Layout.tsx
+++ b/decnet_web/src/components/Layout.tsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { NavLink } from 'react-router-dom';
-import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive, Package, Network, ChevronDown, ChevronRight, HardDrive, Boxes, UserPlus } from 'lucide-react';
+import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive, Package, Network, ChevronDown, ChevronRight, HardDrive, UserPlus } from 'lucide-react';
import './Layout.css';
interface LayoutProps {
@@ -48,7 +48,6 @@ const Layout: React.FC
= ({ children, onLogout, onSearch }) => {
} label="Attackers" open={sidebarOpen} />
} open={sidebarOpen}>
} label="SWARM Hosts" open={sidebarOpen} indent />
- } label="SWARM Deckies" open={sidebarOpen} indent />
} label="Remote Updates" open={sidebarOpen} indent />
} label="Agent Enrollment" open={sidebarOpen} indent />
diff --git a/decnet_web/src/components/SwarmDeckies.tsx b/decnet_web/src/components/SwarmDeckies.tsx
deleted file mode 100644
index 93ebcf3..0000000
--- a/decnet_web/src/components/SwarmDeckies.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import api from '../utils/api';
-import './Dashboard.css';
-import './Swarm.css';
-import { Boxes, PowerOff, RefreshCw } from 'lucide-react';
-
-interface DeckyShard {
- 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;
- compose_hash: string | null;
- updated_at: string;
-}
-
-const SwarmDeckies: React.FC = () => {
- const [shards, setShards] = useState([]);
- const [loading, setLoading] = useState(true);
- const [tearingDown, setTearingDown] = useState>(new Set());
- const [error, setError] = useState(null);
- // Two-click arm/commit replaces window.confirm() — browsers silently
- // suppress confirm() after the "prevent additional dialogs" opt-out.
- const [armed, setArmed] = useState(null);
- const arm = (key: string) => {
- setArmed(key);
- setTimeout(() => setArmed((prev) => (prev === key ? null : prev)), 4000);
- };
-
- const fetch = async () => {
- try {
- const res = await api.get('/swarm/deckies');
- setShards(res.data);
- setError(null);
- } catch (err: any) {
- setError(err?.response?.data?.detail || 'Failed to fetch swarm deckies');
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- fetch();
- const t = setInterval(fetch, 10000);
- return () => clearInterval(t);
- }, []);
-
- const handleTeardown = async (s: DeckyShard) => {
- const key = `td:${s.host_uuid}:${s.decky_name}`;
- if (armed !== key) { arm(key); return; }
- setArmed(null);
- setTearingDown((prev) => new Set(prev).add(s.decky_name));
- try {
- // Endpoint returns 202 immediately; the actual teardown runs in the
- // background on the backend. Shard state flips to 'tearing_down' and
- // the 10s poll picks up the final state (gone on success, or
- // 'teardown_failed' with an error).
- await api.post(`/swarm/hosts/${s.host_uuid}/teardown`, { decky_id: s.decky_name });
- await fetch();
- } catch (err: any) {
- alert(err?.response?.data?.detail || 'Teardown failed');
- } finally {
- setTearingDown((prev) => {
- const next = new Set(prev);
- next.delete(s.decky_name);
- return next;
- });
- }
- };
-
- const byHost: Record = {};
- for (const s of shards) {
- if (!byHost[s.host_uuid]) {
- byHost[s.host_uuid] = { name: s.host_name, address: s.host_address, status: s.host_status, shards: [] };
- }
- byHost[s.host_uuid].shards.push(s);
- }
-
- return (
-
-
-
SWARM Deckies
-
-
-
- {error &&
{error}
}
-
- {loading ? (
-
Loading deckies…
- ) : shards.length === 0 ? (
-
-
No deckies deployed to swarm workers yet.
-
- ) : (
- Object.entries(byHost).map(([uuid, h]) => (
-
-
{h.name} ({h.address}) — {h.status}
-
-
-
- | Decky |
- IP |
- State |
- Services |
- Updated |
- |
-
-
-
- {h.shards.map((s) => (
-
- | {s.decky_name} |
- {s.decky_ip || '—'} |
- {s.state}{s.last_error ? ` — ${s.last_error}` : ''} |
- {s.services.join(', ')} |
- {new Date(s.updated_at).toLocaleString()} |
-
-
- |
-
- ))}
-
-
-
- ))
- )}
-
- );
-};
-
-export default SwarmDeckies;