Skip to content

UI-SP5 — Realtime SSE swap (view-free)#22

Merged
l17728 merged 10 commits into
mainfrom
feat/ui-sp5-realtime-sse
May 20, 2026
Merged

UI-SP5 — Realtime SSE swap (view-free)#22
l17728 merged 10 commits into
mainfrom
feat/ui-sp5-realtime-sse

Conversation

@l17728
Copy link
Copy Markdown
Owner

@l17728 l17728 commented May 20, 2026

Summary

Web UI sub-project 5 of 5 (after UI-SP1 #19, UI-SP2 #20 de9573a, UI-SP3 #21 a32f165). Delivers the locked "single-seam SSE swap, view-free" promise recorded multiple times across SP1/SP2/SP3: useLiveResource internals swap from polling to SSE, every view/page/other composable is unchanged.

  • Backend (1 additive endpoint, zero migration, zero new dep): GET /api/v1/tasks/{id}/stream — hand-rolled text/event-stream via FastAPI StreamingResponse (same idiom as hf_proxy.py). 1 Hz default tick (env DLW_TASK_STREAM_INTERVAL_SECONDS, clamped [0.1, 10.0]). Proven cancel-pattern tenant gate (404 cross-tenant) + per-tick re-filter. Terminates on terminal task status, client disconnect, or controller shutdown. ?max_ticks=N testability hatch (httpx ASGITransport buffers the body until generator close — needed for the multi-snapshot test).
  • Frontend (1 new module + 2 additive composable edits): frontend/src/api/sse.ts (pure parseSseChunk + streamSse fetch+ReadableStream + shouldStream gate); useLiveResource gains optional streamUrl + applyEvent (additive — existing callers untouched); useTaskDetail opts in. 9 other composables stay polling.
  • Pre-execution 2-opus gate caught 1 BLOCKER (Vue 3.5 watch({immediate:true}) fires SYNCHRONOUSLY before stopWatch is assigned → TDZ if vue-query serves a cached snapshot) + 6 IMPORTANT (httpx ASGITransport buffering mitigation; pollingFallback → ref; 403/404 fast-fail; parseSseChunk hasData flag for "0"/"" payloads; new streamSse happy-path/401/404 unit tests; baseline-record). All fixed in plan before code.

Verification

  • Backend: 451 pytest (447 prior + 4 new). spectral + swagger-cli + lint_invariants + lint_no_direct_status_write all green.
  • Frontend: 144 vitest (118 prior + 18 new: 8 parseSseChunk, 7 streamGate, 3 streamSse). vue-tsc strict 0 errors; eslint --max-warnings=0; vite build success.
  • Live headed Playwright smoke: 4 SSE requests observed in DevTools network on /tasks/<id>; live :open\n\ndata: <TaskDetail JSON>\n\n verified via direct curl on the ephemeral :8011 controller with current SP5 code.
  • View-free proof: all existing useTaskDetail.spec.ts (SP1) and TaskDetailSP2.spec.ts (SP2 page) tests pass UNCHANGED. TaskDetail.vue was NOT modified. 9 non-opted composables NOT modified.

Known follow-ups (non-blocking, record-and-accept)

  • task_stream_interval_seconds clamping is runtime-only (Pydantic field has no ge=/le=); a misconfigured env var would be silently capped at request time instead of failing fast at startup.
  • pollingFallback is one-way: once SSE gives up, the composable stays on polling for the lifetime of the consumer (tab refresh restores SSE) — documented in web-ui.md.
  • useTaskDetail route-id reuse: same composable instance across task-id changes would stream the stale id; current router setup mounts fresh per route, so benign today.
  • Deferred: WebSocket transport; SSE for SP2 sub-resources (would break view-free); SSE for low-cadence SP3 composables (negligible push value at 5–30 s). UI-SP4 (AI-Copilot) v2.1 follow-up.

Test plan

  • CI green (OpenAPI, Invariant, pytest, frontend-lint, frontend-build, + others)
  • Squash-merge with --delete-branch

🤖 Generated with Claude Code

l17728 and others added 10 commits May 20, 2026 13:02
1 additive backend SSE endpoint (GET /tasks/{id}/stream, hand-rolled
StreamingResponse, zero new dep, zero migration) + opt-in extension of
useLiveResource with streamUrl/applyEvent (additive; existing callers
unaffected) + ONE consumer (useTaskDetail) opts in. 9 other composables stay
polling = truly view-free. fetch+ReadableStream (Bearer-via-header) over
EventSource (would leak JWT via ?token query string). UI-SP4 AI-Copilot
deferred (v2.1 scope, requires full AI backend not implemented).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 backend (openapi+config; SSE route+4 tests; gate). M2 frontend (parseSseChunk
pure parser; streamSse+shouldStream; gate). M3 cutover (useLiveResource opt-in;
useTaskDetail opts in; full gate + headed smoke + docs). Complete code, no
placeholders; grounded in hf_proxy.py StreamingResponse idiom + post-SP3
on-disk state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BLOCKER (frontend): Vue 3.5 watch({immediate:true}) fires SYNCHRONOUSLY
before stopWatch is assigned → calling stopWatch() in the handler hits TDZ
when vue-query serves a cached snapshot synchronously. Fix: let stopWatch +
optional-call + started flag.

IMPORTANTs:
- Backend: yield b":open\n\n" first byte to defeat httpx 0.27.x ASGITransport
  buffering (could hang the multi-snapshot test).
- Backend: replace hard-coded 447 baseline with record-then-add (SP3 added
  extra tests since the baseline was estimated).
- Frontend: pollingFallback → ref(false) for Vue idiom clarity.
- Frontend: streamSse fast-fail on 403/404 (no point burning 7s of backoff).
- Frontend: parseSseChunk hasData flag preserves "0"/"" payloads (SSE-spec).
- Frontend: NEW streamSse happy-path / 401 / 404 unit tests (3 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s testability hatch for httpx ASGITransport)
@l17728 l17728 merged commit 8c77ca1 into main May 20, 2026
12 checks passed
@l17728 l17728 deleted the feat/ui-sp5-realtime-sse branch May 20, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant