# 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.*