Commit Graph

136 Commits

Author SHA1 Message Date
2c1ccec8fa refactor(decnet_web/SwarmHosts): wire shell + bump coverage floor
SwarmHosts.tsx: 513 -> 161 LOC. Page now composes EnrollmentWizard
+ useSwarmHosts hook; only the arm/confirm UI affordance and the
busy-set tracking remain in the shell.
2026-05-09 06:08:48 -04:00
780d395a46 refactor(decnet_web/SwarmHosts): add useSwarmHosts polled data hook 2026-05-09 06:07:38 -04:00
9def7fd22f refactor(decnet_web/SwarmHosts): extract EnrollmentWizard 2026-05-09 06:06:29 -04:00
3a8519b2a1 refactor(decnet_web/SwarmHosts): extract types + helpers with tests 2026-05-09 06:05:19 -04:00
31f4c54c32 refactor(decnet_web/Webhooks): wire shell + bump coverage floor
Webhooks.tsx: 642 -> 387 LOC. Page now composes FormRow + SecretModal
+ useWebhooks hook; toast policy is the only UI concern left in the
shell. Multi-select delete uses the hook's reload internally.
2026-05-09 06:04:14 -04:00
7408a04a90 refactor(decnet_web/Webhooks): add useWebhooks data hook 2026-05-09 06:02:37 -04:00
1ac64d2ae2 refactor(decnet_web/Webhooks): extract FormRow + SecretModal 2026-05-09 06:01:47 -04:00
44f4dd8c85 refactor(decnet_web/Webhooks): extract types + helpers with tests 2026-05-09 05:49:34 -04:00
ac64329a13 refactor(decnet_web/PersonaGeneration): wire shell + bump coverage floor
PersonaGeneration.tsx: 875 -> 357 LOC. Page now composes the data
hook + PersonaCard + PersonaEditor; bulk-import helpers stay in
helpers.ts; toast policy is the only UI concern left in the shell.
2026-05-09 05:48:16 -04:00
c1a65bf9a3 refactor(decnet_web/PersonaGeneration): add usePersonaGeneration data hook 2026-05-09 05:46:08 -04:00
97e72d975b refactor(decnet_web/PersonaGeneration): extract PersonaCard + PersonaEditor 2026-05-09 05:45:10 -04:00
a19d8bba17 refactor(decnet_web/PersonaGeneration): extract types + helpers with tests 2026-05-09 05:43:59 -04:00
6e0e1c204e refactor(decnet_web/MazeNET): wire hooks + bump coverage floor
Final integration step. The MazeNET page shell is now a thinner
composition of the existing module-level hooks (useMazeApi,
useMazeInteraction, useTopologyEditor, useTopologyStream,
useLayoutPersistor) PLUS the three new ones from this phase
(useFullscreenMode, useTopologyData, useMazeContextMenu).

- MazeNET.tsx: 980 -> 715 LOC. The fullscreen + body-class
  effects, the topology hydrate / SSE stream / deploy /
  flashErr plumbing, and the four context-menu builders are
  all gone from the shell.
- Page still owns the per-operation editor callbacks
  (removeNet/Node/Edge, duplicateNode, addServiceToNode, etc.)
  because they need direct access to setNodes/setEdges/setNets
  for optimistic patches alongside their REST calls — those
  setters are exposed by useTopologyData for that reason.

Coverage floor bumped after the phase:

  lines       17 -> 19
  functions   15 -> 17
  branches    13 -> 14
  statements  16 -> 18

Phase 5 final scoreboard: 37 test files, 172 tests, all green.
2026-05-09 05:39:32 -04:00
f33a011900 refactor(decnet_web/MazeNET): extract useMazeContextMenu
Lift the context-menu builder out of the page shell. The hook
owns ctxMenu open/close state and exposes one builder per
surface (node / net / edge / canvas); the actual operations come
in via callbacks so the page keeps its optimistic-patch logic
unchanged.

- New MazeNET/useMazeContextMenu.tsx
- useMazeContextMenu.test.ts covers menu lifecycle (open/close),
  node-menu items, observed-entity locking, internet-net
  delete-disabled, canvas-menu Add subnet/DMZ items, and the
  edge-menu Remove invocation.
- Wiring into MazeNET.tsx lands next.
2026-05-09 05:34:58 -04:00
5f2a3f4629 refactor(decnet_web/MazeNET): extract useTopologyData
Lift the canvas data plane off the page shell. The hook owns:

  GET /topologies/:id            (hydrates nets/nodes/edges + meta)
  GET services + archetypes      (catalogs, with bundled fallback)
  POST /topologies/:id/deploy
  /topologies/:id/events SSE     (open only when active/degraded)
  flashErr() banner timer        (auto-clears actionErr after 4s)

State setters for nets / nodes / edges are returned so the
per-operation callbacks living in the page can optimistically
patch local state alongside their REST calls (matches the
existing pattern; wholesale lift would mean dragging every
mutation along too).

- New MazeNET/useTopologyData.ts
- useTopologyData.test.ts covers hydrate, loadErr surfacing,
  streamEnabled gating on active/degraded, onDeploy success +
  error paths, and the flashErr 4s auto-clear with fake timers.
- Wiring into MazeNET.tsx lands in the next commit.
2026-05-09 05:33:19 -04:00
212feb49e2 refactor(decnet_web/MazeNET): extract useFullscreenMode
Lift the four fullscreen-related side-effects off the page shell.
The hook owns:

  1. body class toggle so page CSS can hide its chrome
  2. browser fullscreen API request/exit (failures ignored)
  3. fullscreenchange listener so F11/Esc from outside our button
     keeps internal state in sync
  4. Esc keystroke handler

Returns { fullscreen, setFullscreen, toggle }.

- New MazeNET/useFullscreenMode.ts
- useFullscreenMode.test.ts (jsdom) covers initial toggle, body
  class lifecycle, Esc-to-exit, and unmount cleanup.
- MazeNET.tsx loses ~30 LOC of inline state + effects.
2026-05-09 05:31:39 -04:00
171e20e427 refactor(decnet_web/Config): wire hook + bump coverage floor
Final integration. The page shell is now a thin composition of
useConfig + the previously-extracted children:

- Config.tsx: 989 -> 131 LOC. Page owns only the activeTab state
  (and the "drop the users tab if the server didn't send users"
  effect). Every form lives inside its tab; toast wiring lives
  in AppearanceTab; window.alert calls live inside UsersTab.
- Tabs receive their `onSave* / onAddUser / ...` callbacks
  directly from the hook — no intermediate wrapper handlers.

Coverage floor bumped after the split:

  lines       14 -> 17
  functions   13 -> 15
  branches    11 -> 13
  statements  13 -> 16

Phase 4 final scoreboard: 34 test files, 156 tests, all green.
2026-05-09 05:27:47 -04:00
4a9cd90f90 refactor(decnet_web/Config): extract AppearanceTab
APPEARANCE panel — accent-color picker — into its own tab. State
is local since no other tab cares about the value; localStorage
persistence + the document.documentElement[data-accent] mirror
move along with it.

- New Config/tabs/AppearanceTab.tsx
- AppearanceTab.test.tsx covers the matrix default, reading the
  saved accent from localStorage on mount, and the click-to-flip
  flow writing both localStorage and the html data-accent attr.
2026-05-09 05:26:26 -04:00
ccae1612bd refactor(decnet_web/Config): extract GlobalsTab + DangerZone
GLOBAL VALUES panel + the developer-mode-gated DANGER ZONE
(reinit) into one tab file. Two stacked panels because they're
the two pieces of UX you ever see together on the globals tab;
splitting them into separate components would force the page
shell to re-pick the gating predicate.

- New Config/tabs/GlobalsTab.tsx (mutation-interval + DangerZone
  inline, since DangerZone is reinit-specific and won't be reused)
- GlobalsTab.test.tsx covers interval-format validation, the
  DANGER ZONE gating on developer_mode, the two-step reinit
  confirm flow, the totals chip ("PURGED: N logs, N bounties,
  N attacker profiles") on success, and viewer-mode rendering.
2026-05-09 05:25:49 -04:00
be35228191 refactor(decnet_web/Config): extract UsersTab
USER MANAGEMENT panel into its own tab. Owns the per-row UI
state (delete-confirm, reset-password popup) plus the add-user
form state; mutations come in via prop. Errors on per-row
operations stay on window.alert (matches existing behavior); the
add form uses the inline FormMsg chip.

- New Config/tabs/UsersTab.tsx
- UsersTab.test.tsx covers row rendering with the must-change
  badge, the two-step delete confirm flow, the add-user submit
  payload (trimmed username + selected role), and the success
  chip after a successful add.
2026-05-09 05:24:55 -04:00
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