Background picture can miss its pending thumbnail Job and stay unchanged #1025

Open
opened 2026-10-03 18:32:58 +00:00 by kayg · 3 comments
Owner

A freshly uploaded background can stay on the previous background when its first thumbnail read returns 404 before the Files renderer Job finishes.

Evidence from the #759 resume: the production two-User storage probe uploaded a real oriented JPEG, saved it as the Mono light background, and could not reach #app-background[data-ready] within 30 seconds. Source lookup and saved Appearance returned 200 and matched the item. Thumbnail reads initially returned 404. Waiting for a real thumbnail 200 before saving the same background made the unchanged readiness and session-cleanup assertions pass. The native sandbox probe and WebP render both passed on this host.

apps/web/src/lib/appearance/background.svelte.ts catches a failed image decode and keeps the previous picture. It has no bounded readiness retry after that initial read. The failure is consistent with a renderer-readiness race; do not infer that every thumbnail 404 is temporary.

Please cover a pending real renderer Job with a focused regression, then reapply the selected background when its bytes become available without blocking input. Keep the last usable background during loading. Keep #555 User isolation and #765 private no-store thumbnail behavior.

This is a separate Background UX gap. #759's storage probe now prepares its real picture before testing session cleanup; it does not change product retry behavior. No existing test expectation was changed.

A freshly uploaded background can stay on the previous background when its first thumbnail read returns 404 before the Files renderer Job finishes. Evidence from the #759 resume: the production two-User storage probe uploaded a real oriented JPEG, saved it as the Mono light background, and could not reach `#app-background[data-ready]` within 30 seconds. Source lookup and saved Appearance returned 200 and matched the item. Thumbnail reads initially returned 404. Waiting for a real thumbnail 200 before saving the same background made the unchanged readiness and session-cleanup assertions pass. The native sandbox probe and WebP render both passed on this host. `apps/web/src/lib/appearance/background.svelte.ts` catches a failed image decode and keeps the previous picture. It has no bounded readiness retry after that initial read. The failure is consistent with a renderer-readiness race; do not infer that every thumbnail 404 is temporary. Please cover a pending real renderer Job with a focused regression, then reapply the selected background when its bytes become available without blocking input. Keep the last usable background during loading. Keep #555 User isolation and #765 private no-store thumbnail behavior. This is a separate Background UX gap. #759's storage probe now prepares its real picture before testing session cleanup; it does not change product retry behavior. No existing test expectation was changed.
Author
Owner

Starting #1025 work on branch job/bgthumb-1025, based on origin/dev at f2f8491ff5. I am locating the existing background and thumbnail behavior before implementing the pending-renderer retry with regression coverage.

Starting #1025 work on branch job/bgthumb-1025, based on origin/dev at f2f8491ff5c76ab28f140c964542c97362e6b119. I am locating the existing background and thumbnail behavior before implementing the pending-renderer retry with regression coverage.
Author
Owner

Source finding: BackgroundStore.apply() makes one Image.decode() attempt and swallows any failure, so a real Files thumbnail 404 leaves the selected picture unapplied with no automatic retry. imageUrl() also calls thumbUrl(..., 1), which requests immutable HTTP caching; #765 requires the private no-store response because browser HTTP cache entries cannot be cleared on User switch or Share revocation. I will add a bounded, non-blocking decode retry and use the endpoint’s no-store URL.

Source finding: `BackgroundStore.apply()` makes one `Image.decode()` attempt and swallows any failure, so a real Files thumbnail 404 leaves the selected picture unapplied with no automatic retry. `imageUrl()` also calls `thumbUrl(..., 1)`, which requests immutable HTTP caching; #765 requires the private no-store response because browser HTTP cache entries cannot be cleared on User switch or Share revocation. I will add a bounded, non-blocking decode retry and use the endpoint’s no-store URL.
Author
Owner

READY FOR MERGE: yes

Head: f6e2bdf47a37a8b7122a91a4cb889942b974c2fe

Built

  • Added a bounded thumbnail retry to the Appearance background store. It keeps the last painted picture while the selected Files thumbnail is missing, then paints it when a later read decodes. The Settings picker releases its busy state while retries run. User reset cancels stale retries.
  • Removed the immutable thumbnail version from Appearance reads. These reads now use the private no-store thumbnail path required by #765.
  • Added a store regression, a real local-server browser regression, and a hot-path profile. The browser regression uploads a real JPEG, observes the real renderer's initial 404, keeps the old background, then verifies a successful retry=1 response with private, no-store.
  • No existing test expectation changed.

Visual evidence

All captures use macOS platform emulation. Both screens are covered at 390, 820, and 1440 px in light and dark mode.

Screen Light Dark
Appearance 390 · 820 · 1440 390 · 820 · 1440
Files 390 · 820 · 1440 390 · 820 · 1440

Gates

cargo fmt --check exited 0 with no output.

bun run check:

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bgthumb-1025/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Focused Vitest (background-store.test.ts, backgroundGroup.svelte.test.ts):

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/bgthumb-1025/apps/web

 Test Files  2 passed (2)
      Tests  9 passed (9)
   Start at  05:58:48
   Duration  18.12s (transform 86%, import 5%, environment 4%, tests 3%, setup 1%)

  Transform  |component| transforming modules took 14.84s · 83% of tracked time, re-done on every run
             persist transforms across runs with fsModuleCache: true
             learn more at https://vitest.dev/guide/improving-performance#caching-between-reruns

Focused local-server e2e passed. Its relevant output was:

REAL_RENDERER_PENDING first thumbnail read returned 404
PENDING_LAYER_STATE {"itemId":null,"kind":"image","ready":false,"boot":"image","image":"","painted":true}
INITIAL_REAL_THUMBNAIL_STATUS 404
RENDERER_STATUSES_BEFORE_RETRY [404,404,404,404]
USER_AGENT_PLATFORM MacIntel

bun run build and cargo build -p calternal-server both succeeded. No Rust crate or route contract changed, so Rust clippy/test gates were not applicable. cargo clean completed: Removed 7238 files, 4.6GiB total.

Performance

The local profile measured recovery p50/p95 1686.8/1740.1 ms across 350 KB, 6 MB, and 20 MB files. The 20 MB eight-read burst measured p50/p95 57.8/124.4 ms; mean/peak server RSS was 327,417,856 bytes and CPU was 83.12% for 0.11 CPU seconds. Host load average was 4.74/7.34/9.00 before and 6.85/7.62/9.04 after. These local measurements are noisy. The current docs/perf/baseline.json Appearance API p50/p95 is 1.1/2.1 ms, which measures a different path and is not a direct retry-latency comparison.

UX gaps closed / left

Closed: the old picture stays visible during loading, the picker stops waiting on the renderer, retries do not block input, and reset cancels the previous User's retry.

Left: the retry budget is four attempts at 1.5, 3, 4.5, and 6 seconds after the first miss. A renderer that takes longer needs the User to choose the picture again; exhaustion reports the existing alert.

Decisions

DESIGN §35 and #1025 do not set retry timing. I chose four spaced retries, a retry query value to force a fresh request, and a 15-second bounded window. Retries start only after Files resolves the selected item's URL; resolution failures retain the existing failure path.

Files

apps/web/src/lib/appearance/background.svelte.ts, apps/web/src/lib/appearance/background-store.test.ts, apps/web/src/routes/settings/appearance/BackgroundGroup.svelte, apps/web/e2e/background-thumbnail-1025.mjs, apps/web/package.json, bench/bgthumb-1025.mjs.

**READY FOR MERGE: yes** Head: `f6e2bdf47a37a8b7122a91a4cb889942b974c2fe` ### Built - Added a bounded thumbnail retry to the Appearance background store. It keeps the last painted picture while the selected Files thumbnail is missing, then paints it when a later read decodes. The Settings picker releases its busy state while retries run. User reset cancels stale retries. - Removed the immutable thumbnail version from Appearance reads. These reads now use the private `no-store` thumbnail path required by #765. - Added a store regression, a real local-server browser regression, and a hot-path profile. The browser regression uploads a real JPEG, observes the real renderer's initial 404, keeps the old background, then verifies a successful `retry=1` response with `private, no-store`. - No existing test expectation changed. ### Visual evidence All captures use macOS platform emulation. Both screens are covered at 390, 820, and 1440 px in light and dark mode. | Screen | Light | Dark | | --- | --- | --- | | Appearance | [390](https://git.kayg.org/attachments/614e8b2f-2f68-4a1a-88ff-53cf455bd5e0) · [820](https://git.kayg.org/attachments/e12fbf96-9df3-4ec2-ab68-895db343e4c0) · [1440](https://git.kayg.org/attachments/75a010fa-34e4-48d9-be21-8e787a28b99c) | [390](https://git.kayg.org/attachments/05d9b050-ff5f-4e12-971c-5a9b2766ae49) · [820](https://git.kayg.org/attachments/56aced29-89ee-4ba3-b977-40bdcaf9e926) · [1440](https://git.kayg.org/attachments/07a65a90-08f1-4a65-b1cb-02fbb7404178) | | Files | [390](https://git.kayg.org/attachments/db025612-a298-4673-8420-1bd61b065af0) · [820](https://git.kayg.org/attachments/35f03700-1d0e-40d6-b90f-ab90fb38c8f6) · [1440](https://git.kayg.org/attachments/ef9deb18-b7c4-4cd3-8c85-1fdb2f5e52fa) | [390](https://git.kayg.org/attachments/bc8741bc-c4e2-4e7e-a394-a451ba982b93) · [820](https://git.kayg.org/attachments/8e6ca061-0749-4935-9275-cd0ba5ec0a80) · [1440](https://git.kayg.org/attachments/e8dac40a-e1bd-4e9d-aa3c-0d7d5fb791fc) | ### Gates `cargo fmt --check` exited 0 with no output. `bun run check`: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bgthumb-1025/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest (`background-store.test.ts`, `backgroundGroup.svelte.test.ts`): ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/bgthumb-1025/apps/web Test Files 2 passed (2) Tests 9 passed (9) Start at 05:58:48 Duration 18.12s (transform 86%, import 5%, environment 4%, tests 3%, setup 1%) Transform |component| transforming modules took 14.84s · 83% of tracked time, re-done on every run persist transforms across runs with fsModuleCache: true learn more at https://vitest.dev/guide/improving-performance#caching-between-reruns ``` Focused local-server e2e passed. Its relevant output was: ```text REAL_RENDERER_PENDING first thumbnail read returned 404 PENDING_LAYER_STATE {"itemId":null,"kind":"image","ready":false,"boot":"image","image":"","painted":true} INITIAL_REAL_THUMBNAIL_STATUS 404 RENDERER_STATUSES_BEFORE_RETRY [404,404,404,404] USER_AGENT_PLATFORM MacIntel ``` `bun run build` and `cargo build -p calternal-server` both succeeded. No Rust crate or route contract changed, so Rust clippy/test gates were not applicable. `cargo clean` completed: `Removed 7238 files, 4.6GiB total`. ### Performance The local profile measured recovery p50/p95 1686.8/1740.1 ms across 350 KB, 6 MB, and 20 MB files. The 20 MB eight-read burst measured p50/p95 57.8/124.4 ms; mean/peak server RSS was 327,417,856 bytes and CPU was 83.12% for 0.11 CPU seconds. Host load average was 4.74/7.34/9.00 before and 6.85/7.62/9.04 after. These local measurements are noisy. The current `docs/perf/baseline.json` Appearance API p50/p95 is 1.1/2.1 ms, which measures a different path and is not a direct retry-latency comparison. ### UX gaps closed / left Closed: the old picture stays visible during loading, the picker stops waiting on the renderer, retries do not block input, and reset cancels the previous User's retry. Left: the retry budget is four attempts at 1.5, 3, 4.5, and 6 seconds after the first miss. A renderer that takes longer needs the User to choose the picture again; exhaustion reports the existing alert. ### Decisions DESIGN §35 and #1025 do not set retry timing. I chose four spaced retries, a `retry` query value to force a fresh request, and a 15-second bounded window. Retries start only after Files resolves the selected item's URL; resolution failures retain the existing failure path. ### Files `apps/web/src/lib/appearance/background.svelte.ts`, `apps/web/src/lib/appearance/background-store.test.ts`, `apps/web/src/routes/settings/appearance/BackgroundGroup.svelte`, `apps/web/e2e/background-thumbnail-1025.mjs`, `apps/web/package.json`, `bench/bgthumb-1025.mjs`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#1025
No description provided.