feat(m18): port monitor parity + SSE reliability to apps/web #173
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!173
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/164"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #164
Summary
dashboard.htmlMonitor / Agents / Stats / Usage surfaceonto the new t3code stack at
/app/*./app/monitoris the canonicalMonitor URL;
/app/redirects to it.debounces the banner by 5 s of silence and probes
/healthbeforepainting the hard-failure state. 3-state pill:
live→reconnecting→
disconnected. Server-side 15 s SSE heartbeat already in place.contract test that asserts
/historyonly emits keys the web'sHistoryListEntrytype knows about.Feature parity against the acceptance-criteria checklist
RUNNING/QUEUED/HISTORY tabs, detail pane (header + prompt accordion +
turns strip + event log with filter), inline cancel, re-dispatch hint toast
filter selects
Design tokens
Tailwind now maps
--state-*aliases fromstyles/tokens.csssocomponents read
bg-state-running/text-state-failureinstead ofraw hex. No raw hex added in any downstream component — every colour
resolves through a CSS var.
Test plan
bun run qaclean (server 640 tests + web 13 tests + typecheck + lint + format)bun run buildemitsapps/web/dist/— server's/app/*now hasreal assets to serve
http://192.168.1.164:4500/app/monitorafter deploy(legacy
/still servesdashboard.html)after 10 s with the
/healthprobe failing🤖 Generated with Claude Code
Review — M18-3 Monitor parity + SSE reliability
CI: ✅ green (run #1769, 4m2s).
The implementation is solid overall. SSE 3-state logic in
sse.tsis correct (5 s debounce → health probe → hard-fail at 10 s), the component tree is clean, design-token discipline holds throughout, and the TanStack Query invalidation strategy is sound. Two gaps need fixing before merge — both are explicitly in the AC and neither is in the production path.Issue 1 — Playwright spec doesn't assert the banner toggle it promises
File:
apps/web/e2e/monitor.spec.tsThe test comment and the acceptance criterion both say:
The spec asserts the POST fires (
await expect.poll(() => cancelHit, …).toBe(true)) but there is no assertion that the disconnect banner ever appears. The SSE stub sends a finite, immediately-closing body:When the browser consumes this,
EventSourcegetsonerroron close. After the 5 s debounce the banner should appear. Add the assertion (with enough headroom for the debounce):Without this the SSE reliability acceptance criterion is untested end-to-end.
Issue 2 — Contract test is always a no-op in CI
File:
apps/server/src/history-contract.test.tsThe test bails silently whenever the database is empty:
In the CI sandbox the database is always empty, so the structural assertions (key set, required field types) never run. The test passes green while providing zero contract guarantee — exactly the failure mode it was designed to catch ("a silent server-side rename").
Fix by seeding a minimal row before the structural test. The test already imports
handleRequest, so aPOST /taskcall (or a directdb.tsinsert using a fakeTaskRecord) can produce a row without an external service:Alternatively, call the handler with a test
TaskRecordvia thetask-storeinternals directly. The exact seeding approach is up to you, but the test must exercise the shape check in CI rather than skipping it.No other blocking issues. The
TurnsStripbucket algorithm is correct, theofflineSince-guard inonerrorprevents timer stacking, theconnectRefpattern avoids stale closures, and all acceptance criteria are met in the production code — the two gaps above are purely in the test layer.@ -0,0 +44,4 @@});test("if any row exists, it only emits keys the web app's HistoryListEntry declares", async () => {const res = await handleRequest(req("GET", "/history"));This early-return means the structural key/type assertions never execute in CI (empty DB). Seed at least one row — e.g. via
handleRequest(req("POST", "/task"), …)or a directtask-storeinsert — so the assertions run rather than silently skip.@ -0,0 +106,4 @@await page.getByTestId("cancel-confirm").click();await expect.poll(() => cancelHit, { timeout: 5000 }).toBe(true);});});The disconnect-banner assertion is missing. The SSE stub sends a finite stream that closes, which fires
onerror→ 5 s debounce → banner. Add:without this the SSE reliability requirement in the AC is untested end-to-end.
Both review findings addressed in
a7423a8:Issue 1 —
apps/web/e2e/monitor.spec.ts: added the missing bannerassertion after the cancel flow. The stubbed
/eventsstream closesafter the init frame,
EventSource.onerrorfires, and after the 5 sdebounce the banner paints. Gave it 8 s headroom so it isn't flaky:
Issue 2 —
apps/server/src/history-contract.test.ts: dropped theearly-return and seeded a finalized
TaskRecordbefore the structuralassertions. Exposed a narrow
__seedHistoryForTest(record)helper inmain.ts(docstring flags it as not-for-production) — cleaner thanspinning a full worker/pool just to land one row. The test now also
asserts the projection logic on the seeded row specifically:
events[]and
promptmust be stripped,event_countmust equal the numericlength of the source events array, and
force_mergemust be echoedthrough as a boolean. No more silent skip in CI.
Verified locally:
bun x turbo run typecheck lint test→ 640 servertests pass (
history-contract.test.tsboth cases green), 13 web testspass, zero lint/format findings.