diff --git a/decnet_web/src/components/MazeNET/Inspector.tsx b/decnet_web/src/components/MazeNET/Inspector.tsx index 085093a6..fff5a3e5 100644 --- a/decnet_web/src/components/MazeNET/Inspector.tsx +++ b/decnet_web/src/components/MazeNET/Inspector.tsx @@ -1,6 +1,9 @@ -import React from 'react'; -import { Trash2 } from 'lucide-react'; -import type { Net, MazeNode, Edge, PendingChange } from './types'; +import React, { useMemo } from 'react'; +import { + ArrowLeft, ArrowRight, Crosshair, Globe, GitMerge, MousePointer2, Plus, + Server, Trash2, X, +} from 'lucide-react'; +import type { Net, MazeNode, Edge } from './types'; export type Selection = | { type: 'net'; id: string } @@ -13,73 +16,177 @@ interface Props { nets: Net[]; nodes: MazeNode[]; edges: Edge[]; - pending: PendingChange[]; + topologyStatus?: string; onClose?: () => void; onDeleteNet?: (id: string) => void; onDeleteNode?: (id: string) => void; onDeleteEdge?: (id: string) => void; + onAddDecky?: (netId: string) => void; + setSelection?: (sel: Selection) => void; + pendingChanges?: number; } -const Inspector: React.FC = ({ selection, nets, nodes, edges, pending, onClose, onDeleteNet, onDeleteNode, onDeleteEdge }) => { +const Inspector: React.FC = ({ + selection, nets, nodes, edges, topologyStatus, onClose, + onDeleteNet, onDeleteNode, onDeleteEdge, onAddDecky, setSelection, + pendingChanges = 0, +}) => { const net = selection?.type === 'net' ? nets.find((n) => n.id === selection.id) : undefined; const node = selection?.type === 'node' ? nodes.find((n) => n.id === selection.id) : undefined; const edge = selection?.type === 'edge' ? edges.find((e) => e.id === selection.id) : undefined; + const activeNetIds = useMemo(() => { + const s = new Set(); + edges.forEach((e) => { + const f = nodes.find((n) => n.id === e.from); + const t = nodes.find((n) => n.id === e.to); + if (f) s.add(f.netId); + if (t) s.add(t.netId); + }); + return s; + }, [edges, nodes]); + + const typeLabel = selection ? selection.type.toUpperCase() : 'IDLE'; + const isGateway = node?.kind === 'decky' && !!node.decky_config?.forwards_l3; + const isObserved = node?.kind === 'observed'; + return ( ); diff --git a/decnet_web/src/components/MazeNET/MazeNET.css b/decnet_web/src/components/MazeNET/MazeNET.css index a920945b..611585c0 100644 --- a/decnet_web/src/components/MazeNET/MazeNET.css +++ b/decnet_web/src/components/MazeNET/MazeNET.css @@ -129,6 +129,34 @@ .maze-net-box.internet { border-color: var(--alert); background: rgba(255, 65, 65, 0.04); } +.maze-net-box.dmz { + border-color: var(--alert); background: rgba(255, 65, 65, 0.06); + border-style: dashed; +} +.maze-net-box.dmz .maze-net-box-head { + color: var(--alert); border-bottom-color: rgba(255, 65, 65, 0.45); +} +/* Deployed: topology is active/degraded — make it visually unmistakable. + * Subnet LANs glow matrix-green; DMZ stays hot red (and gets a stronger + * glow so you can tell it's live). */ +.maze-net-box.deployed { + border-style: solid; + border-color: var(--matrix); + background: rgba(0, 255, 65, 0.05); + box-shadow: 0 0 0 1px rgba(0, 255, 65, 0.25) inset, var(--matrix-glow); +} +.maze-net-box.deployed .maze-net-box-head { + color: var(--matrix); border-bottom-color: rgba(0, 255, 65, 0.45); +} +.maze-net-box.deployed.dmz { + border-color: var(--alert); + background: rgba(255, 65, 65, 0.09); + box-shadow: 0 0 0 1px rgba(255, 65, 65, 0.35) inset, + 0 0 16px rgba(255, 65, 65, 0.5); +} +.maze-net-box.deployed.dmz .maze-net-box-head { + color: var(--alert); border-bottom-color: rgba(255, 65, 65, 0.55); +} .maze-net-box.inactive { opacity: 0.42; filter: grayscale(0.7); border-style: dotted; } @@ -182,6 +210,18 @@ } .maze-node.observed { border-style: dashed; } .maze-node.dragging { opacity: 0.8; z-index: 10; cursor: grabbing; } +.maze-node.deployed { + border-color: var(--matrix); + box-shadow: var(--matrix-glow); + background: rgba(0, 255, 65, 0.04); +} +.maze-node.deployed .mn-head { color: var(--matrix); } +.maze-node.deployed.dmz-gateway { + border-color: var(--alert); + box-shadow: 0 0 12px rgba(255, 65, 65, 0.55); + background: rgba(255, 65, 65, 0.06); +} +.maze-node.deployed.dmz-gateway .mn-head { color: var(--alert); } .maze-node .mn-head { display: flex; align-items: center; gap: 6px; font-size: 0.74rem; font-weight: 700; letter-spacing: 0.5px; @@ -289,6 +329,15 @@ padding: 6px 12px 4px; border-bottom: 1px solid var(--border); margin-bottom: 4px; } +.ctx-title { + font-size: 0.58rem; letter-spacing: 2px; opacity: 0.5; + padding: 6px 12px 4px; border-bottom: 1px solid var(--border); + margin-bottom: 4px; +} +.ctx-item-wrap { position: relative; } +.ctx-icon { display: inline-flex; width: 14px; align-items: center; justify-content: center; opacity: 0.8; } +.ctx-label { flex: 1; } +.ctx-chev { opacity: 0.6; } .ctx-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 12px; font-size: 0.74rem; cursor: pointer; letter-spacing: 0.5px; @@ -304,6 +353,13 @@ .ctx-item.disabled { opacity: 0.35; cursor: not-allowed; } .ctx-item.disabled:hover { background: transparent; color: inherit; } .ctx-divider { height: 1px; background: var(--border); margin: 4px 0; } +.palette-ghost { + position: fixed; z-index: 2000; pointer-events: none; + padding: 4px 10px; font-family: var(--font-mono); font-size: 0.68rem; + letter-spacing: 1.5px; background: var(--panel); + border: 1px solid var(--violet); color: var(--violet); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), var(--violet-glow); +} .ctx-submenu { position: absolute; left: 100%; top: 0; background: var(--panel); border: 1px solid var(--violet); @@ -312,6 +368,144 @@ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8); } +/* ── Inspector: rich visual layout ──────────── */ +.inspector-type-label { + margin-left: auto; + font-size: 0.6rem; + letter-spacing: 1px; +} +.inspector-close-btn { + background: transparent; + border: 1px solid var(--border); + color: rgba(255, 255, 255, 0.5); + padding: 3px 5px; + cursor: pointer; + display: flex; + transition: all 0.15s; +} +.inspector-close-btn:hover { color: var(--alert); border-color: var(--alert); } + +.inspector-head { + display: flex; + align-items: center; + gap: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} +.inspector-head-title { font-size: 0.9rem; font-weight: 700; } +.inspector-head-chip { margin-left: auto; } + +.inspector-section-label { margin-bottom: 6px; } +.type-label { + font-size: 0.6rem; + letter-spacing: 1.5px; + opacity: 0.6; + text-transform: uppercase; +} + +.inspector-conn-row { + font-size: 0.7rem; + padding: 6px 0; + border-bottom: 1px dashed var(--border); + display: flex; + gap: 6px; + align-items: center; +} +.inspector-conn-chip { margin-left: auto; } + +.inspector-member-row { + font-size: 0.72rem; + padding: 5px 0; + display: flex; + gap: 6px; + align-items: center; + cursor: pointer; +} +.inspector-member-row:hover { color: var(--violet); } +.inspector-member-arch { margin-left: auto; font-size: 0.6rem; } + +.inspector-empty-line { + font-size: 0.68rem; + padding: 4px 0; +} + +.inspector-diff-block { + border-top: 1px solid var(--border); + padding-top: 12px; +} + +.inspector-status-block { margin-top: 12px; } + +.dim { opacity: 0.5; } + +/* Status dots used in inspector head + member rows */ +.status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 0; + flex-shrink: 0; +} +.status-dot.active { background: var(--matrix); box-shadow: 0 0 8px var(--matrix); } +.status-dot.idle { background: #30363d; } +.status-dot.hot { background: var(--alert); box-shadow: 0 0 8px var(--alert); + animation: decnet-pulse 1s infinite alternate; } +.status-dot.mutating { background: var(--violet); animation: decnet-blink 1s infinite; } + +/* Chips — inline badges for archetypes, traffic, severity */ +.chip { + font-size: 0.65rem; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--matrix); + color: var(--matrix); + background: var(--matrix-tint-10); + letter-spacing: 1px; + white-space: nowrap; +} +.chip.violet { border-color: var(--violet); color: var(--violet); background: var(--violet-tint-10); } +.chip.matrix { border-color: var(--matrix); color: var(--matrix); background: var(--matrix-tint-10); } +.chip.alert { border-color: var(--alert); color: var(--alert); background: var(--alert-tint-10); } +.chip.dim-chip { border-color: var(--border); color: rgba(0, 255, 65, 0.6); background: transparent; } + +.chip-mini { + font-size: 0.55rem; + padding: 1px 5px; + border: 1px solid var(--border); + letter-spacing: 1px; + color: rgba(255, 255, 255, 0.55); +} + +/* Inspector buttons */ +.maze-btn.small { padding: 5px 10px; font-size: 0.68rem; } +.maze-btn.alert { + border-color: var(--alert); + color: var(--alert); + opacity: 0.85; +} +.maze-btn.alert:hover { + background: var(--alert); + color: #000; + box-shadow: 0 0 10px rgba(255, 65, 65, 0.5); + opacity: 1; +} + +/* Service tag reuse for inspector "SERVICES" chip row */ +.inspector-service-row { + display: flex; + gap: 4px; + flex-wrap: wrap; +} +.maze-inspector .service-tag { + font-size: 0.6rem; + padding: 2px 6px; + letter-spacing: 0.5px; + border: 1px solid var(--violet); + color: var(--violet); + border-radius: 2px; +} + .dragging-from-palette { position: fixed; pointer-events: none; z-index: 200; background: var(--panel); border: 1px solid var(--violet); diff --git a/decnet_web/src/components/MazeNET/MazeNET.tsx b/decnet_web/src/components/MazeNET/MazeNET.tsx index c5cc2123..e4c5d2e1 100644 --- a/decnet_web/src/components/MazeNET/MazeNET.tsx +++ b/decnet_web/src/components/MazeNET/MazeNET.tsx @@ -445,7 +445,7 @@ const MazeNET: React.FC = () => {

MAZENET · {topoName || topologyId}

- {topoStatus.toUpperCase()} · v{topoVersion} ·{' '} + NETWORK OF NETWORKS · {topoStatus.toUpperCase()} · v{topoVersion} ·{' '} {nets.length} NETS · {nodes.length} NODES · {edges.length} PATHS {loadErr && · {loadErr}} {actionErr && · {actionErr}} @@ -514,6 +514,7 @@ const MazeNET: React.FC = () => { {inspectorOpen && ( { onDeleteNet={removeNet} onDeleteNode={removeNode} onDeleteEdge={removeEdge} + onAddDecky={(netId) => { + const net = nets.find((n) => n.id === netId); + if (!net) return; + onPaletteDrop( + { kind: 'archetype', slug: archetypes[0]?.slug ?? 'deaddeck', + services: archetypes[0]?.services.slice(0, 2) ?? [], + label: archetypes[0]?.name ?? 'DECKY', + clientX: 0, clientY: 0 }, + { x: net.x + 40, y: net.y + 60 }, netId, null, + ); + }} /> )}