Skip to content

fix(docker): stop blank page and stale update badge during container updates - #17

Merged
Inch-high merged 2 commits into
mainfrom
fix/docker-blank-page-and-stale-update-badge
Jun 2, 2026
Merged

Inch-high merged 2 commits into
mainfrom
fix/docker-blank-page-and-stale-update-badge

Conversation

@Inch-high

Copy link
Copy Markdown
Owner

Summary

Fixes two distinct failures on /Docker that occur while a container update is in flight, both rooted in how boot.ts handles the snapshot fetch. Diagnosed live against a running server with the browser devtools.

1. Blank page on refresh during an update

docker-state.php returns containers: [] — still HTTP 200 with valid JSON — during the window the stock update_container flow rewrites the webui-info docker.json that getAllInfo() reads. Because the fetch succeeds, resync()'s try/catch never fires; it overwrote the populated store with the empty list and poisoned the sessionStorage SWR cache via writeCachedSnapshot(). 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) called clearAllUpdating() before resync(), 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 stale updateAvailable: 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) before clearAllUpdating mops up any probe that didn't self-reconcile.

Tests

  • New tests/unit-ts/docker/boot.test.ts covering isTransientEmptySnapshot (transient-empty, first-load, non-empty).
  • tests/unit-ts/docker/store.test.ts pins the onBatchComplete ordering contract so a future reorder back to clear-then-resync fails loudly.
  • Full docker suite green (119 passed); lint clean; build OK.

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

Inch-high and others added 2 commits June 2, 2026 18:57
…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>
@Inch-high

Copy link
Copy Markdown
Owner Author

Follow-up (3900ed8): moved the transient-empty guard server-side

Addresses a gap in the original client-side isTransientEmptySnapshot() heuristic: it suppressed any empty snapshot while the store held rows, so it couldn't distinguish a transient mid-update empty from a genuinely empty server. Deleting every container would leave ghost rows on screen until a manual reload.

Fix — disambiguate at the source. docker-state.php now compares the two backends it already calls:

  • getAllInfo() — reads the webui-info docker.json the stock update_container flow rewrites (goes empty mid-update).
  • getDockerContainers() — hits the docker socket directly, unaffected by that rewrite = source of truth.

When info is empty but the daemon still lists containers → mid-rewrite → respond 503. fetchSnapshot() throws on the non-200 and resync()'s existing try/catch keeps current state + skips the SWR cache write, then retries. A genuinely empty server (daemon also empty) returns 200 + [] and renders the empty state.

Why this is better than the client heuristic

  • Fixes the inverse bug: a real delete-all now renders empty instead of ghosting.
  • Trigger-agnostic — works for updates from the stock UI, CA Auto Update, or the modernui page, because it keys off the daemon rather than the modernui update probes (which only the modernui page's own buttons set).

Changes

  • docker-state.php: modernui_is_transient_empty(infoCount, daemonCount) + 503 dispatch.
  • boot.ts: removed isTransientEmptySnapshot() and its guard (server is now authoritative).
  • Tests: new docker-state.test.php (4 branches); dropped the dead isTransientEmptySnapshot cases from boot.test.ts. The onBatchComplete ordering fix (feat(docker): update-progress panel + Update all + UX fixes #2) is untouched.

Verification: TS 116 passed · biome ci clean · build OK · docker-state.test.php PASS · php -l + php-cs-fixer --check clean.

🤖 Generated with Claude Code

@Inch-high
Inch-high merged commit af9c6bd into main Jun 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant