build-logs: SSE live streaming + raw download (#726 phase 4)

Backend (hive-c0re):
- build_logs.rs: add tokio::sync::broadcast::Sender<i64> to BuildLogs;
  append() and finish() notify subscribers after each sqlite write.
  Add BuildLogProgress struct and get_progress(id, stdout_cursor,
  stderr_cursor) for incremental delta reads.
- dashboard.rs: two new endpoints —
    GET /api/build-logs/id/{id}/stream  SSE; streams BuildLogFrame
      {stdout_append, stderr_append, status?, done} deltas until the
      build finishes or the browser disconnects. Backed by an mpsc
      channel task that watches the per-build broadcast notifications.
    GET /api/build-logs/id/{id}/raw    text/plain download with
      Content-Disposition: attachment; filename build-log-{agent}-{id}.txt

Frontend (dashboard):
- tabs.js: running builds (status === null) connect an EventSource to
  /stream and append lines live; "live" badge pulses amber while active,
  flips to ok/fail on done. Finished builds still use the JSON fetch path.
  Collapsing a running panel closes the EventSource; re-expanding
  reconnects. Adds a "⬇ raw" download link to every expanded row.
- dashboard.css: .build-logs-dl inline download link; .build-logs-live
  live pulse @keyframes animation.

Docs: web-ui.md updated for all three new endpoints + behaviour.
This commit is contained in:
iris 2026-05-31 21:23:01 +02:00 committed by mara
commit 050e130eba
5 changed files with 365 additions and 14 deletions

View file

@ -642,12 +642,17 @@ fetch entirely.
- `↳ build logs · <agent>` — opens the side panel and fetches
the last 10 build-log headers via
`GET /api/build-logs/{agent}` (status chip + kind + age +
truncated cmdline per row). Clicking a row lazy-fetches its
full stdout+stderr from `GET /api/build-logs/id/{id}` and
expands it inline as a scrollable `<pre>`. A refresh button
re-fetches the header list. Backed by the `build_logs.sqlite`
store that `lifecycle::run` and `lifecycle::prebuild_toplevel`
write into.
truncated cmdline per row). Clicking a row expands the detail
inline: finished builds lazy-fetch `GET /api/build-logs/id/{id}`
(full JSON); running builds (`status: null`) instead open an
`EventSource` to `GET /api/build-logs/id/{id}/stream` and
stream stdout/stderr live — the "live" badge pulses amber while
the build runs and flips to the final status colour on
completion. A `⬇ raw` link downloads the full log as
`text/plain` via `GET /api/build-logs/id/{id}/raw`. A refresh
button re-fetches the header list. Backed by the
`build_logs.sqlite` store that `lifecycle::run` and
`lifecycle::prebuild_toplevel` write into.
- Plain navigation links (config repo, forge profile,
`dashboardLinks` extras) now live in the icon-only nav strip
on Line 1 — see above. The agent's `config` link
@ -855,6 +860,21 @@ that's a browser-level decision, not ours.
`BuildLogFull` (JSON): all header fields plus `stdout` and
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
404 when the row is missing (vacuum-reaped or stale id).
- `GET /api/build-logs/id/{id}/stream` — SSE stream for live
monitoring of a running build. The client connects while the
build's `status` is `null`; the server sends incremental
`BuildLogFrame` JSON frames: `{ stdout_append, stderr_append,
status?, done }`. The first frame carries the full accumulated
log since build start (cursors at 0); subsequent frames carry
only new bytes. `done: true` on the final frame signals the
browser to close the `EventSource`. The stream closes on its
own once the build finishes, or if the row is vacuum-reaped
mid-stream. An `error` event name signals a server-side failure
(row not found, etc.).
- `GET /api/build-logs/id/{id}/raw` — plain-text download of the
full log (stdout followed by `\n--- stderr ---\n` + stderr when
non-empty). `Content-Disposition: attachment` triggers a
browser download; filename is `build-log-{agent}-{id}.txt`.
- `GET /api/journal/{name}?unit=&lines=` — journalctl viewer for
a managed container; rendered in the side panel.
- `GET /api/approval-diff/{id}?base=applied|approved|previous`