Background picture intermittently replaced by the theme gradient (boot and in-app navigation) #535

Closed
opened 2026-09-30 17:22:40 +00:00 by kayg · 6 comments
Owner

Owner report (2026-09-30, screenshot on calternal.cloud at aa372eef6): "can we fix this intermittent theme gradient appearing even though i have set a background?"

While navigating Files (Files › Photos › 2026 › 2026-09-29, showing "Loading 0 items…"), the page shows the theme's default gradient instead of the User's picture background. It happens intermittently: on load, and sometimes during in-app navigation.
Find the real cause (do not guess), then fix it:

  • Boot: is the picture applied only after /api/v1/appearance resolves? Persist the last applied appearance (scheme, theme, background kind, and a cached low-res picture data URL or blob URL) locally, and apply it in the inline boot script before first paint.
  • Navigation: does #app-background unmount or remount on route change, or does its data-ready flip while the image reloads (a missing cache header or Cache-Control on the background image route; a new object URL each time)? The background layer must live in the root layout, never re-render on navigation, and keep the decoded image across routes.
  • The picture's URL must be stable and cacheable (ETag and immutable cache for content-addressed items).
  • Never fall back to the theme gradient while a picture is set; show the last known picture until the new one is decoded (decode() before swap).
    Proof: an e2e with a picture background that navigates 30 times across tabs and folders (with a throttled network and a cold reload), sampling the background layer each frame. It fails if any frame shows the theme gradient. Screenshots and frame strips. Coordinate with #522 (instant loading, which also touches boot); share one boot-appearance implementation. Web gates, plus Rust per crate if you change the cache headers.
## Owner report (2026-09-30, screenshot on calternal.cloud at aa372eef6): "can we fix this intermittent theme gradient appearing even though i have set a background?" While navigating Files (Files › Photos › 2026 › 2026-09-29, showing "Loading 0 items…"), the page shows the theme's default gradient instead of the User's picture background. It happens intermittently: on load, and sometimes during in-app navigation. **Find the real cause (do not guess), then fix it:** - Boot: is the picture applied only after `/api/v1/appearance` resolves? Persist the last applied appearance (scheme, theme, background kind, and a cached low-res picture data URL or blob URL) locally, and apply it in the inline boot script before first paint. - Navigation: does `#app-background` unmount or remount on route change, or does its `data-ready` flip while the image reloads (a missing cache header or `Cache-Control` on the background image route; a new object URL each time)? The background layer must live in the root layout, never re-render on navigation, and keep the decoded image across routes. - The picture's URL must be stable and cacheable (ETag and immutable cache for content-addressed items). - Never fall back to the theme gradient while a picture is set; show the last known picture until the new one is decoded (decode() before swap). **Proof:** an e2e with a picture background that navigates 30 times across tabs and folders (with a throttled network and a cold reload), sampling the background layer each frame. It fails if any frame shows the theme gradient. Screenshots and frame strips. Coordinate with #522 (instant loading, which also touches boot); share one boot-appearance implementation. Web gates, plus Rust per crate if you change the cache headers.
Author
Owner

Starting #535 on branch job/bg-flash, based on aa372eef6c. I am tracing the inline boot appearance, root layout lifetime, and the background image request/cache path before changing behavior. I will coordinate the boot implementation with #522 and add the requested navigation proof.

Starting #535 on branch job/bg-flash, based on aa372eef6c9312403f59a65d5b90cb3648ff52ed. I am tracing the inline boot appearance, root layout lifetime, and the background image request/cache path before changing behavior. I will coordinate the boot implementation with #522 and add the requested navigation proof.
Author
Owner

Finding from the code trace: the #app-background element is in apps/web/src/app.html, outside SvelteKit route content, so in-app navigation does not unmount it. The boot script restores scheme/theme/fonts but no background. backgroundStore.load() runs only after the signed-in layout learns the session, then apply() resolves the item through /api/v1/files/items/{id} and waits for image decode. If that image request fails, apply() sets data-kind=theme, which explains the gradient while the saved kind is still image. The current image URL is path-based (/api/v1/files/download?path=...) and does not carry a content-addressed cache policy. I will fix the boot gap, preserve the last decoded picture on navigation/failure, and add a versioned, authenticated thumbnail URL with an ETag and user-partitioned immutable cache.

Finding from the code trace: the `#app-background` element is in `apps/web/src/app.html`, outside SvelteKit route content, so in-app navigation does not unmount it. The boot script restores scheme/theme/fonts but no background. `backgroundStore.load()` runs only after the signed-in layout learns the session, then `apply()` resolves the item through `/api/v1/files/items/{id}` and waits for image decode. If that image request fails, `apply()` sets `data-kind=theme`, which explains the gradient while the saved kind is still `image`. The current image URL is path-based (`/api/v1/files/download?path=...`) and does not carry a content-addressed cache policy. I will fix the boot gap, preserve the last decoded picture on navigation/failure, and add a versioned, authenticated thumbnail URL with an ETag and user-partitioned immutable cache.
Author
Owner

Finding from the existing Appearance effects E2E: node apps/web/e2e/settings-effects.mjs reached the Photos/Backgrounds folder upload, received HTTP 204 for folder-small.jpg, then timed out after 30 seconds waiting for the existing accessible button Use folder-small.jpg as background at apps/web/e2e/settings-effects.mjs:675. I did not change that expected action name. The #535 cold-load/navigation E2E passes and checks the versioned thumbnail URL; this timeout is in the separate #422 folder-upload UI flow and remains for follow-up.

Finding from the existing Appearance effects E2E: `node apps/web/e2e/settings-effects.mjs` reached the Photos/Backgrounds folder upload, received HTTP 204 for `folder-small.jpg`, then timed out after 30 seconds waiting for the existing accessible button `Use folder-small.jpg as background` at `apps/web/e2e/settings-effects.mjs:675`. I did not change that expected action name. The #535 cold-load/navigation E2E passes and checks the versioned thumbnail URL; this timeout is in the separate #422 folder-upload UI flow and remains for follow-up.
Author
Owner

Completed

Root cause: #app-background stays mounted in the static app shell, but the inline boot script restored only the theme. The saved picture waited for /api/v1/appearance, Files item resolution, and image decode. During that gap, or after a failed image load, the theme mesh could appear. The old picture URL was path-based and did not provide an immutable thumbnail response.

The boot script now reads a bounded calternal.appearance-boot snapshot before first paint. It paints the last decoded picture when available. If no preview exists, it shows the paper color and hides the mesh. A replacement picture stays hidden until Image.decode() succeeds. Appearance now uses a content-hash thumbnail URL with explicit v=1. Only v=1 gets an authorized, User-private ETag response, immutable caching, and Vary: Cookie. The unversioned thumbnail route remains private, no-store.

The real-server browser proof passed with cache disabled and a throttled network. It sampled 1,408 frames and completed 30 tab/folder navigation cycles. The first picture frame painted before /api/v1/appearance resolved:

FRAME AUDIT {"frames":1408,"pictureFrames":1408,"firstPictureFrame":{"frame":1,"route":"/files","boot":"image","kind":null,"ready":false,"meshVisible":false,"painted":true,"itemId":null,"appearanceResolvedAt":null}}

The 18 screenshots cover Files, Photos, and Appearance at 390, 820, and 1440 px in light and dark.

Decisions

  • Store one versioned boot mirror in calternal.appearance-boot. It holds scheme, theme, family, kind, item ID, veil, and a JPEG preview capped at 128 px and 64 KiB.
  • Keep the previous decoded picture during replacement. If there is no preview, use paper and keep the mesh hidden until decode succeeds.
  • Cache only explicit v=1 thumbnail URLs. The ETag uses the content hash, size, and encoder version. The response is private and varies on Cookie.

Files

  • Boot: apps/web/src/app.html, apps/web/src/lib/appearance/background.svelte.ts, apps/web/src/lib/styles/background.css
  • Thumbnail cache and contract: apps/web/src/lib/files/api.ts, crates/plugins/files/src/thumbnails.rs, crates/plugins/files/src/lib.rs, contracts/openapi.json, packages/api-client/src/generated.ts
  • Proof and profile: apps/web/e2e/background-stability-535.mjs, apps/web/e2e/settings-effects.mjs, apps/web/package.json, bench/appearance.mjs, tests/adversarial/hostile_bytes.mjs, tests/adversarial/run.sh

Gate output

cargo fmt --check
(exit 0; no output)

cargo clippy -p calternal-plugin-files --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.74s

cargo test -p calternal-plugin-files
test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 127.35s

doc-tests calternal_plugin_files
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 24s

cargo test -p calternal-server
test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 12.84s

bun run --cwd apps/web check
svelte-check found 0 errors and 0 warnings

bun run --cwd apps/web test
 Test Files  140 passed (140)
      Tests  914 passed (914)
   Duration  197.21s

bun run --cwd apps/web build
✓ built in 1m 34s
> Using @sveltejs/adapter-static
  Wrote site to "build"
  ✔ done

HOSTILE_BYTES_ONLY=1 tests/adversarial/run.sh
==== HOSTILE BYTES FINDINGS 0

cargo clean
     Removed 15956 files, 8.4GiB total

The production build emitted vendor use-client directive warnings and completed. The separate settings-effects E2E uploaded folder-small.jpg with HTTP 204, then timed out after 30 seconds waiting for its existing Use folder-small.jpg as background action. I did not change that expectation; I posted this finding separately.

Local performance profile

The local profile ran on calternal-dev with load average [36.5, 29.81, 32.1]. Treat the result as noisy. The nearest baseline is docs/perf/baseline.json, GET /api/v1/appearance: p50 1.1 ms and p95 2.1 ms over 50 responses. That JSON route is not directly comparable with thumbnail downloads.

Input Thumbnail p50/p95 Eight-read burst p50/p95 Server RSS mean/peak CPU mean/peak
350 KB 224.9 / 258.6 ms 230.7 / 486.5 ms 175.7 / 187.3 MB 28.23 / 80.15%
6 MiB 70.5 / 104.4 ms 155.0 / 171.5 ms 256.8 / 344.5 MB 28.27 / 180.18%
20 MiB 77.6 / 89.7 ms 132.1 / 161.6 ms 409.8 / 597.2 MB 27.08 / 211.34%

Evidence

Files light and dark screenshot matrix

Photos light and dark screenshot matrix

Appearance light and dark screenshot matrix

Frame strip from the navigation run

Full-resolution screenshots and performance JSON

Head: a62a8156bd on job/bg-flash. The required origin/dev merge is included. No push, deploy, or issue close was made.

## Completed Root cause: #app-background stays mounted in the static app shell, but the inline boot script restored only the theme. The saved picture waited for /api/v1/appearance, Files item resolution, and image decode. During that gap, or after a failed image load, the theme mesh could appear. The old picture URL was path-based and did not provide an immutable thumbnail response. The boot script now reads a bounded calternal.appearance-boot snapshot before first paint. It paints the last decoded picture when available. If no preview exists, it shows the paper color and hides the mesh. A replacement picture stays hidden until Image.decode() succeeds. Appearance now uses a content-hash thumbnail URL with explicit v=1. Only v=1 gets an authorized, User-private ETag response, immutable caching, and Vary: Cookie. The unversioned thumbnail route remains private, no-store. The real-server browser proof passed with cache disabled and a throttled network. It sampled 1,408 frames and completed 30 tab/folder navigation cycles. The first picture frame painted before /api/v1/appearance resolved: ~~~text FRAME AUDIT {"frames":1408,"pictureFrames":1408,"firstPictureFrame":{"frame":1,"route":"/files","boot":"image","kind":null,"ready":false,"meshVisible":false,"painted":true,"itemId":null,"appearanceResolvedAt":null}} ~~~ The 18 screenshots cover Files, Photos, and Appearance at 390, 820, and 1440 px in light and dark. ## Decisions - Store one versioned boot mirror in calternal.appearance-boot. It holds scheme, theme, family, kind, item ID, veil, and a JPEG preview capped at 128 px and 64 KiB. - Keep the previous decoded picture during replacement. If there is no preview, use paper and keep the mesh hidden until decode succeeds. - Cache only explicit v=1 thumbnail URLs. The ETag uses the content hash, size, and encoder version. The response is private and varies on Cookie. ## Files - Boot: apps/web/src/app.html, apps/web/src/lib/appearance/background.svelte.ts, apps/web/src/lib/styles/background.css - Thumbnail cache and contract: apps/web/src/lib/files/api.ts, crates/plugins/files/src/thumbnails.rs, crates/plugins/files/src/lib.rs, contracts/openapi.json, packages/api-client/src/generated.ts - Proof and profile: apps/web/e2e/background-stability-535.mjs, apps/web/e2e/settings-effects.mjs, apps/web/package.json, bench/appearance.mjs, tests/adversarial/hostile_bytes.mjs, tests/adversarial/run.sh ## Gate output ~~~text cargo fmt --check (exit 0; no output) cargo clippy -p calternal-plugin-files --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.74s cargo test -p calternal-plugin-files test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 127.35s doc-tests calternal_plugin_files running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 24s cargo test -p calternal-server test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 12.84s bun run --cwd apps/web check svelte-check found 0 errors and 0 warnings bun run --cwd apps/web test Test Files 140 passed (140) Tests 914 passed (914) Duration 197.21s bun run --cwd apps/web build ✓ built in 1m 34s > Using @sveltejs/adapter-static Wrote site to "build" ✔ done HOSTILE_BYTES_ONLY=1 tests/adversarial/run.sh ==== HOSTILE BYTES FINDINGS 0 cargo clean Removed 15956 files, 8.4GiB total ~~~ The production build emitted vendor use-client directive warnings and completed. The separate settings-effects E2E uploaded folder-small.jpg with HTTP 204, then timed out after 30 seconds waiting for its existing Use folder-small.jpg as background action. I did not change that expectation; I posted this finding separately. ## Local performance profile The local profile ran on calternal-dev with load average [36.5, 29.81, 32.1]. Treat the result as noisy. The nearest baseline is docs/perf/baseline.json, GET /api/v1/appearance: p50 1.1 ms and p95 2.1 ms over 50 responses. That JSON route is not directly comparable with thumbnail downloads. | Input | Thumbnail p50/p95 | Eight-read burst p50/p95 | Server RSS mean/peak | CPU mean/peak | |---|---:|---:|---:|---:| | 350 KB | 224.9 / 258.6 ms | 230.7 / 486.5 ms | 175.7 / 187.3 MB | 28.23 / 80.15% | | 6 MiB | 70.5 / 104.4 ms | 155.0 / 171.5 ms | 256.8 / 344.5 MB | 28.27 / 180.18% | | 20 MiB | 77.6 / 89.7 ms | 132.1 / 161.6 ms | 409.8 / 597.2 MB | 27.08 / 211.34% | ## Evidence ![Files light and dark screenshot matrix](https://git.kayg.org/attachments/c357ca2d-c0f7-402d-b9a8-868a219e5aeb) ![Photos light and dark screenshot matrix](https://git.kayg.org/attachments/3f48253d-5fcd-4a87-a81a-061e9eb33e2f) ![Appearance light and dark screenshot matrix](https://git.kayg.org/attachments/624c3136-cb69-46a6-8f6b-11ced33a9907) ![Frame strip from the navigation run](https://git.kayg.org/attachments/a0f6e33d-3329-4f29-90a2-74a56b4330f5) [Full-resolution screenshots and performance JSON](https://git.kayg.org/attachments/668f9dee-a4f8-4fd7-bb3c-be362241b41c) Head: a62a8156bd84073c438118d9ff2a4a3c5c058c51 on job/bg-flash. The required origin/dev merge is included. No push, deploy, or issue close was made.
Author
Owner

#535 is complete on job/bg-flash, with #555 isolation in the same branch. The existing visual design is retained. All 1,592 sampled normal-navigation frames painted the selected picture, including the first frame before Appearance resolved. The server-generated hint prevents expired sessions from painting it. The shared User storage replaces the raw #535 keys. Attached review evidence covers all widths/themes. The common final report and exact gates follow. Neither issue is closed; no push, deploy or merge was performed except the requested origin/dev sync.

Completed #555 and the #535 cache integration on job/bg-flash.
Head: 88325a89cfe90d4a467cd8575d62eee5d703a4ae. Base: 15e17aeafc8ea160c109e62fba57f6961c39d21f.
The required single fetch/merge of origin/dev returned Already up to date.
No push, deploy or other merge ran. Neither issue is closed.

Built

  • Shared User storage for localStorage, sessionStorage, IndexedDB and Cache Storage. Keys identify the User; local/session values also carry an owner tag. Captured adapters and async operations refuse a different document User. Cleanup waits for all four backends.
  • Migrated every private browser key found on dev: Appearance preview/pictures/fonts, settings, Calendar preferences, panel widths, Files/Notes/Photos choices, Composer drafts/calendar, Money Budget selection, search recents, reminder history and push preferences. The AST reuse gate permits only documented device or anonymous Public-link exceptions.
  • The server writes the non-secret hint from the resolved session on document responses. Expired/anonymous documents remove it before boot. The inline reader paints paper for absent/mismatched hints or tags.
  • Sign-out, private API 401 and User switches clear private state and the hint. Private DOM unmounts synchronously before async deletion. Navigation opens a fresh document so Note/search/upload singletons cannot reach the next session. Other tabs reload on a session revision. Request epochs ignore late old-session 401 replies. Private streams/uploads share the expiry hook.
  • #535 boot rendering remains instant. Private thumbnails retain private caching. The push service worker has no fetch handler and does not cache private responses.
  • Added owner-tag/upgrade/late-reply tests, two-User production-browser regressions, a #331 matrix appendix and a hot-path benchmark.

Evidence

The stricter frame sampler found two frames of the previous Budget title while async deletion waited. Synchronous page removal fixes this. The fixture no longer installs duplicate cleanup listeners: the real app owns cleanup.
All assertions stayed intact. The revoked-document fixture starts rejection in the new document; the previous document has not received a 401. Sign-in screenshots wait for the real form.

All three cases passed: explicit sign-out, live revoked-session 401 and revoked-cookie document boot. Each uses two real Users in one context. A uploads a picture, opens real Mail and creates a real Money Budget. Test-only values populate the four backends. Every rAF is sampled; DOM and storage are checked after B first paints and on Mail, Money and Appearance.

#555 has 24 sign-in/Mail/Money/Appearance screenshots: 390, 820 and 1440 px, light and dark. #535 has 18 Files/Photos/Appearance screenshots and a frame strip. Claude owns visual review; screenshots are not committed.
The throttled/cold #535 navigation audit sampled 1,592 frames. Every sampled frame painted the selected picture. The first frame painted the boot picture before Appearance resolved.

Gate output (verbatim summaries)

cargo fmt --check exited 0 with no output.

cargo clippy -p calternal-plugin-files --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 01s

cargo test -p calternal-plugin-files:

test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 162.86s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.06s

cargo test -p calternal-server:

test result: ok. 94 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.22s

bun run check:

User browser caches use userStorage; only documented device/public-link exceptions remain.
svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  143 passed (143)
      Tests  930 passed (930)
   Duration  258.99s (transform 52%, environment 19%, import 15%, tests 10%, setup 3%)

bun run test src/lib/userStorage.svelte.test.ts (final async guard regression):

 Test Files  1 passed (1)
      Tests  9 passed (9)
   Duration  17.20s (transform 51%, environment 31%, tests 11%, setup 6%, import 1%)

API-client tests:

 9 pass
 0 fail
 31 expect() calls
Ran 9 tests across 1 file. [910.00ms]

bun run build:

✓ built in 1m 25s
✓ built in 105ms
✓ built in 2m 48s

The full web run passed 930 tests. The final async owner guard then added one regression; its targeted file passed nine tests. Rust gates used four build jobs, no incremental build and line-table debug info, per crate only. Full logs remain in artifacts/bg-555/*-final.log.

Corrected browser output:

PASS #555 signout: no A frame, DOM content or storage survives into B
PASS #555 expired-live: no A frame, DOM content or storage survives into B
PASS #555 expired-document: no A frame, DOM content or storage survives into B

The bounded isolation round covered this session failure class. No broad API attack suite ran.

Performance

bench/user-storage.mjs ran once on the perf VM under /root/perf.lock, with the shared release server. No compile ran there. Load inside the lock was 0.21/0.24/0.43 initially and 0.11/0.20/0.39 after setup.

Keys Read p50/p95 Write p50/p95 Clear p50/p95 Eight writers Browser CPU Browser RSS
50 0.4/2.7 ms 1.0/15.1 ms 1.1/1.7 ms 5.7 ms 1.04 s 489.5 MiB
500 3.4/4.4 ms 9.1/22.8 ms 6.8/11.6 ms 6.1 ms 2.37 s 517.3 MiB

docs/perf/baseline.json has no equivalent profile. This starts that baseline; no valid regression ratio exists. The attached #555 archive includes raw counters. docs/perf/user-storage-555.md records the run.

Known gaps

  • #423's persistent Mail rows, Money cache and warm route caches are not on this dev base. Real Mail's empty state and backend test values are covered here. #423 must adopt this module and test actual cached rows. Its branch was not edited.
  • Browser evidence is Chromium. Claude must review the screenshots. No o2 deploy was authorized.
  • An idle remote tab learns revocation on its next private 401 or document request. The hint never authorizes access.

Decisions

  • Drop legacy unowned values. Migrate only owner-keyed drafts/recents/reminder history for the resolved current User.
  • Keep documented non-personal device scalars and anonymous per-link tab passwords outside the User namespace. Panel widths are User preferences.
  • Set the hint on HTML responses rather than all API replies, so late replies cannot replace a new hint.
  • Open a fresh document at session end/switch after deletion. Remove old private DOM synchronously during the wait.
  • Use one IndexedDB database and one Cache Storage cache per User. Keep Storage-compatible seams, plus async methods for future caches.

Files

apps/web/e2e/background-stability-535.mjs
apps/web/e2e/harness.mjs
apps/web/e2e/settings-effects.mjs
apps/web/e2e/user-storage-555.mjs
apps/web/e2e/user-storage-fixtures.mjs
apps/web/package.json
apps/web/scripts/check-user-storage.mjs
apps/web/src/app.html
apps/web/src/lib/actions/edgeResize.ts
apps/web/src/lib/appearance/background-store.test.ts
apps/web/src/lib/appearance/background.svelte.ts
apps/web/src/lib/auth/components/CreateAccountFlow.svelte
apps/web/src/lib/auth/components/RecoverFlow.svelte
apps/web/src/lib/auth/session-expiry.svelte.test.ts
apps/web/src/lib/calendar/prefs.ts
apps/web/src/lib/components/app-sidebar.svelte
apps/web/src/lib/components/search-dialog.svelte
apps/web/src/lib/composer/Composer.svelte
apps/web/src/lib/composer/drafts.svelte.ts
apps/web/src/lib/composer/drafts.test.ts
apps/web/src/lib/composer/nlp.ts
apps/web/src/lib/editor/format/reminderTimeHistory.ts
apps/web/src/lib/files/api.ts
apps/web/src/lib/files/prefs.svelte.ts
apps/web/src/lib/files/uploads.svelte.ts
apps/web/src/lib/mail/attachments.ts
apps/web/src/lib/money/store.svelte.ts
apps/web/src/lib/notes/NoteView.svelte
apps/web/src/lib/notes/NotesExplorer.svelte
apps/web/src/lib/notifications/push.ts
apps/web/src/lib/photos/PhotosView.svelte
apps/web/src/lib/search/recent.ts
apps/web/src/lib/stores/settings-store.ts
apps/web/src/lib/styles/background.css
apps/web/src/lib/themes.test.ts
apps/web/src/lib/userStorage-boot.test.ts
apps/web/src/lib/userStorage.svelte.test.ts
apps/web/src/lib/userStorage.ts
apps/web/src/routes/+layout.svelte
apps/web/src/routes/login/+page.svelte
apps/web/src/routes/settings/admin/BackupsGroup.svelte
apps/web/src/routes/settings/admin/ConfigGroup.svelte
apps/web/src/routes/settings/appearance/BackgroundGroup.svelte
bench/appearance.mjs
bench/user-storage.mjs
contracts/openapi.json
crates/calternal-server/src/wire.rs
crates/plugins/files/src/lib.rs
crates/plugins/files/src/thumbnails.rs
docs/audits/browser-user-storage-555.md
docs/audits/cross-user-2026-09-28.md
docs/perf/user-storage-555.md
packages/api-client/src/generated.ts
packages/api-client/src/index.ts
tests/adversarial/hostile_bytes.mjs
tests/adversarial/run.sh

Cleanup

cargo clean exited 0:

     Removed 15956 files, 8.4GiB total

Removed the production web build, SvelteKit output and job temporary files. Review artifacts remain in the worktree. The branch is clean.

#535 is complete on `job/bg-flash`, with #555 isolation in the same branch. The existing visual design is retained. All 1,592 sampled normal-navigation frames painted the selected picture, including the first frame before Appearance resolved. The server-generated hint prevents expired sessions from painting it. The shared User storage replaces the raw #535 keys. Attached review evidence covers all widths/themes. The common final report and exact gates follow. Neither issue is closed; no push, deploy or merge was performed except the requested origin/dev sync. Completed #555 and the #535 cache integration on `job/bg-flash`. Head: `88325a89cfe90d4a467cd8575d62eee5d703a4ae`. Base: `15e17aeafc8ea160c109e62fba57f6961c39d21f`. The required single fetch/merge of `origin/dev` returned `Already up to date.` No push, deploy or other merge ran. Neither issue is closed. ## Built - Shared User storage for localStorage, sessionStorage, IndexedDB and Cache Storage. Keys identify the User; local/session values also carry an owner tag. Captured adapters and async operations refuse a different document User. Cleanup waits for all four backends. - Migrated every private browser key found on dev: Appearance preview/pictures/fonts, settings, Calendar preferences, panel widths, Files/Notes/Photos choices, Composer drafts/calendar, Money Budget selection, search recents, reminder history and push preferences. The AST reuse gate permits only documented device or anonymous Public-link exceptions. - The server writes the non-secret hint from the resolved session on document responses. Expired/anonymous documents remove it before boot. The inline reader paints paper for absent/mismatched hints or tags. - Sign-out, private API 401 and User switches clear private state and the hint. Private DOM unmounts synchronously before async deletion. Navigation opens a fresh document so Note/search/upload singletons cannot reach the next session. Other tabs reload on a session revision. Request epochs ignore late old-session 401 replies. Private streams/uploads share the expiry hook. - #535 boot rendering remains instant. Private thumbnails retain private caching. The push service worker has no fetch handler and does not cache private responses. - Added owner-tag/upgrade/late-reply tests, two-User production-browser regressions, a #331 matrix appendix and a hot-path benchmark. ## Evidence The stricter frame sampler found two frames of the previous Budget title while async deletion waited. Synchronous page removal fixes this. The fixture no longer installs duplicate cleanup listeners: the real app owns cleanup. All assertions stayed intact. The revoked-document fixture starts rejection in the new document; the previous document has not received a 401. Sign-in screenshots wait for the real form. All three cases passed: explicit sign-out, live revoked-session 401 and revoked-cookie document boot. Each uses two real Users in one context. A uploads a picture, opens real Mail and creates a real Money Budget. Test-only values populate the four backends. Every rAF is sampled; DOM and storage are checked after B first paints and on Mail, Money and Appearance. #555 has 24 sign-in/Mail/Money/Appearance screenshots: 390, 820 and 1440 px, light and dark. #535 has 18 Files/Photos/Appearance screenshots and a frame strip. Claude owns visual review; screenshots are not committed. The throttled/cold #535 navigation audit sampled 1,592 frames. Every sampled frame painted the selected picture. The first frame painted the boot picture before Appearance resolved. - [review-555.zip](https://git.kayg.org/attachments/4015dd44-6938-4fe7-94ef-f3798f94955b) (#555) - [review-535.zip](https://git.kayg.org/attachments/6c2e937f-6fdb-4043-b9ac-ca0fac2101b5) (#535) - [appearance-b-dark-1440.png](https://git.kayg.org/attachments/1851f9a4-25c7-4946-ad7a-73de2d17ccbd) (#555) - [mail-b-light-390.png](https://git.kayg.org/attachments/f3b09630-7d08-42a6-b140-ea45f1f33587) (#555) - [navigation-frame-strip.png](https://git.kayg.org/attachments/924e43c2-6991-4785-b82b-82b80d307754) (#535) ## Gate output (verbatim summaries) `cargo fmt --check` exited 0 with no output. `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 7m 01s ``` `cargo test -p calternal-plugin-files`: ``` test result: ok. 136 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 162.86s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.06s ``` `cargo test -p calternal-server`: ``` test result: ok. 94 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.22s ``` `bun run check`: ``` User browser caches use userStorage; only documented device/public-link exceptions remain. svelte-check found 0 errors and 0 warnings ``` `bun run test`: ``` Test Files 143 passed (143) Tests 930 passed (930) Duration 258.99s (transform 52%, environment 19%, import 15%, tests 10%, setup 3%) ``` `bun run test src/lib/userStorage.svelte.test.ts (final async guard regression)`: ``` Test Files 1 passed (1) Tests 9 passed (9) Duration 17.20s (transform 51%, environment 31%, tests 11%, setup 6%, import 1%) ``` `API-client tests`: ``` 9 pass 0 fail 31 expect() calls Ran 9 tests across 1 file. [910.00ms] ``` `bun run build`: ``` ✓ built in 1m 25s ✓ built in 105ms ✓ built in 2m 48s ``` The full web run passed 930 tests. The final async owner guard then added one regression; its targeted file passed nine tests. Rust gates used four build jobs, no incremental build and line-table debug info, per crate only. Full logs remain in `artifacts/bg-555/*-final.log`. Corrected browser output: ``` PASS #555 signout: no A frame, DOM content or storage survives into B PASS #555 expired-live: no A frame, DOM content or storage survives into B PASS #555 expired-document: no A frame, DOM content or storage survives into B ``` The bounded isolation round covered this session failure class. No broad API attack suite ran. ## Performance `bench/user-storage.mjs` ran once on the perf VM under `/root/perf.lock`, with the shared release server. No compile ran there. Load inside the lock was 0.21/0.24/0.43 initially and 0.11/0.20/0.39 after setup. | Keys | Read p50/p95 | Write p50/p95 | Clear p50/p95 | Eight writers | Browser CPU | Browser RSS | | --- | --- | --- | --- | --- | --- | --- | | 50 | 0.4/2.7 ms | 1.0/15.1 ms | 1.1/1.7 ms | 5.7 ms | 1.04 s | 489.5 MiB | | 500 | 3.4/4.4 ms | 9.1/22.8 ms | 6.8/11.6 ms | 6.1 ms | 2.37 s | 517.3 MiB | `docs/perf/baseline.json` has no equivalent profile. This starts that baseline; no valid regression ratio exists. The attached #555 archive includes raw counters. `docs/perf/user-storage-555.md` records the run. ## Known gaps - #423's persistent Mail rows, Money cache and warm route caches are not on this dev base. Real Mail's empty state and backend test values are covered here. #423 must adopt this module and test actual cached rows. Its branch was not edited. - Browser evidence is Chromium. Claude must review the screenshots. No o2 deploy was authorized. - An idle remote tab learns revocation on its next private 401 or document request. The hint never authorizes access. ## Decisions - Drop legacy unowned values. Migrate only owner-keyed drafts/recents/reminder history for the resolved current User. - Keep documented non-personal device scalars and anonymous per-link tab passwords outside the User namespace. Panel widths are User preferences. - Set the hint on HTML responses rather than all API replies, so late replies cannot replace a new hint. - Open a fresh document at session end/switch after deletion. Remove old private DOM synchronously during the wait. - Use one IndexedDB database and one Cache Storage cache per User. Keep Storage-compatible seams, plus async methods for future caches. ## Files ``` apps/web/e2e/background-stability-535.mjs apps/web/e2e/harness.mjs apps/web/e2e/settings-effects.mjs apps/web/e2e/user-storage-555.mjs apps/web/e2e/user-storage-fixtures.mjs apps/web/package.json apps/web/scripts/check-user-storage.mjs apps/web/src/app.html apps/web/src/lib/actions/edgeResize.ts apps/web/src/lib/appearance/background-store.test.ts apps/web/src/lib/appearance/background.svelte.ts apps/web/src/lib/auth/components/CreateAccountFlow.svelte apps/web/src/lib/auth/components/RecoverFlow.svelte apps/web/src/lib/auth/session-expiry.svelte.test.ts apps/web/src/lib/calendar/prefs.ts apps/web/src/lib/components/app-sidebar.svelte apps/web/src/lib/components/search-dialog.svelte apps/web/src/lib/composer/Composer.svelte apps/web/src/lib/composer/drafts.svelte.ts apps/web/src/lib/composer/drafts.test.ts apps/web/src/lib/composer/nlp.ts apps/web/src/lib/editor/format/reminderTimeHistory.ts apps/web/src/lib/files/api.ts apps/web/src/lib/files/prefs.svelte.ts apps/web/src/lib/files/uploads.svelte.ts apps/web/src/lib/mail/attachments.ts apps/web/src/lib/money/store.svelte.ts apps/web/src/lib/notes/NoteView.svelte apps/web/src/lib/notes/NotesExplorer.svelte apps/web/src/lib/notifications/push.ts apps/web/src/lib/photos/PhotosView.svelte apps/web/src/lib/search/recent.ts apps/web/src/lib/stores/settings-store.ts apps/web/src/lib/styles/background.css apps/web/src/lib/themes.test.ts apps/web/src/lib/userStorage-boot.test.ts apps/web/src/lib/userStorage.svelte.test.ts apps/web/src/lib/userStorage.ts apps/web/src/routes/+layout.svelte apps/web/src/routes/login/+page.svelte apps/web/src/routes/settings/admin/BackupsGroup.svelte apps/web/src/routes/settings/admin/ConfigGroup.svelte apps/web/src/routes/settings/appearance/BackgroundGroup.svelte bench/appearance.mjs bench/user-storage.mjs contracts/openapi.json crates/calternal-server/src/wire.rs crates/plugins/files/src/lib.rs crates/plugins/files/src/thumbnails.rs docs/audits/browser-user-storage-555.md docs/audits/cross-user-2026-09-28.md docs/perf/user-storage-555.md packages/api-client/src/generated.ts packages/api-client/src/index.ts tests/adversarial/hostile_bytes.mjs tests/adversarial/run.sh ``` ## Cleanup `cargo clean` exited 0: ``` Removed 15956 files, 8.4GiB total ``` Removed the production web build, SvelteKit output and job temporary files. Review artifacts remain in the worktree. The branch is clean.
Author
Owner

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).

Shipped in merge round 4, deployed to calternal.cloud in 1af8ead26 (healthy).
kayg closed this issue 2026-10-01 09:17:50 +00:00
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#535
No description provided.