Commit graph hyperhive/frontend/packages/dashboard/src/flow.js
Author SHA1 Message Date
iris
7110a25cf6 frontend: extract themed dialogs + async-form handler to shared, wire agent UI
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.

- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
  updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
  alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
  (no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
  themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
  bindAsyncForms(), and replaced all 8 window.confirm() sites with
  themedConfirm (async, wrapped in a fire-and-forget IIFE where the
  call site needs a synchronous boolean return, e.g. the slash-command
  dispatcher).

Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
2026-07-27 18:55:17 +02:00
damocles
cafef519a9 dashboard frontend: consume rfc3339 timestamps from the api 2026-07-02 22:28:30 +02:00
iris
905b38b6c7 dashboard: point frontend at the /api/* backend routes
Phase 2 of the dashboard route consolidation. The backend now double-registers
every bare top-level route (approve/deny/kill/restart/start/rebuild/destroy/
update-all/answer-question/cancel-question/purge-tombstone/matrix-account-login/
cancel-reminder/retry-reminder/request-spawn/op-send/meta-update/dashboard-stream/
dashboard-history) at an additional /api/<same> path. Switch every dashboard-pkg
fetch / EventSource / form action to the /api/ form so "backend = /api/*" holds
on the frontend side too.

Bare paths still answer, so this is independently deployable; once it ships the
backend can drop the bare registrations (phase 3). webhook/knowledge stays a
distinct webhook prefix (forge-driven, no SPA caller). app.js's /rebuild,/start
are the agent harness UI (different server) and are untouched.

Part of #1846 (phase 2).
2026-06-22 00:45:26 +02:00
iris
2db94a920a chore(frontend): scrub pre-existing issue-tag refs from source comments
The no-issue-tags-in-code rule (knowledge/hive-rules.md) was enforced on
recent PRs but several pre-existing parenthetical refs remained in the
frontend source. Replace them with plain prose — issue numbers are opaque
in source, and prose survives repo migrations + doesn't confuse static
analysis. Comment-only; no behaviour change.

- tabs.js: 4× operator-inbox refs → prose
- flow.js / flow.css: agent-filter + sent/delivered-collapse refs → prose
- home.js / home.css: H0M3 page-script + page-chrome refs → prose
- dashboard.html: 1NB0X inbox ref → prose

Closes #1549.
2026-06-09 00:24:49 +02:00
iris
5d0f3d060b feat(dashboard): generic server-warnings banner on every page (#1518)
Per operator request: instead of a disk-specific alert, surface a generic
server-warnings banner at the very top of every page, so new system
warnings can be added backend-side with no frontend change.

- hive-c0re `host_stats`: `server_warnings() -> Vec<ServerWarning>`
  (`{ kind, level, message }`). The threshold logic lives server-side; the
  host disk-pressure check (a `statvfs` probe of `/nix`: ≥85% used → warn,
  ≥95% → crit) is the first and only producer today. No new deps (libc).
- `/api/state` carries `server_warnings` (replaces the disk-specific
  field). Empty when all clear.
- frontend: `renderServerWarnings` / `initServerWarnings` in `common.js`
  inject a sticky top-of-<body> banner and render the list, coloured by
  `level`. Wired on every page — dashboard (live, via refreshState),
  FL0W, L0GS, H0M3. No per-warning frontend code; adding a warning kind
  is a pure backend change.

cargo check/clippy/fmt + npm run build green. Closes #1518.
2026-06-08 20:55:15 +02:00
iris
8ddab401f9 feat(dashboard): serve H0M3 at /, relocate dashboard to /dashboard.html (#1464)
Step 2 of the nav restructure: make the H0M3 menu hub the landing page
at / and move the dashboard SPA to /dashboard.html, with every surface
linking back to the hub.

Mechanism (frontend-only, no host-side change — confirmed against the
ServeDir route table): the dashboard's ServeDir fallback serves
index.html at / via append_index_html_on_directories, and there is no
SPA path-routing catch-all to break. The dashboard SPA is served as the
plain file dashboard.html so it never shadows the exact-match
/dashboard/stream + /dashboard/history SSE routes registered before the
fallback.

- Swap the HTML entry files: the H0M3 page becomes index.html (loads
  home.js → served at /), and the dashboard SPA becomes dashboard.html
  (loads tabs.js → served at /dashboard.html). build.mjs copies the new
  set; JS bundle names are unchanged (referenced by absolute /static/
  paths, independent of the HTML filename).
- H0M3 Dashboard tile now points at /dashboard.html.
- The dashboard gains a "← home" back-link in its chrome; flow.html and
  logs.html relabel their back-link from "← dashboard" to "← home"
  (href stays / — which is the hub now). Pages link to the hub, not to
  each other.
- Agent page (app.js + stats.js): the "↑ dashboard" link now targets
  /dashboard.html. The API base (rebuild / answer-question /
  mark-all-read POSTs) stays the origin root, unchanged.
- Comment-only: tabs.js / flow.js / common.js references to the
  dashboard's old index.html filename updated to dashboard.html.

Note for review: git renders the file swap as a deleted home.html + an
added dashboard.html + a heavily-modified index.html, because index.html
exists on both sides with swapped content. It's a content swap, not a
rewrite — the built dist/ is verified (index.html→home.js, dashboard.html
→tabs.js).

Deferred to a follow-up: removing the FL0W / L0GS / M4TR1X "→" page-links
from the dashboard tab strip (touches tabs.js gating/overflow), and the
shared reusable chrome component (a later step).
2026-06-07 21:00:34 +02:00
iris
e949129e75 refactor(flow): drop operator-inbox UI, now lives on Y3R C4LL (#1483)
The operator inbox moved to the dashboard's Y3R C4LL tab in #1469
(◆ 1NB0X ◆ section, with per-message + mark-all read). FL0W goes back
to being the pure event firehose, so remove its now-redundant inbox UI:

- flow.js: drop the operatorInbox store, inboxAppendFromEvent,
  buildInboxListNode, renderInbox, the inbox-pill click wiring, and the
  onAnyEvent hook that fed them. The side panel was only used for the
  inbox flyout on this page, so drop Panel.bind() + the Panel import too.
- flow.html: remove the inbox pill, the offscreen inbox-section div, and
  the side-panel markup.
- flow.css: remove the .flow-pill* and .flow-inbox-headless rules.
- home.html: Flow tile desc → "live all-agents message firehose".
- docs/web-ui/dashboard.md: drop the FL0W 0PER4T0R 1NB0X section, point
  at Y3R C4LL, and fix the count-pill + /op-send descriptions.

The agent filter, sent→delivered collapse, compose box, and OS
notifications on operator-bound traffic are unaffected.
2026-06-07 20:40:20 +02:00
iris
4c77eefc51 feat(flow): filter the timeline by agent
Adds an agent filter to the FL0W header (#1473): a select listing the
live agents narrows the timeline to messages involving the chosen agent
(matched on `from` OR `to`). Each message row now carries
`data-from`/`data-to`; non-matching rows get `.flow-hidden`. New rows
pick up the active filter at render time; changing the filter re-scans
existing rows. The selection persists in localStorage so a reload or
tab-switch keeps the view. The dropdown is populated from the live
container list (and stays current on container add/remove); a saved
selection survives even if that agent isn't currently listed.

Pure frontend; composes with the sent+delivered collapse (the surviving
collapsed row keeps its `data-from`/`data-to`). Closes #1473.
2026-06-06 12:05:02 +02:00
iris
a2fe3f2e01 feat(flow): collapse back-to-back sent+delivered into one line
When a message directly wakes its recipient, the broker emits `sent`
then `delivered` for the same row id ~instantly, rendering two
near-identical lines on the FL0W timeline. Track each recent `sent`
row and, when its `delivered` lands within COLLAPSE_SECS (3s), upgrade
that row in place (arrow → green ✓, title "sent + delivered") instead
of adding a second line. A delivery that arrives later (recipient was
busy) still renders as its own row, so genuine delivery latency stays
visible.

`Sent` and `Delivered` share the same broker row id
(dashboard_events.rs), so matching is exact. The recentSent map is
bounded to 256 entries. Reply threading is unaffected (msgRowMap still
resolves the surviving row).

Closes #1472.
2026-06-06 11:50:01 +02:00
iris
f87a674e14 fix(flow): flashError was inserting at top of terminal (invisible)
The compose box's send-error path used:
  flow.insertBefore(row, flow.firstChild)

This inserts the error before the first (oldest) child — at the top of
the terminal's scroll area — which is completely out of view since the
operator is at the bottom watching new messages.

Changed to flow.append(row) + scroll the wrap element to the bottom so
the error appears where the operator is looking. Also prefixed the
message with '✗ ' so it's visually distinct from regular flow rows.
2026-06-05 13:02:48 +02:00
damocles
89665b94de feat(#1014): rename manager agent root→ruth across all crates + frontend 2026-06-02 18:18:06 +02:00
iris
146a6b7cd5 chore: replaceChildren + http case in flow.js and logs.js
Same cleanup as the previous pass (tabs.js, app.js): replace
innerHTML = '' with replaceChildren() and lowercase the one
uppercase 'HTTP' error in logs.js.
2026-06-01 23:13:50 +02:00
iris
f7e38c0b42 common.js + flow.js + index.html + flow.html: cookie scrub + doc pointers (#712 batch 13 cont.)
Final dashboard frontend cleanup. Four files at 0 cookies each.

common.js (11 → 0):
- #406 module-split history → drop, present-state intro
- #448 SharedWorker SSE pipe (×3) → docs/web-ui.md SSE multiplexing
  paragraph pointer
- #515 worker-death self-heal (×3) → docs pointer + brief inline
- #453 bfcache argus-nit (×2) → drop framing, keep inline why
- #451 drag-to-resize → docs/web-ui.md::Side panel pointer
- #233 marked autolinks new-tab → drop cookie

flow.js (8 → 0):
- #406 module-split history → drop
- #389 banner-in-footer → drop framing
- #375 stacking context → docs/web-ui.md::Per-agent page Terminal-wrap
- #408 server-side filter, #499 backend allow-list → drop cookies,
  keep inline why
- #448 SharedWorker per-URL keying → docs pointer
- #163 SSE catchup drift → drop cookie, keep inline why

index.html (13 → 0):
- #389 chrome history → drop
- #459 schedules, #607 matrix, #609 nginx-front, #15 → drop cookies,
  keep functional comments + docs/web-ui.md::Tab strip + gateway.md
  pointers for matrix
- #385 dropped C0NTAINERS heading → drop framing
- #460 reminders-moved-here → drop framing
- #444 SchedulesChanged SSE absence → drop cookie, keep inline why
- #443 selection bar → docs/web-ui.md::Selection bar pointer
- #564 fold-create-into-table → drop framing
- #369#issuecomment-3437 → drop attribution
- #406 step 3 bundle rename → drop history

flow.html (3 → 0):
- #389 slug + #362 pill pattern + #406 step 2 bundle → drop cookies,
  preserve functional descriptions

Total this batch (across all 4 files): **35 cookies scrubbed**.
Net effect: the dashboard SPA's HTML + JS comments all point at
docs/web-ui.md for substantive design context now, with inline
comments only retaining present-state operational descriptions.
2026-05-31 15:09:54 +02:00
iris
ac4c8456e1 flow.js: subscribe to /dashboard/stream with ?kinds= filter (closes #408)
Frontend half of #408 — wires flow.js's terminal subscribe URL to
the kinds-filter damocles shipped in #499. Only the four kinds
flow.js actually handles get sent on the wire:

- sent / delivered → broker terminal renderer
- container_state_changed / container_removed → local autocomplete
  cache (flowContainers Map used by the @-mention composer)

Everything else (approval_*, question_*, transient_*, tombstones_*,
meta_*, rebuild_queue_*) is now dropped server-side before the
JSON-serialise, instead of being deserialised + dispatched to
flow.js's no-op `_default: () => {}` per frame.

Tabs.js keeps the unfiltered `/dashboard/stream` subscribe since
it routes every mutation kind into its derived stores.

## SharedWorker note

`openStream` keys subscriptions by full URL (#448, #453). The
filtered URL is therefore its own upstream connection to
hive-c0re — when both /index.html and /flow.html are open at the
same time the worker holds two upstreams (one filtered, one full)
instead of today's single shared upstream. The trade is small
per-frame wire-byte + serialise savings vs +1 backend connection
in the both-open case. The flow-page-only case is a pure win.
Acceptable per #408's intent ("split flow messages from main
endpoint").

Round-trip note: if the backend's `kind_tag` strings ever drift
from what's listed here, the affected kind silently never matches
and flow loses that event class. damocles's #499 added a serde
round-trip test against `kind_tag` to guard the backend side;
the frontend mirrors the spelling exactly from the PR body.
2026-05-26 23:02:52 +02:00
iris
a2c5348ca4 dashboard: rename src/app.js → src/tabs.js (closes #406)
#406 was the JS-split refactor: pull shared helpers into
common.js (step 1), pull the flow-only IIFEs into flow.js
(step 2), then rename the legacy combined entry from app.js
to tabs.js (step 3 — this commit) to reflect that the bundle
now owns the dashboard tabs surface only.

What moved:
- `frontend/packages/dashboard/src/app.js` → `tabs.js`
- `build.mjs` entry: `src('app.js')` → `src('tabs.js')`;
  output is now `static/tabs.js`
- `index.html` `<script>` src: `/static/app.js` →
  `/static/tabs.js`; the FL0W-section comment retouched
- `flow.html` reference from `/static/app.js`-as-tabs to
  `/static/tabs.js`; notif + offscreen-inbox comments rewired
  to point at the correct owners (common.js for NOTIF,
  flow.js for renderInbox)
- `common.js`, `flow.js`, `tabs.js` headers: scrub stale
  "app.js" references; document that #406 steps 2 + 3 are
  done and both pages import directly from common.js
- `dashboard.css` comments: every "by app.js" → "by tabs.js"
- `docs/web-ui.md`, `docs/conventions.md`: legacy
  `assets/app.js` → `assets/tabs.js` (the path prefix is
  itself stale from a much-earlier rename, but consistent
  with the rest of those docs)
- `CLAUDE.md` file map: refresh the dashboard package's
  src/ and dist/ layouts to reflect the post-split shape
  (tabs.js, flow.js, common.js, stream-worker.js)

What did NOT move:
- `frontend/packages/agent/src/app.js` (per-agent UI entry)
  stays named `app.js` — it's a separate package, has only
  one entry point, no split happened there
- The two "no-op when target absent" guards in renderContainers
  and renderQuestions are kept as belt-and-suspenders for any
  future page that adds tabs.js without the corresponding
  sections; comments updated to note this rather than the
  pre-split flow.html reason

Functional behaviour identical; this is a pure rename + comment
sweep.
2026-05-26 19:39:27 +02:00
iris
4504f9ede3 dashboard: SharedWorker for SSE multiplexing (closes #448)
mara on #448: "firefox disconnects bc of too many tabs. needs bg
service worker". picked SharedWorker over full Service Worker:
smaller change, addresses the actual problem (shared connection
across tabs), no offline-cache scope creep.

architecture

per-tab `new EventSource('/dashboard/stream')` replaced with a
SharedWorker-backed facade. one SharedWorker instance per origin
holds ONE upstream EventSource and fans every server-sent event
out to every connected tab via MessagePort. N hyperhive tabs now
share ONE backend connection, immune to Firefox's per-tab SSE
throttling under many-open-tabs pressure.

wire protocol (port.postMessage):

  tab → worker
    { kind: 'subscribe',   url: '/dashboard/stream' }
    { kind: 'unsubscribe', url: '/dashboard/stream' }

  worker → tab
    { kind: 'open',    url }
    { kind: 'message', url, data: '<raw SSE data>' }
    { kind: 'error',   url }

subscription tracking is per (port, url). a late subscriber that
joins after the upstream is already OPEN gets a synthetic 'open'
event so its onStreamOpen handler still runs (triggers the
snapshot re-sync that recovers events lost during the join gap).
unsubscribing the last port for a URL closes the upstream
EventSource so we don't leak idle streams.

files

- frontend/packages/dashboard/src/stream-worker.js: new — the
  worker. multi-URL multiplexing via Map<url, {es, ports}>.
- frontend/packages/dashboard/src/common.js: new exported helper
  openStream(url) — returns an EventSource-shaped facade backed
  by the SharedWorker. graceful fallback to direct EventSource
  when SharedWorker is unavailable.
- frontend/packages/dashboard/src/app.js: replaces the inline
  new EventSource('/dashboard/stream') with openStream.
- frontend/packages/dashboard/src/flow.js: passes
  streamFactory: openStream to termCreate so the broker
  terminal's SSE goes through the worker too.
- frontend/packages/shared/src/terminal.js: accepts an optional
  streamFactory(url) option. default unchanged — non-dashboard
  consumers (per-agent UI) keep using direct EventSource.
- frontend/packages/dashboard/build.mjs: new esbuild entry for
  stream-worker.js → dist/static/stream-worker.js (separate
  bundle because SharedWorker scripts run in a different global
  scope and can't be inlined into app.js).

scope kept tight

- per-agent UI's /events/stream stays on direct EventSource. the
  agent UI's tab count per agent is typically 1; SharedWorker
  helps when you have N tabs hitting the SAME stream and the
  per-agent stream URLs differ. if mara wants the agent UI to
  share its workers too it's a separate small PR.
- no offline-cache, no push notifications — those need full
  Service Worker; explicit non-goal here per the design Q.

validation

- npm run build --workspace=@hive/dashboard clean.
- stream-worker.js bundle: 1.8 kb.
- app.js: 154 kb → 158 kb. flow.js: 29.9 kb → 32 kb.
- browser smoke test isn't possible from inside iris's container;
  the EventSource-shaped facade preserves the exact onmessage /
  onopen / onerror surface the existing IIFE consumers use.
2026-05-26 01:26:56 +02:00
iris
918dccfedd flow.js: drop dead window.HiveTerminal (#406 step 2 follow-up)
Per @argus on PR #412: after the common.js step (#410)
`appendText` uses the direct `termLinkify` import and `termCreate`
is called directly in flow.js — nothing reads `window.HiveTerminal`
anywhere. Drop the line + the stale comment alongside.
2026-05-25 02:19:19 +02:00
iris
06c23e0bdc dashboard: extract flow.js as separate /flow.html entry (#406 step 2)
Splits the single bundle into two: `app.js` (entry for /index.html —
tab renderers + tab routing + refreshState) and a new `flow.js`
(entry for /flow.html — operator inbox derived store + inbox pill
flyout + broker terminal + @-mention composer). Both bundles inline
`./common.js` (DOM helpers, Panel, NOTIF, path linkification).

## What `flow.js` owns

- Operator inbox derived store (`operatorInbox`, `INBOX_LIMIT`,
  `inboxAppendFromEvent`, `buildInboxListNode`, `renderInbox`) +
  inbox-pill click wiring
- Broker terminal init (`HiveTerminal.create({ logEl: msgflow, ... })`)
  with `renderMsg`, `pulseBanner`, `msgRowMap` reply-thread indicator,
  and the renderers map for `sent` / `delivered` broker rows
- @-mention composer (`#op-compose-input` IIFE — sticky recipient,
  autocomplete, parseAddressed, /op-send POST)
- A small local `flowContainers` cache for the composer's
  autocomplete, refreshed on cold load + on every SSE reconnect via
  `onStreamOpen`, and live-updated by `container_state_changed` /
  `container_removed` SSE events (the dashboard's `containersState`
  lives in `app.js` and isn't available here)

## What `app.js` no longer does

- Drops the inbox derived store, the bindFlowInboxPill IIFE, the
  broker-terminal IIFE, and the composer IIFE — all moved
- Drops the `renderInbox()` call in `refreshState` (dashboard has
  no #inbox-section element)
- Drops `setTabCount('flow', operatorInbox.length)` — the FL0W tab
  count lives in flow.js now (cross-page count broadcasting is a
  future follow-up; the slot currently stays hidden on /index.html)
- Drops the `window.HiveTerminal` global — the bare-import pattern
  in common.js / flow.js made it unused on the dashboard

## What changes for /flow.html

- `<script src>` switches from `/static/app.js` → `/static/flow.js`
- Mutation events on the dashboard stream (`approval_added`,
  `container_state_changed`, etc.) are silently ignored on /flow.html
  via a `_default: () => {}` renderer (the dashboard tabs aren't on
  this page; firing the legacy applyXxx handlers from here just
  mutated dead stores). #408 follow-up filters this at the SSE level

## Validation

- `npm run build` clean.
- Bundle deltas:
    - `app.js`: 154kb → 135kb (dropped ~19kb of flow code)
    - `flow.js`: NEW 29kb (was bundled into the old 154kb app.js)
    - `flow.html` page total: 154kb → 29kb (flow.js + inlined common,
      no tab renderers shipped)
- Source: `app.js` 2287 → 1907 lines (-380); `flow.js` 423 lines (new)
- No HTML / CSS changes besides the `<script src>` swap on /flow.html.

## Known limitations (out of scope; tracked separately)

- /index.html still has no live SSE subscription — the dashboard
  updates only on cold load + after async-form submits. Pre-existing
  behaviour; the SSE wiring also lived in the flow IIFE before.
  Step 3 of #406 (or its own bug fix) re-wires it.
- /flow.html's `_default: noop` drops mutation events; #408 fixes
  the duplicate-traffic by splitting the SSE endpoint server-side.
- The FL0W tab-strip count pill on /index.html stays hidden — the
  count source is now in flow.js. Broadcast via localStorage /
  BroadcastChannel is a small follow-up if both pages are open.

Browser smoke test isn't possible from inside iris's container.
Worth eyeballing post-deploy:
  - /flow.html: terminal renders broker rows; inbox pill shows count
    + opens flyout; composer autocomplete suggests known agents
    + sends successfully
  - /index.html: tab renderers all work; notification toggle still
    binds; side panel still opens for diffs / file previews / logs
2026-05-25 02:19:19 +02:00