diff --git a/decnet_web/src/components/MazeNET/MazeNET.tsx b/decnet_web/src/components/MazeNET/MazeNET.tsx index f2f4d080..d34d8257 100644 --- a/decnet_web/src/components/MazeNET/MazeNET.tsx +++ b/decnet_web/src/components/MazeNET/MazeNET.tsx @@ -7,6 +7,7 @@ import { } from '../../icons'; import './MazeNET.css'; import axios from '../../utils/api'; +import { useSwarmHosts } from '../../hooks/useSwarmHosts'; import Palette from './Palette'; import Canvas from './Canvas'; import Inspector from './Inspector'; @@ -39,12 +40,15 @@ const MazeNET: React.FC = () => { const [params] = useSearchParams(); const topologyId = params.get('topology') ?? ''; + const { byUuid: hostsByUuid } = useSwarmHosts(); const [nets, setNets] = useState([]); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const [topoStatus, setTopoStatus] = useState('pending'); const [topoName, setTopoName] = useState(''); const [topoVersion, setTopoVersion] = useState(0); + const [topoTargetHost, setTopoTargetHost] = useState(null); + const [topoMode, setTopoMode] = useState('unihost'); const [selection, setSelection] = useState(null); const [inspectorOpen, setInspectorOpen] = useState(true); const [paletteOpen, setPaletteOpen] = useState(true); @@ -580,6 +584,8 @@ const MazeNET: React.FC = () => { setTopoStatus(h.topology.status); setTopoName(h.topology.name); setTopoVersion(h.topology.version); + setTopoMode(h.topology.mode ?? 'unihost'); + setTopoTargetHost(h.topology.target_host_uuid ?? null); setLoadErr(null); } catch (err) { setLoadErr((err as Error)?.message ?? 'topology load failed'); @@ -658,6 +664,16 @@ const MazeNET: React.FC = () => {

MAZENET · {topoName || topologyId}

NETWORK OF NETWORKS · {topoStatus.toUpperCase()} · v{topoVersion} ·{' '} + HOST:{' '} + {topoMode === 'agent' && topoTargetHost ? ( + + + {hostsByUuid.get(topoTargetHost)?.name ?? topoTargetHost.slice(0, 8)} + + ) : ( + MASTER + )} + {' · '} {nets.length} NETS · {nodes.length} NODES · {edges.length} PATHS ·{' '} {runningDeckies}/{deckyNodes.length} DECKIES RUNNING {streamEnabled && ( diff --git a/decnet_web/src/components/MazeNET/useMazeApi.ts b/decnet_web/src/components/MazeNET/useMazeApi.ts index 9157783f..9166fb52 100644 --- a/decnet_web/src/components/MazeNET/useMazeApi.ts +++ b/decnet_web/src/components/MazeNET/useMazeApi.ts @@ -40,6 +40,7 @@ export interface TopologySummary { id: string; name: string; mode: string; + target_host_uuid: string | null; status: string; version: number; } diff --git a/decnet_web/src/components/TopologyList/TopologyList.tsx b/decnet_web/src/components/TopologyList/TopologyList.tsx index e54442c3..175ad74b 100644 --- a/decnet_web/src/components/TopologyList/TopologyList.tsx +++ b/decnet_web/src/components/TopologyList/TopologyList.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull } from '../../icons'; +import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull, Server, Cpu } from '../../icons'; import api from '../../utils/api'; +import { useSwarmHosts } from '../../hooks/useSwarmHosts'; import { clearLayout } from '../MazeNET/useMazeLayoutStore'; import CreateTopologyWizard from './CreateTopologyWizard'; import EmptyState from '../EmptyState/EmptyState'; @@ -42,6 +43,7 @@ const statusClass = (s: string): string => { const TopologyList: React.FC = () => { const navigate = useNavigate(); + const { byUuid: hostsByUuid } = useSwarmHosts(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); @@ -205,7 +207,17 @@ const TopologyList: React.FC = () => { {r.status}
- mode: {r.mode} + {r.mode === 'agent' && r.target_host_uuid ? ( + + + {hostsByUuid.get(r.target_host_uuid)?.name ?? `host:${r.target_host_uuid.slice(0, 8)}`} + + ) : ( + + + master + + )} v{r.version} {new Date(r.created_at).toLocaleString()}
diff --git a/decnet_web/src/hooks/useSwarmHosts.ts b/decnet_web/src/hooks/useSwarmHosts.ts new file mode 100644 index 00000000..0633fc59 --- /dev/null +++ b/decnet_web/src/hooks/useSwarmHosts.ts @@ -0,0 +1,42 @@ +import { useCallback, useEffect, useState } from 'react'; +import api from '../utils/api'; + +export interface SwarmHost { + uuid: string; + name: string; + address: string; + agent_port: number; + status: string; + last_heartbeat: string | null; +} + +/** + * Lookup of enrolled swarm hosts. One-shot fetch on mount, with a manual + * refresh callback. Used to resolve `target_host_uuid` → display name in + * places where we don't already have a host name in hand (topology list, + * war-map header). + * + * Failure is treated as "no agents enrolled" — callers fall back to the + * uuid prefix or a generic label rather than blocking on this lookup. + */ +export function useSwarmHosts(): { + hosts: SwarmHost[]; + byUuid: Map; + refresh: () => Promise; +} { + const [hosts, setHosts] = useState([]); + + const refresh = useCallback(async () => { + try { + const { data } = await api.get('/swarm/hosts'); + setHosts(data ?? []); + } catch { + setHosts([]); + } + }, []); + + useEffect(() => { refresh(); }, [refresh]); + + const byUuid = new Map(hosts.map((h) => [h.uuid, h])); + return { hosts, byUuid, refresh }; +}