feat: add /api/events SSE endpoint for live run events (#178)#332
Closed
spboyer wants to merge 1 commit into
Closed
feat: add /api/events SSE endpoint for live run events (#178)#332spboyer wants to merge 1 commit into
spboyer wants to merge 1 commit into
Conversation
Implements server-sent events streaming to satisfy the live view
contract expected by web/src/hooks/useSSE.ts. Adds:
- internal/webapi/sse.go: in-memory pub/sub Broker and HandleEvents
SSE handler that emits text/event-stream with a connected
comment, periodic heartbeats, and JSON-encoded events shaped as
{type, data, timestamp}. Supports task_start, task_complete,
grader_result, run_complete; other types are dropped.
- internal/webapi/sse_tailer.go: SessionLogTailer that watches the
configured results directory for *-session.jsonl files written by
'waza run --session-log' and republishes their events to the
broker. Synthesizes run_complete from session_complete.
- internal/webserver: wires a Broker and StartSessionLogTailer into
the dashboard server lifecycle.
- New RegisterRoutesWithHandlers entrypoint so callers can attach
state (such as a Broker) before exposing routes.
- Tests covering broker fan-out, unknown-type filtering, unsubscribe
idempotency, SSE response headers/body, route registration, the
503 path when no broker is configured, the tailer's event
translation, and skipping pre-existing logs.
Verified manually against the issue repro: 'waza serve --port 18080
--no-browser' now returns Content-Type: text/event-stream from
/api/events instead of the dashboard HTML shell.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a backend Server-Sent Events (SSE) stream at GET /api/events so the dashboard Live view can receive real-time run events, using a new in-process broker plus a filesystem session-log tailer (for the waza serve / waza run separate-process model).
Changes:
- Introduces an SSE
Brokerand/api/eventshandler that emitstask_start,task_complete,grader_result, andrun_completeframes with heartbeats. - Adds a
SessionLogTailerthat polls*-session.jsonlfiles and republishes translated events to the broker. - Wires the broker/tailer into the webserver route registration and adds tests for broker/handler/tailer behavior.
Show a summary per file
| File | Description |
|---|---|
| internal/webserver/server.go | Creates a broker on server startup and starts the session-log tailer during ListenAndServe. |
| internal/webserver/routes.go | Threads broker into handler setup and registers routes via a pre-configured Handlers instance. |
| internal/webapi/handlers.go | Adds broker field + /api/events route registration and a new RegisterRoutesWithHandlers helper. |
| internal/webapi/sse.go | Implements the SSE broker and the /api/events streaming handler. |
| internal/webapi/sse_tailer.go | Implements polling/tailing of *-session.jsonl and translation into SSE-shaped events. |
| internal/webapi/sse_test.go | Adds tests for broker fan-out/filtering, SSE handler behavior, route registration, and tailer translation/skip logic. |
Copilot's findings
- Files reviewed: 6/6 changed files
- Comments generated: 3
Comment on lines
+176
to
+191
| case "task_complete": | ||
| passed := stringField(raw.Data, "status") == "pass" | ||
| if passed { | ||
| e.passCount++ | ||
| } | ||
| e.totalTasks++ | ||
| broker.Publish(SSEEvent{ | ||
| Type: "task_complete", | ||
| Timestamp: raw.Timestamp, | ||
| Data: map[string]any{ | ||
| "taskName": stringField(raw.Data, "task_name"), | ||
| "outcome": stringField(raw.Data, "status"), | ||
| "score": floatField(raw.Data, "score"), | ||
| "duration": floatField(raw.Data, "duration_ms"), | ||
| }, | ||
| }) |
Comment on lines
+227
to
+230
| writeEvent(t, "task_start", map[string]any{"task_name": "alpha", "task_num": 1, "total_tasks": 2}) | ||
| writeEvent(t, "task_complete", map[string]any{"task_name": "alpha", "status": "pass", "score": 1.0, "duration_ms": int64(123)}) | ||
| writeEvent(t, "grader_result", map[string]any{"grader_name": "code", "grader_type": "code", "passed": true, "score": 1.0, "feedback": "ok"}) | ||
| writeEvent(t, "session_complete", map[string]any{"total_tests": 1, "passed": 1, "failed": 0, "errors": 0, "duration_ms": int64(456)}) |
Comment on lines
+22
to
+25
| // It is intentionally simple: it polls the directory for the most | ||
| // recently modified `*-session.jsonl` file and tails it. When a newer | ||
| // session log appears, it switches to the new file. The previous file | ||
| // is closed and any unread tail is read first to flush late events. |
11 tasks
Member
Author
|
Closing in favor of #397 (latest iteration, green CI). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #178.
Summary
The Live view in the dashboard (
web/src/hooks/useSSE.ts+LiveView.tsx) opensEventSource('/api/events')and expectstask_start,task_complete,grader_result, andrun_completeevents. The backend never registered that route, socurl /api/eventsreturned the dashboard HTML shell. This PR adds the SSE endpoint and wires it to the existing run/session event stream.Changes
internal/webapi/sse.go— newBroker(drop-oldest pub/sub) +HandleEventsSSE handler. Sends: connectedpreamble, 15s heartbeats, JSON-encoded{type, data, timestamp}frames. Filters to the four contract event types.internal/webapi/sse_tailer.go—SessionLogTailerpolls the results directory, tails new*-session.jsonlfiles written bywaza run --session-log, and republishes events through the broker. Maps session-event field names (task_name,duration_ms, …) to the camelCase keys the frontend reads. Synthesizesrun_completefromsession_complete. Skips pre-existing finished runs.internal/webapi/handlers.go— addsbrokertoHandlers, factors outregisterHandlerRoutes, exposes newRegisterRoutesWithHandlersso callers can pre-configure state. RegistersGET /api/events.internal/webserver/{server,routes}.go— creates aBrokerinNew, callsStartSessionLogTaileragainstcfg.ResultsDirfromListenAndServe, threads the broker throughregisterRoutes.internal/webapi/sse_test.go— 8 new tests covering broker fan-out, unknown-type filtering, unsubscribe idempotency, response headers/body, route registration, the 503 path when no broker is configured, tailer event translation, and skipping pre-existing logs.Process model note
waza serveandwaza runare separate processes. The only shared channel is the filesystem session log, which is why the wiring tails*-session.jsonlrather than using an in-process bus. If we later run dashboard + run in one process, we can additionally havecmd_runpublish directly viaHandlers.PublishLiveEvent.Validation
go test ./...— all packages greengolangci-lint run ./...— 0 issuesHTTP/1.1 200,Content-Type: text/event-stream, and a: connectedbody — previously returned the dashboard HTML.Out of scope
useSSE.tsalready speaks this contract.