diff --git a/docs/BOX-UI-DESIGN.md b/docs/BOX-UI-DESIGN.md new file mode 100644 index 0000000..d918500 --- /dev/null +++ b/docs/BOX-UI-DESIGN.md @@ -0,0 +1,536 @@ +# BOX-UI-DESIGN.md: Box Console UI/UX Design + +## Overview + +The box console (`https://box.muse-dev.online/`) is the operator interface for +the NetVM fleet. It is the human face of the Box API (see BOX-API-SPEC.md): +every UI action calls the same REST endpoints agents use. The UI teaches the +API; the API steers what agents can do. + +**Design principle:** "UI for Creativity, API for Steering." The console +surfaces fleet state for exploration and lets operators act quickly, but every +action is a thin client over the API — no hidden functionality, no UI-only +features. Each form shows its API equivalent ("View as curl") so agents +watching the UI learn to automate via the API directly. + +**Audience:** operators (human + fleet agents with ops auth). Not a public +page. Function over polish, but it must be usable at 3am during an incident. + +**Auth:** ops/PIN-gated. Unauthenticated visitors see only the shared PIN login +page. All pages send `Cache-Control: no-store`. All mutating actions are +written to `/srv/box/audit.jsonl`. + +--- + +## Tech Decision + +**Server-rendered pages + unified theme engine, with vanilla-JS progressive +enhancement. Not a separate SPA.** + +Rationale: + +1. **Auth model fits SSR.** Ops auth is a PIN session cookie (or Bearer for + agents). Server rendering keeps the session server-side; a SPA would need + to manage tokens in JS, a worse posture for an ops console. + +2. **No build step.** The front-door fleet deploys via `publish.sh` tarballs. + A JS framework adds a build pipeline, node_modules, and version drift + across machines. Server-rendered HTML + a single `box.js` (~200 lines of + vanilla JS) deploys as static files. + +3. **Matches the existing estate.** The ops console, net page, and board all + use the unified theme engine (`bin/make-theme.py` injects it; four modes: + dark/light/sepia/contrast persisted in cookie + localStorage). The box + console should look and feel like the rest of the operator tooling — + one visual language, one set of expectations. + +4. **Live updates don't need a framework.** The dynamic parts are status + polling (timers, fleet health, pending confirmations). A 15s + `fetch()` + DOM patch loop covers it. No virtual DOM needed for tables + that change every few minutes. + +5. **Agents consume the API, not the DOM.** The DOM-mapping work (DOM-*.md) + exists because agents drive muse.ai through a browser. The box console + is the opposite: agents call the API directly. The UI's DOM needs no + testid contract — but keeping selectors sane costs nothing. + +**Page structure:** one HTML shell per section (dashboard, timers, jobs, dms, +board, requests, audit), server-rendered with current data, plus `box.js` +for polling refresh, form validation, and the "View as curl" toggles. +All API calls from the UI use the session cookie; agents use Bearer. + +--- + +## Global Layout + +Every page shares the same shell: + +``` ++------------------------------------------------------------------+ +| BOX box.muse-dev.online [theme: ◐] [logout] | ++--------+---------------------------------------------------------+ +| | | +| Dash |
| +| Timers | | +| Jobs | | +| DMs | | +| Board | | +| Reqs | | +| Audit | | +| | | ++--------+---------------------------------------------------------+ +| fleet: ●muse ●pip ●646 ●opm pending: 2 updated 12s ago | ++------------------------------------------------------------------+ +``` + +- **Top bar:** product mark, hostname, theme toggle (◐ cycles + dark/light/sepia/contrast), logout. +- **Sidebar nav:** Dashboard, Timers, Jobs, DMs, Board & Chat, Requests, + Audit Log. Active section highlighted. Badge counts on Requests (pending) + and DMs (unverified). +- **Status bar (footer):** per-agent health dots (green/amber/red, from the + health ingest), pending-confirmation count, "updated Ns ago" freshness. + This is the at-a-glance fleet pulse — visible on every page. +- **"API" affordance:** every form and table has a small `` toggle that + reveals the exact API call (curl) the UI is about to make / just made. + This is the steering mechanism made visible. + +--- + +## 1. Dashboard + +**Purpose:** fleet pulse. "Is everything alive, and what happened recently?" + +``` ++-------------------+-------------------+-------------------+ +| AGENTS [refresh] | ++-------------------+-------------------+-------------------+ +| muse ● healthy | pip ● healthy | 646 ● healthy | +| CDP 9410 12ms | CDP 9420 9ms | CDP 9430 14ms | +| chrome up 3d 4h | chrome up 3d 4h | chrome up 2d 1h | +| last dm 2m ago | last dm 2m ago | last dm 5m ago | ++-------------------+-------------------+-------------------+ +| opm ● healthy | JOB SYSTEM | +| CDP 9440 11ms | timers: 4 active | +| chrome up 1h 12m | jobs: 6 defined | +| last dm 40s ago | pending reqs: 2 | ++-------------------+-------------------+-------------------+ ++---------------------------------------------------------+ +| RECENT JOB RUNS [view all -> Jobs]| ++---------------------------------------------------------+ +| 03:55 heartbeat opm sidechat dispatched 12s | +| 03:50 board-watch muse main dispatched 8s | +| 03:45 heartbeat opm sidechat dispatched 11s | +| 03:40 canary-test opm main FAILED timeout | ++---------------------------------------------------------+ +| PENDING CONFIRMATIONS [view all -> Reqs]| ++---------------------------------------------------------+ +| a1b2c3d4 timer create job-foo pending 2m [nudge] | +| e5f6a7b8 job trigger board-watch confirmed 1m ✓ | ++---------------------------------------------------------+ +| RECENT ALERTS [view all -> Audit]| ++---------------------------------------------------------+ +| 03:52 WARN opm chrome restarted by watchdog | +| 03:31 INFO sidechat reuse mapped heartbeat-opm -> uuid | ++---------------------------------------------------------+ +``` + +**Data sources:** +- Agent cards: `GET /api/box/activity` (existing), CDP latency via + `muse-chat-api.py` probe, chrome uptime from `agent-health.sh` state. +- Recent job runs: tail of `job-log.jsonl` (types `job_dispatched`, + `job_sent`, failures). +- Pending confirmations: `GET /api/box/requests?status=pending`. +- Alerts: `audit.jsonl` filtered to WARN+. + +**Interactions:** +- Each agent card links to that agent's DM composer (prefilled `to:`). +- "nudge" re-sends the `[REQ id]` DM (counts as an attempt). +- Clicking a job run jumps to Jobs → run history filtered to that job. + +--- + +## 2. Timers + +**Purpose:** manage systemd timers = the fleet's heartbeat. + +``` ++---------------------------------------------------------+ +| TIMERS [+ New timer] | ++---------------------------------------------------------+ +| name schedule agent active next run last| ++---------------------------------------------------------+ +| job-heartbeat */5 * * * * opm ● 04:00 ok 12s| +| job-board-watch */5 * * * * muse ● 04:00 ok 8s | +| job-accts-health */15 * * * * opm ● 04:05 ok 30s| +| job-canary 0 * * * * opm ○ -- -- | ++---------------------------------------------------------+ +| [trigger] [start] [stop] [enable] [disable] [delete] | +| selected: job-heartbeat | ++---------------------------------------------------------+ +| API: GET /api/box/timers [ copy]| ++---------------------------------------------------------+ +``` + +**Row actions** (per selected timer): Trigger now, Start, Stop, Enable, +Disable, Delete (with confirm modal: "Delete job-heartbeat.timer? This stops +the 5-minute heartbeat. [Keep job YAML] [Delete]"). + +**New timer form** (slide-over panel): + +``` ++---------------------------------------------------------+ +| NEW TIMER [x] | ++---------------------------------------------------------+ +| name: [board-watch____________] ^[a-z0-9-]+$ | +| schedule: [*/5 * * * *___________] (cron expr) | +| scheduler:(•) systemd ( ) cron ( ) at | +| agent: [muse ▾] | +| timeout: [300____] seconds | +| sidechat: [ ] create reuse key: [__________] | +| prompt template: | +| +-----------------------------------------------------+ | +| | Check the board for new posts... | | +| | {job_id} will be substituted | | +| +-----------------------------------------------------+ | +| [Validate] [Create timer] | ++---------------------------------------------------------+ +| API equivalent: [ copy] | +| curl -X POST -H "Authorization: Bearer $TOKEN" \ | +| -H "Content-Type: application/yaml" \ | +| --data-binary @job.yaml \ | +| https://box.muse-dev.online/api/box/timers | ++---------------------------------------------------------+ +``` + +**Behaviors:** +- `Validate` dry-runs the YAML schema check server-side and shows + errors inline (missing fields, bad cron, unknown agent) before create. +- `Create timer` → `POST /api/box/timers` → response includes `request_id`; + the UI polls `GET /api/box/requests/{id}` and shows the confirmation + state inline ("pending → confirmed ✓" or "failed: …"). +- Delete of a timer with an active schedule requires typing the timer name + (destructive-action guard). +- The ` copy` button copies the curl to clipboard. This is the + "UI teaches API" loop made concrete. + +--- + +## 3. Jobs + +**Purpose:** job definitions + run history. Timers are *when*; jobs are *what*. + +Two tabs: **Definitions** | **Run history**. + +### Definitions + +``` ++---------------------------------------------------------+ +| JOB DEFINITIONS [+ New job] | ++---------------------------------------------------------+ +| name agent schedule timer sidechat [view] | ++---------------------------------------------------------+ +| heartbeat opm */5 * * * * ● heartbeat-opm | +| board-watch muse */5 * * * * ● - | +| refine-dm opm - ○ refine-{date} | ++---------------------------------------------------------+ +``` + +Clicking a row opens the definition viewer: rendered YAML with syntax +highlight, the parsed fields (schedule, agent, prompt_template with +`{variables}` highlighted, timeout, sidechat config), plus "Edit" (goes +through the timer create flow as an update) and "Trigger now". + +### Run history + +Sourced from `job-log.jsonl`, newest first, filterable: + +``` ++---------------------------------------------------------+ +| RUN HISTORY [job: all ▾] [type: all ▾] [search ___] | ++---------------------------------------------------------+ +| time job_id event detail| ++---------------------------------------------------------+ +| 03:55 heartbeat-...-63b57a71 job_dispatched | +| 03:55 heartbeat-...-63b57a71 job_sent sidechat| +| 03:55 heartbeat-...-63b57a71 job_sidechat_mapped| +| 03:50 board-watch-...-a1b2c3 job_dispatched | +| 03:45 heartbeat-...-99d32faf job_sidechat_created| ++---------------------------------------------------------+ +| < prev page 1 of 42 next > | ++---------------------------------------------------------+ +``` + +- Event types color-coded: dispatched (blue), sent (green), created/mapped + (teal), failures/timeouts (red). +- Clicking a `job_id` filters to that run's full event chain — this is how + you trace "what did the 03:55 heartbeat actually do." +- Search box greps the raw JSONL (for digging into odd payloads). + +--- + +## 4. DMs + +**Purpose:** compose DMs to operators + inspect the DM log. The DM system is +the fleet's nervous system; this is its console. + +### Composer (left) + Log (right) + +``` ++---------------------------+--------------------------------+ +| COMPOSE DM | DM LOG [to: all ▾][type: all ▾] | ++---------------------------+--------------------------------+ +| to: [muse ▾] | 03:54 d665dc43 → muse SENT ✓ | +| target: (•) main ( ) sidechat | 03:54 268ef823 → pip SENT ✓ | +| sidechat:[____________] | 03:54 4a4031e8 → 646 SENT ✓ | +| type: [chat ▾] | 03:54 a9f5161f ← 646 RECV ✓ | +| | "ACK - received [4a4031e8]" | +| message: | 03:53 fd553ef5 → muse SENT ✓ | +| +-----------------------+ | | +| | | | click row → full detail: | +| | | | text, metadata, signature | +| +-----------------------+ | verification, timestamps | +| metadata (JSON, opt): | | +| +-----------------------+ | | +| | {"priority":"high"} | | | +| +-----------------------+ | | +| [Send DM] | | ++---------------------------+--------------------------------+ +| API: POST /api/box/dms [ copy] | ++--------------------------------------------------------+ +``` + +**Behaviors:** +- `type` select: `chat` | `job` | `alert` | `canary` (from the DM type + table). Choosing `job` reveals a `job_id` field; the UI prepends the + `[JOB {id}]` wire format automatically. +- Target `sidechat` reveals a name/UUID field; the UI validates it against + `GET /api/box/chat/rooms?agent=`. +- Send → `POST /api/box/dms` → the response's `request_id`/DM id appears + in the log immediately with status `SENT`, flipping to `VERIFIED ✓` + when recipient-side read-back confirms (poll `GET /api/box/dms?limit=`). +- **Signature indicator:** each log row shows signature verification + state (✓ verified / ⚠ unverified / ✗ failed). Unsigned or + bad-signature DMs are visually distinct — trust is visible. +- Clicking a row expands: full text, metadata JSON, signature block + (truncated), timestamps (sent / verified), and the confirmation chain + (`[REQ]` → `[CONFIRM]`) if applicable. +- Character/field guidance: the board's 500-char limit does not apply to + DMs, but the UI warns past ~2000 chars (read-back reliability). + +--- + +## 5. Board & Chat + +**Purpose:** post to shared surfaces (board channels, chatrooms) + the +unified message API made visible. + +### Board composer + +``` ++---------------------------------------------------------+ +| POST TO BOARD | ++---------------------------------------------------------+ +| channel: [#lobby ▾] (#lobby, #operators, #jobs) | +| | +| message (500 max): | +| +-----------------------------------------------------+ | +| | Job board-watch completed: 3 new posts. | | +| +-----------------------------------------------------+ | +| 42 / 500 | +| [Post to #lobby] | ++---------------------------------------------------------+ +| API: POST /api/box/board/post [ copy] | ++---------------------------------------------------------+ +| RECENT POSTS — #lobby [channel: ▾] | ++---------------------------------------------------------+ +| #1234 operator-main 03:52 Job board-watch completed… | +| #1233 opm-agent 03:47 Heartbeat sidechat mapped… | ++---------------------------------------------------------+ +``` + +- Live character counter (board enforces 500 chars server-side; the UI + prevents wasted submits). +- Recent posts per channel below the composer, auto-refreshing. + +### Chatroom sender + unified message + +``` ++---------------------------------------------------------+ +| SEND TO CHATROOM | ++---------------------------------------------------------+ +| room: [main ▾] (main + sidechats for selected agent) | +| agent: [opm ▾] | +| message: [________________________________] [Send] | ++---------------------------------------------------------+ +| UNIFIED MESSAGE (advanced) | +| target: [dm:muse___________] e.g. board:#lobby, | +| chat:main, chat:, dm: | +| message: [________________________________] [Send] | +| Routes via POST /api/box/message — one endpoint, | +| the server picks the transport. [ copy] | ++---------------------------------------------------------+ +``` + +The unified sender is the power tool: one target syntax, all surfaces. +It exists in the UI so operators learn the `target:` grammar, then use it +from jobs and agents via API. + +--- + +## 6. Requests (Confirmations) + +**Purpose:** the confirmation system made visible. Every side-effecting API +call lands here as a tracked request. + +``` ++---------------------------------------------------------+ +| REQUESTS [status: all ▾] [sweep] | ++---------------------------------------------------------+ +| id action status age attempts | ++---------------------------------------------------------+ +| a1b2c3d4 timer create job-foo PENDING 2m 1/3 [nudge]| +| e5f6a7b8 job trigger board-… ✓ confirmed 1m 1/3 | +| c9d0e1f2 timer delete job-old ✗ FAILED 9m 3/3 [retry]| +| b7c8d9e0 dm send → muse ⏱ TIMEOUT 12m 3/3 [retry]| ++---------------------------------------------------------+ +| [sweep] runs the pending-request sweeper now (normally | +| every minute): retries or times out stale pendings. | ++---------------------------------------------------------+ +``` + +- Status pills: `PENDING` (amber, pulsing), `✓ confirmed` (green), + `✗ FAILED` (red), `⏱ TIMEOUT` (gray). +- `nudge` re-sends the `[REQ id]` DM without resetting the attempt count. +- `retry` on failed/timeout re-issues the original API call as a new + request (linked to the old one in the audit log). +- Clicking a row shows the full chain: API call → DM sent → attempts → + `[CONFIRM]` text or failure reason. + +This page is the answer to "did it actually work?" — the UI never claims +success on send alone. + +--- + +## 7. Audit Log + +**Purpose:** append-only truth. Who did what, when. + +``` ++---------------------------------------------------------+ +| AUDIT LOG [actor: all ▾] [action: all ▾] | ++---------------------------------------------------------+ +| time actor action target result | ++---------------------------------------------------------+ +| 03:55 operator-main box.timer.trigger job-heartbeat ok | +| 03:54 box-api dm.send → 646 ok | +| 03:52 watchdog chrome.restart opm ok | +| 03:50 operator-646 box.timer.create job-foo denied | ++---------------------------------------------------------+ +``` + +- Sourced from `/srv/box/audit.jsonl`. Filter by actor and action class. +- Denied actions (auth failures, validation rejections) shown in red — + the audit log is also the intrusion-detection surface. +- Export button: download the filtered range as JSONL. + +--- + +## Cross-Cutting UX Rules + +1. **Every mutation shows its API call.** The `` toggle is on all forms + and destructive buttons. Copy-to-clipboard included. This is the + steering mechanism; it must be impossible to miss but easy to ignore + once learned. + +2. **Never claim success on send.** Any action that triggers a DM shows + the request in `PENDING` state until `[CONFIRM]` arrives. Buttons that + would previously say "Done" now say "Requested — tracking #a1b2c3d4" + with a link to Requests. + +3. **Destructive actions need friction.** Delete timer / disable heartbeat: + type-the-name confirmation. Trigger-now and nudge: single click (they're + recoverable). The friction scales with blast radius. + +4. **Freshness is truth.** Every auto-refreshed region shows "updated Ns + ago". Stale data (>60s without refresh) gets an amber banner, not + silent staleness. (Same rule as the health system: freshness is truth.) + +5. **Errors teach the API.** An API error in the UI renders as: + `400 INVALID_YAML: missing field 'schedule'.` + `Try: curl …` with the + failing call prefilled. Operators learn the API by breaking things + safely. + +6. **Mobile is read-only-ish.** The layout collapses to single column; + tables become card lists. Mutating actions remain available but require + the same confirmations. No separate mobile app — the PIN-gated page + works in any browser. + +7. **Keyboard:** `g d/t/j/m/b/r/a` jumps to sections (Dashboard, Timers, + Jobs, DMs, Board, Requests, Audit). `?` shows the shortcut overlay. + Operators live in this console; let them move fast. + +--- + +## Information Architecture Summary + +``` +/ (dashboard) fleet pulse: agents, jobs, confirmations, alerts +/timers timer table + new/edit slide-over + per-timer actions +/jobs definitions tab + run-history tab (job-log.jsonl) +/dms composer + signed DM log with verification states +/board board composer + recent posts + chatroom sender + + unified message sender +/requests confirmation tracking: pending/confirmed/failed/timeout +/audit append-only audit log with filters + export +``` + +All under `https://box.muse-dev.online/`, all ops-gated, all `no-store`. + +--- + +## Implementation Notes (for the builder) + +- **Backend first:** the Box API endpoints in BOX-API-SPEC.md don't exist + yet. The UI is a thin client; build the API, then the pages are mostly + `fetch()` + templates. Suggested order: timers → jobs → dms → + board/chat → requests → audit → dashboard (dashboard aggregates the rest). +- **Reuse existing pieces:** the board server on the VM already serves the + box host branch and `_box_api`; extend it rather than standing up a new + service. It already proxies to bl over SSH — the Box API's SSH command + mapping slots into the same path. +- **job-log.jsonl / dm-log.jsonl / audit.jsonl** live on bl (`bl`) and the + VM (`/srv/box/`) respectively — the API needs read paths to both. Decide + early which logs the box server tails directly vs. proxies. +- **Rate limits visible:** the API's per-agent limits (10 timer ops/min, + 10 board posts/min) should surface in the UI as disabled buttons with + "rate limited, retry in Ns" rather than silent 429s. +- **The confirmation sweeper** (every minute) is a server-side job, not UI + logic. The UI's `[sweep]` button just triggers it on demand. +- **Don't build the SPA.** If a future need genuinely outgrows SSR + (realtime collaboration, complex state), revisit — but the current scope + (tables, forms, polling) is squarely SSR-shaped. + +--- + +## Open Questions + +1. **Job YAML vs JSON:** the dispatcher on bl reads `jobs/*.json` (no + PyYAML on bl), but the spec writes YAML. Does the box API accept YAML + and convert, or should the spec move to JSON? The New Timer form should + match whatever the API actually validates. +2. **Timer/job naming:** `job-.timer` vs bare names — pick one and + make the UI + API agree. +3. **Who owns the DM log UI?** `dm-log.jsonl` is on bl; `/srv/box/dm-log.jsonl` + is on the VM. Unify or proxy? +4. **Agent-facing vs human-facing auth on the same endpoints:** Bearer for + agents, cookie for humans — already specced, but the rate limits and + audit attribution need to distinguish them cleanly. +5. **Should `trigger now` require confirmation?** It's side-effecting + (runs a job). Spec says yes. The UI should make this feel instant + anyway: optimistic "triggered" + live request tracking. + +--- + +*Design v0.1. Owner: operator-main. For implementation, start with the API +(BOX-API-SPEC.md), then build pages in the order listed above.*