diff --git a/api/openapi.yaml b/api/openapi.yaml index 01ca044..b3bc614 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -529,6 +529,25 @@ paths: items: {$ref: '#/components/schemas/TaskEvent'} next_cursor: {type: string, nullable: true} + /tasks/{taskId}/stream: + parameters: + - $ref: '#/components/parameters/TaskId' + get: + tags: [tasks] + summary: Live TaskDetail SSE stream (UI-SP5) + operationId: streamTaskDetail + responses: + '200': + description: SSE stream of TaskDetail snapshots (text/event-stream) + content: + text/event-stream: + schema: + type: string + description: | + Each event is `data: \n\n`. Stream terminates + on terminal task status, client disconnect, or controller + shutdown. Keep-alive comment lines (`:keepalive`) may appear. + /tasks/{taskId}/subtask-chunks: parameters: - $ref: '#/components/parameters/TaskId' diff --git a/docs/operator/web-ui.md b/docs/operator/web-ui.md index eb40eb4..fd47c35 100644 --- a/docs/operator/web-ui.md +++ b/docs/operator/web-ui.md @@ -129,3 +129,40 @@ drain/restart, metrics history, heartbeat history; HF-token rotation; license-policy CRUD; source-driver registration; maintenance mode; `/quota/usage` (declared but no backing tables); ML forecast; chargeback PDF; real-time audit tail (UI-SP5). + +## UI-SP5 — Realtime SSE swap (delivered) + +`useLiveResource` now supports an opt-in SSE transport. The locked promise +recorded in SP1/SP2/SP3 — *"SP5 swaps internals to SSE/WS with zero view +changes"* — is honored: every view, page, and other composable is unchanged. + +- **Backend**: `GET /api/v1/tasks/{id}/stream` — hand-rolled + `text/event-stream` via FastAPI `StreamingResponse` (same idiom as + `hf_proxy.py`). 1 Hz default tick rate (env + `DLW_TASK_STREAM_INTERVAL_SECONDS` overrides; clamped `[0.1, 10.0]`). + Tenant-scoped via the proven cancel-pattern (404 cross-tenant). Terminates + on terminal task status, client disconnect, or controller shutdown. A + `?max_ticks=N` query param is supported for testability (httpx + `ASGITransport` buffers the response body until the generator closes, so + multi-tick tests need a natural-termination hatch). +- **Frontend**: `frontend/src/api/sse.ts` (pure `parseSseChunk` + `streamSse` + fetch + ReadableStream with exponential backoff, Bearer-via-header). The + `useLiveResource` composable gains optional `streamUrl` + `applyEvent` + fields; when both are set, the composable opens an SSE connection after + the first snapshot and writes events into the vue-query cache. On 3 + consecutive failures the stream gives up and polling resumes automatically. + On 401 the streamer calls `auth.logout()`; on 403/404 it fails fast (no + backoff burn). +- **One consumer opts in**: `useTaskDetail`. Every other composable + (`useTaskList`, `useQuota`, the 4 SP2 sub-resource composables, the 3 SP3 + composables) stays on polling. + +**At a glance**: open `/tasks/` in DevTools Network — you'll see one +long-lived `text/event-stream` connection per visit instead of a 1 Hz polling +loop. + +**Deferrals**: WebSocket transport (SSE delivers the same outcome simpler); +streaming for the 4 SP2 sub-resources (would force a multi-resource envelope +that breaks view-free); SSE for the low-frequency SP3 composables (push value +is negligible at 5–30 s cadences). UI-SP4 (AI-Copilot) remains the v2.1 +follow-up. diff --git a/docs/superpowers/plans/2026-05-20-ui-sp5-realtime-sse.md b/docs/superpowers/plans/2026-05-20-ui-sp5-realtime-sse.md new file mode 100644 index 0000000..8b4a167 --- /dev/null +++ b/docs/superpowers/plans/2026-05-20-ui-sp5-realtime-sse.md @@ -0,0 +1,1179 @@ +# UI-SP5 — Realtime SSE Swap Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add 1 additive SSE endpoint (`GET /api/v1/tasks/{id}/stream`) + extend `useLiveResource` with `streamUrl` / `applyEvent` options + opt in ONE consumer (`useTaskDetail`) — delivering the locked "single-seam SSE swap, view-free" promise from SP1/SP2/SP3. + +**Architecture:** Backend = hand-rolled `text/event-stream` via FastAPI `StreamingResponse` (same idiom as `src/dlw/api/hf_proxy.py:99-110`); fresh DB session per 1 Hz tick; tenant-gated via the proven cancel-pattern. Frontend = new `api/sse.ts` (pure `parseSseChunk` + `streamSse` fetch+ReadableStream with exponential backoff) + additive `LiveOptions` fields (`streamUrl`, `applyEvent`); `useTaskDetail` is the ONLY consumer opting in. **9 other composables stay polling = truly view-free.** + +**Tech Stack:** FastAPI · SQLAlchemy 2 async · asyncpg · Pydantic v2 · pytest (httpx `AsyncClient.stream()` for SSE tests) · OpenAPI 3.1 (spectral + swagger-cli) · Vue 3.5 `