@crewhaus/hangar-ui 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +309 -0
  2. package/assets/hangar.css +1726 -0
  3. package/assets/index.html +21 -0
  4. package/assets/js/api.js +368 -0
  5. package/assets/js/app.js +734 -0
  6. package/assets/js/dom.js +228 -0
  7. package/assets/js/keys.js +159 -0
  8. package/assets/js/markdown.js +131 -0
  9. package/assets/js/notify.js +152 -0
  10. package/assets/js/omnibox.js +307 -0
  11. package/assets/js/router.js +280 -0
  12. package/assets/js/routes.js +793 -0
  13. package/assets/js/shapes.js +36 -0
  14. package/assets/js/supervision.js +1334 -0
  15. package/assets/js/util.js +426 -0
  16. package/assets/js/views/activity.js +140 -0
  17. package/assets/js/views/approvals.js +155 -0
  18. package/assets/js/views/channels.js +455 -0
  19. package/assets/js/views/control.js +253 -0
  20. package/assets/js/views/costs.js +103 -0
  21. package/assets/js/views/creds.js +610 -0
  22. package/assets/js/views/data.js +456 -0
  23. package/assets/js/views/deploy.js +106 -0
  24. package/assets/js/views/evals-lab.js +1029 -0
  25. package/assets/js/views/evals.js +342 -0
  26. package/assets/js/views/feedback.js +555 -0
  27. package/assets/js/views/health.js +176 -0
  28. package/assets/js/views/inbox.js +103 -0
  29. package/assets/js/views/inspect.js +751 -0
  30. package/assets/js/views/jobs.js +110 -0
  31. package/assets/js/views/library.js +605 -0
  32. package/assets/js/views/memory-fabric.js +2026 -0
  33. package/assets/js/views/memory.js +228 -0
  34. package/assets/js/views/onboarding.js +253 -0
  35. package/assets/js/views/overview.js +196 -0
  36. package/assets/js/views/panes.js +132 -0
  37. package/assets/js/views/proc.js +280 -0
  38. package/assets/js/views/review.js +164 -0
  39. package/assets/js/views/runs.js +452 -0
  40. package/assets/js/views/runtime.js +450 -0
  41. package/assets/js/views/schedulers.js +133 -0
  42. package/assets/js/views/security.js +925 -0
  43. package/assets/js/views/sessions.js +265 -0
  44. package/assets/js/views/settings.js +489 -0
  45. package/assets/js/views/spec-edit.js +1289 -0
  46. package/assets/js/views/spec.js +66 -0
  47. package/assets/js/views/thredz.js +1853 -0
  48. package/assets/js/views/token.js +49 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.js +145 -0
  51. package/package.json +36 -0
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # @crewhaus/hangar-ui
2
+
3
+ The Hangar console's static UI — the browser side of the harness manager.
4
+ One HTML shell, one CSS file, and a set of hand-written browser ES modules,
5
+ embedded as text and exported as a serve-path map for the hangar server to
6
+ host.
7
+
8
+ M1 was read-only over harness state. **M2 makes the console a driver**: it
9
+ starts, stops, restarts and drains supervised harnesses, watches a run live,
10
+ pokes scheduler lanes through `crewhaus.control.v1`, and settles parked
11
+ approvals and review items — and every one of those actions shows the exact
12
+ CLI command it runs. **M3 adds the detail surface**: eight more harness tabs
13
+ and three more fleet screens over a 178-route contract, covering the spec's
14
+ write side, the memory fabric's write side, the eval/dataset/feedback loops,
15
+ credentials + channels + security, Thredz, and the raw inspectors.
16
+
17
+ ## Zero-build by design
18
+
19
+ There is no build step, no framework, and no npm dependency. The `assets/`
20
+ tree IS the app: plain ES modules loaded by the browser exactly as checked
21
+ in. `src/index.ts` imports every asset `with { type: "text" }` — static
22
+ imports only, because `bun build --compile` embeds only what the import
23
+ graph references statically; a runtime `readFileSync` of a
24
+ package-relative path resolves inside the compiled binary's virtual
25
+ filesystem where the files were never placed, and bricks the CLI at boot.
26
+ A unit test enforces that the embed map and the `assets/` tree stay in
27
+ lockstep (every file present, every relative import resolving inside the
28
+ map, every module transpiler-clean), so the zero-build path still has a
29
+ compile-shaped safety net.
30
+
31
+ ## Exported API
32
+
33
+ ```ts
34
+ import { hangarAssets, CONTENT_TYPES, contentTypeFor } from "@crewhaus/hangar-ui";
35
+
36
+ hangarAssets; // Readonly<Record<string, { body: string; contentType: string }>>
37
+ contentTypeFor("/assets/js/app.js"); // "text/javascript; charset=utf-8"
38
+ ```
39
+
40
+ `hangarAssets` is keyed by serve path — `"/"` is the SPA shell (the app is
41
+ a hash router, so `/` is the only HTML route) and everything else lives
42
+ under `/assets/…` exactly as laid out on disk. Hand the map to the hangar
43
+ server's static-asset option; a server route for `path` responds with
44
+ `body` and `Content-Type: contentType`.
45
+
46
+ ## The app
47
+
48
+ - **Auth bootstrap** — reads `#t=<token>` from the URL fragment into
49
+ sessionStorage (and strips it via `history.replaceState`), then sends
50
+ `Authorization: Bearer` on every request. Fragments never reach server
51
+ logs; no cookies means no CSRF surface. Any 401 swaps in a token-paste
52
+ screen that points at the token file (`~/.crewhaus/hangar/token` by
53
+ default). The live run feed is hand-rolled over `fetch` for the same
54
+ reason `EventSource` cannot be used: it carries no bearer header.
55
+ - **Hash router, deep-linkable** — `#/` (Library), the fleet screens
56
+ `#/{runs, approvals, review, activity, credentials, feedback, thredz}`,
57
+ `#/h/<id>` (Overview), and `#/h/<id>/{spec, runs[/<runId>], schedulers,
58
+ sessions[/<sess>], evals[/<runId>[/<sampleId>]], memory[/wiki/<slug>],
59
+ data, feedback, costs, creds, channels, security, thredz, deploy, inspect,
60
+ dev}`. The eight M3 tabs capture their trailing segments GENERICALLY
61
+ (`M3_TABS` → a `rest` array), so a sub-screen inside one of them never
62
+ needs a new `parseRoute` case.
63
+ - **Library** — dense sortable table (name + dir tail, shape badge, model,
64
+ supervision state + parked-approval count, eval health dot + text,
65
+ sessions, spend 7d, capability badges, group chips); a sub-rail of stored
66
+ groups plus client-computed smart groups (Failing evals, Unbudgeted, Has
67
+ Thredz, Recently active, Ungrouped, Missing); missing-dir cards with
68
+ relocate/remove; Scan and Add-harness actions; a one-line fleet rollup.
69
+ - **Detail** — Overview (header with click-to-copy dir and capability
70
+ chips, health checklist, eval-trend SVG, memory mini-cards, cost
71
+ mini-chart, expandable preflight report), Spec (masked YAML with line numbers,
72
+ env-ref presence checklist, parse issues), Sessions (TTL countdowns,
73
+ evicted rows fall through to summaries, per-kind transcript rendering
74
+ with a metadata gutter and a Raw toggle), Evals (history with
75
+ flaky/partial/replayed/baseline badges, run + sample drill-down),
76
+ Memory (facts with tombstones folded, wiki with a minimal safe-subset
77
+ markdown reader, continuity, dream, watchme), Costs (per-model table +
78
+ 7-day bars).
79
+
80
+ ### M2 — the driving surfaces
81
+
82
+ - **Runs & daemons** (`#/runs`) — every registered harness's supervision
83
+ state in one board: state pill, pid, uptime, adopted badge, restarts in
84
+ the window with the next-restart countdown, last exit + failure class,
85
+ control availability. Row actions Start / Stop / Restart / Drain, each
86
+ disabled WITH ITS REASON; a blocked start opens the refusal modal. Under
87
+ it: the global job queue (running / pending / interrupted) and a failure
88
+ board grouping the fleet by how it died ("3 harnesses exited 31 —
89
+ provider funding").
90
+ - **Run console** (`#/h/<id>/runs/<runId>`) — the live SSE feed rendered in
91
+ the M1 transcript vocabulary, with interleaved (scrubbed) prose, a
92
+ stats/cost HUD folded from the frames, and the terminal exit banner with
93
+ its failure class and remediation. The `replay`-first / `done`-last
94
+ grammar means a finished run and a live one take the same code path.
95
+ - **Schedulers** (`#/h/<id>/schedulers`) — the four-lane timeline with
96
+ cadence + its source, last outcome, and next due — or an honest "cadence
97
+ only" naming why the phase is unknown. Wake respects `pokeable`; a
98
+ `tick_in_flight` refusal retries, a `draining` one does not.
99
+ - **Approvals** (`#/approvals`) and **Review** (`#/review`) — cross-harness
100
+ triage with the verbatim (masked) tool input, keyboard-first (j/k to
101
+ move, one key per verdict, `.` row menu, `?` bindings). Review offers
102
+ thumbs only where they can land on a session turn.
103
+ - **Activity** (`#/activity`) — what changed since yesterday, grouped by
104
+ kind with per-group filters, every row deep-linking into the screen that
105
+ owns it.
106
+ - **Deployments** (`#/h/<id>/deploy`) — the records with class (local /
107
+ PaaS / cf-worker) and health, and an honest empty state naming the file
108
+ that will hold them.
109
+ - **Action faces on the M1 screens** — session retention pin, eval baseline
110
+ re-pin, dream "Run now" (through the job queue), plus the supervision
111
+ pill, bundle-freshness badge and control availability on the detail
112
+ header, where a plan that cannot be built renders its remedy as a button.
113
+
114
+ ### M3 — the detail surface
115
+
116
+ Eleven new view modules (`views/{spec-edit, memory-fabric, evals-lab, data,
117
+ feedback, creds, channels, security, thredz, inspect, runtime}.js`) draw
118
+ eight new harness tabs and three new fleet screens:
119
+
120
+ - **Spec** (`#/h/<id>/spec`) gains the write side — the trust-tier table
121
+ (auto-tunable rows edit inline; human-owned rows demand the typed spec-name
122
+ confirm or route to `crewhaus propose`), the diff interstitial, version
123
+ history with pin/rollback, and the builders (templates, graders, dataset,
124
+ MCP connectors).
125
+ - **Memory** (`#/h/<id>/memory`) gains the fabric's write side — facts with
126
+ forget/sweep (tombstones, never a delete), continuity trash + restore, the
127
+ wiki editor with `expectedVersion` and its stale retry, watch-me analytics,
128
+ reports, intents and the synthesize review.
129
+ - **Datasets** (`#/h/<id>/data`), **Feedback** (`#/h/<id>/feedback`) and the
130
+ eval lab inside **Evals** cover the quality loops: registry + hygiene +
131
+ growth, the fewshot/faq/lessons/advice feeds, and the matrix / trends /
132
+ judge / graders / coverage / sentinel screens.
133
+ - **Credentials**, **Channels** and **Security** (`#/h/<id>/{creds,channels,
134
+ security}`) show env-key PRESENCE (never a value), the channel doctor and
135
+ provisioning, and the egress / PII / compliance / on-chain / retention
136
+ panels.
137
+ - **Thredz** (`#/h/<id>/thredz`) is proxied server-side — the workspace key
138
+ never reaches the browser.
139
+ - **Inspect** (`#/h/<id>/inspect`) is the raw store browser, and **Dev & MCP**
140
+ (`#/h/<id>/dev`) drives the dev server and the MCP servers.
141
+ - Fleet-wide: **Credentials** (`#/credentials`, the provider × harness
142
+ matrix), **Feedback** (`#/feedback`) and **Thredz** (`#/thredz`).
143
+
144
+ Two conventions make that surface cheap to extend:
145
+
146
+ 1. **The client wrappers are GENERATED.** `routes.js` carries all 178 M3
147
+ routes as pure data tagged with a `group`, and `api.js` builds one wrapper
148
+ per key from the map rather than hand-writing them. A hand-written name
149
+ WINS a collision, so an M1/M2 verb keeps its bespoke wrapper.
150
+ 2. **Every M3 read answers `{present, note, verb, …}`** — is there anything
151
+ to show, why is it empty, and which CLI verb creates it. Most of this
152
+ surface is normally empty on a given harness, so the views render that
153
+ triple directly through `emptyState(message, verb)`; an empty panel is
154
+ never indistinguishable from a broken one.
155
+
156
+ ### UX invariants
157
+
158
+ Dark theme default with an explicit light toggle; traffic-light dots are
159
+ always paired with text; every empty state names the CLI verb that creates
160
+ its data; 404s render as "nothing yet", never as errors; loading skeletons
161
+ everywhere; cached figures show their as-of time; the footer shows the
162
+ hangar version + wire-protocol pair. No animation beyond opacity.
163
+
164
+ Three more that M2 adds, because a console that drives things can lie in
165
+ new ways:
166
+
167
+ 1. **Every action shows its CLI twin** — the exact command, with a copy
168
+ button. The CLI and the console drive the same harness-local state tree,
169
+ so the twin is both a trust affordance and a fallback. Where no CLI verb
170
+ exists yet (the retention pin), the UI says so instead of inventing one.
171
+ 2. **A disabled control always says why.** `expected: true` control
172
+ refusals (`no_control_port`, `lane_not_armed`, `draining`) render
173
+ disabled-with-reason, never as an error toast.
174
+ 3. **Say which answer you got.** Bundle freshness distinguishes the
175
+ hash-exact verdict from the mtime approximation; a last exit read off
176
+ the run ledger rather than observed by this manager is badged as such.
177
+
178
+ ### Injection safety
179
+
180
+ Rendering is 100% `createElement`/`textContent` via a tiny `el()`/`text()`
181
+ toolkit — markup-string assignment is banned and a unit test scans every
182
+ embedded module for it. The wiki renderer parses a minimal markdown subset
183
+ (headings, bold, inline code, lists, fenced code) into tokens and renders
184
+ them as text nodes; raw HTML in an article body stays literal text.
185
+
186
+ ## Server routes consumed
187
+
188
+ This package is the client of record for the whole route surface, and the
189
+ contract is EXECUTABLE: `assets/js/routes.js` holds every route as pure
190
+ data (method, path template, body-shape name), `api.js` builds every
191
+ request from that map, and the hangar server's `contract.test.ts` imports
192
+ the same map and drives each route against a live fixture server — writes
193
+ must take effect, reads must carry every field the views dereference. A
194
+ unit test here drives every `api.js` wrapper against the same map, so the
195
+ client cannot drift from it either. The two sides cannot drift silently.
196
+
197
+ Reads: `/api/harnesses[?hydrate=1]`, `/api/registry/groups`, `/api/h/:id`,
198
+ and per-harness `spec`, `preflight`, `sessions[/:sess]`,
199
+ `evals[/:runId[/:sampleId]]`,
200
+ `memory/{facts,wiki[/:slug],state,dream,watchme}`, `costs`, plus
201
+ `/api/version` — and for M2 `proc`, `runs[/:runId]`,
202
+ `runs/:runId/events` (SSE), `control/status`, `schedulers`, `deployments`,
203
+ `/api/{approvals,review,activity,jobs}`. Writes: registry CRUD
204
+ (`POST /api/harnesses` `{dir}`, `POST /api/scan`,
205
+ `POST /api/registry/groups` `{name}`, `POST /api/registry/scan-roots`
206
+ `{dir}`, `PUT /api/h/:id/{groups,tags,pin,notes}`,
207
+ `POST /api/h/:id/relocate` `{newDir}`, `DELETE /api/h/:id`) plus the M2
208
+ verbs: `POST /api/h/:id/proc/{start,stop,restart,drain}`,
209
+ `POST /api/h/:id/control/{wake,drain}`,
210
+ `POST /api/h/:id/approvals/:apprId/{grant,deny}`,
211
+ `POST /api/h/:id/review/:itemId`, `POST /api/h/:id/jobs`,
212
+ `POST /api/h/:id/sessions/:sess/pin`, `POST /api/h/:id/evals/baseline`.
213
+
214
+ M3 adds 178 more, grouped `spec | memory | evals | data | feedback | creds |
215
+ channels | security | thredz | inspect | runtime` — enumerated in
216
+ `assets/js/routes.js` and mirrored route-for-route by the server's
217
+ `M3_ROUTES`, which the contract test asserts is the same set (key, method and
218
+ path). They are not re-listed here: the map is the inventory, and a README
219
+ copy of it would be the thing that goes stale.
220
+
221
+ All payload readers are tolerant: unknown fields are ignored, missing
222
+ fields degrade to "—", and unknown transcript/trace kinds still render. A
223
+ failed write is never silent — non-2xx surfaces as a toast, and a zero-root
224
+ Scan says "no scan roots configured" and offers the add-scan-root input
225
+ instead of pretending success. Two refusals are deliberately NOT errors:
226
+ `POST /proc/start`'s 409 becomes the preflight modal, and a control
227
+ envelope with `expected: true` disables its control with the server's own
228
+ sentence.
229
+
230
+ The Library paints twice by design: instantly from the cache-only feed
231
+ (figures labeled with their `cachedAt` as-of time), then again from a
232
+ background `?hydrate=1` refetch with freshly computed rollups.
233
+
234
+ ## M4 — the polish layer
235
+
236
+ | Module | What it owns |
237
+ |---|---|
238
+ | `assets/js/keys.js` | the app-level keyboard reducer: who owns ⌘K, Escape and `?` (HM-190) |
239
+ | `assets/js/omnibox.js` | the ⌘K overlay — navigation rows are links, ACTION rows are proposals |
240
+ | `assets/js/notify.js` | the ONE notification poll: the evaluating GET, shared (HM-183) |
241
+ | `assets/js/views/health.js` | the explained score, per harness and as the fleet board |
242
+ | `assets/js/views/onboarding.js` | first boot: scan-root picker, demo mode, direct registration |
243
+ | `assets/js/views/settings.js` | notification rules, read-only mode, the plugin inventory |
244
+ | `assets/js/views/panes.js` | sandboxed plugin panes |
245
+
246
+ **Keyboard ownership is a reducer, not a race.** M2 shipped the inbox half of
247
+ HM-190 (`triageKey` in `supervision.js`: j/k, one-key verdicts, `.`, `?`).
248
+ `keys.js` adds the layer above it and decides between them: a field owns its
249
+ keys (including ⌘K), the open omnibox owns everything but Escape, and `?`
250
+ goes to the app sheet only when no inbox is mounted — two sheets opening on
251
+ one keypress is the bug that rule exists to prevent. The reducer claims
252
+ nothing else, so the inbox keeps receiving j/k/g/d untouched.
253
+
254
+ The field rule has exactly one exception, and the omnibox is it: the overlay
255
+ focuses its own input, so from the moment it opens every key arrives from a
256
+ field. Escape and the ⌘K toggle are therefore claimed even in a field while
257
+ the omnibox is open — otherwise the overlay swallows both of its own exits
258
+ and the only way out is a mouse click, on the feature whose whole point is
259
+ the keyboard. Nothing else is taken: the query still types.
260
+
261
+ **One evaluating poll, shared.** `GET /api/notifications` IS the rules
262
+ evaluation (the manager runs no timer of its own), and every delivery it
263
+ returns is deduped away server-side and never returned again — so a second
264
+ caller does not read that state, it consumes it. `notify.js` owns the only
265
+ call: it polls on an interval so a console left open actually notifies,
266
+ toasts what each pass delivered, keeps the nav badge honest, and hands the
267
+ same snapshot to the Settings screen rather than letting it issue a GET that
268
+ would eat a toast. The PUT that saves the rules answers with the same
269
+ evaluated view, so its answer is folded back in through the same path. The
270
+ asset-hygiene suite pins the single caller.
271
+
272
+ **The omnibox proposes; it never acts.** Enter on a navigation row goes
273
+ somewhere. Enter on an ACTION row opens a confirm, showing the exact CLI
274
+ command it will run, and only the four process verbs are executable at all —
275
+ anything else the server proposes renders with a reason instead.
276
+
277
+ **The score is never rendered alone.** Every deduction row is a link to the
278
+ tab that fixes it; signals the server could not read are shown as unknowns
279
+ rather than folded into the number.
280
+
281
+ **Panes are the one deliberate exception to the markup-string ban.** A pane
282
+ document is handed to an `<iframe>` via `srcdoc` with `sandbox="allow-scripts"`
283
+ and no `allow-same-origin`, so it becomes a SANDBOXED document with an opaque
284
+ origin — never part of this one. The asset-hygiene suite pins the exception
285
+ to that single module, and `paneSandbox` strips `allow-same-origin` even if a
286
+ payload asks for it.
287
+
288
+ ## Testing
289
+
290
+ `bun test src` — the embed-map completeness/hygiene suite plus unit tests
291
+ for the pure browser modules (`util.js`, `supervision.js`, `markdown.js`,
292
+ `router.js`, `routes.js`, `shapes.js`) and the `api.js` client under a
293
+ stubbed `fetch` (every wrapper asserted against the route map's
294
+ method/path/body). All DOM-free at import time and imported directly by
295
+ bun. Deterministic: clocks are injected, no network, no subprocesses. The
296
+ server side of the same contract runs in `@crewhaus/hangar-server`'s
297
+ `contract.test.ts`.
298
+
299
+ The M2 screens stay testable without a browser by keeping the decisions —
300
+ state → row, envelope → disabled-with-reason, SSE frame → feed item,
301
+ refusal → modal model, keystroke → next state — as pure functions in
302
+ `supervision.js`; `views/*.js` are thin DOM builders over them.
303
+
304
+ The M3 suites follow the same shape: `routes.test.ts` pins the route map's
305
+ grammar, `api.test.ts` drives every generated M3 wrapper against it under a
306
+ stubbed `fetch`, and `creds-views.test.ts` / `memory-fabric.test.ts` unit-test
307
+ the pure decisions those tabs make. The embed-map suite additionally asserts
308
+ that no module is ORPHANED — reachable from `app.js`'s import graph — so a
309
+ view that loses its last importer cannot keep shipping to the browser.