fix(nix-web-monitor): bust stale UI cache, harden WebSocket send and lag resync#806
Merged
Merged
Conversation
…lag resync Three fixes after the WebSocket switch: - index.html is now served with Cache-Control: no-store. It names the content-hashed asset files, so a browser that cached it across a rebuild requests assets the new server no longer has. nix store mtimes are a constant epoch, so no-cache would 304 the stale copy; no-store forces a fresh fetch each load. This is the durable fix for the "Expected a JavaScript module but got text/html" load failure. - A missing asset now 404s instead of falling back to index.html. Serving HTML for a missing /assets/*.js is what surfaces as the wrong-MIME error; the SPA fallback was unnecessary (the dashboard has no client-side routing). - Each WebSocket frame send is bounded by a 30s timeout, so a client that completes the handshake then stops reading drops instead of pinning its task. - On broadcast Lagged, the server now re-subscribes under the read lock alongside the reset snapshot, dropping the buffered backlog instead of replaying it on top of the reset (replay double-applied non-idempotent appends like log lines).
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
Blast radius
2 added, 0 removed pie showData title Rebuilt checks by category
"image" : 13
"rust" : 10
"blast" : 1
"lint" : 1
flowchart LR
c0["ix-mcp"]
c1["nix-web-monitor-0.1.0"]
c2["blast-radius-test"]
c3["lint"]
c4["rust-nix-web-monitor.clippy"]
c0 --> k1["image-kernel-dev"]
c0 --> k2["image-minecraft"]
c0 --> k3["image-minecraft-bedrock"]
c0 --> k4["image-minecraft-status"]
c0 --> k5["image-minecraft_1.21.11-fabric"]
c1 --> k8["rust-nix-web-monitor.nix-web-monitor-all"]
c1 --> k9["rust-nix-web-monitor.nix-web-monitor-tests-delta_encode_round_trips_through_msgpack"]
c1 --> k10["rust-nix-web-monitor.nix-web-monitor-tests-state_endpoint_serves_snapshot_json"]
c1 --> k11["rust-nix-web-monitor.nix-web-monitor-tests-ws_route_requires_websocket_upgrade"]
c1 --> k12["rust-nix-web-monitor.package"]
changed checks (23)
|
Andrew Gazelka (andrewgazelka)
added a commit
that referenced
this pull request
Jun 7, 2026
…dropped reset seed (#811) Redesign of the build view after the WebSocket switch (#804) and cache fix (#806), addressing the reported issues: derivations floated as many orphan roots instead of one goal, rows were noisy (`.drv` + store hash + version all competing), there was no keyboard navigation, and the log drawer named the pinned build by an opaque activity id. Also fixes a latent bug that left the build tree empty on a page loaded mid-build. ## Single goal-rooted tree Every derivation now hangs under one synthetic root labeled with the Nix command (`nix build .#ix`), so the view is always a single tree from the goal instead of a forest. Before, any derivation whose parent closure had not yet been queried floated as its own top-level root; mid-build that meant a wall of orphans (the reported screenshot). Now those nest under the command root, and the real goal settles as its single child once edges resolve. The root row carries a live rollup: `succeeded/total` plus `running`/`failed` counts and an elapsed clock that freezes when the build finishes. `command` is threaded server → snapshot → client (and through the client working-model so it survives projection). ## Readable rows `splitDerivation` now yields `{ name, version }` with the `.drv` suffix and store hash dropped: the package **name** leads, the **version** trails dimmed, the full path stays in the row title. Applies to the tree and the flat list. ## Vim navigation The build panel owns `j`/`k` (move), `h` (collapse / step to parent), `l` (expand / step to first child), `gg`/`G` (first/last), `o`/Enter (pin the cursor's logs). Arrows mirror them. The cursor is a distinct highlight from the click selection and scrolls into view as it moves. The log drawer keeps only `/` and Esc so the two key handlers never contend. ## Logs name the build The log drawer's selection chip shows the pinned build's package name instead of `build #<id>`. ## Bug fix: WebSocket reset seed was discarded `runSession` did `applyDelta(working, delta)` and ignored the return. `applyDelta` mutates in place for incremental deltas but returns a fresh object for `reset` (the seed), so the seed — which carries the builds/activities for a page loaded mid-build — was silently dropped (logs survived only because their delta mutates in place). A page loaded onto an in-progress build showed an empty tree. Fixed by capturing the return (`working = applyDelta(...)`). This is the bug that made every browser-rendered tree come up empty until now. ## Cleanup (existing code improved) Shared row helpers (`whereLabel`, `isRemote`, `elapsedMs`, `durationLabel`) moved to `lib/build-row.ts`, removing the copy in both views. Pure tree helpers (`flattenVisible`, `hasChildren`, `parentByDrv`) live in `build-tree.ts`, reused by the keyboard layer. ## Validation - `nix build .#nix-web-monitor` (Rust + Svelte: svelte-check 0 errors, eslint clean, vite build) - **Live Playwright run against a real `nix build`**: confirmed the single command root, `name`+dimmed-version rows with no `.drv`, vim `j` moving the cursor, the logs chip showing `hello`, and builds/activities rendering (regression fix). Server `/api/state` was diffed against the browser DOM to catch the reset-seed drop. AI-generated with Claude Opus 4.8. <!-- Macroscope's pull request summary starts here --> <!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. --> <!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. --> > [!NOTE] > ### Add goal-rooted build tree, vim navigation, and readable row labels to nix-web-monitor > - Adds a sticky root row to the build tree labeled with the invoked Nix command (e.g. `nix build .#foo`), showing aggregated build stats and elapsed time; the command is seeded from the server's [`MonitorState::new`](https://github.com/indexable-inc/index/pull/811/files#diff-b199662dc1ba66ad897244a9b7d9bf18327c06101b3425c83e9ff61f8ec6d627) and propagated through snapshots to the UI. > - Implements vim-style keyboard navigation (j/k/h/l, gg/G, Enter/o to expand/collapse) in [`BuildTable.svelte`](https://github.com/indexable-inc/index/pull/811/files#diff-c6118e9e853915cf57a07cc4b8b260cf6439944c6ef4e07cf86989dc1c6a64a9) with cursor highlight and auto-scroll. > - Updates derivation row labels to show `name` and `version` instead of store hash prefixes, using a refactored [`splitDerivation`](https://github.com/indexable-inc/index/pull/811/files#diff-d9f5256217da8c7536b8dc20f19d3ad1f66d8f8bad822ed6e2790cacddf18553) that strips `.drv` and splits out version. > - Extracts shared duration and host helpers into [`build-row.ts`](https://github.com/indexable-inc/index/pull/811/files#diff-40e6796962fb82eb6d7abecbfce89d974555a898a4541b731e29fd65f351687a) and unifies their use across `BuildTable`, `BuildTree`, and `LogPanel`. > - Fix: [`monitor-transport.ts`](https://github.com/indexable-inc/index/pull/811/files#diff-3600e88edc89da306e3911677f724371bfc3126f68f93276d8bc303912c1c52a) now reassigns `working` on `applyDelta`, so a `reset` delta (initial seed on page load) correctly replaces client state instead of being silently discarded. > > <!-- Macroscope's review summary starts here --> > > <sup><a href="https://app.macroscope.com">Macroscope</a> summarized 672c507.</sup> > <!-- Macroscope's review summary ends here --> > <!-- macroscope-ui-refresh --> <!-- Macroscope's pull request summary ends here -->
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.
Follow-ups after the WebSocket switch (#804), including the user-reported "Expected a JavaScript module but got text/html" UI load failure.
index.html stale-cache → wrong-MIME load failure (the visible bug)
index.htmlnames the content-hashed asset files. It was served with no cache headers, and nix store mtimes are a constant epoch, so a browser kept a staleindex.htmlacross a rebuild and asked for/assets/index-<oldhash>.js, which the new server no longer has. The SPA fallback then answered that missing asset withindex.html(text/html), and strict MIME checking rejected it.Two fixes:
index.htmlis served withCache-Control: no-storeso the browser always re-fetches it and its asset references stay in sync with the server. (no-cachewouldn't be enough: the constant epoch mtime would 304 the stale copy.)index.html. The dashboard has no client-side routing, so the HTML fallback was unnecessary; serving HTML for a missing.jsis exactly what produced the MIME error.WebSocket hardening (from the #804 review)
Lagged, the server now re-subscribes under the read lock alongside the reset snapshot, dropping the buffered backlog instead of replaying it on top of the reset (replay double-counted non-idempotent appends like log lines).Validation
nix build .#nix-web-monitorGET /→cache-control: no-store; real asset →200 text/javascript; missing asset →404; WS still seeds theDelta::Resetindex_is_served_no_store,missing_asset_404s_instead_of_html_fallbackAI-generated with Claude Opus 4.8.
Note
Fix stale UI cache and harden WebSocket send/lag resync in nix-web-monitor
index.htmlwithCache-Control: no-storevia a dedicatedserve_indexhandler, busting stale browser caches. Missing static assets now return 404 instead of falling back toindex.html.send_frame()helper that wraps each WebSocket send with a 30sSEND_TIMEOUT, dropping unresponsive clients that stop reading.RecvError::Lagged), the server now re-subscribes to drop the stale backlog and sends a freshResetsnapshot instead of replaying non-idempotent deltas.index.htmlis read from disk at startup; the server will fail to start if the file is missing.index.htmlis absent will now error immediately rather than at request time.Macroscope summarized 7a291d1.