diff --git a/decnet_web/src/components/DeckyFleet.css b/decnet_web/src/components/DeckyFleet.css index 374dd0b6..2a839453 100644 --- a/decnet_web/src/components/DeckyFleet.css +++ b/decnet_web/src/components/DeckyFleet.css @@ -127,6 +127,61 @@ } .decky-hits { font-variant-numeric: tabular-nums; } +/* Schema-driven per-service config form (shared with MazeNET Inspector). */ +.service-config-form { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 8px; + padding: 10px; + border: 1px dashed var(--border); + border-radius: 2px; +} +.svc-cfg-row { display: flex; flex-direction: column; gap: 4px; } +.svc-cfg-label { + font-size: 0.62rem; + letter-spacing: 1px; + text-transform: uppercase; + opacity: 0.7; +} +.svc-cfg-secret-tag { font-size: 0.55rem; opacity: 0.6; letter-spacing: 1px; } +.svc-cfg-input { + flex: 1; + font-size: 0.72rem; + padding: 4px 6px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border); + color: inherit; + font-family: inherit; +} +.svc-cfg-input:focus { outline: 1px solid var(--violet); } +.svc-cfg-pw-wrap { display: flex; gap: 6px; align-items: stretch; } +.svc-cfg-help { font-size: 0.62rem; opacity: 0.55; } +.svc-cfg-actions { + display: flex; + gap: 6px; + align-items: center; + justify-content: flex-end; + margin-top: 4px; +} +.svc-cfg-dirty-tag { + font-size: 0.6rem; + letter-spacing: 1px; + color: var(--violet); + margin-right: auto; +} +.svc-cfg-toggle-btn { + background: transparent; + border: none; + color: inherit; + cursor: pointer; + font-size: 0.62rem; + letter-spacing: 1px; + opacity: 0.7; + padding: 0; +} +.svc-cfg-toggle-btn:hover { opacity: 1; } + /* Status dots */ .status-dot { display: inline-block; diff --git a/decnet_web/src/components/DeckyFleet.tsx b/decnet_web/src/components/DeckyFleet.tsx index 21d9dd39..9e41a03d 100644 --- a/decnet_web/src/components/DeckyFleet.tsx +++ b/decnet_web/src/components/DeckyFleet.tsx @@ -8,6 +8,7 @@ import { ARCHETYPES as FALLBACK_ARCHETYPES, DEFAULT_SERVICES } from './MazeNET/d import { useToast } from './Toasts/useToast'; import Modal from './Modal/Modal'; import { useServiceRegistry } from '../hooks/useServiceRegistry'; +import ServiceConfigForm from './ServiceConfigForm'; import './DeckyFleet.css'; // ─── Types ──────────────────────────────────────────────────────────────── @@ -157,6 +158,7 @@ const DeckyCard: React.FC = ({ const [addSlug, setAddSlug] = useState(''); const [busy, setBusy] = useState(null); const [opError, setOpError] = useState(null); + const [openCfgSvc, setOpenCfgSvc] = useState(null); const removeService = async (slug: string) => { setOpError(null); @@ -269,7 +271,21 @@ const DeckyCard: React.FC = ({
{decky.services.map((s) => ( - {s} + {liveServicesEnabled ? ( + + ) : ( + {s} + )} {liveServicesEnabled && (
diff --git a/decnet_web/src/components/MazeNET/Inspector.tsx b/decnet_web/src/components/MazeNET/Inspector.tsx index 8022c5b0..de721a2b 100644 --- a/decnet_web/src/components/MazeNET/Inspector.tsx +++ b/decnet_web/src/components/MazeNET/Inspector.tsx @@ -5,6 +5,7 @@ import { } from '../../icons'; import type { Net, MazeNode, Edge } from './types'; import { DEFAULT_SERVICES } from './data'; +import ServiceConfigForm from '../ServiceConfigForm'; export type Selection = | { type: 'net'; id: string } @@ -18,6 +19,9 @@ interface Props { nets: Net[]; nodes: MazeNode[]; edges: Edge[]; + /** Topology ID (MazeNET-only) — required for the schema-driven service + * config form to hit the per-topology REST path. Omit for fleet. */ + topologyId?: string; topologyStatus?: string; onClose?: () => void; onDeleteNet?: (id: string) => void; @@ -46,7 +50,7 @@ interface Props { } const Inspector: React.FC = ({ - selection, nets, nodes, edges, topologyStatus, onClose, + selection, nets, nodes, edges, topologyId, topologyStatus, onClose, onDeleteNet, onDeleteNode, onDeleteEdge, onRemoveService, onLiveAddService, onLiveRemoveService, availableServices = [], onToggleGateway, @@ -440,6 +444,18 @@ const Inspector: React.FC = ({
SUBNET
{serviceParentNet?.label ?? '—'}
+ {topologyId && serviceParent && serviceParent.kind !== 'observed' && ( + > } | undefined) + ?.service_config?.[serviceSel.id]) ?? {} + } + /> + )} {onRemoveService && serviceParent && serviceParent.kind !== 'observed' && (