Final integration step. The page shell is now a thin composition
of the hook + the previously-extracted children:
- DeckyFleet.tsx: 1,674 -> 274 LOC. Page owns only the
pure-UI state (filter, search, armed-confirm, modal visibility,
selected-card-for-inspect) and the toast-wrapping handlers that
translate hook results into toast tone. Polling, REST plumbing,
role lookup, and archetype catalog all moved to useDeckyFleet
in the prior commit.
- New DeckyFilters.tsx (header pill row + DEPLOY shortcut) +
DeckyGridEmpty.tsx (fleet-empty vs. filter-empty copy).
- DeckyFilters.test.tsx + DeckyGridEmpty.test.tsx cover count
rendering, filter-click callbacks, and admin-gated DEPLOY
visibility.
Two-step teardown arming logic stays in the page (it's pure UI).
Toast tone branching on { ok, reason } from useDeckyFleet
results moves the policy decision out of the data layer.
Lift the multi-step deploy wizard (~520 LOC) plus its private
INI-builder helpers (PLACEHOLDER_LINES, b64encodeUtf8, buildIni,
PickMode type) into their own file. Verbatim move; the
underscore-prefixed helpers drop the leading underscore now that
they're file-local rather than competing with hoisted parent
constants.
- New DeckyFleet/DeployWizard.tsx
- DeployWizard.test.tsx covers the closed render guard, the
open-at-step-0 archetype list, NEXT-disabled-until-archetype,
and CANCEL -> onClose. ServiceConfigFields is vi.mock'd to a
stub since it pulls schemas via api.get() that are out of
scope for these tests.
- DeckyFleet.tsx loses the wizard plus the now-unused imports
(DEFAULT_SERVICES, Modal, PickIcon, ServiceConfigFields and
its type aliases).
Lift the per-decky tile (~430 LOC) into its own file. Tarpit
controls, live add/remove service flow, and the per-service config
toggle stay inside the card — those are tile-local UI concerns and
only ever rendered from this component anyway.
- New DeckyFleet/DeckyCard.tsx
- DeckyCard.test.tsx covers identity row + services rendering,
admin-gated FORCE MUTATE visibility, the FORCE MUTATE callback,
TEARDOWN -> CONFIRM toggle when armed matches, and card-body
click firing onInspect. AddServiceConfigModal +
ServiceConfigForm are vi.mock'd so we don't need MSW handlers
for their unrelated network fetches.
- DeckyFleet.tsx loses the inline component plus the now-unused
imports it dragged in (Network/PowerOff/RefreshCw/Plus/X icons,
ServiceConfigForm, AddServiceConfigModal, useCallback).
Verbatim move of the per-decky mutation-interval modal (~60 LOC)
into its own file. Saves null when the toggle is off, minutes
otherwise.
- New DeckyFleet/IntervalEditor.tsx
- IntervalEditor.test.tsx covers null-current disabled path,
numeric-current enabled path, and CANCEL not firing onSave.
- src/test/fixtures/decky.ts now derives DeckyFixture from the
canonical Decky type (the fixture's loose swarm shape was
missing host_address/host_status; aligning to Decky catches
that statically).
Lift the right-side inspect drawer (~115 LOC) into its own file.
This is a verbatim move — same JSX, same useEscapeKey + body
overflow lock, same swarm-section gating. Underscore-prefixed
helper calls (_dotFor, _stateColor) drop the leading underscore
since they're now imported from helpers.tsx.
- New DeckyFleet/DeckyInspectPanel.tsx
- DeckyInspectPanel.test.tsx covers identity-row rendering, the
SERVICES chip list, the conditional SWARM block, and the close
button callback.
- DeckyFleet.tsx loses the panel + the now-unused useEscapeKey
import.
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).
Pre-this-commit, ~80 rgba() literals across 24 files were
hardcoding alert-red, warn-amber, info-cyan, panel-dark, and
white-text-with-alpha shades that bypassed the token cascade.
Net effect in light mode: the .eml/SESSREC drawers, AttackerDetail
verdict pills, MazeNET net-box headers, OPEN/REPLAY action
buttons, threat-intel cards, and all the dim 'whitish' overlays
stayed on their dark-mode hex values, producing the unreadable
panels in the screenshots.
Sweep maps each rgba colour family onto the existing token by
alpha bucket — rgba(13,17,23,*) -> var(--panel),
rgba(255,65,65,*) -> var(--alert)/-tint-10,
rgba(255,170,0,*) and rgba(224,160,64,*) -> var(--warn)/-tint-10,
rgba(0,200,255,*) -> var(--info)/-tint-10,
rgba(255,255,255,*) -> var(--fg-N)/var(--matrix-tint-N) by alpha.
VERDICT_TONE in AttackerDetail (MALICIOUS/SUSPICIOUS/BENIGN/
NO SIGNAL) was the worst offender — string literals
'#ff4d4d'/'#ffae42'/'#5fd07a'/rgba(255,255,255,0.4) baked into
inline JS styles. Now resolves at render time via var(--alert)/
var(--warn)/var(--ok)/var(--fg-4).
New tokens in :root:
- --bg-color (alias of --bg) — drawers used this name with
#0d1117 fallback that fired in every browser because nothing
defined --bg-color. Adding the alias makes drawers re-tone.
- --info / --info-tint-10 / --info-tint-30 — REPLAY buttons and
any future neutral-secondary use.
- --ok — semantic alias for 'verified good' (matrix in dark,
emerald in light) so BENIGN pills stay readable across themes.
Login.css left intentionally — pre-auth surface, not themed.
ApiError: defined once in utils/api.ts, replaces 9 ad-hoc anonymous casts
across MazeNET, Inspector, DeckyFleet, SwarmHosts, Webhooks, PersonaGeneration,
ServiceConfigFields, CanaryTokens.
hex4 renamed to tempIdSuffix — the name now matches the comment that already
explained its purpose.
NET_GRID_{W,H,GAP,COLS} extracted from inline magic numbers to module-level
constants in MazeNET.tsx.
onPaletteDrop (130-line useCallback) split into three module-level handlers
(_dropNetwork, _dropArchetype, _dropService); the callback becomes a 10-line
router.
- Dashboard: fix invisible bar at bottom of LIVE FEED by constraining
max-height on the section instead of the inner container; same fix
for side panels
- Page icons: add violet-accent icon beside h1 on all 9 missing pages
(CanaryTokens, RealismConfig, SyntheticFiles, PersonaGeneration,
Attackers, Webhooks, LiveLogs, Topologies, DecoyFleet)
- Attackers filter chips: replace ad-hoc chip buttons with seg-group
tabs (ALL / ACTIVE N / PASSIVE N / INACTIVE N) matching Credential
Vault style; country chips use same seg-group treatment
- Credential Vault: add sortable headers to REUSE tab (LAST SEEN,
PRINCIPAL, KIND, TARGETS, ATTEMPTS); reuses same SortTh pattern
- Bounty: remove CREDENTIALS and PAYLOADS tabs; keep ALL, ARTIFACTS,
FINGERPRINTS; add EMAIL (artifact subtype, filtered client-side)
- DeckyFleet: card click opens inspect side-drawer instead of
auto-filtering (localSearch filter behavior removed)
- Dashboard: LIVE FEED / DECKIES UNDER SIEGE / TOP ATTACKERS panels
now have fixed max-height with overflow scroll instead of growing
- parseEventBody: defensive RFC 5424 header strip so raw syslog lines
from the collector render as k=v pills instead of raw text
- Attackers: search placeholder updated; activity (Active/Passive/
Inactive) and country chip filters added on top of existing IP search
- Credentials + Bounty: sortable column headers (click to asc/desc/clear)
- SwarmHosts + RemoteUpdates: icon extracted from <h1> into flex div
with violet-accent class, matching site-wide Identities pattern
- Swarm.css: fix --panel-border undefined variable → --border so the
title border-bottom line is visible on SwarmHosts and RemoteUpdates
- DeckyServiceAddRequest gains an optional `config: dict` field, validated
against the service's config_schema before any state mutation (400 on
bad type, no half-written rows).
- Engine: add_service threads `config` into _add_topology_service /
_add_fleet_service, persisting validated cfg to decky_config.service_config
BEFORE compose regen so the first `up -d --build` materialises the env on
the new container. No follow-up apply needed.
- Frontend: shared AddServiceConfigModal — same wizard accordion shape, used by:
* DeckyCard's ADD SERVICE picker (Fleet & MazeNET inspectors via shared component)
* MazeNET Inspector's ADD SERVICE picker
* MazeNET palette drag-drop onto a deployed decky
Empty-schema services short-circuit to a one-click add (no modal flash).
Operator can cancel; errors surface in the modal.
- Tests: add_service config plumbing — persist, drop unknown keys, 400-equivalent
on bad types, back-compat empty-config.
- Drive-by: fix stale repo-method names in test_services_live.py
(create_topology_decky → add_topology_decky, get_topology_decky → list+pick helper,
service.added → service_added topic).
- Declarative config_schema on RDP, Telnet, MySQL, Redis, SMTP, SMTP_Relay
matching the keys each service already reads at compose time.
- TODO marker on the 19 services that accept service_cfg but never read it,
so future contributors know where to plug schemas in.
- Wizard base64-wraps all textarea values at INI emit (DeckyFleet
buildIni); validate_cfg detects the b64: sentinel and decodes back to
UTF-8. Plain raw strings still pass through for direct API submitters.
- HTTPS image entrypoint accepts PEM content or path in TLS_CERT/TLS_KEY:
detects a BEGIN header, writes content to /opt/tls/, and re-exports
the on-disk path so server.py keeps reading paths.
- Tests cover schema/compose alignment for each new service plus
textarea base64 round-trip (incl. UTF-8) and HTTPS PEM end-to-end.
Setting a password, banner or TLS material AFTER deployment forces a
container recreate on every change. The deploy wizard now lets the
operator set service config up-front so the initial build has the
right env from the start.
Mechanics:
- Extracted the schema-driven field rendering out of ServiceConfigForm
into a standalone ServiceConfigFields component (no API/buttons,
just inputs + onChange). ServiceConfigForm now delegates to it.
- Wizard step 2 (CONFIGURATION) renders one accordion block per
selected service; clicking a service reveals its schema-driven
inputs and a 'N set' badge tracks how many overrides are populated.
Removing a service (back to step 1) drops its config so the INI
doesn't carry orphans.
- _buildIni emits one [<prefix>.<svc>] group subsection per service
with at least one override. The INI loader's prefix-matcher
applies it to every ${prefix}-NN decky in the batch, so one block
covers all clones.
- Multi-line string values (PEM textareas etc.) are escaped as \n
on the way into INI; downstream consumers re-expand.
The CONFIGURATION step had a stale disabled placeholder textarea
("per-service overrides") from before the schema-driven Inspector
landed. Replaced with a one-line info banner pointing at the Inspector,
which is now where per-service config actually lives.
The DEPLOY step's CLI preview was rendering '--archetype custom' when
pickMode==='services', but no such archetype is registered — only the
preset archetypes plus 'services' (free-form list). Drop the
--archetype line entirely in the services-mode preview so the rendered
command reflects what the API actually receives.
ServiceConfigForm.tsx fetches /topologies/services/{slug}/schema and renders
typed inputs (string/password/int/bool/textarea/enum) with reveal toggles for
secrets. SAVE persists via PUT (no restart); APPLY persists + force-recreates
the service container after a confirm dialog (matches the forwards_l3 pattern).
Mounts:
- DeckyFleet DeckyCard: clicking a service tag toggles the form below the
EXPOSED row, gated on liveServicesEnabled (admin + non-swarm).
- MazeNET Inspector: renders the form above REMOVE SERVICE when a service
is selected on a non-observed decky.
UI test plan is manual — no jsdom test infra in decnet_web yet.
DeckyCard grows the same per-chip × + dashed '+ ADD' affordances we
just shipped on the MazeNET Inspector. Wired to POST/DELETE
/api/v1/deckies/{name}/services{,/svc}; the response's services list
flows back through onServicesChanged to update the parent's deckies
state without a refetch.
Gated on isAdmin && !decky.swarm — swarm deckies live on a remote
agent and the W3 endpoint runs docker compose locally, same gap as
the canary planter has for agent-pinned topologies. Out of scope
here; flagged as a known limitation.
stopPropagation on the inline buttons + add-row container keeps the
card-level click (which selects the decky for inspection) from firing
on intra-row interactions.