Box UI design: operator dashboard and management pages\n\n- 7 sections: dashboard, timers, jobs, DMs, board/chat, requests, audit\n- Server-rendered + theme engine (not SPA)\n- Every mutation shows its API equivalent (steering made visible)

This commit is contained in:
operator-main
2026-10-04 03:58:36 +00:00
parent ef8e98abf0
commit 458314cbf6
+536
View File
@@ -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 | <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.*