Repository navigation
fix(docker): stop blank page and stale update badge during container updates - #17
Conversation
…updates Two distinct failures on /Docker while a container update is in flight, both rooted in how boot.ts handles the snapshot fetch. Blank page on refresh: docker-state.php returns `containers: []` (still HTTP 200) during the window the stock update_container flow rewrites the webui-info docker.json that getAllInfo() reads. resync()'s try/catch never fired (it's a 200), so it overwrote the populated store with the empty list AND poisoned the sessionStorage SWR cache via writeCachedSnapshot — leaving the page blank across refreshes until the update finished. Guard with isTransientEmptySnapshot(): when a snapshot is empty but the store already holds rows, skip the overwrite and wait for the next resync. First load (store empty) still renders a genuinely-empty server. Stale "update available" badge after a successful update: reconcileUpdating() already force-clears the badge when a container's id rotates (recreated = pull succeeded), trusting the rotation over Unraid's lagging digest-status cache — but only while the update probe still exists. onBatchComplete cleared all probes BEFORE resyncing, so the rotated-id snapshot had no probe to match and the badge fell back to the snapshot's stale updateAvailable=true. Resync first, then clearAllUpdating() for any probe that didn't self-reconcile. Tests: new boot.test.ts for isTransientEmptySnapshot; store.test.ts pins the onBatchComplete ordering contract. Full docker suite green (119). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…host The blank-page guard from the prior commit lived in boot.ts as isTransientEmptySnapshot(): an empty snapshot was suppressed whenever the store already held rows. That heuristic couldn't tell a transient mid-update empty from a genuinely empty server, so deleting every container left ghost rows on screen until a manual reload. Move the disambiguation server-side, where the authoritative signal exists. docker-state.php now compares getAllInfo() (reads the webui-info docker.json the stock update_container flow rewrites) against getDockerContainers() (hits the docker socket directly, unaffected by that rewrite). When info is empty but the daemon still lists containers, it's a mid-rewrite window -> respond 503. fetchSnapshot() throws on the non-200 and resync()'s existing catch keeps current state + skips the SWR cache write, then retries on the next resync. A genuinely empty server (daemon also empty) returns 200 with [] and renders the empty state. This is trigger-agnostic (stock UI, CA Auto Update, or the modernui page) because it keys off the daemon, not the modernui update probes. Removed isTransientEmptySnapshot() + its boot.test.ts cases; added modernui_is_transient_empty() with docker-state.test.php coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up (3900ed8): moved the transient-empty guard server-sideAddresses a gap in the original client-side Fix — disambiguate at the source.
When info is empty but the daemon still lists containers → mid-rewrite → respond Why this is better than the client heuristic
Changes
Verification: TS 116 passed · 🤖 Generated with Claude Code |
Summary
Fixes two distinct failures on
/Dockerthat occur while a container update is in flight, both rooted in howboot.tshandles the snapshot fetch. Diagnosed live against a running server with the browser devtools.1. Blank page on refresh during an update
docker-state.phpreturnscontainers: []— still HTTP 200 with valid JSON — during the window the stockupdate_containerflow rewrites the webui-infodocker.jsonthatgetAllInfo()reads. Because the fetch succeeds,resync()'stry/catchnever fires; it overwrote the populated store with the empty list and poisoned the sessionStorage SWR cache viawriteCachedSnapshot(). Result: the page went blank and stayed blank across refreshes until the update finished.Fix:
isTransientEmptySnapshot()— when a snapshot is empty but the store already holds rows, skip the overwrite and the cache write, and wait for the next resync. First load (store still empty) falls through, so a genuinely-empty server still renders the empty state.2. Stale "UPDATE AVAILABLE" badge after a successful update
reconcileUpdating()already force-clears the badge when a container's id rotates (recreated = pull succeeded), trusting the id rotation over Unraid's lagging digest-status cache — but only while the update probe still exists.onBatchComplete(fired on the/sub/docker_DONE_marker) calledclearAllUpdating()beforeresync(), wiping the probe baseline first. So when the rotated-id snapshot arrived, reconcile had nothing to match and the badge fell back to the snapshot's staleupdateAvailable: true— stuck until a manual reload.Fix: resync first, then clear —
void resync().then(() => store.clearAllUpdating()). The rotated-id snapshot reconciles (badge cleared + probe removed) beforeclearAllUpdatingmops up any probe that didn't self-reconcile.Tests
tests/unit-ts/docker/boot.test.tscoveringisTransientEmptySnapshot(transient-empty, first-load, non-empty).tests/unit-ts/docker/store.test.tspins theonBatchCompleteordering contract so a future reorder back to clear-then-resync fails loudly.Verification notes
Both fixes were deployed to a live server via dev-mirror and the page-level behavior confirmed (blank page no longer reproduces; backend caches confirmed correct). The stale-badge fix applies to future updates — it relies on the id-rotation reconcile path, which is unit-tested but was not exercised end-to-end against a live update to avoid disrupting running containers.
🤖 Generated with Claude Code