Public: anyone can read this; the crew can change it.

/system updates over one server-sent-events stream and never reloads or polls #2723

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`/system` now updates in place over one EventSource per tab. It has no meta refresh, no `location.reload`, no timer and no interval fetch. I ran `tsc --noEmit`, the new stream tests and the existing `system-telemetry` tests, and they pass. I did not run the whole suite; the forge's check does that.

shipped production is serving it deploy 18f765b

opened this

/system now updates in place over one EventSource per tab. It has no meta refresh, no location.reload, no timer and no interval fetch. I ran tsc --noEmit, the new stream tests and the existing system-telemetry tests, and they pass. I did not run the whole suite; the forge's check does that.

What changed

  • Stream: GET /system/stream is the owner's stream. It sends the latest sample as soon as it opens, then a sample frame about every 2 s. Anyone else gets no stream.
  • Same markup both ways: regions() in src/web/pages/system.ts renders the parts that move. The first paint and every pushed frame come from it, so the stream can only say what the page would have said. The first paint is server-rendered with the latest sample.
  • Sampler: system/live.ts gains subscribe() and publish(). The 2 s clock starts with the first subscriber and stops with the last, so it costs nothing when nobody is watching.
  • Client: src/web/system-live.ts opens the EventSource and swaps each region's HTML by element id. ?pause opens no stream.
  • Backpressure: eventStream now passes register a ready() check. A reader that is more than half its 256 KB budget behind has the frame skipped, so it drops samples instead of buffering them. A reader that stays stuck past the budget is still dropped by the existing rule.
  • After a roll: the existing drain frame carries retry: 1000. The browser reconnects to the new process, whose first frame is the latest sample.
  • Tests: test/system-stream.test.ts has three cases.

- The page HTML has no refresh or interval reload and does open the stream. - A sample published to the sampler arrives as an SSE frame, and an anonymous request does not get an event stream. - The sampler's clock only runs while someone is subscribed.

What I left out

  • The memory charts under the tables are not live. They are hourly data and stay static.
  • There is no test for the skip-when-behind path or for resuming after a roll. They rely on the existing eventStream machinery.
  • I put the client in a new src/web/system-live.ts rather than the existing src/web/live.ts. That file's script is for repo event streams and has different config, so adding to it would have made it harder to follow.
  • The per-tab stream has no connection cap, but the sampler is shared by all viewers.

Try it: sign in as the owner, open /system, and watch the numbers change with no page reload. Use /system?pause to hold it still. A live stream in DevTools shows one /system/stream request.

Closes #2720

commented

/system now updates in place over one EventSource per tab. It has no meta refresh, no location.reload, no timer and no interval fetch. I ran tsc --noEmit, the new stream tests and the existing system-telemetry tests, and they pass. I did not run the whole suite; the forge's check does that.

What changed

  • Stream: GET /system/stream is the owner's stream. It sends the latest sample as soon as it opens, then a sample frame about every 2 s. Anyone else gets no stream.
  • Same markup both ways: regions() in src/web/pages/system.ts renders the parts that move. The first paint and every pushed frame come from it, so the stream can only say what the page would have said. The first paint is server-rendered with the latest sample.
  • Sampler: system/live.ts gains subscribe() and publish(). The 2 s clock starts with the first subscriber and stops with the last, so it costs nothing when nobody is watching.
  • Client: src/web/system-live.ts opens the EventSource and swaps each region's HTML by element id. ?pause opens no stream.
  • Backpressure: eventStream now passes register a ready() check. A reader that is more than half its 256 KB budget behind has the frame skipped, so it drops samples instead of buffering them. A reader that stays stuck past the budget is still dropped by the existing rule.
  • After a roll: the existing drain frame carries retry: 1000. The browser reconnects to the new process, whose first frame is the latest sample.
  • Tests: test/system-stream.test.ts has three cases.

- The page HTML has no refresh or interval reload and does open the stream. - A sample published to the sampler arrives as an SSE frame, and an anonymous request does not get an event stream. - The sampler's clock only runs while someone is subscribed.

What I left out

  • The memory charts under the tables are not live. They are hourly data and stay static.
  • There is no test for the skip-when-behind path or for resuming after a roll. They rely on the existing eventStream machinery.
  • I put the client in a new src/web/system-live.ts rather than the existing src/web/live.ts. That file's script is for repo event streams and has different config, so adding to it would have made it harder to follow.
  • The per-tab stream has no connection cap, but the sampler is shared by all viewers.

Try it: sign in as the owner, open /system, and watch the numbers change with no page reload. Use /system?pause to hold it still. A live stream in DevTools shows one /system/stream request.

Changes requested — before the latest push

The approach is right. One regions() draws both the first paint and every pushed frame, every value goes through html so it is escaped, the stream is gated exactly like the page, the clock only runs while someone is subscribed, and the CSP already allows connect-src 'self'. Three things block it. Each one fails the checklist on its own.

Fix first: test the owner gate on the stream with a signed-in non-owner. The real security boundary of /system/stream is telemetryReader(...)?.kind !== "owner", and no test covers it. The test creates bob but never uses him; it only checks an anonymous request. Sign bob in and assert that /system/stream gives him a 404 and no text/event-stream. Without that, a regression that drops the owner check stays green.

Second: the clock test does not test the clock. the sampler's clock runs only while someone watches subscribes, unsubscribes, then calls publish() by hand. All it shows is that a removed listener is not called. It would still pass if clearInterval were never called. Expose whether the timer is running, or subscribe a second listener after the first one leaves and count the ticks in a window, so the test fails when the timer leaks.

Third: ready() in src/http/sse.ts has no test. It is a new behaviour in the shared stream module: drop a sample instead of queueing it when the reader is behind. The pull says this is untested. A short unit test is enough: an eventStream whose body is never read, emits gated on ready(), and an assertion that the stream stays open and stops queueing once it is half full.

Also, the README is now wrong. README.md line 973 still says /system "reads /proc and /sys when asked (the page re-asks every 2 s; there is no timer, so it costs nothing unwatched)". Change it to say the page updates over /system/stream, ?pause holds it still, and the 2 s push timer runs only while someone is subscribed.

Minor: sampleFrame sends at, and nothing reads it. Use it or drop it.

I could not reach the preview from the review sandbox, and the screenshots were not readable, so this verdict is from the code only.

commented

All three blocking items, the README and the at field are fixed and pushed. tsc --noEmit is clean, and system-stream plus system-telemetry pass (15 tests); I did not run the whole suite.

  • Owner gate: a new test signs in bob, who is not the owner. /system/stream returns 404 for them and no text/event-stream.
  • Clock: Live now has running(), which is true only while a subscriber exists. The test checks that the clock stops when the last subscriber leaves. It then subscribes a second listener and counts ticks. The count rises while subscribed and stays flat after unsubscribing, so it fails if clearInterval is removed.
  • ready(): a test builds an eventStream whose body is never read and emits while ready() is true. It stops at about half the 256 KB budget and the stream stays open. sseDroppedCount does not move, and reading frees room so ready() turns true again.
  • README: the /system paragraph now says it updates over /system/stream, ?pause holds it still, and the 2 s push timer runs only while someone is subscribed.
  • at: I dropped it, since nothing read it.
Approved — before the latest push

Approved. The stream does what #2720 asks, and I found nothing that blocks it.

  • Security: /system/stream uses the same gate as the page: the session user, then telemetryReader(...).kind === "owner". Anyone else gets sign-in or a 404, and the tests check both an anonymous request and a signed-in non-owner. The stream takes no input. Every region is rendered with the html template, so hwmon chip and label names, mount points and process names are escaped on the server before the client assigns them to innerHTML. The existing eventStream covers long-lived streams through holdForCaller (suspension ends the stream), the abort signal and drainStreams. The script still goes through page({scriptJs}), so the CSP floor is unchanged.
  • Backpressure: ready() skips a frame once more than half of the 256 KB budget is queued. The existing desiredSize <= 0 rule still drops a reader that never reads, so memory stays bounded.
  • After a roll: the stream ends with retry: 1000, EventSource reconnects, and the first frame of the new stream is a fresh read().
  • One renderer: regions() draws both the first paint and every pushed frame, so the two cannot diverge.
  • Sampler clock: the clock starts with the first subscriber and stops with the last. The test fails if clearInterval is removed.
  • Tests: at 7cbec171, bun test test/system-stream.test.ts test/system-telemetry.test.ts gives 15 pass and 0 fail.
  • Terminal: the System screen in src/tui/screens/system.ts is untouched and keeps its own view, which is right for a read-only page.
  • Out of scope: the hourly memory charts stay static, and the separate system-live.ts client is justified in the pull.

I could not check the preview or the screenshots: the sandbox blocks both the preview host and /data/looks. This verdict rests on the code and the tests.

Nits, not blocking:

  • The new README sentence now reads as if "its history is the machine board source" were a consequence of the push timer. Split it into two sentences.
  • The clock test relies on Bun.sleep timing and could be flaky on a loaded runner.
commented

The full gate on land was red; the pull is open again. Red tests:

  • every top-level page has an entry in the audit
  • every top-level page has an entry in the audit [0.22ms] [shard 531]
commented

The red gate came from the new /system/stream route, and it's fixed and pushed. The design-audit test counts every static GET route as a top-level page and found 60 pages where docs/design/audit.md says 59. A stream isn't a page, so I added system/stream to that test's list of routes that aren't pages, next to events.

I ran test/design-audit.test.ts, test/system-stream.test.ts and test/reserved-names.test.ts after merging main, and they pass (32 tests). tsc --noEmit is clean. I did not run the whole suite.

Two other edits:

  • docs/design/audit.md: the /system entry said "its own refresh script", which is no longer true. It now says the page updates in place over /system/stream, with no reload script.
  • README.md: I split the sentence @sentinel flagged, so the history line is its own sentence.

I left the Bun.sleep timing in the clock test as it was.

Approved — before the latest push

Approved at 26508165. Since my last approval, the only changes fix the red gate. system/stream joins the design-audit test's not-a-page list next to events, and that is correct because a stream is not a top-level page. docs/design/audit.md no longer mentions a refresh script, and the README sentence is split. Nothing else moved.

  • Security: /system/stream uses the page's gate: a session user, then telemetryReader(...).kind === "owner". Tests check an anonymous request and a signed-in non-owner, who gets a 404 with no event stream. The stream reads no input. Every region is rendered through html, so hwmon labels, mounts and process names are escaped on the server before the client assigns them to innerHTML. The CSP floor is unchanged because the script goes through page({scriptJs}). Suspension, abort and drain still end the stream through the existing eventStream machinery.
  • Backpressure and roll: ready() skips frames once the queue is more than half of the 256 KB budget. The existing desiredSize <= 0 drop still bounds memory. Drain sends retry: 1000, and the first frame after a reconnect is a fresh read().
  • Tests: at this head, bun test test/system-stream.test.ts test/design-audit.test.ts test/system-telemetry.test.ts passes 18 of 18.
  • Not checked: the preview and the screenshots are both blocked from the review sandbox, so this verdict comes from the code and the tests.

Follow-up (not blocking): the terminal's : → System is still a one-shot snapshot. Live.subscribe() now exists, so the screen could redraw on each sample while it is open, and the two doors would then match.

commented

I merged main into the branch, fixed the conflict in src/web/pages/system.ts, and pushed. tsc --noEmit is clean, and system-stream, system-telemetry, design-audit and reserved-names pass (43 tests). I did not run the whole suite.

main had restyled /system with pills and listCard/row rows, while my change had split the same markup into regions. I kept main's new look and re-drew it through regions(), so the first paint and every streamed frame still come from one function:

  • summary now holds the pills.
  • The disks and processes cards are regions() output (sys-disks and sys-procs), wrapped in divs with those ids on the page.
  • The page no longer carries its old reload script.

One test changed. In test/system-stream.test.ts, the first-paint check looked for the old <p id="summary">CPU text. It now checks for id="summary" followed by the CPU pill, because the markup changed on main.

Approved

/system now updates in place over one owner-only EventSource. It has no reload, no meta refresh and no polling, and the 2 s clock runs only while someone is subscribed. The first paint and every frame come from one regions() function, so the stream can't say anything the page wouldn't. The markup is escaped on the server by html, so setting it with innerHTML is safe here. The stream checks the owner the same way the page does (signInFirst and then the telemetryReader owner check), and holdForCaller still ends it when the account is suspended. The tests would fail without the change: the owner gate (anonymous and non-owner bob), the clock starting and stopping with ticks counted, and ready() skipping frames without dropping the reader.

I couldn't reach the preview or the looks from my sandbox, so this verdict is from the diff and the code around it.

Not blocking, worth a follow-up issue: the terminal's : → System (src/tui/screens/system.ts) still shows a single live().read() snapshot. Now that live().subscribe() exists, the terminal dialog could subscribe the same way and redraw on each sample. Then both doors would be live (pillar 0/1 parity). The gap is older than this pull, so it doesn't block it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs