Commit Graph

1251 Commits

Author SHA1 Message Date
8807da218b refactor(decnet_web/Config): extract LimitsTab
DEPLOYMENT LIMITS panel into its own tab file. Owns the input
state, preset-button shortcuts, and the inline FormMsg chip; the
hook mutation is passed in via prop so this component is fully
reusable as a presentation-only piece.

- New Config/tabs/LimitsTab.tsx
- LimitsTab.test.tsx covers viewer-vs-admin rendering, the
  1-500 validation message, and success/error chip display.
2026-05-09 05:23:42 -04:00
f2fd314dd6 refactor(decnet_web/Config): extract useConfig data hook
Lift the GET /config fetch and every admin-side mutation off the
page shell:

  GET    /config
  PUT    /config/deployment-limit
  PUT    /config/global-mutation-interval
  POST   /config/users
  DELETE /config/users/:uuid
  PUT    /config/users/:uuid/role
  PUT    /config/users/:uuid/reset-password
  DELETE /config/reinit (returns { logs, bounties, attackers })

Mutations return { ok: true } | { ok: false; reason: string } so
the upcoming tab components can render the inline FormMsg chip
without touching axios error shapes. reinit additionally returns
the deletion totals so the danger-zone confirmation can echo
"PURGED: N logs, N bounties, N attackers".

- New Config/useConfig.ts
- useConfig.test.ts MSW-covers initial load, isAdmin role
  surfacing, setDeploymentLimit ok + 400 paths, addUser, deleteUser
  refused, and reinit success.
- Wiring into Config.tsx + tab extractions land in follow-up commits.
2026-05-09 05:23:04 -04:00
b1fbf4630e refactor(decnet_web/Config): move WorkersPanel out
Verbatim move of the worker-status pollster (~390 LOC) plus its
RealismBadge sidekick into its own file. Owns its own polling +
stop/start/start-all mutations; toast push comes in via prop so
the parent stays the one source of toast tone.

- New Config/WorkersPanel.tsx
- WorkersPanel.test.tsx (MSW) covers worker-row rendering, the
  BUS OFFLINE banner, and the error panel on /workers 500.
- Config.tsx loses the inline WorkersPanel + RealismBadge plus
  the now-unused icon imports (Square, RefreshCw, Play).
2026-05-09 05:22:10 -04:00
209efd1a74 refactor(decnet_web/Config): extract types
Foundation for the Config split. UserEntry / ConfigData move out
of the page so the upcoming hook + tab extractions can import
without reaching back through Config.tsx. New ConfigTab union and
FormMsg type for the inline success/error chip pattern that
repeats across every admin form on the page.

- New Config/types.ts (UserEntry, ConfigData, ConfigTab, FormMsg)
- Config.tsx loses the inline interfaces and the `as any` cast on
  setActiveTab in the tab-switcher.
2026-05-09 05:19:50 -04:00
6ba12cc571 refactor(decnet_web/CanaryTokens): wire hook + bump coverage floor
Final integration step. The page shell is now a thin composition
of useCanaryTokens + the previously-extracted children:

- CanaryTokens.tsx: 1,334 -> 210 LOC. Page owns only the
  pure-UI state (tab, search/state/scope filters, modal
  visibility, drawer selection, local fileDrops log) and the
  thin handlers that translate hook results into confirm/alert
  prompts. Initial parallel fetch + deleteBlob mutation moved
  to useCanaryTokens in the prior commit.
- Modals plug directly into the hook's optimistic helpers
  (prependToken / prependBlob / markTokenRevoked) so the page
  doesn't reach into the data shape.

Coverage floor bumped after the split:

  lines       11 -> 14
  functions   10 -> 13
  branches     8 -> 11
  statements  11 -> 13

Phase 3 final scoreboard: 28 test files, 131 tests, all green.
2026-05-09 05:17:52 -04:00
c5cbe084cb refactor(decnet_web/CanaryTokens): extract list views
Lift the three tab bodies — tokens, blobs, file drops — into
their own files. Each takes plain props (data + the operations
its rows need), so the page shell stops mixing tab markup with
data plumbing.

- New CanaryTokens/TokenListView.tsx (text search + state/scope
  filter selectors + flat row grid; visibleTokens memo lives here
  now). Exports StateFilter / ScopeFilter union types so the page
  can declare its filter useState with the right shape.
- New CanaryTokens/BlobListView.tsx (delete refused while a token
  references a blob; ref count badge reuses the disabled button).
- New CanaryTokens/FileDropListView.tsx (CLEAR LIST hidden when
  the local log is empty).
- Three companion tests cover empty states, filter behavior,
  delete refused-vs-allowed, and the per-tab callback wiring.

Wiring into CanaryTokens.tsx + the hook lands next.
2026-05-09 05:16:18 -04:00
0c8c74a89d refactor(decnet_web/CanaryTokens): extract useCanaryTokens hook
Lift the parallel initial-load fetch and the deleteBlob mutation
off the page shell. Modal-driven optimistic merges (created
token, uploaded blob, drawer-revoked token) flow through narrow
setter helpers so the modals don't have to know how state is
shaped internally.

  GET    /canary/tokens
  GET    /canary/blobs (silent 403 -> empty list, viewer-friendly)
  GET    /deckies
  GET    /topologies/?status=active
  DELETE /canary/blobs/:uuid

deleteBlob returns { ok, reason } so the page can branch the
toast/alert tone without seeing the axios error type. Wiring
into CanaryTokens.tsx lands in the next commit.

- New CanaryTokens/useCanaryTokens.ts
- useCanaryTokens.test.ts MSW-covers happy load, viewer 403 ->
  empty blobs, deleteBlob ok + refused-with-detail paths, and the
  markTokenRevoked optimistic write.
2026-05-09 05:14:48 -04:00
69f547f75e refactor(decnet_web/CanaryTokens): move FileDropModal + LS helpers
Verbatim move of the file-drop modal (~310 LOC) and its
localStorage glue (FILEDROP_LS_KEY, FileDropEntry type,
loadFileDrops, saveFileDrops) into one file. The list view that
shows these entries lives in the page; the persistence layer
travels with the writer.

- New CanaryTokens/FileDropModal.tsx (modal + LS helpers + entry type)
- FileDropModal.test.tsx covers loadFileDrops empty / round-trip /
  200-row cap / malformed-JSON, plus modal title rendering, the
  bypass-warning banner, and CANCEL -> onClose.
- CanaryTokens.tsx loses the inline modal + LS glue plus the
  now-unused imports (useRef/X/AlertTriangle/useEscapeKey/
  useFocusTrap, plus BTN_PRIMARY/BTN_GHOST/Field that only the
  modals consumed).
2026-05-09 05:13:51 -04:00
b664655dcb refactor(decnet_web/CanaryTokens): move UploadModal out
Verbatim move of the artifact upload modal (~130 LOC) into its
own file. Drop-or-browse picker, server-side-injection warning
banner, and the multipart POST stay unchanged.

- New CanaryTokens/UploadModal.tsx
- UploadModal.test.tsx covers title rendering, empty drop-zone
  hint, server-injection warning banner, UPLOAD-disabled-until-
  file, and CANCEL -> onClose.
2026-05-09 05:11:46 -04:00
e30455551d refactor(decnet_web/CanaryTokens): move CreateTokenModal out
Verbatim move of the canary-token creation modal (~280 LOC) into
its own file. Renamed from CreateModal to CreateTokenModal so the
component name carries scope across the package boundary.

- New CanaryTokens/CreateTokenModal.tsx
- CreateTokenModal.test.tsx covers title rendering, CANCEL ->
  onClose, empty-deckies hint, and the Operator-upload mode
  switch revealing the no-blobs message. useFocusTrap is
  vi.mock'd to avoid jsdom focus shenanigans.
- CanaryTokens.tsx loses the inline modal + its now-unused
  imports (KNOWN_GENERATORS, KIND_OPTIONS, GeneratorName).
2026-05-09 05:10:27 -04:00
a35048b174 refactor(decnet_web/CanaryTokens): extract types + helpers + ui
Foundation for the CanaryTokens split. Types, error/format helpers,
and the inline style + small primitives move out of the page so
the upcoming modal/list extractions can import without reaching
back through CanaryTokens.tsx.

- New CanaryTokens/types.ts (BlobRow, DeckyOption, TopologyOption,
  Scope, KNOWN_GENERATORS / GeneratorName, KIND_OPTIONS, STATE_COLOR)
- New CanaryTokens/helpers.ts (extractError, fmt, fmtBytes)
- New CanaryTokens/ui.tsx (INPUT_STYLE, BTN_PRIMARY, BTN_GHOST,
  Field, Stat)
- CanaryTokens.tsx loses ~110 LOC of inline definitions; behavior
  unchanged.
2026-05-09 05:08:37 -04:00
08c274486e test(decnet_web): raise coverage floor after DeckyFleet split
Phase 2 lands. DeckyFleet.tsx dropped from 1,674 to 274 LOC; the
fleet page is now a thin composition of useDeckyFleet + 6
extracted children (DeckyInspectPanel, IntervalEditor, DeckyCard,
DeployWizard, DeckyFilters, DeckyGridEmpty), each with co-located
tests.

Lock the gain by bumping the threshold floor in vite.config.ts:

  lines       7  -> 11
  functions   6  -> 10
  branches    5  -> 8
  statements  7  -> 11

Phase 2 final scoreboard: 21 test files, 98 tests, all green.
2026-05-09 05:06:08 -04:00
9da6f6983e refactor(decnet_web/DeckyFleet): wire hook + extract filter UI
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.
2026-05-09 05:05:31 -04:00
9ddeb1a08c refactor(decnet_web/DeckyFleet): extract useDeckyFleet data hook
Lift every read- and write-side data flow off the page shell:

  GET  /system/deployment-mode  (decides which list endpoint to hit)
  GET  /deckies | /swarm/deckies (mode-switched + shape-normalized)
  GET  /config (role -> isAdmin)
  GET  /topologies/archetypes (live catalog with bundled fallback)
  POST /deckies/:name/mutate
  PUT  /deckies/:name/mutate-interval
  POST /swarm/hosts/:uuid/teardown
  10s polling loop refreshing mode + list

Operations return discriminated results ({ok:true} | {ok:false,
reason:...}) so the page can branch toast tone without seeing the
axios error type. Toasts, arm-confirm, and modal visibility stay
in the consuming page — the hook is pure data.

- New DeckyFleet/useDeckyFleet.ts
- useDeckyFleet.test.ts MSW-covers initial load, swarm-mode shape
  normalization, mutate ok/error paths, teardown ok path, and
  applyServicesChange optimistic write.
- DeckyFleet.tsx wiring lands in the next commit so the diff stays
  reviewable.
2026-05-09 05:03:31 -04:00
1e2bc41ab1 refactor(decnet_web/DeckyFleet): move DeployWizard out
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).
2026-05-09 05:01:33 -04:00
849caffaf1 refactor(decnet_web/DeckyFleet): move DeckyCard out
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).
2026-05-09 04:58:25 -04:00
b6ff288dcf refactor(decnet_web/DeckyFleet): move IntervalEditor out
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).
2026-05-09 04:55:30 -04:00
032ffbb4eb refactor(decnet_web/DeckyFleet): move DeckyInspectPanel out
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.
2026-05-09 04:54:10 -04:00
8c168c64a8 refactor(decnet_web/DeckyFleet): extract types + helpers
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).
2026-05-09 04:52:48 -04:00
6d7c0b6419 test(decnet_web): raise coverage floor after AttackerDetail split
Phase 1 of the UI refactor is in. AttackerDetail dropped from
2,579 LOC inline data + JSX to a 408-LOC shell composed of
extracted sections, each with co-located tests. Lock the gain by
bumping the threshold floor in vite.config.ts:

  lines       0 -> 7
  functions   0 -> 6
  branches    0 -> 5
  statements  0 -> 7

Future PRs raise these; never lower. Phase 1 final scoreboard:
9 test files, 45 tests, all green.
2026-05-09 04:49:32 -04:00
d5efebd73d refactor(decnet_web/AttackerDetail): extract MailLogPanel section
Lift STORED MAIL into its own section and pull the mail drawer
selection state along with it. Section signals admin-gating
through the section's own props (mailForbidden), since the data
hook already converts a 403 into that boolean.

- New AttackerDetail/sections/MailLogPanel.tsx
- MailLogPanel.test.tsx covers row rendering, mailForbidden empty
  state, no-mail empty state, from_hdr/from_addr/mail_from
  fallback, and drawer open/close. MailDrawer vi.mock'd same as
  ArtifactDrawer.
- AttackerDetail.tsx loses the mail JSX block, mailItem state,
  and now-unused Mail/MailDrawer imports.
2026-05-09 04:48:44 -04:00
14713eb294 refactor(decnet_web/AttackerDetail): extract ArtifactsPanel section
Lift CAPTURED ARTIFACTS into its own section, taking the drawer
selection state with it (the parent shell no longer owns
artifact-modal state).

- New AttackerDetail/sections/ArtifactsPanel.tsx
  Drawer is rendered as a sibling of the section so its z-index
  and focus-trap behavior mirror the original.
- ArtifactsPanel.test.tsx covers row rendering with parsed SD
  fields, empty state, missing stored_as (no OPEN button), and
  the open/close cycle. ArtifactDrawer is vi.mock'd to a stub
  so we don't need MSW handlers for its content fetch.
- AttackerDetail.tsx loses the artifact JSX block, the artifact
  state, and now-unused Paperclip/Package/ArtifactDrawer imports.
2026-05-09 04:47:17 -04:00
9cee4b2e71 refactor(decnet_web/AttackerDetail): extract CommandsViewer section
Lift the COMMANDS collapsible — paginated table with header-bar
prev/next controls — into its own section. The page math
(cmdTotalPages = ceil(total/limit)) and conditional empty state
both live in the section now.

- New AttackerDetail/sections/CommandsViewer.tsx
- CommandsViewer.test.tsx covers title formatting (unfiltered vs.
  filtered), empty state, single-page pagination hiding, and
  prev/next button behavior
- AttackerDetail.tsx loses the IIFE-wrapped commands JSX block
  plus now-unused ChevronLeft/ChevronRight/Terminal imports
2026-05-09 04:45:41 -04:00
7b21f31078 refactor(decnet_web/AttackerDetail): extract ServicesTargeted section
Lift the SERVICES TARGETED collapsible — interactive two-tone badge
chips with click-to-filter — into its own section. The selection
state was already lifted into useAttackerDetail in the prior
commits, so the section just consumes serviceFilter /
setServiceFilter as props.

- New AttackerDetail/sections/ServicesTargeted.tsx
- ServicesTargeted.test.tsx covers badge rendering, empty state,
  inactive-click-sets-filter, and active-click-clears-filter
- AttackerFixture grows ip_leaks/ip_leaks_total fields so the
  TimelineSection rotation test (added in the prior commit) keeps
  passing under the new factory shape
2026-05-09 04:44:25 -04:00
95e1a4ab7a refactor(decnet_web/AttackerDetail): extract TimelineSection
Lift the TIMELINE collapsible (timestamps, ASN, reverse DNS,
leaked-IPs row with rotation detection) into its own section.
LeakedIPsRow + the rotation/inline-limit constants come along
since they were only ever used here.

Also moves the shared `Section` collapsible primitive into
AttackerDetail/ui.tsx so the remaining sections can adopt the
template without re-importing through the parent module.

- New AttackerDetail/sections/TimelineSection.tsx (LeakedIPsRow
  inline as a private helper)
- AttackerDetail/ui.tsx now exports both Tag and Section
- AttackerDetail.tsx loses LeakedIPsRow, the Section helper, the
  Timeline JSX block, and now-unused imports (ChevronUp, ChevronDown,
  AttackerData)
- TimelineSection.test.tsx covers timestamps, unknown-origin path,
  rotation badge, empty leaks, collapse, and toggle callback
2026-05-09 04:43:13 -04:00
f524d283b7 refactor(decnet_web/AttackerDetail): extract AttackerStats section
Lift the 5-up counter grid + the conditional scan-vs-interact row
into AttackerStats. The activity row's visibility predicate
collapses into a single boolean inside the section so the parent
no longer encodes UX rules.

- New AttackerDetail/sections/AttackerStats.tsx
- AttackerStats.test.tsx covers all-five counters, activity present,
  activity empty, and service_activity undefined paths.
2026-05-09 04:40:34 -04:00
653ae04e88 refactor(decnet_web/AttackerDetail): extract AttackerHeader section
Lift the header (IP, country tag, traversal badge, identity badge)
into its own section component. Tag helper moves to a shared
AttackerDetail/ui.tsx so future sections can reuse it without
re-importing through AttackerDetail.tsx.

- New AttackerDetail/sections/AttackerHeader.tsx (~50 LOC)
- New AttackerDetail/ui.tsx for shared presentational helpers
- AttackerDetail.tsx imports both; local Tag definition deleted
- AttackerHeader.test.tsx covers country present/absent,
  TRAVERSAL badge, IDENTITY click-through, identity null path
2026-05-09 04:39:30 -04:00
22cfb10617 refactor(decnet_web/AttackerDetail): extract data layer into useAttackerDetail
The AttackerDetail page body owned all 7 REST fetches plus 2 SSE
streams inline as 200+ lines of useEffect plumbing. Lift them into
a single hook so section components extracted in follow-up commits
consume typed values, not setState pairs.

- New ./AttackerDetail/types.ts holds the canonical AttackerData,
  BehaviouralObservation, AttributionPrimitiveState plus newly-named
  ArtifactLog / SessionLog / SmtpTargetRow / MailLog / CommandRow
  (previously inline anonymous types).
- New ./AttackerDetail/useAttackerDetail.ts owns:
  * GET /attackers/:id (404 -> ATTACKER NOT FOUND)
  * GET /attackers/:id/attribution (silent-tolerant)
  * GET /attackers/:id/commands paged with 422 alert preserved
  * GET /attackers/:id/{artifacts,smtp-targets,mail,transcripts}
    (mail surfaces a 403 boolean for the admin-gated viewer)
  * useAttackerStream + useIdentityStream subscriptions, including
    the live attribution-state-changed merge.
- AttackerDetail.tsx re-exports BehaviouralObservation /
  AttributionPrimitiveState so AttackerDetail.behaviour_panel.test
  and any future external importer keeps working unchanged.
- New useAttackerDetail.test.ts covers loading -> success, 404,
  paged commands offset, serviceFilter resets cmdPage, and mail 403
  via MSW handlers (the SSE hooks are vi.mock'd; jsdom can't host
  EventSource).

No behavior change for the rendered page; all 37 tests green.
2026-05-09 04:36:35 -04:00
07a7d4918c test(decnet_web): MSW-based test foundation for UI refactor
Phase 0 of the decnet_web refactor: stand up an MSW server, fixtures,
and a router-aware render helper so the upcoming god-component splits
(AttackerDetail first) can land with same-commit test coverage.

- msw devDep + setupServer wired into src/test/setup.ts
- src/test/server.ts re-exports server, http, HttpResponse, apiUrl()
- src/test/fixtures/{attacker,decky,canary,topology}.ts factories
- src/test/renderWithRouter.tsx wraps MemoryRouter + ToastProvider
- baseline coverage thresholds (0%) in vite.config.ts; raise per PR
- coverage/ added to decnet_web/.gitignore

Existing Orchestrator/AttackerDetail/ThemeLab tests stay on vi.mock
and continue to pass; new tests use MSW.
2026-05-09 04:30:51 -04:00
3318b15044 fix(decnet_web/Layout): theme toggle icon stays visible on hover
The global button:hover rule in index.css forces color: var(--bg)
+ matrix-glow on the lucide icon's currentColor stroke, making
the sun/moon icon disappear into the toggle button's tinted
background on hover. Pin color: var(--accent) and box-shadow:
none on .theme-toggle-btn:hover so the icon stays in its base
colour and the button doesn't pick up the wider button-hover
halo.
2026-05-09 04:18:48 -04:00
5a34b1846c fix(decnet_web/Layout): kill residual theme-swap open flash
Even with fill: 'both', the new pseudo paints once at its default
style (no clip-path = full size) before the JS animation
registers — the brief open flash that survived the previous fix.

Pre-publish click coords as --reveal-x / --reveal-y on <html>
before calling startViewTransition. The static CSS rule on
::view-transition-new(root) now sets clip-path: circle(0px at
var(--reveal-x) var(--reveal-y)) as the pseudo's default, so
the very first paint is already fully clipped. The animation
then grows the circle outward from there.
2026-05-09 04:17:50 -04:00
ccff1467b1 fix(decnet_web/Layout): outward theme reveal, no flash either end
ANTI prefers the new theme growing outward from the click point
(visually clearer cause-and-effect than the old theme burning
away). The original outward implementation flashed at the start
because the new pseudo defaulted to its computed style (no
clip-path = fully visible) for one frame before the JS animation
registered.

Switching the animation's fill from 'forwards' to 'both' enforces
the start keyframe (circle(0) at click point) before the first
paint, in addition to pinning the end keyframe through pseudo
teardown. New layer is invisible until the animation begins,
fully visible until cleanup. No flash either end.
2026-05-09 04:17:07 -04:00
6d1fc3a081 fix(decnet_web/Layout): theme swap end-of-animation flash
Without fill: 'forwards' the clip-path keyframes release at
animation end and the pseudo reverts to its computed style
(no clip-path), so the old layer flashes back at full size for
a frame before View Transitions tears the pseudo-elements down.
Pinning the final keyframe with fill-forwards keeps the old
layer fully clipped through to teardown.
2026-05-09 04:15:44 -04:00
a81ea3f973 fix(decnet_web/Layout): theme swap animation no longer flashes opposite mode
Growing the NEW theme layer from circle(0) outward leaves a
one-frame gap where the new pseudo is fully opaque at full size
(the default state) before the clip-path animation registers.
Result: a flash of the destination theme right before the
reveal starts.

Inverted the layering and animation direction:
 - NEW theme snapshot sits on the bottom (z-index 0), static
 - OLD theme snapshot sits on top (z-index 1), shrinks via
   clip-path from circle(N) at click point down to circle(0)

The new layer is now hidden behind the old one until the old
shrinks away — no flash possible because the new layer was
never visible before the animation. Same 520ms duration, same
ease curve, same direction-of-travel from the user's POV
(circle expanding from cursor).
2026-05-09 04:14:54 -04:00
438a6e3e45 feat(decnet_web/Layout): topbar dark/light toggle with circular reveal
User-facing theme toggle ships now that the design system has
been audited end-to-end. A Sun/Moon button lives between the
threat indicator and the SYSTEM status pill in the topbar — same
slim 28x28 voice as the rest of the topbar controls, no chrome
shouting at the user.

Click coords drive a View Transitions API circle clip-path that
grows from the cursor to the farthest viewport corner over 520ms
with the project's standard --ease curve. Browsers without
startViewTransition (older Firefox, Safari < 18) fall through to
an unanimated swap — the hook returns instantly in that case.

Persistence is two-tier:
 - localStorage decnet_theme — the user's saved preference, the
   thing the topbar toggle writes. Survives reloads, applies
   everywhere.
 - sessionStorage decnet_theme_lab — dev-mode lab override (Task
   3). Tab-scoped, wins on boot so devs can A/B without nuking
   the saved preference.

App.tsx hydrates both on first mount in the right order so the
correct theme is on <html> before the first paint.

useThemeToggle is a small hook in lib/ rather than a Layout-only
helper so the same toggle can be reused later from a settings page
or hotkey.
2026-05-09 04:01:24 -04:00
9cab37db3a fix(decnet_web/css): three light-mode dimness fixes
--dim-color and --danger-color were referenced across drawers and
RemoteUpdates but never defined; --dim-color silently inherited
(defeating its purpose) and --danger-color fell back to literal
#f88 salmon (the 'ugly red' WifiOff icon next to UNREACHABLE
hosts). Added both as aliases in :root: --dim-color = var(--fg-3),
--danger-color = var(--alert).

--fg-2/3/4 alphas in light mode were tuned identical to dark
(0.78/0.55/0.35), but ink-on-cream needs more punch than
matrix-on-black at the same alpha — the deploy preview code
block (.code-block .comment / .key) and every dim caption
rendered too faint. Bumped to 0.88/0.70/0.50.

.maze-net-box.inactive applies opacity 0.42 + grayscale(0.7) for
the 'no traffic' signal. On cream that fades the LAN out of
visibility entirely. Override in light mode keeps the dotted
border as the dim-state cue and bumps opacity to 0.85 so the
header text stays legible.
2026-05-09 03:56:06 -04:00
388a968d89 fix(decnet_web/css): sweep violet rgba literals to tokens
Credentials drawer code-block labels (printable:, b64:) and a
dozen other violet wash/tint sites still carried bare rgba(238,
130, 238, *) literals — bright magenta in light mode where
--violet has resolved to charcoal-purple #2d1b4e. Mirrors the
prior matrix/alert/warn/info sweeps: by-alpha buckets land on
var(--violet-tint-10) or var(--violet).
2026-05-09 03:50:29 -04:00
aa0b22aacb fix(decnet_web/css): sweep rgba colour literals to tokens app-wide
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.
2026-05-09 03:48:05 -04:00
11b2da7d54 fix(decnet_web/css): light-mode contrast across wizards, code blocks, hovers
Sweeps four invariant violations that were leaking dark surfaces
into light mode and producing the unreadable / inverted areas:

  1. Hardcoded `color: #000` in 14 :hover rules across 11 CSS
     files swapped to `color: var(--bg)` — collapses to #000 in
     dark mode (no-op), becomes cream in light. Fixes DEPLOY
     DECKIES (button hover was rendering charcoal-purple text on
     charcoal-purple background).
  2. Hardcoded `background: #000` (3 sites) and `#0d1117`
     (3 sites) replaced with `var(--bg)` / `var(--panel)`. Fixes
     code blocks and modal panels staying dark on cream — the
     deploy-wizard preview, topology-creation NAME input, and the
     MazeNET canvas backdrop now follow the active theme.
  3. `rgba(0,0,0,0.35)` and `rgba(0,0,0,0.5)` input/card
     backgrounds (ServiceConfigForm, DeckyFleet .input)
     swapped to `var(--panel)`. Fixes per-service config rows
     in the deploy wizard rendering as dark slabs.
  4. SVG arrow markers in MazeNET Canvas.tsx hardcoded
     `fill="#00ff41"` / "#ee82ee" — replaced with currentColor +
     style hook so they re-resolve on theme change.

New behaviour: light-mode hovers tint instead of inverting. The
dark-mode rules fully fill bg with --matrix/--violet/--alert and
flip text to --bg; that lands cream-on-near-ink in light mode
and reads as a jarring colour inversion every cursor move. Light
mode now layers a *-tint-10 background and keeps text in its
base colour. Single override block in index.css targets every
scoped `.X-btn`/`.btn`/`button:hover` via :is() + [class*="-btn"]
so we don't have to chase every component file.
2026-05-09 03:43:47 -04:00
34c778277a refactor(decnet_web/css): promote hardcoded matrix/warn/crit colours to tokens
37 bare rgba(0, 255, 65, ...) literals across 10 component CSS
files were forcing matrix-green to bleed into light mode no matter
what data-theme=light overrode in :root. They're now mapped onto
existing tokens by alpha bucket (0.025-0.05 -> --matrix-tint-5,
0.08-0.10 -> --matrix-tint-10, 0.18-0.30 -> --matrix-tint-30,
0.4 -> --fg-4, 0.5-0.6 -> --fg-3, 0.7-0.8 -> --fg-2).

Adds --warn (#e0a040), --amber (alias of --warn), --crit
(#e74c3c), and their tint-10 variants to :root, with
ink-friendly light-mode overrides. Sweeps bare #ffaa00 / #e0a040
/ #f59e0b / #ff4d4d / #e74c3c usages in the same files onto the
new tokens.

Files with var(--token, #fallback) patterns left alone — those
were already token-driven and the fallbacks just provide safety.
Login.css and inline TSX hex left for the per-page sweep.
2026-05-09 03:36:04 -04:00
df0c8e12e7 fix(decnet_web/css): light theme goes ink-monotone, not green-on-cream
Initial light-theme palette kept --matrix as a darker emerald
and --violet as a darker purple, which washed out badly on
warm cream — auth-helper chips, ACTIVE/PASSIVE/INACTIVE pills,
and CREDS/REUSE tabs all became unreadable because their tint
backgrounds + low-saturation text collapsed to sludge.

Light mode now collapses --matrix and --violet to near-ink
shades (#0d0d0d and #2d1b4e). --alert stays the one
saturated colour — the only element allowed to shout.
Dark mode is untouched; the matrix-vibe identity stays
exclusive to dark.

Also collapses the matrix/violet accent knob in light mode:
data-accent only flavours dark mode now, since two ink
shades are visually identical.
2026-05-09 03:31:18 -04:00
47c57271e7 feat(decnet_web/theme-lab): light theme tokens + dev toggle
Adds html[data-theme="light"] block to index.css overriding the
core six tokens (bg, matrix, violet, panel, border, alert), the
matrix/violet/alert tints, and the foreground opacity ramp to a
cream-on-ink palette anchored on #dbdad6. Glows are no-op'd —
light mode trades neon haloes for hard 1px borders.

Lab page gets a Dark/Light toggle that flips
html.dataset.theme and persists to sessionStorage
(decnet_theme_lab) — intentionally tab-scoped, not user-facing.
App.tsx hydrates the same key on boot so a tab reload keeps the
dev's chosen theme. The user-facing localStorage toggle ships
later via Config.
2026-05-09 03:23:50 -04:00
f3f7bff717 feat(decnet_web/theme-lab): kitchen-sink component zoo
Renders every primitive in the design system on the lab page so
theme-token edits can be evaluated against all states at once:
colour swatches with WCAG contrast vs --bg, the full type scale,
buttons (5 variants × default/hover/disabled), badges and status
pills, info/error banners, metric cards, table rows
(default/hover/selected/drop-target), form inputs, drawer panel
sample, and net-box compose states (internet/inactive/selected/
drop-target — independent classes layering, per memory).

Wrapper uses .fleet-root so global .btn/.btn.violet/etc resolve
identically to real pages. Lab-local CSS owns layout only — every
colour comes from index.css tokens.
2026-05-09 03:22:21 -04:00
846a50dbbf feat(decnet_web/theme-lab): scaffold dev-gated /theme-lab route
Adds VITE_DECNET_DEVELOPER build-time gate: when unset, the
isDeveloperMode() helper collapses to a constant false and Vite
tree-shakes both the lazy import and the conditional <Route> out
of the prod bundle.

ThemeLab is currently a header stub; subsequent tasks fill it
with the design-system primitive zoo plus a Dark/Light toggle
for live token tuning. Route is intentionally absent from
ROUTE_LABELS / sidebar — direct URL only.
2026-05-09 03:18:34 -04:00
65ddaaa681 fix(behave_shell/F.0): tighten prompt detector — log lines ending in '>' no longer vote
_detect_prompt_suffix accepted ANY line ending in $#%> as a PS1 prompt,
so a single `cat /var/log/dpkg.log` (195 lines closing in `<none>`)
flooded environmental.shell_type votes and flipped a plainly-bash
session to fish.

A prompt line now requires either a trailing space after the suffix
(default PS1 shape across bash/zsh/fish/PowerShell) or a PS1-shape
token (user@host, "PS " prefix, or a Windows drive-letter prefix).

Regression tests pin the dpkg.log false-positive and a $-terminated
prose line.
2026-05-09 02:57:40 -04:00
0c1fc68b13 feat(deploy): wire attribution worker — CLI + systemd unit + registry
* decnet attribution — Typer command mirroring decnet reuse-correlate
  (--multi-actor-tick, --daemon flags). Calls run_attribution_loop
  with the dependency-injected repo.
* deploy/decnet-attribution.service.j2 — systemd unit mirroring
  decnet-reuse-correlator.service.j2: ExecStart=decnet attribution,
  same hardening posture (NoNewPrivileges, ProtectSystem=full,
  ProtectHome=read-only, dedicated /var/log/decnet/decnet.attribution.log).
* worker_registry.KNOWN_WORKERS += "attribution" — heartbeat already
  publishes as system.attribution.health from
  attribution_worker._WORKER_NAME, so the Workers panel surfaces the
  row the moment the unit is enabled.
* api_start_all_workers preferred-order list + "attribution" between
  reuse-correlator and enrich so a fresh start-all brings it up
  alongside its peers.

After this commit `systemctl enable --now decnet-attribution` (or
the dashboard's start-all) actually launches the engine.
2026-05-09 02:31:59 -04:00
5253b32319 feat(decnet_web/AttackerDetail): attribution state badges (Phase 6)
Per-primitive state badge rendered next to each value in the
Behavioural Primitives panel. Five-state vocabulary, frozen, mirrors
decnet/correlation/attribution/aggregate.py:

  * STABLE      — green, low-key
  * DRIFTING    — amber, draws the eye
  * CONFLICTED  — red
  * MULTI-ACTOR — purple, loudest (cross-primitive escalation lives
                  in attribution.multi_actor_suspected, not the
                  per-primitive badge)
  * UNKNOWN     — neutral border, no fill

Wiring:

* GET /api/v1/attackers/{id}/attribution on mount + on id change.
  Failures swallowed silently (the worker may be off in dev).
* useAttackerStream gains attribution.state_changed +
  attribution.multi_actor_suspected named events. The state-changed
  handler merges by primitive and locks last_change_ts when the
  state did not actually flip (defensive — backend already gates
  these on transition, but a future relaxation shouldn't lie about
  "stable since X" on the badge tooltip).
* multi_actor_suspected is wired but unused by the badges; the
  per-primitive multi_actor signal already shows on each contributing
  primitive. The handler is in place so a future "two operators
  detected" banner has a live source.

Vitest: 4 new tests (badge renders only for mapped primitives, all
five states render with distinct labels, no badge when prop omitted)
on top of the existing 4. 7 of 7 pass; tsc + vite build clean.
2026-05-09 02:28:11 -04:00
5de4b5e290 feat(decnet_web/AttackerDetail): visual refresh of Behavioural Primitives panel
* Per-domain icons (Keyboard / Cpu / Clock / Activity / Globe / Sparkles).
* Domain headers use BEHAVIOUR_DOMAIN_LABELS with letter-spacing +
  primitive-count badge on the right.
* Bordered domain groups instead of flat list; aligned leaf / value /
  confidence columns with monospace value rendering.
* Section title: BEHAVIOURAL PRIMITIVES -> BEHAVE PRIMITIVES (matches
  the BEHAVE-SHELL extractor naming).
2026-05-09 02:24:37 -04:00
9cc3272a0d test(correlation/attribution): v0 calibration lockdown (Phase 7)
Four synthetic operator-behaviour scenarios at the merger level
(aggregate_observations) that pin v0's calibration:

* Stable HUMAN over 7 sessions   -> all primitives stable
* HUMAN switches to LLM mid-week -> primitives flip stable -> drifting
* Two operators alternating      -> primitives flag multi_actor
                                    (per-primitive; the cross-
                                    primitive multi_actor_suspected
                                    correlator is exercised by Phase 5)
* Single short session           -> all primitives unknown

Plus a threshold-lockdown test that asserts every named constant in
_thresholds.py against its v0 ship value. Anyone adjusting a
threshold without updating the scenarios fails this file.

This closes DEBT-051 at v0 — the attribution engine has a calibrated,
test-locked answer to "is this attacker stable / drifting / showing
multiple operators?" without crossing the persona-attribution bright
line. v1 (cross-attacker clustering, KD simhash linkage signal) is
gated on this v0 surface being stable in production for >= 1 month.
2026-05-09 02:23:10 -04:00
33f7d5a9ff feat(web): expose attribution state on AttackerDetail backend (Phase 6)
GET /api/v1/attackers/{uuid}/attribution

Returns the merger output for an attacker's identity:

    {
        "identity_uuid": "abc..." | null,
        "primitives": [
            {primitive, current_value, state, confidence,
             observation_count, last_change_ts, last_observation_ts},
            ...
        ]
    }

Pre-attribution-worker: identity_uuid=null, primitives=[]. Surfacing
identity_uuid keeps the cross-attacker rollup story visible to the
frontend ahead of v1's clusterer landing.

api_events SSE relay also subscribes to attribution.> and forwards
to the AttackerDetail page filtered on payload.identity_uuid (the
identity is resolved at stream open from the URL's attacker_uuid;
attribution payloads are identity-keyed, not attacker-keyed). New
SSE event names: attribution.state_changed,
attribution.multi_actor_suspected.

Frontend (AttackerDetail.tsx badge rendering, useAttackerStream
consumer) deferred — there's already WIP on AttackerDetail.tsx in
the working tree; merging the badge logic is a separate commit
once that lands.

Tests: 4 endpoint scenarios — 401 unauth, 404 unknown attacker,
200 empty (no stub), 200 with primitive-ordered rows.
2026-05-09 02:21:59 -04:00