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:
@@ -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.*
|
||||||
Reference in New Issue
Block a user