Appearance: uploaded background picture shows 'Adding…' then nothing; plus a visible-effect e2e sweep for every Settings control #422

Closed
opened 2026-09-29 09:31:27 +00:00 by kayg · 31 comments
Owner

Bug (owner, 2026-09-29, on calternal.cloud at a291809c)

"I added a background via Settings → Appearance → Background → Picture and uploaded a picture. It said 'Adding' and then did nothing. You really gotta test these things."

Evidence from production (orchestrator)

  • Two uploads of the same 349,693-byte JPEG reached .calternal/backgrounds/: background-mumh5lfv.jpg finished at 09:29:11.7Z, and background-mumh5zjo.jpg at 09:29:29.4Z. Both TUS uploads succeeded (patch_total about 140–160 ms).
  • The first attempt never saved a setting. Only the second attempt wrote .calternal/settings.json, at 09:29:29.7Z. Hypothesis: installFile in apps/web/src/routes/settings/appearance/BackgroundGroup.svelte goes from uploads.one to stat() to pick(), and stat() runs before the item is indexed or has an itemId. It throws, and the catch shows a generic message, or none if the message is not visible in the owner's layout. Prove or refute this.
  • The saved value is appearance.backgrounds.bloom.light = {kind: image, item_id: 2ef6c246-…, source: upload, dim: 0}, while auto_scheme.mode is auto. After the save, the owner still saw no background. Hypotheses to test:
    • (a) The picture is saved for the variant that is not showing (scheme or theme mismatch: which theme and scheme does the page apply?).
    • (b) #app-background never reaches data-ready for an image under the hidden .calternal/ folder: the content or thumbnail route may refuse hidden paths, or the item-id lookup may fail.
    • (c) The busy state or uploads.one never settles in some path.
  • The existing e2e (apps/web/e2e/appearance-review.mjs:331) passes with a small fixture, so it does not reflect real use. Find what differs: the real photo size (use a 350 KB and a 6 MB camera JPEG with EXIF orientation), theme Bloom with auto scheme, a fresh User with an empty .calternal/backgrounds/, and a second upload.

Fix

  • Root cause first, written on this issue.
  • Make the flow deterministic: after the upload, get the item ID from the upload result or the server response instead of racing the index.
  • Visible state at every step: uploading with progress, applying, and then the background visible behind the Settings sheet. A real error names the failed step and offers Retry. It is never silent.
  • Save the picture for the variant the User is looking at, and say so ("for Bloom · Light"), with an obvious way to use it for both schemes.
  • Do not leave orphans: a failed attempt deletes its uploaded file, or reuses it on retry.

"Test these things" (owner)

Add a production-build e2e sweep, apps/web/e2e/settings-effects.mjs. For every control in Settings → Appearance (and then every other Settings section, as far as time allows), it changes the value the way a User does and asserts the visible effect on the real page, not just that a request was sent. Examples: the background image is rendered and painted (sample pixels), a theme changes computed colours, fonts change computed font-family, and the calendar format changes rendered times. It runs against a fresh User and against a User with existing settings. Record which controls have no visible-effect assertion yet, as a list on this issue.

Proof

Screenshots before and after at 390, 820 and 1440 px, light and dark: the background visible behind Settings and on Calendar. The new e2e must pass, and must fail on the pre-fix head (show that run). Gates as in the preamble.

## Bug (owner, 2026-09-29, on calternal.cloud at a291809c) "I added a background via Settings → Appearance → Background → Picture and uploaded a picture. It said 'Adding' and then did nothing. You really gotta test these things." ## Evidence from production (orchestrator) - Two uploads of the same 349,693-byte JPEG reached `.calternal/backgrounds/`: `background-mumh5lfv.jpg` finished at 09:29:11.7Z, and `background-mumh5zjo.jpg` at 09:29:29.4Z. Both TUS uploads succeeded (`patch_total` about 140–160 ms). - **The first attempt never saved a setting.** Only the second attempt wrote `.calternal/settings.json`, at 09:29:29.7Z. Hypothesis: `installFile` in `apps/web/src/routes/settings/appearance/BackgroundGroup.svelte` goes from `uploads.one` to `stat()` to `pick()`, and `stat()` runs before the item is indexed or has an `itemId`. It throws, and the catch shows a generic message, or none if the message is not visible in the owner's layout. Prove or refute this. - The saved value is `appearance.backgrounds.bloom.light = {kind: image, item_id: 2ef6c246-…, source: upload, dim: 0}`, while `auto_scheme.mode` is auto. After the save, the owner still saw no background. Hypotheses to test: - (a) The picture is saved for the variant that is *not* showing (scheme or theme mismatch: which theme and scheme does the page apply?). - (b) `#app-background` never reaches `data-ready` for an image under the hidden `.calternal/` folder: the content or thumbnail route may refuse hidden paths, or the item-id lookup may fail. - (c) The busy state or `uploads.one` never settles in some path. - The existing e2e (`apps/web/e2e/appearance-review.mjs:331`) passes with a small fixture, so it does not reflect real use. Find what differs: the real photo size (use a 350 KB and a 6 MB camera JPEG with EXIF orientation), theme Bloom with auto scheme, a fresh User with an empty `.calternal/backgrounds/`, and a second upload. ## Fix - Root cause first, written on this issue. - Make the flow deterministic: after the upload, get the item ID from the upload result or the server response instead of racing the index. - Visible state at every step: uploading with progress, applying, and then the background visible behind the Settings sheet. A real error names the failed step and offers Retry. It is never silent. - Save the picture for the variant the User is looking at, and say so ("for Bloom · Light"), with an obvious way to use it for both schemes. - Do not leave orphans: a failed attempt deletes its uploaded file, or reuses it on retry. ## "Test these things" (owner) Add a production-build e2e sweep, `apps/web/e2e/settings-effects.mjs`. For **every** control in Settings → Appearance (and then every other Settings section, as far as time allows), it changes the value the way a User does and asserts the **visible effect** on the real page, not just that a request was sent. Examples: the background image is rendered and painted (sample pixels), a theme changes computed colours, fonts change computed font-family, and the calendar format changes rendered times. It runs against a fresh User and against a User with existing settings. Record which controls have no visible-effect assertion yet, as a list on this issue. ## Proof Screenshots before and after at 390, 820 and 1440 px, light and dark: the background visible behind Settings and on Calendar. The new e2e must pass, and must fail on the pre-fix head (show that run). Gates as in the preamble.
Author
Owner

Starting work on branch job/bg-422 from dev at base SHA 191b179baa. I am tracing the upload and rendering flow before changing it.

Starting work on branch job/bg-422 from dev at base SHA 191b179baac3ef4f5bebfe07ce91c4b7a887ace2. I am tracing the upload and rendering flow before changing it.
Author
Owner

Owner decision: background pictures are stored in the visible Photos/Backgrounds/ folder (file over app), not in .calternal/backgrounds/. A picture chosen from the Photos library is referenced in place. Existing files migrate once, keeping their item IDs. The job was relaunched with this in scope.

Owner decision: background pictures are stored in the visible Photos/Backgrounds/ folder (file over app), not in .calternal/backgrounds/. A picture chosen from the Photos library is referenced in place. Existing files migrate once, keeping their item IDs. The job was relaunched with this in scope.
Author
Owner

Starting issue #422 on job/bg-422, based on dev at 191b179baac3ef4f5bebfe07ce91c4b7a887ace2. I am tracing the production upload-to-setting path and the existing Photos/storage APIs before changing it. I will record the root cause with evidence once reproduced or disproved.

Starting issue #422 on `job/bg-422`, based on `dev` at `191b179baac3ef4f5bebfe07ce91c4b7a887ace2`. I am tracing the production upload-to-setting path and the existing Photos/storage APIs before changing it. I will record the root cause with evidence once reproduced or disproved.
Author
Owner

Root cause finding: the evidence does not support the stat() index race. In crates/plugins/files/src/uploads.rs, Tus finalization calls complete_install, which commits and verifies the files_index row before the final PATCH response is returned. The current component then calls stat() after uploads.one() settles. I cannot attribute the first production attempt to a specific failed request because its response trace is not available here.

The confirmed UI defect is in BackgroundGroup.svelte: installFile() awaits pick(...) but ignores its boolean result. backgroundStore.saveBackground() returns false on a rejected Appearance PUT and rolls the visible value back; installFile() still reaches finally, clears busy, and gives no step-specific retry. The flow also has one “Adding picture…” label for both transfer and apply, so a successful upload followed by a failed save appears to stop. The fix will use the stable item ID from the Tus completion response and keep the uploaded item available for a save retry.

Root cause finding: the evidence does not support the `stat()` index race. In `crates/plugins/files/src/uploads.rs`, Tus finalization calls `complete_install`, which commits and verifies the `files_index` row before the final PATCH response is returned. The current component then calls `stat()` after `uploads.one()` settles. I cannot attribute the first production attempt to a specific failed request because its response trace is not available here. The confirmed UI defect is in `BackgroundGroup.svelte`: `installFile()` awaits `pick(...)` but ignores its boolean result. `backgroundStore.saveBackground()` returns `false` on a rejected Appearance PUT and rolls the visible value back; `installFile()` still reaches `finally`, clears `busy`, and gives no step-specific retry. The flow also has one “Adding picture…” label for both transfer and apply, so a successful upload followed by a failed save appears to stop. The fix will use the stable item ID from the Tus completion response and keep the uploaded item available for a save retry.
Author
Owner

Finding: the suspected upload/index race is refuted by the Tus finalization path. The server commits the installed Files item before it returns the successful final PATCH response. The client still now uses the item ID returned in that response, removing the lookup dependency entirely.

The concrete UI defect is that the old installFile path discarded the false result from saveBackground. A failed Appearance write rolled back locally and left the uploaded file with no step-specific message or retry. The failed first production attempt is consistent with this defect, though its exact HTTP failure was not recorded. I am also moving legacy hidden-folder images during Appearance reads so existing saved item IDs resolve through the visible Photos folder.

Finding: the suspected upload/index race is refuted by the Tus finalization path. The server commits the installed Files item before it returns the successful final PATCH response. The client still now uses the item ID returned in that response, removing the lookup dependency entirely. The concrete UI defect is that the old `installFile` path discarded the `false` result from `saveBackground`. A failed Appearance write rolled back locally and left the uploaded file with no step-specific message or retry. The failed first production attempt is consistent with this defect, though its exact HTTP failure was not recorded. I am also moving legacy hidden-folder images during Appearance reads so existing saved item IDs resolve through the visible Photos folder.
Author
Owner

Report — implementation committed; production proof incomplete

Root cause finding

The suspected stat()-before-index race is refuted by the Tus finalization flow: the server commits the Files item before it returns the successful final PATCH response. The old installFile path did discard saveBackground()'s false result, however. That leaves an uploaded file unapplied with no failed-step message or retry. Production logs did not capture the first attempt's exact HTTP failure, so I cannot claim that was the specific trigger in that incident. Appearance reads now also migrate legacy hidden-folder files, preserving their Files item IDs.

Built

  • Tus completion returns the indexed Files item ID. The Appearance upload uses that identity directly.
  • Picture installation shows upload progress, applying and paint states, the active family/scheme, and a step-specific Retry. A failed apply reuses the uploaded item. The User can apply it to both schemes.
  • Uploads and Unsplash files go to Photos/Backgrounds/; uploads keep their original name with collision suffixes. Unsplash images get an XMP Sidecar with dc:creator and dc:source. Photos picks stay in place.
  • Legacy hidden backgrounds migrate on the first Appearance read. The Backgrounds folder has a stable Photos route and Copy link, and stays out of the Photos timeline and Memories.
  • Added apps/web/e2e/settings-effects.mjs for production Settings controls, real 350 KB and 6 MB oriented JPEGs, a forced Appearance write failure/retry, painted pixels, and the requested screenshot matrix.

Commits

Head: 5372331db2c90ab7337bae05c379a3ea398dfef1 (docs: explain deterministic background upload identities). Earlier feature commits: 92046968, c6f53f15, b7987302, 70045132, 26f1dc87, b116a540, cc521c1b, 2b37df9d; merged local dev once at 3c3937c5.

Gates and evidence

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

bun run build
✓ built in 1m 5s
  Wrote site to "build"
  ✔ done

bun run check
svelte-check found 0 errors and 0 warnings

bun run test (post-merge)
 Test Files  126 passed (126)
      Tests  809 passed (809)
   Start at  17:05:37
   Duration  221.57s (transform 50%, environment 21%, import 14%, tests 11%, setup 4%)

bunx vitest run src/lib/photos/photos-units.test.ts src/lib/files/UploadToast.svelte.test.ts (post-merge)
 Test Files  2 passed (2)
      Tests  9 passed (9)
   Start at  17:02:46
   Duration  39.74s (transform 89%, environment 5%, import 5%, setup 1%, tests 1%)

cargo test -p calternal-plugin-files (before merge; source unchanged by merge)
test result: ok. 131 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 134.63s

cargo test -p calternal-plugin-photos (before merge; source unchanged by merge)
test result: ok. 45 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 15.64s

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

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

cargo clippy/test -p calternal-fs
Passed before merge: 39 unit tests and 42 storage tests.

cargo clippy -p calternal-server --all-targets -- -D warnings
Stopped at the four-hour job limit while compiling dependencies (exit 130).

cargo clean output:

Removed 12729 files, 5.2GiB total

Outstanding proof and controls

I did not finish the server clippy/test gates, build the local server, run the new production e2e, run its pre-fix failure check, run the adversarial probe, or capture/attach the 390/820/1440 light/dark screenshots. The e2e is committed but unverified. The full issue proof remains incomplete.

The sweep has no visible-effect assertion yet for Unsplash search/download/credit (the isolated User has no Unsplash key), Display and Mono font roles, or Settings sections beyond Appearance and Calendar date/time formats. It also does not exercise every theme/font option.

Decisions not already covered by DESIGN

  • The final Tus response field is Upload-Item-ID, so the client gets a stable identity without another lookup.
  • The idempotent migration runs on the first Appearance read; it logs and defers a failed move so the next read can retry.
  • Unsplash filenames use a stable BLAKE3 digest of the remote photo ID, so retries reuse the file and the remote ID never becomes a path component.
  • The e2e runner uses Node because Sharp's native loader failed under Bun on this host.
## Report — implementation committed; production proof incomplete ### Root cause finding The suspected `stat()`-before-index race is refuted by the Tus finalization flow: the server commits the Files item before it returns the successful final PATCH response. The old `installFile` path did discard `saveBackground()`'s `false` result, however. That leaves an uploaded file unapplied with no failed-step message or retry. Production logs did not capture the first attempt's exact HTTP failure, so I cannot claim that was the specific trigger in that incident. Appearance reads now also migrate legacy hidden-folder files, preserving their Files item IDs. ### Built - Tus completion returns the indexed Files item ID. The Appearance upload uses that identity directly. - Picture installation shows upload progress, applying and paint states, the active family/scheme, and a step-specific Retry. A failed apply reuses the uploaded item. The User can apply it to both schemes. - Uploads and Unsplash files go to `Photos/Backgrounds/`; uploads keep their original name with collision suffixes. Unsplash images get an XMP Sidecar with `dc:creator` and `dc:source`. Photos picks stay in place. - Legacy hidden backgrounds migrate on the first Appearance read. The Backgrounds folder has a stable Photos route and Copy link, and stays out of the Photos timeline and Memories. - Added `apps/web/e2e/settings-effects.mjs` for production Settings controls, real 350 KB and 6 MB oriented JPEGs, a forced Appearance write failure/retry, painted pixels, and the requested screenshot matrix. ### Commits Head: `5372331db2c90ab7337bae05c379a3ea398dfef1` (`docs: explain deterministic background upload identities`). Earlier feature commits: `92046968`, `c6f53f15`, `b7987302`, `70045132`, `26f1dc87`, `b116a540`, `cc521c1b`, `2b37df9d`; merged local `dev` once at `3c3937c5`. ### Gates and evidence ```text cargo fmt --check (exit 0; no output) bun run build ✓ built in 1m 5s Wrote site to "build" ✔ done bun run check svelte-check found 0 errors and 0 warnings bun run test (post-merge) Test Files 126 passed (126) Tests 809 passed (809) Start at 17:05:37 Duration 221.57s (transform 50%, environment 21%, import 14%, tests 11%, setup 4%) bunx vitest run src/lib/photos/photos-units.test.ts src/lib/files/UploadToast.svelte.test.ts (post-merge) Test Files 2 passed (2) Tests 9 passed (9) Start at 17:02:46 Duration 39.74s (transform 89%, environment 5%, import 5%, setup 1%, tests 1%) cargo test -p calternal-plugin-files (before merge; source unchanged by merge) test result: ok. 131 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 134.63s cargo test -p calternal-plugin-photos (before merge; source unchanged by merge) test result: ok. 45 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 15.64s cargo clippy -p calternal-plugin-files --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 54s cargo clippy -p calternal-plugin-photos --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 18m 43s cargo clippy/test -p calternal-fs Passed before merge: 39 unit tests and 42 storage tests. cargo clippy -p calternal-server --all-targets -- -D warnings Stopped at the four-hour job limit while compiling dependencies (exit 130). ``` `cargo clean` output: ```text Removed 12729 files, 5.2GiB total ``` ### Outstanding proof and controls I did not finish the server clippy/test gates, build the local server, run the new production e2e, run its pre-fix failure check, run the adversarial probe, or capture/attach the 390/820/1440 light/dark screenshots. The e2e is committed but unverified. The full issue proof remains incomplete. The sweep has no visible-effect assertion yet for Unsplash search/download/credit (the isolated User has no Unsplash key), Display and Mono font roles, or Settings sections beyond Appearance and Calendar date/time formats. It also does not exercise every theme/font option. ### Decisions not already covered by DESIGN - The final Tus response field is `Upload-Item-ID`, so the client gets a stable identity without another lookup. - The idempotent migration runs on the first Appearance read; it logs and defers a failed move so the next read can retry. - Unsplash filenames use a stable BLAKE3 digest of the remote photo ID, so retries reuse the file and the remote ID never becomes a path component. - The e2e runner uses Node because Sharp's native loader failed under Bun on this host.
Author
Owner

Starting proof round 2 on branch job/bg-422 at 5372331db2; base is origin/dev 43c1377c8f. I read CLAUDE.md, CONTEXT.md, and DESIGN.md §35. The worktree is clean. I’m now validating the pre-fix failure, current production build, exact upload/migration paths, and the required gates.

Starting proof round 2 on branch job/bg-422 at 5372331db2c90ab7337bae05c379a3ea398dfef1; base is origin/dev 43c1377c8fd3b1b495841446989a64156f8ae1e4. I read CLAUDE.md, CONTEXT.md, and DESIGN.md §35. The worktree is clean. I’m now validating the pre-fix failure, current production build, exact upload/migration paths, and the required gates.
Author
Owner

Pre-fix proof, built from b116a540b4 (the parent of the accepted client fix): the isolated production Settings repro used a fresh User, Bloom, Auto, and an exact 350,000-byte JPEG with EXIF orientation 6. It failed as expected when the one-shot stale-Index probe returned 404 to the old post-upload Files stat request: .calternal/backgrounds/background-mumwqqjj.jpg. The client did not reach the Appearance assignment. This proves the old flow depends on the race-prone path lookup when the Index has not published the upload. The probe uses a controlled 404; it does not claim that the race reproduced naturally on this local host.

Pre-fix proof, built from b116a540b4babdc59a76ec4e3f26819fc3188c24 (the parent of the accepted client fix): the isolated production Settings repro used a fresh User, Bloom, Auto, and an exact 350,000-byte JPEG with EXIF orientation 6. It failed as expected when the one-shot stale-Index probe returned 404 to the old post-upload Files stat request: `.calternal/backgrounds/background-mumwqqjj.jpg`. The client did not reach the Appearance assignment. This proves the old flow depends on the race-prone path lookup when the Index has not published the upload. The probe uses a controlled 404; it does not claim that the race reproduced naturally on this local host.
Author
Owner

Current-head proof: after rebuilding the production SPA and server from the fixed branch, the isolated fresh-User Bloom/Auto repro passed with an exact 350,000-byte JPEG carrying EXIF orientation 6. It completed the Tus PATCH with an Upload-Item-ID, made no post-upload Files stat request, saved the ID to the currently visible Bloom · Dark slot, and painted it behind Settings. The first run exposed a harness ordering race: the optimistic background could paint before the Appearance PUT finished. The probe now waits for the successful PUT before checking persisted server state; the rerun passed.

Current-head proof: after rebuilding the production SPA and server from the fixed branch, the isolated fresh-User Bloom/Auto repro passed with an exact 350,000-byte JPEG carrying EXIF orientation 6. It completed the Tus PATCH with an Upload-Item-ID, made no post-upload Files stat request, saved the ID to the currently visible Bloom · Dark slot, and painted it behind Settings. The first run exposed a harness ordering race: the optimistic background could paint before the Appearance PUT finished. The probe now waits for the successful PUT before checking persisted server state; the rerun passed.
Author
Owner

The first migration test run stopped at the item-ID lookup with RowNotFound: its fixture inserted a files_index row without the filesystem fingerprint fields used by Files identity checks. The migrated file was present, but that incomplete row could not prove a stable ID. I changed the fixture to run Files reconciliation against the real Home entry, then assert the reconciled ID stays indexed at Photos/Backgrounds and remains writable from saved Appearance state. I am rerunning the focused test now.

The first migration test run stopped at the item-ID lookup with `RowNotFound`: its fixture inserted a files_index row without the filesystem fingerprint fields used by Files identity checks. The migrated file was present, but that incomplete row could not prove a stable ID. I changed the fixture to run Files reconciliation against the real Home entry, then assert the reconciled ID stays indexed at Photos/Backgrounds and remains writable from saved Appearance state. I am rerunning the focused test now.
Author
Owner

Migration proof on the current head passed. A real indexed file at .calternal/backgrounds/legacy.jpg moved to Photos/Backgrounds/legacy.jpg; the file ID stayed the same, the saved Bloom · Light Appearance value still resolved, and a normal Appearance PUT accepted it.

Focused test output:

test appearance::tests::legacy_background_migration_preserves_saved_appearance_reference ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 87 filtered out; finished in 2.52s
Migration proof on the current head passed. A real indexed file at `.calternal/backgrounds/legacy.jpg` moved to `Photos/Backgrounds/legacy.jpg`; the file ID stayed the same, the saved Bloom · Light Appearance value still resolved, and a normal Appearance PUT accepted it. Focused test output: ```text test appearance::tests::legacy_background_migration_preserves_saved_appearance_reference ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 87 filtered out; finished in 2.52s ```
Author
Owner

The requested full calternal-server test gate exposed a process abort after 32 tests. The unrelated Unsplash route test overflowed its stack:

thread 'appearance::tests::unsplash_is_proxied_downloaded_tracked_and_saved_once' has overflowed its stack
fatal runtime error: stack overflow, aborting
error: test failed, to rerun pass `-p calternal-server --bin calternal-server`

Caused by:
  process didn't exit successfully: `/mnt/hdd/targets/jobs/bg-422/debug/deps/calternal_server-d6f413c076fbe06a` (signal: 6, SIGABRT: process abort signal)

I am reproducing that test in isolation to determine whether the failure comes from its own future size or parallel test-thread pressure.

The requested full `calternal-server` test gate exposed a process abort after 32 tests. The unrelated Unsplash route test overflowed its stack: ```text thread 'appearance::tests::unsplash_is_proxied_downloaded_tracked_and_saved_once' has overflowed its stack fatal runtime error: stack overflow, aborting error: test failed, to rerun pass `-p calternal-server --bin calternal-server` Caused by: process didn't exit successfully: `/mnt/hdd/targets/jobs/bg-422/debug/deps/calternal_server-d6f413c076fbe06a` (signal: 6, SIGABRT: process abort signal) ``` I am reproducing that test in isolation to determine whether the failure comes from its own future size or parallel test-thread pressure.
Author
Owner

Isolation confirms the stack failure is deterministic at the default test-thread stack: the single Unsplash test aborts with stack overflow. Running the same already-built test binary with RUST_MIN_STACK=8388608 passes in 1.59s. I am tracing the route and runtime stack settings before choosing a code fix; raising the stack only for a gate would hide a possible server-thread crash.

Isolation confirms the stack failure is deterministic at the default test-thread stack: the single Unsplash test aborts with stack overflow. Running the same already-built test binary with `RUST_MIN_STACK=8388608` passes in 1.59s. I am tracing the route and runtime stack settings before choosing a code fix; raising the stack only for a gate would hide a possible server-thread crash.
Author
Owner

The stack overflow is isolated to the first POST to /api/v1/appearance/unsplash/photo_1: setup, search, and thumbnail proxy all finish on the default stack. This confirms the save/install path is responsible, rather than suite parallelism. The larger-stack run passes, but I am tracing the save path before selecting a fix.

The stack overflow is isolated to the first POST to `/api/v1/appearance/unsplash/photo_1`: setup, search, and thumbnail proxy all finish on the default stack. This confirms the save/install path is responsible, rather than suite parallelism. The larger-stack run passes, but I am tracing the save path before selecting a fix.
Author
Owner

After moving the Unsplash Files install onto its own awaited Tokio task, the full server suite passed unsplash_is_proxied_downloaded_tracked_and_saved_once and 86 other tests, then aborted in appearance::tests::uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder with another stack overflow. I am isolating this separate Appearance image-validation path now.

After moving the Unsplash Files install onto its own awaited Tokio task, the full server suite passed `unsplash_is_proxied_downloaded_tracked_and_saved_once` and 86 other tests, then aborted in `appearance::tests::uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder` with another stack overflow. I am isolating this separate Appearance image-validation path now.
Author
Owner

Round 2 proof update — branch job/bg-422

Head: 5dd7485d5acdfa5c8ea1d6ad1e95664f59ae989d (includes the required merge from origin/dev).

Proof completed before that merge:

  • settings-effects.mjs failed on the pre-fix head for the fresh-User Bloom/Auto 350 KB JPEG case and passed on the feature head using the returned Upload-Item-ID, with no post-upload stat. The 350 KB and 6 MB camera JPEG browser flow, light/dark Settings and Calendar views, “use for both”, and Photos → Backgrounds screenshots are in ignored artifacts/bg-422/.
  • The legacy .calternal/backgrounds/ migration test passed. It moved the file to Photos/Backgrounds/, kept its Files item ID, and retained the saved Appearance reference.

New server finding:

  • The Unsplash Files install overflowed the default worker stack when polled inside the route. Commit aa56b5f14 moves that install to an awaited Tokio task. The focused Unsplash test passed with the default stack.
  • The full server test then exposed a stack overflow in uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder. That test still aborts with the default stack. With RUST_MIN_STACK=4194304, the test passed. A full 4 MiB-stack run reached 85 passed, 1 failed, 2 ignored; the failure was wire::tests::full_app_setup_session_config_and_backup timing out after 67 seconds. This is still open.

Gate output:

  • cargo fmt --check: exit 0, no output.
  • cargo clippy -p calternal-server --all-targets -- -D warnings (before the final origin/dev merge): Finished dev profile [unoptimized + debuginfo] target(s) in 9m 11s
  • The full test command did not pass. Default-stack failure:
    thread 'appearance::tests::uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder' has overflowed its stack
    fatal runtime error: stack overflow, aborting
    error: test failed, to rerun pass -p calternal-server --bin calternal-server``
  • The 4 MiB-stack run ended: test result: 85 passed; 1 failed; 2 ignored; 0 measured; 0 filtered out; finished in 82.04s.

Known gaps: I stopped at the job time limit. The default-stack validation-route crash needs a proper fix. I did not rerun production build, settings-effects.mjs, screenshots, server clippy/test, or the upload/Appearance adversarial round after merging the new origin/dev head. The current merge conflict resolution keeps Upload-Item-ID and installed-path handling, and also keeps the newer per-file conflict policy from origin/dev.

Decision not specified in DESIGN: run the Unsplash Files installation in a separate awaited Tokio task to bound the route's nested future chain while preserving the returned stable item ID and request ordering.

Round 2 proof update — branch `job/bg-422` Head: `5dd7485d5acdfa5c8ea1d6ad1e95664f59ae989d` (includes the required merge from `origin/dev`). Proof completed before that merge: - `settings-effects.mjs` failed on the pre-fix head for the fresh-User Bloom/Auto 350 KB JPEG case and passed on the feature head using the returned Upload-Item-ID, with no post-upload stat. The 350 KB and 6 MB camera JPEG browser flow, light/dark Settings and Calendar views, “use for both”, and Photos → Backgrounds screenshots are in ignored `artifacts/bg-422/`. - The legacy `.calternal/backgrounds/` migration test passed. It moved the file to `Photos/Backgrounds/`, kept its Files item ID, and retained the saved Appearance reference. New server finding: - The Unsplash Files install overflowed the default worker stack when polled inside the route. Commit `aa56b5f14` moves that install to an awaited Tokio task. The focused Unsplash test passed with the default stack. - The full server test then exposed a stack overflow in `uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder`. That test still aborts with the default stack. With `RUST_MIN_STACK=4194304`, the test passed. A full 4 MiB-stack run reached 85 passed, 1 failed, 2 ignored; the failure was `wire::tests::full_app_setup_session_config_and_backup` timing out after 67 seconds. This is still open. Gate output: - `cargo fmt --check`: exit 0, no output. - `cargo clippy -p calternal-server --all-targets -- -D warnings` (before the final origin/dev merge): `Finished dev profile [unoptimized + debuginfo] target(s) in 9m 11s` - The full test command did not pass. Default-stack failure: `thread 'appearance::tests::uploaded_image_backgrounds_must_be_decodable_files_in_the_backgrounds_folder' has overflowed its stack` `fatal runtime error: stack overflow, aborting` `error: test failed, to rerun pass `-p calternal-server --bin calternal-server`` - The 4 MiB-stack run ended: `test result: 85 passed; 1 failed; 2 ignored; 0 measured; 0 filtered out; finished in 82.04s`. Known gaps: I stopped at the job time limit. The default-stack validation-route crash needs a proper fix. I did not rerun production build, `settings-effects.mjs`, screenshots, server clippy/test, or the upload/Appearance adversarial round after merging the new `origin/dev` head. The current merge conflict resolution keeps Upload-Item-ID and installed-path handling, and also keeps the newer per-file conflict policy from `origin/dev`. Decision not specified in DESIGN: run the Unsplash Files installation in a separate awaited Tokio task to bound the route's nested future chain while preserving the returned stable item ID and request ordering.
Author
Owner

Correction to the clippy transcript in my previous update: the exact output line without terminal styling is:

Finished 'dev' profile [unoptimized + debuginfo] target(s) in 9m 11s

That run used the save-task code before the final origin/dev merge. Clippy was not rerun after the merge.

Correction to the clippy transcript in my previous update: the exact output line without terminal styling is: `Finished 'dev' profile [unoptimized + debuginfo] target(s) in 9m 11s` That run used the save-task code before the final origin/dev merge. Clippy was not rerun after the merge.
Author
Owner

Resuming round 3 on job/bg-422, base 5dd7485d5. I will measure the Appearance save/validate/install futures, box the large inner futures, prove the whole save on an explicit 2 MiB thread, rerun the isolated wire test, then merge origin/dev once and run final per-crate gates and release-server pixel/upload/security proof.

Resuming round 3 on job/bg-422, base 5dd7485d5. I will measure the Appearance save/validate/install futures, box the large inner futures, prove the whole save on an explicit 2 MiB thread, rerun the isolated wire test, then merge origin/dev once and run final per-crate gates and release-server pixel/upload/security proof.
Author
Owner

Proof finding: Photos → Backgrounds has no upload or apply control on 5dd7485d5. PhotosView explicitly sets primary to undefined for folder routes; BackgroundsFolderView only renders Open and Copy link. To satisfy the requested uploads from that view, I am reusing Photos upload chrome and the Files queue, resolving the folder by stable ID, and reusing backgroundStore.saveBackground for an explicit Use as background action. The e2e now tests both 350 KB and 6 MB there and compares painted pixels. Decision: a folder upload adds a Files item; the explicit apply action saves it to the currently visible family/scheme, matching Settings.

Proof finding: Photos → Backgrounds has no upload or apply control on 5dd7485d5. PhotosView explicitly sets primary to undefined for folder routes; BackgroundsFolderView only renders Open and Copy link. To satisfy the requested uploads from that view, I am reusing Photos upload chrome and the Files queue, resolving the folder by stable ID, and reusing backgroundStore.saveBackground for an explicit Use as background action. The e2e now tests both 350 KB and 6 MB there and compares painted pixels. Decision: a folder upload adds a Files item; the explicit apply action saves it to the currently visible family/scheme, matching Settings.
Author
Owner

Atomic UI slice committed at a77f555fb. Reused the Photos upload action, Files queue, shared Files event stream, and Appearance store. Production SPA build: ✓ built in 1m 55s, Wrote site to "build", ✔ done. Web check: svelte-check found 0 errors and 0 warnings. Focused Files-upload/Photos tests: Test Files 2 passed (2), Tests 19 passed (19). Initial full web tests hit five-second timeouts in unchanged MailSection, KeyboardShortcutsCard, menu-open-focus, and ThemePicker tests under load average ~73; I stopped that preliminary run and will run the final gate with a longer command timeout and unchanged assertions. Rust future measurement is still compiling dependencies from the cleaned target.

Atomic UI slice committed at a77f555fb. Reused the Photos upload action, Files queue, shared Files event stream, and Appearance store. Production SPA build: `✓ built in 1m 55s`, `Wrote site to "build"`, `✔ done`. Web check: `svelte-check found 0 errors and 0 warnings`. Focused Files-upload/Photos tests: `Test Files 2 passed (2)`, `Tests 19 passed (19)`. Initial full web tests hit five-second timeouts in unchanged MailSection, KeyboardShortcutsCard, menu-open-focus, and ThemePicker tests under load average ~73; I stopped that preliminary run and will run the final gate with a longer command timeout and unchanged assertions. Rust future measurement is still compiling dependencies from the cleaned target.
Author
Owner

Resuming round 4 on job/bg-422, head 02f4e783eff68e1ba71ea8704517a30a93c8540a, tracked base origin/dev 6c87f5ff9442cd658572139bc536d018fd5222a4. I reviewed the two uncommitted files and DESIGN §35. I will measure nested futures, prove Appearance saves on an explicit 2 MiB thread, check the isolated wire test, then merge origin/dev once and run per-crate gates and release-server upload/pixel/input-validation proof. Crash traces will be summarised in one line.

Resuming round 4 on `job/bg-422`, head `02f4e783eff68e1ba71ea8704517a30a93c8540a`, tracked base `origin/dev` `6c87f5ff9442cd658572139bc536d018fd5222a4`. I reviewed the two uncommitted files and DESIGN §35. I will measure nested futures, prove Appearance saves on an explicit 2 MiB thread, check the isolated wire test, then merge origin/dev once and run per-crate gates and release-server upload/pixel/input-validation proof. Crash traces will be summarised in one line.
Author
Owner

Round 4 evidence: the prior test binary reports the Unsplash Files install future as 72,128 bytes, while pair validation is 192 bytes, the save route is 2,816 bytes, and the read route is 1,616 bytes. This identifies install/Sidecar nesting as the largest remaining async state. The uncommitted Files edits heap-pin the install and Sidecar write, and the Appearance edits heap-pin validation, migration and install calls without changing their order. A fresh rebuild and explicit 2 MiB-thread test are in progress; these measurements are from the prior binary, not a final gate. The separate decode already runs in an owned blocking task. The bounded Appearance benchmark and shared oriented JPEG fixtures are committed at c91ed6360. The perf VM lock was busy, so the benchmark will be labelled local.

Round 4 evidence: the prior test binary reports the Unsplash Files install future as 72,128 bytes, while pair validation is 192 bytes, the save route is 2,816 bytes, and the read route is 1,616 bytes. This identifies install/Sidecar nesting as the largest remaining async state. The uncommitted Files edits heap-pin the install and Sidecar write, and the Appearance edits heap-pin validation, migration and install calls without changing their order. A fresh rebuild and explicit 2 MiB-thread test are in progress; these measurements are from the prior binary, not a final gate. The separate decode already runs in an owned blocking task. The bounded Appearance benchmark and shared oriented JPEG fixtures are committed at c91ed6360. The perf VM lock was busy, so the benchmark will be labelled local.
Author
Owner

Starting #422 round 5 on job/bg-422 at HEAD c91ed63609574e0137a621cefe7959761fc2d300; branch base is 5474493b170f5d12b3d0fc9ccf5c3880e3d24bae. I will finish the Appearance save future boxing and small-stack test, run the requested production build and Settings effects e2e, then run the per-crate gates. I will leave tests/adversarial/bg_422_routes.mjs unchanged and will not open or run anything under tests/adversarial/.

Starting #422 round 5 on `job/bg-422` at HEAD `c91ed63609574e0137a621cefe7959761fc2d300`; branch base is `5474493b170f5d12b3d0fc9ccf5c3880e3d24bae`. I will finish the Appearance save future boxing and small-stack test, run the requested production build and Settings effects e2e, then run the per-crate gates. I will leave `tests/adversarial/bg_422_routes.mjs` unchanged and will not open or run anything under `tests/adversarial/`.
Author
Owner

The new 2 MiB regression guard showed that the full Appearance setup, save and read sequence still exceeds the requested worker stack on the current diff. The existing future boxes do not yet cover the full sequence. I am narrowing the remaining future chain before committing; no runtime dump is attached.

The new 2 MiB regression guard showed that the full Appearance setup, save and read sequence still exceeds the requested worker stack on the current diff. The existing future boxes do not yet cover the full sequence. I am narrowing the remaining future chain before committing; no runtime dump is attached.
Author
Owner

The first production settings-effects.mjs run captured the requested baseline and Photos/Backgrounds screenshots, then failed at the final shared-background restore. The folder action applies the picture optimistically; the click handler starts the Appearance PUT without returning its promise. The test waited for painted pixels and navigated to Settings before confirming the server state, so the final read saw the prior dark item. I will wait for the saved item before navigating and rerun the sweep.

The first production `settings-effects.mjs` run captured the requested baseline and Photos/Backgrounds screenshots, then failed at the final shared-background restore. The folder action applies the picture optimistically; the click handler starts the Appearance PUT without returning its promise. The test waited for painted pixels and navigated to Settings before confirming the server state, so the final read saw the prior dark item. I will wait for the saved item before navigating and rerun the sweep.
Author
Owner

The rerun passed the Photos-folder restore check, then timed out on the first Calendar screenshot in the after matrix. settings-effects.mjs waited for the visible Colour scheme control but did not wait for its server-backed Appearance mode before navigating from Settings to Calendar. I will make the control helper wait for the saved mode so the screenshot checks measure the settled User choice.

The rerun passed the Photos-folder restore check, then timed out on the first Calendar screenshot in the after matrix. `settings-effects.mjs` waited for the visible Colour scheme control but did not wait for its server-backed Appearance mode before navigating from Settings to Calendar. I will make the control helper wait for the saved mode so the screenshot checks measure the settled User choice.
Author
Owner

Local performance profile finding: bench/appearance.mjs sends its 20 MiB upload in one Tus PATCH, which returns HTTP 413. The Photos/Files uploader uses 4 MiB PATCH chunks (apps/web/src/lib/files/uploads.svelte.ts:92). I will extend the existing e2e upload helper with an optional chunk size and have the benchmark use 4 MiB chunks, then rerun it. This keeps the profile aligned with the real client request shape and does not change the server contract.

Local performance profile finding: `bench/appearance.mjs` sends its 20 MiB upload in one Tus PATCH, which returns HTTP 413. The Photos/Files uploader uses 4 MiB PATCH chunks (`apps/web/src/lib/files/uploads.svelte.ts:92`). I will extend the existing e2e upload helper with an optional chunk size and have the benchmark use 4 MiB chunks, then rerun it. This keeps the profile aligned with the real client request shape and does not change the server contract.
Author
Owner

Completed — Forgejo #422, branch job/bg-422. Head SHA: 59a232b4d43aa77c274dd180454d5bd8e6ab617c.

Built

  • Boxed the nested Files validation, migration, and save futures in Appearance and Files. Added an Appearance router test that runs the real save path on a thread with a 2 MiB stack. Commit: abc4f222e (fix(appearance): box large save futures).
  • Made the production e2e sweep wait for server-owned Appearance state after optimistic controls start their PUTs. Commit: 7be51d2e6 (test(e2e): wait for saved appearance state).
  • Extended the e2e Tus helper with offset-aware chunking and used the Files UI's 4 MiB chunks in the maximum-size appearance profile. This lets the 20 MiB profile measure the file limit instead of sending one request that receives HTTP 413. Commit: 59a232b4d (test(perf): match Tus chunks in appearance profile).

Changed files: crates/calternal-server/src/appearance.rs, crates/plugins/files/src/lib.rs, apps/web/e2e/settings-effects.mjs, apps/web/e2e/harness.mjs, and bench/appearance.mjs.

Production and visual checks

Production SPA build passed. The settings-effects e2e passed with 350 KB and 6 MB uploads from Settings and Photos → Backgrounds, including the painted-pixel check and Calendar time preview. It captured 36 screenshots across 390, 820, and 1440 px in light and dark. I attached 18 post-change screenshots to this issue for Settings, Photos → Backgrounds, and Calendar at all three widths and both themes. The local screenshots remain in ignored artifacts/bg-422/.

Exact success lines:

✓ built in 2m 1s
Wrote site to "build"
✔ done
PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/bg-422/artifacts/bg-422

Gates

cargo fmt --check exited 0 with no output.

cargo test -p calternal-server appearance:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 35.30s
running 20 tests
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 69 filtered out; finished in 12.65s

The wire test is ignored by default because plugin process-global state must run in a separate process. I ran it alone with -- --ignored --exact --test-threads=1:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 57.52s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 88 filtered out; finished in 24.53s

cargo test -p calternal-server:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 10m 09s
running 89 tests
test result: ok. 87 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 23.16s

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 18m 33s

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

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 8m 16s

cargo test -p calternal-plugin-files:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 12m 20s
running 132 tests
test result: ok. 131 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 179.02s

Final bun run check:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes and UI shape values use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bg-422/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Performance profile

Ran once locally with the debug server and production SPA. Values are upload duration; save p50/p95; eight-save burst p50/p95; mean/peak server CPU; and mean/peak RSS.

Profile Upload ms Save p50/p95 ms Burst p50/p95 ms CPU mean/peak % RSS mean/peak bytes
350 KB 408 70.2 / 147.3 162.3 / 232.1 93.63 / 189.95 137,621,504 / 148,602,880
6 MiB 6,240 644.8 / 892.4 1,308.5 / 1,734.1 64.10 / 267.01 199,358,731 / 287,719,424
20 MiB 8,725 959.3 / 1,485.2 1,921.6 / 2,541.0 60.25 / 207.00 390,776,737 / 572,182,528

The local load averages at run start were 23.93, 26.17, and 29.02 (1, 5, and 15 minutes). docs/perf/baseline.json has no upload/save profile to compare directly. Its closest page-load baseline is Appearance data-ready p50/p95: 3,246/5,190 ms at 390 px and 2,953/4,055 ms at 1440 px, recorded 2026-09-27; these measure a different operation.

Decisions and gaps

  • DESIGN §35 does not specify the stack-test setup. Fixture setup stays on the Tokio test runtime so SQLx tasks keep their runtime; the real router save/read path runs in the requested 2 MiB thread.
  • The e2e sweep now waits for persisted API state after optimistic UI changes, because navigating immediately could hydrate the prior setting.
  • The 20 MiB performance case uses 4 MiB Tus PATCHes, matching the Files UI's chunk size. This is test/measurement behavior only.
  • This narrow round did not open or run anything under tests/adversarial/, as requested. The pre-existing worktree modification to tests/adversarial/bg_422_routes.mjs remains untouched.
  • The production SPA build passed. A release server build was stopped during aws-lc native compilation; e2e and performance runs used the debug server with that production SPA. The local performance values are noisy under the recorded host load.

Cleanup completed: cargo clean reported Removed 20536 files, 10.4GiB total; web build output was deleted. No screenshots or performance artifacts were committed.

**Completed — Forgejo #422, branch `job/bg-422`.** Head SHA: `59a232b4d43aa77c274dd180454d5bd8e6ab617c`. ## Built - Boxed the nested Files validation, migration, and save futures in Appearance and Files. Added an Appearance router test that runs the real save path on a thread with a 2 MiB stack. Commit: `abc4f222e` (`fix(appearance): box large save futures`). - Made the production e2e sweep wait for server-owned Appearance state after optimistic controls start their PUTs. Commit: `7be51d2e6` (`test(e2e): wait for saved appearance state`). - Extended the e2e Tus helper with offset-aware chunking and used the Files UI's 4 MiB chunks in the maximum-size appearance profile. This lets the 20 MiB profile measure the file limit instead of sending one request that receives HTTP 413. Commit: `59a232b4d` (`test(perf): match Tus chunks in appearance profile`). Changed files: `crates/calternal-server/src/appearance.rs`, `crates/plugins/files/src/lib.rs`, `apps/web/e2e/settings-effects.mjs`, `apps/web/e2e/harness.mjs`, and `bench/appearance.mjs`. ## Production and visual checks Production SPA build passed. The settings-effects e2e passed with 350 KB and 6 MB uploads from Settings and Photos → Backgrounds, including the painted-pixel check and Calendar time preview. It captured 36 screenshots across 390, 820, and 1440 px in light and dark. I attached 18 post-change screenshots to this issue for Settings, Photos → Backgrounds, and Calendar at all three widths and both themes. The local screenshots remain in ignored `artifacts/bg-422/`. Exact success lines: ``` ✓ built in 2m 1s Wrote site to "build" ✔ done PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/bg-422/artifacts/bg-422 ``` ## Gates `cargo fmt --check` exited 0 with no output. `cargo test -p calternal-server appearance`: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 35.30s running 20 tests test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 69 filtered out; finished in 12.65s ``` The wire test is ignored by default because plugin process-global state must run in a separate process. I ran it alone with `-- --ignored --exact --test-threads=1`: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 57.52s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 88 filtered out; finished in 24.53s ``` `cargo test -p calternal-server`: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 10m 09s running 89 tests test result: ok. 87 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 23.16s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 18m 33s ``` `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 8m 16s ``` `cargo test -p calternal-plugin-files`: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 12m 20s running 132 tests test result: ok. 131 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 179.02s ``` Final `bun run check`: ``` $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes and UI shape values use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/bg-422/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` ## Performance profile Ran once locally with the debug server and production SPA. Values are upload duration; save p50/p95; eight-save burst p50/p95; mean/peak server CPU; and mean/peak RSS. | Profile | Upload ms | Save p50/p95 ms | Burst p50/p95 ms | CPU mean/peak % | RSS mean/peak bytes | | --- | ---: | ---: | ---: | ---: | ---: | | 350 KB | 408 | 70.2 / 147.3 | 162.3 / 232.1 | 93.63 / 189.95 | 137,621,504 / 148,602,880 | | 6 MiB | 6,240 | 644.8 / 892.4 | 1,308.5 / 1,734.1 | 64.10 / 267.01 | 199,358,731 / 287,719,424 | | 20 MiB | 8,725 | 959.3 / 1,485.2 | 1,921.6 / 2,541.0 | 60.25 / 207.00 | 390,776,737 / 572,182,528 | The local load averages at run start were 23.93, 26.17, and 29.02 (1, 5, and 15 minutes). `docs/perf/baseline.json` has no upload/save profile to compare directly. Its closest page-load baseline is Appearance data-ready p50/p95: 3,246/5,190 ms at 390 px and 2,953/4,055 ms at 1440 px, recorded 2026-09-27; these measure a different operation. ## Decisions and gaps - DESIGN §35 does not specify the stack-test setup. Fixture setup stays on the Tokio test runtime so SQLx tasks keep their runtime; the real router save/read path runs in the requested 2 MiB thread. - The e2e sweep now waits for persisted API state after optimistic UI changes, because navigating immediately could hydrate the prior setting. - The 20 MiB performance case uses 4 MiB Tus PATCHes, matching the Files UI's chunk size. This is test/measurement behavior only. - This narrow round did not open or run anything under `tests/adversarial/`, as requested. The pre-existing worktree modification to `tests/adversarial/bg_422_routes.mjs` remains untouched. - The production SPA build passed. A release server build was stopped during aws-lc native compilation; e2e and performance runs used the debug server with that production SPA. The local performance values are noisy under the recorded host load. Cleanup completed: `cargo clean` reported `Removed 20536 files, 10.4GiB total`; web build output was deleted. No screenshots or performance artifacts were committed.
Author
Owner

Photos/Backgrounds had an event-order race after upload. The Files Index contained the uploaded item and the server published its Files SSE event while BackgroundsFolderView was still resolving the folder's stable item ID. The subscription saw folderPath === null and discarded the event, leaving the folder UI stale until another refresh. I changed the view to retain one pending invalidation and refetch once the folder path resolves; it remains event-driven and does not poll.

The regression now passes in the production Settings sweep, including both image sizes and the real Photos folder action. Evidence: Settings effects screenshots, 390/820/1440 px, light/dark.

PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/merge-web/artifacts/bg-422

Fix and regression are in 192ea5457f1fc17735d876e70b41cb506b2a8274.

Photos/Backgrounds had an event-order race after upload. The Files Index contained the uploaded item and the server published its Files SSE event while `BackgroundsFolderView` was still resolving the folder's stable item ID. The subscription saw `folderPath === null` and discarded the event, leaving the folder UI stale until another refresh. I changed the view to retain one pending invalidation and refetch once the folder path resolves; it remains event-driven and does not poll. The regression now passes in the production Settings sweep, including both image sizes and the real Photos folder action. Evidence: [Settings effects screenshots, 390/820/1440 px, light/dark](https://git.kayg.org/attachments/ef08eb55-75be-4234-a946-b5557db27745). ```text PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/merge-web/artifacts/bg-422 ``` Fix and regression are in `192ea5457f1fc17735d876e70b41cb506b2a8274`.
Author
Owner

Merge round 2 final report

Head: 192ea5457f1fc17735d876e70b41cb506b2a8274 on job/merge-round-2. origin/dev was fetched and merged at 1389119e33831275e6b56b9181ef42838003a4b3; job/bg-422 was already an ancestor (git merge --no-edit job/bg-422 → Already up to date.). No push, deploy, or merge to dev.

Built

Integrated the #391 Location/Appearance, #431 calendar feeds, #459 bug fixes, #470 Photos and #367 performance changes. Follow-up 76d1e71e1 makes Location probes await settled server state. Follow-up 192ea5457 fixes the Photos/Backgrounds SSE ordering race: preserve one event invalidation while the stable folder ID resolves, then refetch once. Updated the production Settings sweep to cover Account → Location and real Photos folder actions.

Files changed by the follow-up:

  • apps/web/src/lib/photos/BackgroundsFolderView.svelte
  • apps/web/e2e/settings-effects.mjs
  • tests/adversarial/appearance_auto_scheme.mjs

Full integration manifest: artifacts/merge-round-2/files.txt.

Gates (output verbatim)

cargo fmt --check: exit 0, no output.

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

cargo test -p calternal-server
    Finished `test` profile [unoptimized + debuginfo] target(s) in 9m 02s
test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.86s

async-imap clippy
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.21s

async-imap test
---- client::tests::test_parsing_error stdout ----
thread 'client::tests::test_parsing_error' panicked at crates/plugins/mail/vendor/async-imap/src/client.rs:2724:9:
assertion failed: session.noop().await.unwrap_err().to_string().contains("220 mail.example.org ESMTP Postcow")
test client::tests::test_parsing_error ... FAILED
test result: FAILED. 69 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.40s

bun run check
Text sizes and UI shape values use shared role tokens.
svelte-check found 0 errors and 0 warnings

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

bun run test (final run)
 ❯ |component| src/lib/themePicker.svelte.test.ts (7 tests | 1 failed) 11924ms
 FAIL |component| src/lib/themePicker.svelte.test.ts > theme menu > reveals the trigger in the sheet scrollport before opening
Error: Test timed out in 5000ms.
 Test Files  1 failed | 138 passed (139)
      Tests  1 failed | 909 passed (910)
   Start at  18:24:29
   Duration  271.28s (transform 61%, environment 16%, import 10%, tests 10%, setup 3%)

generated contract
Finished `dev` profile [unoptimized + debuginfo] target(s) in 13m 26s
Running `/mnt/hdd/targets/jobs/merge-round-2/debug/calternal-server openapi`
$ bunx --package openapi-typescript@7.13.0 openapi-typescript ../../contracts/openapi.json -o src/generated.ts
✨ openapi-typescript 7.13.0
🚀 ../../contracts/openapi.json → src/generated.ts [1.9s]

parity check
Parity matrix: 206 web API actions, 113 shortcuts, 2 static commands, 131 menu actions, 33 settings groups, 188 actions with adapter gaps

Live probes

  • Location: Location API probe: malformed and oversized payloads and file, exact coordinates, Unicode place, concurrent writes, fixture restore, and cross-User isolation passed; Appearance Auto/Fonts/Background burst: 48 concurrent writes, all 200.
  • Webcal: Calendar feeds proof passed; screenshots: /home/kayg/Developer/calternal-wt/merge-web/artifacts/webcal-431.
  • WebDAV race: WebDAV scripted probes passed.
  • Settings and Photos/Backgrounds: PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/merge-web/artifacts/bg-422.
  • Two-User matrix: 328 operations classified; 157 operations replayed; 565 A-ID vs missing-ID comparisons across B, C, D and anonymous; median absolute timing delta 6.1 ms; ownership: 77 comparisons; 0 denial failures.
  • Media had one SLOW-only finding: the PDF worker list took 5.39 s with HTTP 200 against the probe's 5.0 s threshold. No hostile-input or 5xx finding remained.
  • Local photos-perf.mjs --items 1 --home-only: at load average 38.2/42.8/43, server start 2850 ms, indexed 4790 ms, mean/peak RSS 335424222/425738240 bytes, mean/peak CPU 23.37/95.95%, CPU 22.16 s. This one-photo local smoke is not comparable to the 5,000-photo perf VM baseline.

Known gaps and decisions

  • Standalone IMAP tests retain the existing display expectation and fail client::tests::test_parsing_error; clippy passes. No test expectation was changed.
  • The final web test gate has the single five-second ThemePicker timeout above. The component and test are unchanged from origin/dev; this is reported as a gate failure.
  • Media's 5.39-second response is SLOW-only shared-host load.
  • DESIGN §35 does not specify an SSE event arriving before stable folder identity resolves. Decision: coalesce early events into one list refresh after resolution. DESIGN §47 L1 defines Account as Location consent owner; Auto uses the exact saved location.
  • Settings effects used Node 22 because Bun could not load host Sharp (libstdc++.so.6 missing). No app code changed for the host workaround.

Production screenshots

The new Settings/Photos/Backgrounds matrix covers 390/820/1440 px in light and dark: download review ZIP. Existing full matrices: Location, Appearance/Notes/Photos/Log, Calendar feeds. Screenshots and logs are not committed.

## Merge round 2 final report **Head:** `192ea5457f1fc17735d876e70b41cb506b2a8274` on `job/merge-round-2`. `origin/dev` was fetched and merged at `1389119e33831275e6b56b9181ef42838003a4b3`; `job/bg-422` was already an ancestor (`git merge --no-edit job/bg-422` → `Already up to date.`). No push, deploy, or merge to dev. ### Built Integrated the #391 Location/Appearance, #431 calendar feeds, #459 bug fixes, #470 Photos and #367 performance changes. Follow-up `76d1e71e1` makes Location probes await settled server state. Follow-up `192ea5457` fixes the Photos/Backgrounds SSE ordering race: preserve one event invalidation while the stable folder ID resolves, then refetch once. Updated the production Settings sweep to cover Account → Location and real Photos folder actions. Files changed by the follow-up: - `apps/web/src/lib/photos/BackgroundsFolderView.svelte` - `apps/web/e2e/settings-effects.mjs` - `tests/adversarial/appearance_auto_scheme.mjs` Full integration manifest: `artifacts/merge-round-2/files.txt`. ### Gates (output verbatim) `cargo fmt --check`: exit 0, no output. ```text cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 06s cargo test -p calternal-server Finished `test` profile [unoptimized + debuginfo] target(s) in 9m 02s test result: ok. 93 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.86s async-imap clippy Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.21s async-imap test ---- client::tests::test_parsing_error stdout ---- thread 'client::tests::test_parsing_error' panicked at crates/plugins/mail/vendor/async-imap/src/client.rs:2724:9: assertion failed: session.noop().await.unwrap_err().to_string().contains("220 mail.example.org ESMTP Postcow") test client::tests::test_parsing_error ... FAILED test result: FAILED. 69 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.40s bun run check Text sizes and UI shape values use shared role tokens. svelte-check found 0 errors and 0 warnings bun run build ✓ built in 1m 37s > Using @sveltejs/adapter-static Wrote site to "build" ✔ done bun run test (final run) ❯ |component| src/lib/themePicker.svelte.test.ts (7 tests | 1 failed) 11924ms FAIL |component| src/lib/themePicker.svelte.test.ts > theme menu > reveals the trigger in the sheet scrollport before opening Error: Test timed out in 5000ms. Test Files 1 failed | 138 passed (139) Tests 1 failed | 909 passed (910) Start at 18:24:29 Duration 271.28s (transform 61%, environment 16%, import 10%, tests 10%, setup 3%) generated contract Finished `dev` profile [unoptimized + debuginfo] target(s) in 13m 26s Running `/mnt/hdd/targets/jobs/merge-round-2/debug/calternal-server openapi` $ bunx --package openapi-typescript@7.13.0 openapi-typescript ../../contracts/openapi.json -o src/generated.ts ✨ openapi-typescript 7.13.0 🚀 ../../contracts/openapi.json → src/generated.ts [1.9s] parity check Parity matrix: 206 web API actions, 113 shortcuts, 2 static commands, 131 menu actions, 33 settings groups, 188 actions with adapter gaps ``` ### Live probes - Location: `Location API probe: malformed and oversized payloads and file, exact coordinates, Unicode place, concurrent writes, fixture restore, and cross-User isolation passed`; `Appearance Auto/Fonts/Background burst: 48 concurrent writes, all 200`. - Webcal: `Calendar feeds proof passed; screenshots: /home/kayg/Developer/calternal-wt/merge-web/artifacts/webcal-431`. - WebDAV race: `WebDAV scripted probes passed`. - Settings and Photos/Backgrounds: `PASS settings effects: Appearance, Photos/Backgrounds, Calendar time preview; screenshots in /home/kayg/Developer/calternal-wt/merge-web/artifacts/bg-422`. - Two-User matrix: `328 operations classified; 157 operations replayed; 565 A-ID vs missing-ID comparisons across B, C, D and anonymous; median absolute timing delta 6.1 ms`; ownership: `77 comparisons; 0 denial failures`. - Media had one SLOW-only finding: the PDF worker list took 5.39 s with HTTP 200 against the probe's 5.0 s threshold. No hostile-input or 5xx finding remained. - Local `photos-perf.mjs --items 1 --home-only`: at load average 38.2/42.8/43, server start 2850 ms, indexed 4790 ms, mean/peak RSS 335424222/425738240 bytes, mean/peak CPU 23.37/95.95%, CPU 22.16 s. This one-photo local smoke is not comparable to the 5,000-photo perf VM baseline. ### Known gaps and decisions - Standalone IMAP tests retain the existing display expectation and fail `client::tests::test_parsing_error`; clippy passes. No test expectation was changed. - The final web test gate has the single five-second ThemePicker timeout above. The component and test are unchanged from origin/dev; this is reported as a gate failure. - Media's 5.39-second response is SLOW-only shared-host load. - DESIGN §35 does not specify an SSE event arriving before stable folder identity resolves. Decision: coalesce early events into one list refresh after resolution. DESIGN §47 L1 defines Account as Location consent owner; Auto uses the exact saved location. - Settings effects used Node 22 because Bun could not load host Sharp (`libstdc++.so.6` missing). No app code changed for the host workaround. ### Production screenshots The new Settings/Photos/Backgrounds matrix covers 390/820/1440 px in light and dark: [download review ZIP](https://git.kayg.org/attachments/ef08eb55-75be-4234-a946-b5557db27745). Existing full matrices: [Location](https://git.kayg.org/attachments/6078bc47-ceb1-4566-9b5e-63ef5c52b9a1), [Appearance/Notes/Photos/Log](https://git.kayg.org/attachments/2a0c2168-6199-4d45-9ff2-ad676518e88e), [Calendar feeds](https://git.kayg.org/attachments/03f5976a-28a3-4384-9aae-a995bac45409). Screenshots and logs are not committed.
Author
Owner

Merged into dev and deployed to calternal.cloud at aa372eef6 via merge round 2 (server tests 93 passed, web 914/914, live location/webcal/WebDAV/Settings-Photos/two-User probes passed).

Merged into dev and deployed to calternal.cloud at aa372eef6 via merge round 2 (server tests 93 passed, web 914/914, live location/webcal/WebDAV/Settings-Photos/two-User probes passed).
kayg closed this issue 2026-09-30 17:20:57 +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#422
No description provided.