Files
box/docs/BOX-UI-DESIGN.md

24 KiB

BOX-UI-DESIGN.md: Box Console UI/UX Design

Box is the main surface. All operator work goes through Box (box.muse-dev.online). The web UI, box CLI, and agents share the same API endpoints. No UI-only powers.

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   |  <section content>                                     |
| 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=<to>.
  • 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:<sidechat>, dm:<agent>          |
| 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-<name>.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.