Commit Graph

122 Commits

Author SHA1 Message Date
df67cb8a46 fix(web/session): don't stopPropagation on drawer panel — breaks player clicks
The drawer used onClick={onClose} on the backdrop + onClick={e =>
e.stopPropagation()} on the panel to stop inside-clicks from closing
the drawer. That pattern is fine for most React trees, but React's
stopPropagation() also aborts the NATIVE DOM event — and asciinema-
player wires its click-to-play handler via document-level event
delegation. So every click inside the drawer (including the big
play button) died at the panel boundary and never reached the
player's dispatcher. Confirmed end-to-end by calling window.__ap.
play() directly from DevTools: playback started, cast rendered in
full, ended event fired.

Swap to the idiomatic target===currentTarget guard on the backdrop
so only genuine backdrop clicks close the drawer; everything inside
(including native-delegated handlers) gets its events untouched.

All the debug instrumentation from b5c6b8a, 4424138, 6d031ae, and
f032ece (cast logging, lifecycle listeners, window.__ap) is
reverted here — symptom root-cause is known, it was event delegation
not the parser or the cast.
2026-04-24 10:35:11 -04:00
6d031ae18c debug(web/session): expose player instance as window.__ap
The parse path works (metadata event fires with duration: 24.58s,
idle event fires); next unknown is whether clicking play even
reaches core.play(). Stash the player on window so the operator can
call __ap.play() from DevTools to diff UI-click vs direct-call
behaviour and see whether 'play' / 'playing' events fire.

To be reverted once we pin the failure.
2026-04-24 10:31:31 -04:00
442413870d fix(web/session): subscribe to metadata/playing/idle/errored/reset/seeked too
The original short subscribe list missed 'metadata' — which is the
one that carries the parsed duration + theme + marker info AFTER
_initializeDriver (the step that actually parses the cast). Without
it we only saw 'ready' (= UI mounted, parse not yet run) and jumped
to conclusions about the parser.

Add the full lifecycle set so the next repro pins which step the
player is actually getting stuck at.
2026-04-24 10:28:28 -04:00
b5c6b8a073 fix(web/session): preload cast so parse runs at mount, not click
Without preload:true the player only parses the recording when the
user first clicks play. Any parse error during that lazy step
bypasses our lifecycle instrumentation (we only see "ready", which
just means UI mounted), and from the user's POV the play button
stays black because they never see the actual failure.

Forcing preload makes the driver's init() run synchronously-ish with
the "ready" dispatch, so getDuration() resolves to a real number
(or we see an "errored" event with a payload that tells us why).
2026-04-24 10:25:42 -04:00
4a8b13b392 fix(web/session): instrument player lifecycle to catch async init failures
The sync try/catch around AsciinemaPlayer.create() misses async
failures in the player's internal init() promise — those land as
unhandled rejections and are invisible from the component's POV.

Subscribe to every lifecycle event (ready / play / pause / ended /
error / errored / loading) and log the resolved duration. If the
parser produces zero events despite a well-formed cast, duration
resolves to 0 / NaN / rejected — one of those signals will point at
whichever frame the render path is silently failing at.
2026-04-24 10:21:26 -04:00
f032ece678 fix(web/session): log the cast to console when player mounts
Diagnostic for the persistent "player mounts with chrome but plays
black" symptom after the blob-URL fix. The player now gets
{data: cast} correctly and parses at least enough to render the
control bar, but duration shows --:-- and the terminal stays blank.

Log the first 400 chars of the built cast + event/cols/rows so the
operator can confirm in DevTools whether the malformed input is the
cast itself or something downstream in the asciinema parser.
2026-04-24 10:17:57 -04:00
e684feb1fe fix(web/session): feed asciinema-player inline data, not a Blob URL
SessionDrawer built a cast blob, pushed it through URL.createObjectURL,
and passed the blob URL to AsciinemaPlayer.create(). That's racy with
useEffect's cleanup: each new page of events re-fires the effect, the
cleanup revokes the URL, and the player's already-in-flight async
loadRecording() lands on a dead URL with no visible error — result was
a centered play button with an empty black pane, playback never starts.

asciinema-player v3's recording driver accepts {data: <string>} as a
first-class source (see core-DnNOMtZn.js:905-930 doFetch — string/
ArrayBuffer data is wrapped in `new Response(value)` and handed to the
parser). Skip the blob detour entirely, pass the cast text inline.

Also filter events to valid asciicast channels (o/i/r) before feeding
so a future stray SD field can't derail the parser, and log mount
errors to console for next-time debugging.
2026-04-24 01:26:07 -04:00
c282f74bd4 fix(web/dashboard): wrap long kv-chips instead of blowing out the EVENT column
Key:value chips in the live-feed event cell used the default .chip
style, which is white-space: nowrap + inline-flex. A long cmd: value
(attacker-controlled shell strings, URLs, base64 payloads) stretched
the chip horizontally past the column, pushing the whole table into
horizontal scroll and clipping subsequent columns off-screen.

Add a chip-kv variant that allows the value to wrap inside a
max-width: 100% chip (word-break: break-word, overflow-wrap: anywhere
for dense strings with no natural break). The key-label stays on the
first line via flex-shrink: 0. Short values (uid: 0, user: root)
stay tight; long ones wrap onto multiple lines inside the chip.

Also set minWidth: 0 on the EVENT td + nested flex containers so
flex children honour the column width instead of growing to fit
content. Added title={k: v} on each chip for full-value hover in
case the wrap is still clipped.
2026-04-24 00:51:31 -04:00
21e6820714 feat(web/attackers): surface GeoIP country on list cards + detail page
- Attackers list: small country-code chip next to the IP on each card,
  title-tooltip shows the source (e.g. "rir")
- AttackerDetail: country-code tag next to the IP in the header plus an
  ORIGIN field in the TIMELINE section for always-visible origin
- TypeScript interfaces updated with country_code/country_source
2026-04-23 21:21:21 -04:00
8cbb7834ef feat(web): SMTP victim-domain + stored-mail panels on attacker detail
Adds GET /attackers/{uuid}/smtp-targets (viewer) and GET /attackers/{uuid}/mail
(admin) endpoints, plus two new sections on the attacker detail page:
VICTIM DOMAINS rollup (aggregate-only, federation-gossip-safe) and STORED MAIL
with a drawer that decodes headers, lists attachments, and downloads the raw
.eml via the existing artifact endpoint (?service=smtp).
2026-04-22 22:33:53 -04:00
b51095cec5 style(web): unify button sizing across pages (padding/font/spacing) 2026-04-22 18:35:40 -04:00
4bf671b316 style(web/topologies): unify header buttons with shared outlined style 2026-04-22 18:32:43 -04:00
9d64d8a046 style(web/mazenet): tint palette chip with user accent color for contrast 2026-04-22 18:29:19 -04:00
c804d3111a style(web/mazenet): enlarge palette port/proto chip for legibility 2026-04-22 18:26:29 -04:00
602a0e1efc feat(web/mazenet): add Mail, Comms, Observability, Containers groups + remaining services 2026-04-22 18:23:24 -04:00
9c38a3f11a feat(web/mazenet): group Service Fleet items by category (Remote Access, Web, Databases, etc.) 2026-04-22 18:19:21 -04:00
1674316788 feat(web/mazenet): glide transitions for service fleet + inspector panels 2026-04-22 18:16:17 -04:00
e0231bf990 style(web/mazenet): rename PALETTE toggle to SERVICE FLEET 2026-04-22 18:14:17 -04:00
e35358afd1 feat(web/mazenet): fullscreen button also triggers browser fullscreen API 2026-04-22 18:12:38 -04:00
ef34df4a7d feat(web/mazenet): fullscreen canvas mode (hides topbar + sidebar, Esc to exit) 2026-04-22 18:11:37 -04:00
31d02a9726 feat(web/mazenet): toggleable palette (deployer) panel 2026-04-22 18:10:18 -04:00
8985c28fab fix(web/mazenet): stop canvas from overflowing viewport (flex-size shell instead of fixed calc) 2026-04-22 18:08:44 -04:00
f3e366a2a3 fix(web/topologies): stop page from overflowing viewport (min-height off by topbar+padding) 2026-04-22 18:07:09 -04:00
53647d66b7 feat(web/swarm): fold agent enrollment into a wizard on Swarm Hosts 2026-04-22 18:05:26 -04:00
bff350400f style(web/swarm): align Swarm pages with shared page-header primitive 2026-04-22 17:59:27 -04:00
fcfc4eba3b style(web/topologies): drop extra padding so header aligns with fleet/dashboard 2026-04-22 17:55:06 -04:00
f94887393c style(web/topologies): align page header with shared style, center empty state
- TopologyList header now uses .page-header + .page-title-group +
  .page-sub like Dashboard/Attackers/DeckyFleet; title typography and
  separator match the rest of the app.
- Pluralisation fix: '0 topologyies' → '0 TOPOLOGIES', singular '1
  TOPOLOGY'.
- When the list is empty the EmptyState renders in its own flex
  container that fills the viewport so the card is centered both
  axes, with bumped icon/title/hint sizing for the hero treatment.
2026-04-22 17:53:35 -04:00
3f460bab84 feat(web): show MazeNET decky running count + roll into dashboard
MazeNET header now reports '{running}/{total} DECKIES RUNNING' so
operators can see per-topology runtime status at a glance.

Dashboard ACTIVE DECKIES counters used to reflect only the fleet state
file; TopologyDecky rows (MazeNET deployments) are now added in —
deployed_deckies = fleet + all topology rows, active_deckies = fleet
(no runtime field) + topology rows whose state is 'running'.
2026-04-22 17:48:04 -04:00
b802d59c70 style(web): vertically center empty-state in logs table 2026-04-22 17:32:53 -04:00
1472f1da0a style(web): drop border on empty-row td in logs tables 2026-04-22 17:31:50 -04:00
070ad9397c style(web): skip row-hover highlight on empty-state rows
Hovering the empty-state row in LiveLogs/Dashboard tables briefly lit
the full-width td with the data-row glow. Tag the placeholder tr with
.empty-row and scope the .logs-table hover rule to :not(.empty-row).
2026-04-22 17:29:39 -04:00
fe8dd08ba6 style(web): center EmptyState contents with consistent min-height
Base .empty-state now flex-centers its icon/title/hint/CTA with a
140px min-height so icon-bearing empty states in the Dashboard side
panels (DECKIES UNDER SIEGE, TOP ATTACKERS) stop looking cramped.
Component-scoped rules (attackers-root, bounty-root, logs-root)
remain more specific and are unaffected.
2026-04-22 17:27:20 -04:00
4d1e6c0838 feat(web): add ? cheatsheet and / focus-search hotkeys
- New ShortcutsHelp modal enumerates global, nav G-chord and palette
  bindings; openable via ? (Shift+/) or the command palette.
- / dispatches a global decnet:focus-search event; Attackers, Bounty
  and LiveLogs listen and focus their in-page search inputs (pages
  without a local search are skipped per plan).
- Respects the existing editable-element guard and Alt+K palette
  toggle; no rebinds to prior shortcuts.
2026-04-22 17:25:32 -04:00
ecb813ad38 feat(web): retrofit empty states to shared EmptyState primitive
Replace ad-hoc empty-state markup across Dashboard, TopologyList,
LiveLogs, Attackers, Bounty, AttackerDetail, SwarmHosts, RemoteUpdates
and CommandPalette with the new <EmptyState> component. Themed icons
+ hints improve discoverability; TopologyList and SwarmHosts gain
CTAs to their respective creation flows.
2026-04-22 17:22:07 -04:00
de63a0ab5c feat(web/fleet): DeckyFleet reskin, inspect drawer, and modal retrofit
- Fleet grid rewrite: richer decky cards (archetype, services, swarm
  chip, mutation status) with click-to-inspect.
- Deploy wizard: track server-accepted deploys separately so the
  placeholder log stream only auto-closes on success; surface failures.
- DeployWizard + IntervalEditor migrated to the shared <Modal>
  primitive — gains ESC-close, backdrop click, Tab focus trap, and
  body scroll lock without changing visual design.
2026-04-22 17:15:45 -04:00
e14527b382 feat(web): reskin Attackers, Bounty, and LiveLogs pages
Each page gets its own scoped stylesheet and is rewritten around the
shared design language: filter bars, paginated lists, empty-state
blocks, BountyInspector drawer. Behavioural surface is unchanged —
same API calls, same routes, same RBAC gating.
2026-04-22 17:15:35 -04:00
1518475946 feat(web/dashboard): reskin with richer live-activity panels
Rewrites Dashboard.tsx around three stacked panels — live interactions,
deckies-under-siege, and top-attackers — each with its own header,
empty state, and status accents. Dashboard.css fills in the supporting
grid + type system.
2026-04-22 17:15:27 -04:00
ccbe949238 feat(web): command palette, toasts, and global shell chrome
- CommandPalette (Alt+K): fuzzy action launcher with keyboard nav.
- Toasts: ephemeral notification stack + provider.
- useGlobalHotkeys: Alt+K palette toggle, G-chord navigation
  (G D/F/M/L/B/A/S/U/E/C), respects editable-element focus.
- Layout/App: wire ToastProvider at root, mount the palette inside the
  authed shell, introduce the global search box in the top bar.
- MazeNETRoute now renders TopologyList inline when no ?topology is
  present, instead of bouncing through a redirect.
- index.css: a few global token tweaks consumed by the new chrome.

Fixes a latent breakage: Config.tsx and MazeNET already imported
./Toasts/useToast but the directory was never committed.
2026-04-22 17:15:19 -04:00
dca6eddd5f feat(web/topology): hide DELETE on running topologies
The DELETE path on a topology whose containers are still up is a
footgun — even if the backend rejects the delete, surfacing the
button invites mistakes. Gate it so DELETE only shows for pending,
failed, and torn-down topologies. Active/degraded/deploying topologies
must be torn down first, which then reveals DELETE again.
2026-04-22 17:14:17 -04:00
6f537f52c2 fix(topology): remove DMZ gateway auto-attach on LAN create
POST /topologies/{id}/lans previously called _auto_attach_gateway()
whenever a non-DMZ LAN was created, which wired the DMZ gateway decky
to every new subnet. That's why a deployed gateway ended up with
eth0..ethN on every LAN regardless of what the user drew in MazeNET.

Drop the auto-attach helper entirely. The DMZ_ORPHAN deploy-time
validator (decnet/topology/validate.py:65-110) stays strict — users
must explicitly wire the gateway to each subnet they want bridged,
which is the whole point of having a topology editor.

useMazeApi.ts: drop stale auto-bridge reference from comment.
2026-04-22 17:14:09 -04:00
8632cee40a feat(web): retrofit drawers + CreateTopologyWizard with ESC/focus-trap
ArtifactDrawer, SessionDrawer, CreateTopologyWizard all now:
- close on ESC
- trap Tab/Shift+Tab focus within the panel
- lock body scroll while open
- restore prior focus on unmount

Uses the new useEscapeKey + useFocusTrap hooks. No visual changes;
the bespoke CSS shells (ctw-*, inline drawer styling) are preserved.
2026-04-22 17:09:45 -04:00
d0463c2c16 feat(web): add Modal + EmptyState primitives and a11y hooks
- Modal: shared backdrop/panel with ESC-close, backdrop-click-close,
  focus trap, body scroll lock; supports center + drawer-right variants,
  matrix/violet accents, default/wide widths.
- EmptyState: icon + title + hint + optional CTA; compact variant
  for tight rails.
- useEscapeKey, useFocusTrap: reusable hooks powering Modal; will also
  be adopted by CommandPalette and ContextMenu in follow-up commits.

No retrofits yet — primitives only. tsc clean.
2026-04-22 17:04:37 -04:00
73ccf12678 fix(web): allow MazeNET canvas pan from inside net-box body
Pan drag previously required mousedown on the bare canvas (target ===
currentTarget). When zoomed in, net-boxes cover most of the viewport
so there was no bare grid to grab. Drop the guard — node/header/port/
resize handlers all call stopPropagation() already, so only net-box
body mousedowns bubble up to start the pan, which is exactly what
we want.
2026-04-22 16:43:38 -04:00
ef60b086ba feat(web): MazeNET canvas pan + zoom (0.25×–2.5×)
Wheel-to-zoom anchored at the cursor, ZOOM IN/OUT toolbar buttons, and
a live zoom% in the status bar. Pan layer gets transform-origin 0 0 and
a scale(zoom) factor; grid pattern tile scales with zoom; edge SVG is
overflow:visible so long edges don't clip at high zoom. World-space
hit-testing, resize deltas, and palette drops all divide by zoom.
Reset View zeroes pan AND zoom.
2026-04-22 16:40:47 -04:00
1f429cd00e feat(web): MazeNET 7b — service-level selection + inspector panel
Clicking a service tag selects it (stops node drag), extends Selection
discriminant with {type:'service',id,nodeId}, and renders an inspector
panel showing proto/port/subnet/risk chip + REMOVE SERVICE button
(gated off for observed nodes and degraded topologies). Service-tag
styling now pulls `risk` from DEFAULT_SERVICES metadata instead of
node.status alone.
2026-04-22 15:56:55 -04:00
6fbac5d057 feat(web): MazeNET 7a — canvas chrome + node-head visuals
Toolbar (RESET VIEW / AUTO-LAYOUT), status bar (GRAPH LIVE + pan + as-of
timestamp), 4-row legend, and archetype icon + status dot in each node
head.
2026-04-22 15:54:11 -04:00
49a6a674e6 feat(web): wire Workers panel START + START ALL buttons
Per-row START button enabled iff `installed && status !== 'ok'`;
tooltip explains why it's disabled ("Unit not installed" /
"Already running"). Transient `starting` state shows `...` on the
button and auto-clears after 15s so the UI never gets stuck if the
heartbeat is slow.

START ALL WORKERS button in the header calls /workers/start-all and
renders the three counts in the toast:
`STARTED · N · ALREADY RUNNING · M · FAILED · K (first failure: …)`.
Tone flips to alert when K > 0.
2026-04-22 14:13:58 -04:00
246a82774b feat(web): SessionDrawer + Sessions tab in AttackerDetail
Adds asciinema-player dependency, SessionDrawer.tsx that pages the
transcripts API (500 events per request) and rebuilds a v2 .cast blob
for playback, and a Session Transcripts section in AttackerDetail that
deep-links into the drawer. Truncation banner surfaces the 10 MB
per-session cap when it's been hit.
2026-04-21 23:08:39 -04:00
3d047f2100 feat(web): wire REAP ORPHANS button in topology list
Exposes POST /topologies/reap-orphans via an arm-to-confirm button in
the topology list header. Shows a transient status line with removal
counts or the error. Admin-only on the backend; non-admins see the 403.
2026-04-21 22:17:04 -04:00
9ea0abc321 fix(web): correct MazeApi type import in useTopologyEditor
useTopologyEditor imported 'UseMazeApi' but the actual exported type
is 'MazeApi'. tsc --noEmit missed it because the file isn't in the
default tsconfig include path; tsc -b (project references, used by
'npm run build') catches it.
2026-04-21 20:15:24 -04:00