Photos: opening a photo waits on an animation; open and selection must be instant #1367

Closed
opened 2026-10-07 14:08:44 +00:00 by kayg · 67 comments
Owner

Owner report 2026-10-07 on production 0419170fe: "when opening a picture in the photos view, it takes a jarring second (some animation) before the photo opens. please fix that path and please aggressively optimise there. make sure photo opening, selection is instant."
Owner decision (2026-10-07): zoom from the grid tile with content on frame 1. The viewer shows the photo at full viewer size on the FIRST frame after the click, using the already-loaded grid thumbnail scaled up, and swaps to the full-resolution image when decoded (decode off the main thread: img.decode() / createImageBitmap; preload the next and previous photos). The shared spring zoom from the tile's rect runs on top and never delays showing the photo or accepting input; arrow keys, Escape, swipe and pinch work from frame 1. Reduced motion: no zoom, same instant content. Selection (click, Shift/Cmd-click, drag-select, keyboard) updates in the same frame.
Budgets (DESIGN §59): click to photo visible <= 100 ms (it must be the first frame in practice), next/previous photo <= 50 ms when preloaded, selection change in one frame, no long task over 50 ms on open.

Owner report 2026-10-07 on production 0419170fe: "when opening a picture in the photos view, it takes a jarring second (some animation) before the photo opens. please fix that path and please aggressively optimise there. make sure photo opening, selection is instant." Owner decision (2026-10-07): zoom from the grid tile with content on frame 1. The viewer shows the photo at full viewer size on the FIRST frame after the click, using the already-loaded grid thumbnail scaled up, and swaps to the full-resolution image when decoded (decode off the main thread: img.decode() / createImageBitmap; preload the next and previous photos). The shared spring zoom from the tile's rect runs on top and never delays showing the photo or accepting input; arrow keys, Escape, swipe and pinch work from frame 1. Reduced motion: no zoom, same instant content. Selection (click, Shift/Cmd-click, drag-select, keyboard) updates in the same frame. Budgets (DESIGN §59): click to photo visible <= 100 ms (it must be the first frame in practice), next/previous photo <= 50 ms when preloaded, selection change in one frame, no long task over 50 ms on open.
Author
Owner

Starting #1367 on job/photoopen-1367, based on 0419170fe456b58614befcb12d9c9d255118f1c0 (origin/dev). I am tracing the Photos tile-to-viewer path, the shared Quick Look component, selection updates, and existing real-API fixture/evidence tooling before making the first atomic change.

Starting #1367 on `job/photoopen-1367`, based on `0419170fe456b58614befcb12d9c9d255118f1c0` (`origin/dev`). I am tracing the Photos tile-to-viewer path, the shared Quick Look component, selection updates, and existing real-API fixture/evidence tooling before making the first atomic change.
Author
Owner

Finding (source trace): PhotosView.openTile() only starts goto(/p/<id>); the route.photo effect opens it later. The shared OverlaySurface then delays insertion by one requestAnimationFrame and runs a surface/scrim entrance from opacity 0. PhotoTimeline loads the 256 px URL (and a 1024 px srcset candidate), but PhotoViewer.baseItem() supplies a standalone 1024 px URL as Quick Look's preview, so Quick Look does not explicitly reuse the rendered image's currentSrc. Selection also calls data.ordered().map(...) and the shared clickSelect() uses indexOf() across the loaded library for each toggle. I am fixing synchronous open, first-paint preview reuse, bounded rendition warming and photo-local Set/index selection; these are direct causes visible in the current code.

Finding (source trace): `PhotosView.openTile()` only starts `goto(/p/<id>)`; the `route.photo` effect opens it later. The shared `OverlaySurface` then delays insertion by one `requestAnimationFrame` and runs a surface/scrim entrance from opacity 0. `PhotoTimeline` loads the 256 px URL (and a 1024 px srcset candidate), but `PhotoViewer.baseItem()` supplies a standalone 1024 px URL as Quick Look's preview, so Quick Look does not explicitly reuse the rendered image's `currentSrc`. Selection also calls `data.ordered().map(...)` and the shared `clickSelect()` uses `indexOf()` across the loaded library for each toggle. I am fixing synchronous open, first-paint preview reuse, bounded rendition warming and photo-local Set/index selection; these are direct causes visible in the current code.
Author
Owner

Finding for #1367: PhotoTimelineData.dayOf() rebuilt its lookup by walking every loaded tile after each data revision. A selection change after loading a large Home could therefore pay an O(loaded Items) scan before updating the selected-day count. The group-to-day map is now maintained as pages load, append, refresh, and remove; a focused regression covers lookup after load and removal. The benchmark will exercise a full 5,000-Item selection followed by one measured tile toggle.

Finding for #1367: `PhotoTimelineData.dayOf()` rebuilt its lookup by walking every loaded tile after each data revision. A selection change after loading a large Home could therefore pay an O(loaded Items) scan before updating the selected-day count. The group-to-day map is now maintained as pages load, append, refresh, and remove; a focused regression covers lookup after load and removal. The benchmark will exercise a full 5,000-Item selection followed by one measured tile toggle.
Author
Owner

Benchmark fixture finding: uploadEntry() used jpegCount = 6, which included all three 8,000 × 5,000 variants in its repeating JPEG cycle. This would have uploaded about 2,500 40 MP items instead of the requested few. I am changing the regular cycle to use the three ordinary JPEG sizes and keeping the three 40 MP files only at the end of the 5,000-item fixture. The production feature code is unaffected.

Benchmark fixture finding: `uploadEntry()` used `jpegCount = 6`, which included all three 8,000 × 5,000 variants in its repeating JPEG cycle. This would have uploaded about 2,500 40 MP items instead of the requested few. I am changing the regular cycle to use the three ordinary JPEG sizes and keeping the three 40 MP files only at the end of the 5,000-item fixture. The production feature code is unaffected.
Author
Owner

Benchmark review finding: the #1367 profile description listed five single-Item toggle samples at 5,000 selected Items, but measureSelection() performed only one. I am changing the profile to record five toggles with a paint-latency sample for each. This corrects measurement coverage; it does not change Photos selection behavior.

Benchmark review finding: the #1367 profile description listed five single-Item toggle samples at 5,000 selected Items, but `measureSelection()` performed only one. I am changing the profile to record five toggles with a paint-latency sample for each. This corrects measurement coverage; it does not change Photos selection behavior.
Author
Owner

#1367 implementation report

Built the Photos first-paint viewer, bounded thumbnail preloader and incremental selection path. The QuickLook image displays the browser-cached grid thumbnail at viewer size in the first paint. Larger renditions decode offscreen and replace it when ready. The tile-to-viewer spring transforms that visible image and can be interrupted by input. Photo selection uses a key-reactive Set and updates only changed keys and per-day counts. The timeline keeps a group-to-day map as pages change.

Shared components used for visible controls: OverlaySurface, QuickLook, and ImageView; existing Pill and PillGroup viewer controls; existing SelectionBar selection controls. No new visible control was added.

Files changed:

  • apps/web/src/lib/photos/PhotosView.svelte, PhotoTimeline.svelte, PhotoViewer.svelte, timeline.svelte.ts, preload.ts
  • packages/ui/src/components/OverlaySurface.svelte, viewer/QuickLook.svelte, viewer/ImageView.svelte
  • apps/web/src/lib/components/OverlaySurface.svelte.test.ts, apps/web/src/lib/photos/timeline.test.ts, preload.test.ts
  • bench/photoopen-1367.mjs, apps/web/package.json
  • contracts/perf/adoption-1058.json, contracts/perf/exceptions.json, docs/perf/baseline.json

UX gaps closed: the viewer no longer waits for route navigation, an overlay entrance or image decode before displaying its cached thumbnail; the thumbnail swap does not change layout size; the open spring does not block pointer, wheel or keyboard input; hover, focus, selection and adjacent viewer items warm cancellable viewer renditions; selected state is exposed with aria-pressed.

UX and measurement gaps left: the real-upload run reached 223 stored Photos Items at 13m39s. It did not reach 5,000 Items, so click-to-visible, next/previous, 5,000-item selection, long-task measurements and the requested 1440/820/390 light/dark frame sequences were not captured. artifacts/photoopen/ has no screenshots. The perf VM was occupied during this job. The partial attempt is recorded in docs/perf/baseline.json; rerun the committed profile with the perf VM after its current slot is free.

Decisions not set by DESIGN: the thumbnail preloader has a two-decode concurrency cap; the intermediate viewer rendition is 1024 px; Photos skips the shared surface entrance but keeps its exit motion while the image owns the open transform. The timeline group-to-day index is maintained with loaded-page mutations so per-key selection has constant-time day lookup.

Commits are atomic. Head: 34ee68b2af9c.

Gate output excerpts (verbatim)

bun run build
Compressed 919 static variants; saved 21495521 bytes.

bun run check
perf-lint: PASS; 0 violations; 22544 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Warn: Do not use empty rulesets (css)
svelte-check found 0 errors and 1 warning in 1 file

bun run test
 Test Files  2 failed | 286 passed (288)
      Tests  6 failed | 2033 passed (2039)
   Start at  18:38:04
   Duration  1159.04s (transform 29%, environment 26%, import 23%, tests 16%, setup 5%)
error: script "test" exited with code 1

bunx vitest run src/lib/photos/timeline.test.ts src/routes/settings/admin/UsersGroup.svelte.test.ts --maxWorkers=2
 Test Files  2 passed (2)
      Tests  10 passed (10)
   Start at  18:59:47
   Duration  120.57s (transform 91%, import 4%, tests 3%, environment 1%, setup 1%)

The five timeline test fixtures now load through the page API and maintain the day index. The admin test that timed out in the full run passed in the focused rerun. The full suite was not rerun. No Rust source changed; Rust clippy/test gates were not run. A local cargo build -p calternal-server succeeded to support the attempted profile, then cargo clean removed build output.

#1367 implementation report Built the Photos first-paint viewer, bounded thumbnail preloader and incremental selection path. The QuickLook image displays the browser-cached grid thumbnail at viewer size in the first paint. Larger renditions decode offscreen and replace it when ready. The tile-to-viewer spring transforms that visible image and can be interrupted by input. Photo selection uses a key-reactive Set and updates only changed keys and per-day counts. The timeline keeps a group-to-day map as pages change. Shared components used for visible controls: `OverlaySurface`, `QuickLook`, and `ImageView`; existing `Pill` and `PillGroup` viewer controls; existing `SelectionBar` selection controls. No new visible control was added. Files changed: - `apps/web/src/lib/photos/PhotosView.svelte`, `PhotoTimeline.svelte`, `PhotoViewer.svelte`, `timeline.svelte.ts`, `preload.ts` - `packages/ui/src/components/OverlaySurface.svelte`, `viewer/QuickLook.svelte`, `viewer/ImageView.svelte` - `apps/web/src/lib/components/OverlaySurface.svelte.test.ts`, `apps/web/src/lib/photos/timeline.test.ts`, `preload.test.ts` - `bench/photoopen-1367.mjs`, `apps/web/package.json` - `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`, `docs/perf/baseline.json` UX gaps closed: the viewer no longer waits for route navigation, an overlay entrance or image decode before displaying its cached thumbnail; the thumbnail swap does not change layout size; the open spring does not block pointer, wheel or keyboard input; hover, focus, selection and adjacent viewer items warm cancellable viewer renditions; selected state is exposed with `aria-pressed`. UX and measurement gaps left: the real-upload run reached 223 stored Photos Items at 13m39s. It did not reach 5,000 Items, so click-to-visible, next/previous, 5,000-item selection, long-task measurements and the requested 1440/820/390 light/dark frame sequences were not captured. `artifacts/photoopen/` has no screenshots. The perf VM was occupied during this job. The partial attempt is recorded in `docs/perf/baseline.json`; rerun the committed profile with the perf VM after its current slot is free. Decisions not set by DESIGN: the thumbnail preloader has a two-decode concurrency cap; the intermediate viewer rendition is 1024 px; Photos skips the shared surface entrance but keeps its exit motion while the image owns the open transform. The timeline group-to-day index is maintained with loaded-page mutations so per-key selection has constant-time day lookup. Commits are atomic. Head: `34ee68b2af9c`. ## Gate output excerpts (verbatim) ```text bun run build Compressed 919 static variants; saved 21495521 bytes. bun run check perf-lint: PASS; 0 violations; 22544 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Warn: Do not use empty rulesets (css) svelte-check found 0 errors and 1 warning in 1 file bun run test Test Files 2 failed | 286 passed (288) Tests 6 failed | 2033 passed (2039) Start at 18:38:04 Duration 1159.04s (transform 29%, environment 26%, import 23%, tests 16%, setup 5%) error: script "test" exited with code 1 bunx vitest run src/lib/photos/timeline.test.ts src/routes/settings/admin/UsersGroup.svelte.test.ts --maxWorkers=2 Test Files 2 passed (2) Tests 10 passed (10) Start at 18:59:47 Duration 120.57s (transform 91%, import 4%, tests 3%, environment 1%, setup 1%) ``` The five timeline test fixtures now load through the page API and maintain the day index. The admin test that timed out in the full run passed in the focused rerun. The full suite was not rerun. No Rust source changed; Rust clippy/test gates were not run. A local `cargo build -p calternal-server` succeeded to support the attempted profile, then `cargo clean` removed build output.
Author
Owner

Starting continuation of #1367 on job/photoopen-1367, head 34ee68b2af9cadff0a89cf7ad5d6929f2f91c259, based on 0419170fe456b58614befcb12d9c9d255118f1c0 (origin/dev at the prior run). The predecessor report identifies unfinished real-browser proof: 5,000-Item fixture, viewer frame sequence, performance samples, #1357 ArrowRight reproduction/fix, and full web suite. I am continuing those items in this worktree.

Starting continuation of #1367 on `job/photoopen-1367`, head `34ee68b2af9cadff0a89cf7ad5d6929f2f91c259`, based on `0419170fe456b58614befcb12d9c9d255118f1c0` (`origin/dev` at the prior run). The predecessor report identifies unfinished real-browser proof: 5,000-Item fixture, viewer frame sequence, performance samples, #1357 ArrowRight reproduction/fix, and full web suite. I am continuing those items in this worktree.
Author
Owner

Source finding for #1357: when focus remains on a .tile during Quick Look opening, PhotoTimeline.onKeyDown() treats ArrowRight as grid navigation and calls preventDefault(). PhotosView.onWindowKey() then returns for the prevented event, so Quick Look does not advance. I added an explicit viewerOpen handoff and a window-level fallback for the source-tile focus window; the real-server regression check is next.

Source finding for #1357: when focus remains on a `.tile` during Quick Look opening, `PhotoTimeline.onKeyDown()` treats ArrowRight as grid navigation and calls `preventDefault()`. `PhotosView.onWindowKey()` then returns for the prevented event, so Quick Look does not advance. I added an explicit `viewerOpen` handoff and a window-level fallback for the source-tile focus window; the real-server regression check is next.
Author
Owner

Progress at 0e64c377d:

  • The server-side fixture path is working. A 3-Photo EXIF-dated Home wrote in 3 ms; indexing took 14.8 s on this shared local host.
  • The real production-browser cold-open sample showed the decoded grid thumbnail first, then the original Photo in 592 ms. Immediate ArrowRight advanced Quick Look, and the 10 opening plus 10 next/previous sample loops completed.
  • The selection-only browser probe passed: Select all took 448 ms; three toggles measured p50 129.8 ms and p95 196 ms. This small fixture is only a harness smoke, not the requested 5,000-Photo profile.
  • I found and corrected two probe issues: a MutationObserver setup race, then digit parsing of the comma before the count. I also found the update toast was real: the web build used the server directory's 9-character prefix, while /api/v1/version returns the full build hash. I will rebuild with the exact server hash before capturing screenshots.
Progress at `0e64c377d`: - The server-side fixture path is working. A 3-Photo EXIF-dated Home wrote in 3 ms; indexing took 14.8 s on this shared local host. - The real production-browser cold-open sample showed the decoded grid thumbnail first, then the original Photo in 592 ms. Immediate ArrowRight advanced Quick Look, and the 10 opening plus 10 next/previous sample loops completed. - The selection-only browser probe passed: Select all took 448 ms; three toggles measured p50 129.8 ms and p95 196 ms. This small fixture is only a harness smoke, not the requested 5,000-Photo profile. - I found and corrected two probe issues: a MutationObserver setup race, then digit parsing of the comma before the count. I also found the update toast was real: the web build used the server directory's 9-character prefix, while `/api/v1/version` returns the full build hash. I will rebuild with the exact server hash before capturing screenshots.
Author
Owner

#1367 fixture-speed finding at head 03e130346: the server-side fixture wrote 5,000 EXIF-dated JPEGs (152,238,477 bytes) into the disposable test User Home in 1,004 ms. The real Photos Index exposed all 5,000 Items in 180,990 ms (~3.0 minutes) after restart. This replaces the earlier UI upload run that reached only 223 Photos after 13m39s. The local host load averages are included in the in-progress profile report; baseline/current browser measurements are running now.

#1367 fixture-speed finding at head 03e130346: the server-side fixture wrote 5,000 EXIF-dated JPEGs (152,238,477 bytes) into the disposable test User Home in 1,004 ms. The real Photos Index exposed all 5,000 Items in 180,990 ms (~3.0 minutes) after restart. This replaces the earlier UI upload run that reached only 223 Photos after 13m39s. The local host load averages are included in the in-progress profile report; baseline/current browser measurements are running now.
Author
Owner

Finding: server-side seeding made 5,000 EXIF-dated JPEGs (152,238,477 bytes) in 770 ms; the Photos Index exposed all 5,000 in 180,938 ms. The UI upload path had stored only 223 in about 13 minutes. The fixture must also wait for Photos' asynchronous thumbnail jobs: the first 1024 px requests returned 404 until the worker finished. The profile now waits for the first three real thumbnail URLs to return 200 and reloads the real grid before measuring. On 2026-10-08 I also fixed the shared focus trap's activation ring, which the keyboard-open screenshot assertion caught. Current head: 3fa6cb8cc.

Finding: server-side seeding made 5,000 EXIF-dated JPEGs (152,238,477 bytes) in 770 ms; the Photos Index exposed all 5,000 in 180,938 ms. The UI upload path had stored only 223 in about 13 minutes. The fixture must also wait for Photos' asynchronous thumbnail jobs: the first 1024 px requests returned 404 until the worker finished. The profile now waits for the first three real thumbnail URLs to return 200 and reloads the real grid before measuring. On 2026-10-08 I also fixed the shared focus trap's activation ring, which the keyboard-open screenshot assertion caught. Current head: 3fa6cb8cc.
Author
Owner

Finding: the 500-Photo index completed in 40,153 ms, but the profile waited 120 seconds for an s=1024 thumbnail. Photos queues at most 16 missing thumbnails per timeline read (crates/plugins/photos/src/routes.rs). I updated the profile to continue using the real timeline endpoint until the three visible thumbnails are ready, with a ten-minute bound. Bun syntax-check invocation executed the profile without its server environment; node --check and git diff --check both pass. Current head: c4f137ad6.

Finding: the 500-Photo index completed in 40,153 ms, but the profile waited 120 seconds for an s=1024 thumbnail. Photos queues at most 16 missing thumbnails per timeline read (crates/plugins/photos/src/routes.rs). I updated the profile to continue using the real timeline endpoint until the three visible thumbnails are ready, with a ten-minute bound. Bun syntax-check invocation executed the profile without its server environment; node --check and git diff --check both pass. Current head: c4f137ad6.
Author
Owner

Finding: with 500 seeded Photos, origin/dev reproduces #1357 (ArrowRight immediately after opening reports accepted=false). The baseline run then raced the Escape history transition in the click-to-pixel probe; the diagnostic showed the viewer was open but the pointerdown marker had not fired. I changed the profile to use a single browser click and wait for the /photos route before the next sample. Thumbnails for 480 px sources may not have an s=1024 rendition; the grid’s decoded source remains the target for first-frame proof. Current head: 2e7eb12aa.

Finding: with 500 seeded Photos, origin/dev reproduces #1357 (ArrowRight immediately after opening reports accepted=false). The baseline run then raced the Escape history transition in the click-to-pixel probe; the diagnostic showed the viewer was open but the pointerdown marker had not fired. I changed the profile to use a single browser click and wait for the /photos route before the next sample. Thumbnails for 480 px sources may not have an s=1024 rendition; the grid’s decoded source remains the target for first-frame proof. Current head: 2e7eb12aa.
Author
Owner

Finding: the 500-Photo baseline/current measurements completed after syncing the click path. Baseline: opening ArrowRight accepted=false and cold full decode 383.20 ms. This branch: accepted=true and cold full decode 170.20 ms. Screenshot capture then showed the spring had settled before its sample; the capture path used locator.click(), which waits for the target actionability. I changed the spring capture to a direct browser mouse click so the arrow input is sent closer to pointer-up. Local load at 02:03 UTC was 55.81 / 56.94 / 52.54. Current head: a68f80b77.

Finding: the 500-Photo baseline/current measurements completed after syncing the click path. Baseline: opening ArrowRight accepted=false and cold full decode 383.20 ms. This branch: accepted=true and cold full decode 170.20 ms. Screenshot capture then showed the spring had settled before its sample; the capture path used locator.click(), which waits for the target actionability. I changed the spring capture to a direct browser mouse click so the arrow input is sent closer to pointer-up. Local load at 02:03 UTC was 55.81 / 56.94 / 52.54. Current head: a68f80b77.
Author
Owner

Finding: the capture wait timed out because its newest 480×320 source has a real 256-pixel grid thumbnail but no 1024 rendition. The grid falls back to the 256 URL after the optional larger srcset request fails. The profile was incorrectly waiting on the initial currentSrc (1024); it now waits for the required image.src (256) and then verifies the decoded tile. Current head: a09aa09e6.

Finding: the capture wait timed out because its newest 480×320 source has a real 256-pixel grid thumbnail but no 1024 rendition. The grid falls back to the 256 URL after the optional larger srcset request fails. The profile was incorrectly waiting on the initial currentSrc (1024); it now waits for the required image.src (256) and then verifies the decoded tile. Current head: a09aa09e6.
Author
Owner

Finding: capture-only refused to proceed because the shared server build refreshed from d395b6e25, while the web assets still carried ef0aa12b. The build-ID guard correctly stopped the capture before the fixture was seeded. I will rebuild both current and origin/dev web assets with the shared server build ID d395b6e25, then retry. Current head: c95d0142b.

Finding: capture-only refused to proceed because the shared server build refreshed from d395b6e25, while the web assets still carried ef0aa12b. The build-ID guard correctly stopped the capture before the fixture was seeded. I will rebuild both current and origin/dev web assets with the shared server build ID d395b6e25, then retry. Current head: c95d0142b.
Author
Owner

Finding: the 500-Photo macOS-emulated capture reached keyboard-open Quick Look and failed the no-ring assertion: .tile:focus-visible still matched one source tile after Enter. The dialog itself had no visible focus ring. I am tracing the focus handoff and will add browser evidence for the fix. Current head: c95d0142b.

Finding: the 500-Photo macOS-emulated capture reached keyboard-open Quick Look and failed the no-ring assertion: `.tile:focus-visible` still matched one source tile after Enter. The dialog itself had no visible focus ring. I am tracing the focus handoff and will add browser evidence for the fix. Current head: c95d0142b.
Author
Owner

Correction: the capture assertion sampled immediately after the dialog appeared, before the shared focus trap completed its two requestAnimationFrame opening handoff. The source tile still matched :focus-visible at that instant; this did not establish a settled UI ring regression. I will wait until focus is inside Quick Look, then assert that neither the source tile nor the viewer shows a ring. Current head: c95d0142b.

Correction: the capture assertion sampled immediately after the dialog appeared, before the shared focus trap completed its two requestAnimationFrame opening handoff. The source tile still matched :focus-visible at that instant; this did not establish a settled UI ring regression. I will wait until focus is inside Quick Look, then assert that neither the source tile nor the viewer shows a ring. Current head: c95d0142b.
Author
Owner

Finding: the profile records the current build first-visible source as the 1024 px thumbnail (p50 52.5 ms, p95 105.6 ms; full-resolution p50 180.2 ms, p95 246.7 ms). A screenshot requested after the Playwright click round trip sometimes saw the later full source, so that capture was not a valid first-frame check. I replaced that probe with CDP compositor screencast frames correlated to per-animation-frame image source and transform samples. Current head: a29376ad5.

Finding: the profile records the current build first-visible source as the 1024 px thumbnail (p50 52.5 ms, p95 105.6 ms; full-resolution p50 180.2 ms, p95 246.7 ms). A screenshot requested after the Playwright click round trip sometimes saw the later full source, so that capture was not a valid first-frame check. I replaced that probe with CDP compositor screencast frames correlated to per-animation-frame image source and transform samples. Current head: a29376ad5.
Author
Owner

Finding: the CDP frame sampler records the first 4 visible frames from the grid thumbnail, then the full 1200 px image; every sampled transform stayed identity. Reduced motion was false. The opening FLIP origin is being lost before ImageView receives it, even though the first-visible thumbnail timing is correct. I am tracing the route reconciliation that clears the click launch rect. Current head: 4ba697270.

Finding: the CDP frame sampler records the first 4 visible frames from the grid thumbnail, then the full 1200 px image; every sampled transform stayed identity. Reduced motion was false. The opening FLIP origin is being lost before ImageView receives it, even though the first-visible thumbnail timing is correct. I am tracing the route reconciliation that clears the click launch rect. Current head: 4ba697270.
Author
Owner

Finished #1367 on job/photoopen-1367.

Head: 721ae156d5bd676ba457b21470c3a55ba9a274fc. This includes the one required merge from origin/dev (86af2e89e). No push, deploy, merge into dev, or issue close was done.

Built

  • Photos Quick Look paints the clicked tile's decoded thumbnail immediately, then swaps to the larger rendition after paint/decode. The opening spring stays interruptible. Existing tile intent also warms viewer renditions, and selection updates only changed Set keys and day counts.
  • Fixed #1357: ArrowRight sent immediately after opening Quick Look now advances the Photo, counter, and deep link. Added a real-browser profile assertion and an E2E assertion.
  • Extracted the existing offline Photos fixture writer from the #367 profile into a shared helper. Both profiles seed files into the disposable User Home before server start. The fixture has varied JPEG sizes and EXIF dates.
  • Used shared QuickLook/ImageView and OverlaySurface; no new visible controls or one-off styles were added.

Files: apps/web/src/lib/photos/{PhotosView.svelte,PhotoTimeline.svelte,PhotoViewer.svelte,timeline.svelte.ts,preload.ts} and their tests; packages/ui/src/components/{OverlaySurface.svelte,viewer/ImageView.svelte,viewer/QuickLook.svelte}; apps/web/src/lib/a11y/{focusTrap.ts,focusTrap.test.ts,inputModality.ts}; apps/web/src/lib/components/OverlaySurface.svelte.test.ts; apps/web/e2e/{photos.mjs,photos-perf.mjs,photo-library.mjs}; bench/photoopen-1367.mjs; apps/web/package.json; contracts/perf/{adoption-1058.json,exceptions.json}; docs/perf/baseline.json.

Browser proof

The attached proof archive contains 42 actual-size PNG captures and a Playwright trace. Captures use the macOS platform emulation at 1440×900, 820×1000, and 390×844 in light and dark themes. They include the grid, keyboard-open, click+1 frame, mid-spring, full-resolution settled, ArrowRight during spring, and settled viewer states. The browser assertions passed: the first visible viewer source is the decoded grid thumbnail; the full-resolution source later settles; ArrowRight changes the selected Photo while the spring is in flight; click and shortcut-open leave no focus ring.

The server-side fixture wrote 500 Photos (15,100,696 bytes) in 36 ms; the real Photos Index exposed them in 24,410 ms. The profiling run before the helper extraction wrote the same fixture in 81 ms and indexed it in 28,273 ms. Both runs used three EXIF-dated days. Capture-only verification after extraction passed against matching client/server build IDs d395b6e25da20d8c67b8b9fc1a593b813939c8a7.

Performance

The comparison used the origin/dev production client at d395b6e25da20d8c67b8b9fc1a593b813939c8a7 against the branch client with the same server and 500-Photo Home. Measurements ran locally on calternal-dev; load average was 25.02/28.24/34.06 before and 32.2/29.31/33.61 after. The perf VM was busy at the closeout status check, so these are load-sensitive local numbers, not a controlled perf-VM comparison. The values are recorded in docs/perf/baseline.json.

Operation origin/dev p50 / p95 branch p50 / p95
Click → first Photo pixels (10 samples) 198.9 / 265.3 ms 127.1 / 348.3 ms
Click → full resolution (10) 374.0 / 738.7 ms 440.9 / 669.4 ms
ArrowRight → next Photo visible (10) 59.7 / 131.4 ms 115.2 / 392.4 ms
Selection toggle (5) 175.1 / 313.5 ms 304.7 / 310.5 ms

Long tasks: 0 in both runs. Browser mean RSS/CPU was 681,238,809 B/116.34% at baseline and 766,510,839 B/99.56% on branch. Server mean RSS/CPU was 320,996,455 B/20.42% and 409,523,700 B/62.55%. High host load makes the latency and resource differences inconclusive.

Gates

cargo fmt --check exited 0 with empty output. No Rust crate changed, so Rust clippy/test gates did not apply. cargo clean completed.

cd apps/web && bun run check exited 0. Output:

perf-lint: PASS; 0 violations; 22544 scoped exceptions
User browser caches use userStorage; only documented device/public-link exceptions remain.
Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css.
Text sizes and UI shape values use shared role tokens.
Keyboard focus rings use the shared focus tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
svelte-check found 0 errors and 4 warnings in 2 files

The four Svelte warnings are existing unused CSS selectors in Mail and Money files, outside this change.

cd apps/web && bun run test --maxWorkers=2 exited 0:

Not implemented: Window's scrollTo() method
Not implemented: Window's scrollTo() method
Could not parse CSS stylesheet
Could not parse CSS stylesheet

 Test Files  298 passed (298)
      Tests  2121 passed (2121)
   Start at  03:22:19
   Duration  803.89s (environment 29%, transform 24%, import 24%, tests 17%, setup 7%)

Environment  |component| jsdom was created 122 times · 380.67s total, 32% of tracked time
             create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files)
             learn more: https://vitest.dev/guide/improving-performance#test-environments

node --check passed for the benchmark and shared seed helper; git diff --check and JSON parsing of docs/perf/baseline.json passed.

UX gaps closed / left

Closed: immediate first Photo pixels, full-resolution swap, spring-time input, the #1357 ArrowRight handoff, and no mouse/shortcut focus ring.

Left: CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs timed out during setup at all five uploaded burst thumbnails are ready, before reaching the viewer assertions. The focused real-browser profile did pass the immediate ArrowRight case. For the merge round, rerun that E2E command and confirm burst setup completes and the #1357 route/counter assertion passes.

Decisions

  • Keep the matching viewerLaunch rectangle through the route update, because the clicked Photo stays selected while its stable URL changes.
  • Use untrack(cancelOpenMotion) in the spring effect, because tracking the progress writes made the effect cancel its own transition.
  • Extract and share the existing offline seeder so the fixture bypasses upload round trips while preserving the #367 benchmark path.
Finished #1367 on `job/photoopen-1367`. **Head:** `721ae156d5bd676ba457b21470c3a55ba9a274fc`. This includes the one required merge from `origin/dev` (`86af2e89e`). No push, deploy, merge into `dev`, or issue close was done. ## Built - Photos Quick Look paints the clicked tile's decoded thumbnail immediately, then swaps to the larger rendition after paint/decode. The opening spring stays interruptible. Existing tile intent also warms viewer renditions, and selection updates only changed Set keys and day counts. - Fixed #1357: ArrowRight sent immediately after opening Quick Look now advances the Photo, counter, and deep link. Added a real-browser profile assertion and an E2E assertion. - Extracted the existing offline Photos fixture writer from the #367 profile into a shared helper. Both profiles seed files into the disposable User Home before server start. The fixture has varied JPEG sizes and EXIF dates. - Used shared `QuickLook`/`ImageView` and `OverlaySurface`; no new visible controls or one-off styles were added. Files: `apps/web/src/lib/photos/{PhotosView.svelte,PhotoTimeline.svelte,PhotoViewer.svelte,timeline.svelte.ts,preload.ts}` and their tests; `packages/ui/src/components/{OverlaySurface.svelte,viewer/ImageView.svelte,viewer/QuickLook.svelte}`; `apps/web/src/lib/a11y/{focusTrap.ts,focusTrap.test.ts,inputModality.ts}`; `apps/web/src/lib/components/OverlaySurface.svelte.test.ts`; `apps/web/e2e/{photos.mjs,photos-perf.mjs,photo-library.mjs}`; `bench/photoopen-1367.mjs`; `apps/web/package.json`; `contracts/perf/{adoption-1058.json,exceptions.json}`; `docs/perf/baseline.json`. ## Browser proof The [attached proof archive](https://git.kayg.org/attachments/35ff9a75-9465-498a-861e-9b6029e8e2cc) contains 42 actual-size PNG captures and a Playwright trace. Captures use the macOS platform emulation at 1440×900, 820×1000, and 390×844 in light and dark themes. They include the grid, keyboard-open, click+1 frame, mid-spring, full-resolution settled, ArrowRight during spring, and settled viewer states. The browser assertions passed: the first visible viewer source is the decoded grid thumbnail; the full-resolution source later settles; ArrowRight changes the selected Photo while the spring is in flight; click and shortcut-open leave no focus ring. The server-side fixture wrote 500 Photos (15,100,696 bytes) in 36 ms; the real Photos Index exposed them in 24,410 ms. The profiling run before the helper extraction wrote the same fixture in 81 ms and indexed it in 28,273 ms. Both runs used three EXIF-dated days. Capture-only verification after extraction passed against matching client/server build IDs `d395b6e25da20d8c67b8b9fc1a593b813939c8a7`. ## Performance The comparison used the `origin/dev` production client at `d395b6e25da20d8c67b8b9fc1a593b813939c8a7` against the branch client with the same server and 500-Photo Home. Measurements ran locally on `calternal-dev`; load average was 25.02/28.24/34.06 before and 32.2/29.31/33.61 after. The perf VM was busy at the closeout status check, so these are load-sensitive local numbers, not a controlled perf-VM comparison. The values are recorded in `docs/perf/baseline.json`. | Operation | origin/dev p50 / p95 | branch p50 / p95 | |---|---:|---:| | Click → first Photo pixels (10 samples) | 198.9 / 265.3 ms | 127.1 / 348.3 ms | | Click → full resolution (10) | 374.0 / 738.7 ms | 440.9 / 669.4 ms | | ArrowRight → next Photo visible (10) | 59.7 / 131.4 ms | 115.2 / 392.4 ms | | Selection toggle (5) | 175.1 / 313.5 ms | 304.7 / 310.5 ms | Long tasks: 0 in both runs. Browser mean RSS/CPU was 681,238,809 B/116.34% at baseline and 766,510,839 B/99.56% on branch. Server mean RSS/CPU was 320,996,455 B/20.42% and 409,523,700 B/62.55%. High host load makes the latency and resource differences inconclusive. ## Gates `cargo fmt --check` exited 0 with empty output. No Rust crate changed, so Rust clippy/test gates did not apply. `cargo clean` completed. `cd apps/web && bun run check` exited 0. Output: ```text perf-lint: PASS; 0 violations; 22544 scoped exceptions User browser caches use userStorage; only documented device/public-link exceptions remain. Glass alpha, blur and backdrop-filter roles use packages/ui/src/tokens.css. Text sizes and UI shape values use shared role tokens. Keyboard focus rings use the shared focus tokens. UI transitions and animation options use shared motion tokens or documented exceptions. svelte-check found 0 errors and 4 warnings in 2 files ``` The four Svelte warnings are existing unused CSS selectors in Mail and Money files, outside this change. `cd apps/web && bun run test --maxWorkers=2` exited 0: ```text Not implemented: Window's scrollTo() method Not implemented: Window's scrollTo() method Could not parse CSS stylesheet Could not parse CSS stylesheet Test Files 298 passed (298) Tests 2121 passed (2121) Start at 03:22:19 Duration 803.89s (environment 29%, transform 24%, import 24%, tests 17%, setup 7%) Environment |component| jsdom was created 122 times · 380.67s total, 32% of tracked time create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) learn more: https://vitest.dev/guide/improving-performance#test-environments ``` `node --check` passed for the benchmark and shared seed helper; `git diff --check` and JSON parsing of `docs/perf/baseline.json` passed. ## UX gaps closed / left Closed: immediate first Photo pixels, full-resolution swap, spring-time input, the #1357 ArrowRight handoff, and no mouse/shortcut focus ring. Left: `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` timed out during setup at `all five uploaded burst thumbnails are ready`, before reaching the viewer assertions. The focused real-browser profile did pass the immediate ArrowRight case. For the merge round, rerun that E2E command and confirm burst setup completes and the #1357 route/counter assertion passes. ## Decisions - Keep the matching `viewerLaunch` rectangle through the route update, because the clicked Photo stays selected while its stable URL changes. - Use `untrack(cancelOpenMotion)` in the spring effect, because tracking the progress writes made the effect cancel its own transition. - Extract and share the existing offline seeder so the fixture bypasses upload round trips while preserving the #367 benchmark path.
Author
Owner

Performance interpretation: the local p50 values do not prove the DESIGN §59 budgets (≤100 ms click-to-visible, ≤50 ms next Photo, one-frame selection). The recorded click-to-visible, ArrowRight and selection p50 values exceed those targets. The run had load averages of 25–34 and the perf VM was held by another job at closeout, so this is not a controlled budget pass or a reliable regression finding. Both runs recorded zero long tasks. The merge round can repeat bun ../../bench/photoopen-1367.mjs --count 5000 --baseline-build <origin-dev-build> through perf-run calternal photoopen-1367 30 -- ... when the perf VM is available; performance is not a merge gate.

Performance interpretation: the local p50 values do not prove the DESIGN §59 budgets (≤100 ms click-to-visible, ≤50 ms next Photo, one-frame selection). The recorded click-to-visible, ArrowRight and selection p50 values exceed those targets. The run had load averages of 25–34 and the perf VM was held by another job at closeout, so this is not a controlled budget pass or a reliable regression finding. Both runs recorded zero long tasks. The merge round can repeat `bun ../../bench/photoopen-1367.mjs --count 5000 --baseline-build <origin-dev-build>` through `perf-run calternal photoopen-1367 30 -- ...` when the perf VM is available; performance is not a merge gate.
Author
Owner

Starting follow-up on job/photoopen-1367 from base 721ae156d5. I will preserve the requested #1357 fix from job/bash3-filesphotos, investigate the capture and latency regressions, then run the focused gates and perf comparison.

Starting follow-up on job/photoopen-1367 from base 721ae156d5bd676ba457b21470c3a55ba9a274fc. I will preserve the requested #1357 fix from job/bash3-filesphotos, investigate the capture and latency regressions, then run the focused gates and perf comparison.
Author
Owner

Finding after #1357 merge: the attached local artifact artifacts/photoopen-1367-500-proof/1440-light-click-frame-1.png contains the Photos grid with the first tile focused; it is not a viewer frame. The profile currently chooses the screencast frame nearest a later RAF sample, so this does not yet establish which UI frame followed pointer-up. I am instrumenting pointer-up and its first RAF before judging the product path. Also, merged bun run check exited 1 because perf-lint marked PhotosView.svelte#each:0263a050f9b8ec37:1 as an unused or changed exact exception after the #1357 guard; svelte-check itself reported 0 errors and 4 warnings in Mail and Money. I will refresh the exact live pin after the source changes settle.

Finding after #1357 merge: the attached local artifact artifacts/photoopen-1367-500-proof/1440-light-click-frame-1.png contains the Photos grid with the first tile focused; it is not a viewer frame. The profile currently chooses the screencast frame nearest a later RAF sample, so this does not yet establish which UI frame followed pointer-up. I am instrumenting pointer-up and its first RAF before judging the product path. Also, merged bun run check exited 1 because perf-lint marked PhotosView.svelte#each:0263a050f9b8ec37:1 as an unused or changed exact exception after the #1357 guard; svelte-check itself reported 0 errors and 4 warnings in Mail and Money. I will refresh the exact live pin after the source changes settle.
Author
Owner

Source review identified three workload causes behind the regressions: PhotoViewer rebuilt a ViewerItem for every loaded Photo on every index change; selection queued 1024px decodes; and replacing the neighbor preload set could cancel the Photo that had just become current. These fixes are committed in 49f4e048a and have 32 passing focused tests.

The capture profile now records the first animation frame after pointer-up and selects the first compositor frame at or after it. That evidence change is committed in e4962ee18 and still needs a production-build run. The #1357 route and counter assertion remains from the merged job/bash3-filesphotos branch.

Source review identified three workload causes behind the regressions: PhotoViewer rebuilt a ViewerItem for every loaded Photo on every index change; selection queued 1024px decodes; and replacing the neighbor preload set could cancel the Photo that had just become current. These fixes are committed in 49f4e048a and have 32 passing focused tests. The capture profile now records the first animation frame after pointer-up and selects the first compositor frame at or after it. That evidence change is committed in e4962ee18 and still needs a production-build run. The #1357 route and counter assertion remains from the merged job/bash3-filesphotos branch.
Author
Owner

The first required CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs run reached the burst fixture but failed after its 60-second wait because all five thumbnail jobs were not ready. The server log recorded SQL pool acquisition delays of 7.6–8.1 seconds while the seeded library's thumbnail queue was active. I changed only this readiness deadline to 120 seconds, matching the other real-thumbnail fixture, and committed 333b63970. The rerun is in progress. The existing #1357 route and counter assertion in apps/web/e2e/photos.mjs remains intact.

The first required `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` run reached the burst fixture but failed after its 60-second wait because all five thumbnail jobs were not ready. The server log recorded SQL pool acquisition delays of 7.6–8.1 seconds while the seeded library's thumbnail queue was active. I changed only this readiness deadline to 120 seconds, matching the other real-thumbnail fixture, and committed `333b63970`. The rerun is in progress. The existing #1357 route and counter assertion in `apps/web/e2e/photos.mjs` remains intact.
Author
Owner

The rerun passed the burst readiness check, timeline, zoom, scrubber, #1357 route/counter assertion, Info shortcut, Back-to-/photos check, and the direct-link Inspector matrix. It then timed out at the existing Tags toast assertion in apps/web/e2e/photos.mjs:1265. The production call in PhotosView.svelte:950 passes policy: 'drop', and the shared toast helper in apps/web/src/lib/stores/toasts.svelte.ts:204 returns without showing a toast for that policy or for a message without an action/background result. I kept the existing assertion unchanged per the test-expectation rule. This e2e mismatch needs the orchestrator's decision.

The rerun passed the burst readiness check, timeline, zoom, scrubber, #1357 route/counter assertion, Info shortcut, Back-to-/photos check, and the direct-link Inspector matrix. It then timed out at the existing Tags toast assertion in `apps/web/e2e/photos.mjs:1265`. The production call in `PhotosView.svelte:950` passes `policy: 'drop'`, and the shared toast helper in `apps/web/src/lib/stores/toasts.svelte.ts:204` returns without showing a toast for that policy or for a message without an action/background result. I kept the existing assertion unchanged per the test-expectation rule. This e2e mismatch needs the orchestrator's decision.
Author
Owner

Perf VM finding: the 5,000-Photo interleaved run completed opening and navigation samples in all four blocks, then stopped in the origin/dev selection phase before writing a report. Its 5-second selection-count wait expired; at the catch, the count label read “Clear selection, 5,000 selected”. The diagnostic also looked for aria-pressed, which origin/dev does not use. Commit 1e4cd2e62 raises that bounded wait to 30 seconds without changing the expected count and reports the shared .selected state. I am rerunning the complete comparison under perf-run; the failed attempt produced no benchmark report.

Perf VM finding: the 5,000-Photo interleaved run completed opening and navigation samples in all four blocks, then stopped in the origin/dev selection phase before writing a report. Its 5-second selection-count wait expired; at the catch, the count label read “Clear selection, 5,000 selected”. The diagnostic also looked for aria-pressed, which origin/dev does not use. Commit 1e4cd2e62 raises that bounded wait to 30 seconds without changing the expected count and reports the shared .selected state. I am rerunning the complete comparison under perf-run; the failed attempt produced no benchmark report.
Author
Owner

The updated Info-shortcut capture found a real focus regression in the shared FloatingSurface path. focusTrap already used focusQuietly for initial focus, but FloatingSurface then focused the same target again with plain .focus(); the Inspector close button painted a 2px blue outline after the i shortcut. Commit 05240a869 routes that activation handoff through focusQuietly with scrolling suppressed and documents the shared rule. The focused focusTrap suite passed (9 tests). I am rebuilding and recapturing before the final matrix.

The updated Info-shortcut capture found a real focus regression in the shared FloatingSurface path. focusTrap already used focusQuietly for initial focus, but FloatingSurface then focused the same target again with plain .focus(); the Inspector close button painted a 2px blue outline after the `i` shortcut. Commit 05240a869 routes that activation handoff through focusQuietly with scrolling suppressed and documents the shared rule. The focused focusTrap suite passed (9 tests). I am rebuilding and recapturing before the final matrix.
Author
Owner

The first-frame proof had a timing defect: requestAnimationFrame’s supplied frame timestamp can precede a pointerup that arrived during that frame. The 1440 captures showed that case (sample time 8–34 ms before pointerup), and the CDP screenshot reply took about 320–360 ms. The harness now timestamps inside the callback, asserts that it follows pointerup, records the viewer image and launch-tile rectangles, and requires them to match within 2 px. Commit e7dbbd8b6 adds this stricter check. I am rerunning a focused 1440 capture to determine whether the viewer paint or the returned screenshot is late.

The first-frame proof had a timing defect: requestAnimationFrame’s supplied frame timestamp can precede a pointerup that arrived during that frame. The 1440 captures showed that case (sample time 8–34 ms before pointerup), and the CDP screenshot reply took about 320–360 ms. The harness now timestamps inside the callback, asserts that it follows pointerup, records the viewer image and launch-tile rectangles, and requires them to match within 2 px. Commit e7dbbd8b6 adds this stricter check. I am rerunning a focused 1440 capture to determine whether the viewer paint or the returned screenshot is late.
Author
Owner

The first-frame check confirmed the viewer image painted at its full fit size before the delayed FLIP. Commit ad317f5f5 now measures the image target in the mount effect and sets the source transform before the first paint. The focused 1440 production capture passed the new geometry assertion: the first post-pointerup rAF placed the 1024 px cached thumbnail at x=286.83, y=188.16, 270.93×180.71 px; the clicked tile was x=286.80, y=188.19, 271×180.66 px. Both had zero painted focus rings. The screenshot path is being changed to save the matching compositor frame, since the direct CDP reply arrived 190–265 ms later.

The first-frame check confirmed the viewer image painted at its full fit size before the delayed FLIP. Commit ad317f5f5 now measures the image target in the mount effect and sets the source transform before the first paint. The focused 1440 production capture passed the new geometry assertion: the first post-pointerup rAF placed the 1024 px cached thumbnail at x=286.83, y=188.16, 270.93×180.71 px; the clicked tile was x=286.80, y=188.19, 271×180.66 px. Both had zero painted focus rings. The screenshot path is being changed to save the matching compositor frame, since the direct CDP reply arrived 190–265 ms later.
Author
Owner

The completed perf VM A/B run on 5,000 real EXIF-dated Photos confirmed the selection cost remains high before the final preload change: origin/dev toggle p50/p95 110.2/128.3 ms; current branch 134/219.2 ms. Other hot paths improved in the same interleaved run: next Photo p50 59.9 → 32 ms and click-to-full-resolution p95 632.7 → 328.9 ms. The current 500-Photo run had selection p50 92.6 vs origin/dev 88.2 ms. I have added a selection-mode guard that cancels hover/focus 1024 px decode requests and will rerun the 500 and 5,000 A/B profiles against the updated production build.

The completed perf VM A/B run on 5,000 real EXIF-dated Photos confirmed the selection cost remains high before the final preload change: origin/dev toggle p50/p95 110.2/128.3 ms; current branch 134/219.2 ms. Other hot paths improved in the same interleaved run: next Photo p50 59.9 → 32 ms and click-to-full-resolution p95 632.7 → 328.9 ms. The current 500-Photo run had selection p50 92.6 vs origin/dev 88.2 ms. I have added a selection-mode guard that cancels hover/focus 1024 px decode requests and will rerun the 500 and 5,000 A/B profiles against the updated production build.
Author
Owner

Finding: The requested CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs run reaches the existing Photos tag-save assertion and fails there. PhotosView.saveTags passes policy:'drop'; the shared toast store intentionally suppresses dropped notices, so the asserted success label is never rendered. I am keeping the assertion and adding a real Undo action for the saved tag state.

Finding: The requested CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs run reaches the existing Photos tag-save assertion and fails there. PhotosView.saveTags passes policy:'drop'; the shared toast store intentionally suppresses dropped notices, so the asserted success label is never rendered. I am keeping the assertion and adding a real Undo action for the saved tag state.
Author
Owner

E2E finding (2026-10-08): CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs reached the tag-save flow, then Playwright timed out clicking Undo because the toast button never stabilized. The failure capture showed an Update available/Reload toast. I queried the shared server's public /api/v1/version route: it returned d395b6e25da20d8c67b8b9fc1a593b813939c8a7, while this build's .calternal-build-id was d395b6e25 (the short REV from CURRENT). Since the app compares full IDs, the persistent update toast remained in the stack. I am rebuilding with the full verified server ID and will rerun the E2E before treating the click as a code defect.

E2E finding (2026-10-08): `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` reached the tag-save flow, then Playwright timed out clicking Undo because the toast button never stabilized. The failure capture showed an Update available/Reload toast. I queried the shared server's public `/api/v1/version` route: it returned `d395b6e25da20d8c67b8b9fc1a593b813939c8a7`, while this build's `.calternal-build-id` was `d395b6e25` (the short `REV` from `CURRENT`). Since the app compares full IDs, the persistent update toast remained in the stack. I am rebuilding with the full verified server ID and will rerun the E2E before treating the click as a code defect.
Author
Owner

E2E finding: the matched-build run passed the #1357 ArrowRight route/counter checks, then failed the existing month scroll assertion with monthTop.y=44.7. The captured screen shows the route title at the top and the actual .timeline February 2024 heading around y=220, inside the existing 60–260 bound. getByRole('heading', { name: 'February 2024' }) matched the route title because its accessible name contains the month label. I will scope that same heading lookup to .timeline; the assertion range and behavior requirement remain unchanged.

E2E finding: the matched-build run passed the #1357 ArrowRight route/counter checks, then failed the existing month scroll assertion with `monthTop.y=44.7`. The captured screen shows the route title at the top and the actual `.timeline` February 2024 heading around y=220, inside the existing 60–260 bound. `getByRole('heading', { name: 'February 2024' })` matched the route title because its accessible name contains the month label. I will scope that same heading lookup to `.timeline`; the assertion range and behavior requirement remain unchanged.
Author
Owner

E2E resource finding: the full Mac run passed the route assertion and then missed the burst thumbnail deadline. The only relevant media error was NamespaceResources; this runtime's local CALTERNAL_MEDIA_SANDBOX_NPROC file was 1559 while the live host already had 1612 user threads. The required local ceiling fix 440e19dce is already an ancestor of this branch, so I will not merge origin/dev again. For the next local E2E only, I will provide the wrapper's existing supported cap of 65536, which is within its explicit bound, then rerun once.

E2E resource finding: the full Mac run passed the route assertion and then missed the burst thumbnail deadline. The only relevant media error was `NamespaceResources`; this runtime's local `CALTERNAL_MEDIA_SANDBOX_NPROC` file was 1559 while the live host already had 1612 user threads. The required local ceiling fix `440e19dce` is already an ancestor of this branch, so I will not merge `origin/dev` again. For the next local E2E only, I will provide the wrapper's existing supported cap of 65536, which is within its explicit bound, then rerun once.
Author
Owner

E2E finding: the burst flow's Undo toast uses the shared 8-second action window. The test waited for a second timeline API read before clicking; under this host's slow DB reads the toast expired, and the failure screenshot shows it removed. The toast store pauses that shared timer on hover. I will hover the existing Undo action while the unchanged timeline assertion completes, then click it; the API and Undo assertions remain intact.

E2E finding: the burst flow's Undo toast uses the shared 8-second action window. The test waited for a second timeline API read before clicking; under this host's slow DB reads the toast expired, and the failure screenshot shows it removed. The toast store pauses that shared timer on hover. I will hover the existing Undo action while the unchanged timeline assertion completes, then click it; the API and Undo assertions remain intact.
Author
Owner

During the production Mac-emulated E2E run, the tag Undo toast stayed mounted but was unreachable inside full-screen Quick Look. A DOM probe measured the Undo button at y=-49 and its toast at y=-56 in a 1440×900 viewport. AppToaster.svelte used the active modal's top edge (0) as a bottom offset, which places the toast above the viewport. I am fixing the shared dock selection and adding a regression check; this also explains why hovering Undo timed out after the app action had succeeded.

During the production Mac-emulated E2E run, the tag Undo toast stayed mounted but was unreachable inside full-screen Quick Look. A DOM probe measured the Undo button at y=-49 and its toast at y=-56 in a 1440×900 viewport. `AppToaster.svelte` used the active modal's top edge (0) as a bottom offset, which places the toast above the viewport. I am fixing the shared dock selection and adding a regression check; this also explains why hovering Undo timed out after the app action had succeeded.
Author
Owner

A focused shared-toaster regression check found a second dock-ordering case: with an active nested dialog at y=400 and a parent overlay dock at y=600, the previous selector chose the parent dock (CSS bottom offset 180px) instead of the active dialog edge (380px). The toaster now selects docks within the active overlay first, so a covered parent cannot move a toast over the child surface's controls. The focused AppToaster suite passes 5/5.

A focused shared-toaster regression check found a second dock-ordering case: with an active nested dialog at y=400 and a parent overlay dock at y=600, the previous selector chose the parent dock (CSS bottom offset 180px) instead of the active dialog edge (380px). The toaster now selects docks within the active overlay first, so a covered parent cannot move a toast over the child surface's controls. The focused AppToaster suite passes 5/5.
Author
Owner

The focused photos.mjs --band-only probe reproduced the selection failure in the real production UI. After the first band update, Chromium reported gotpointercapture on the timeline, then lostpointercapture at the next pointer event; the rectangle remained 10×15 px and only 1 Photo was selected. The test's later cross-day move did not update the band. I am replacing root-only drag tracking with temporary window listeners during the active pointer gesture, which also handles movement outside the timeline.

The focused `photos.mjs --band-only` probe reproduced the selection failure in the real production UI. After the first band update, Chromium reported `gotpointercapture` on the timeline, then `lostpointercapture` at the next pointer event; the rectangle remained 10×15 px and only 1 Photo was selected. The test's later cross-day move did not update the band. I am replacing root-only drag tracking with temporary window listeners during the active pointer gesture, which also handles movement outside the timeline.
Author
Owner

E2E finding: the test served web assets built as d395b6… against the shared server refreshed to af202e151. This showed an “Update available” toast; the first Escape dismissed that toast, so the Inspector remained open and the detach assertion timed out. I am rebuilding the production web assets with the server's current build ID before rerunning the required flow.

E2E finding: the test served web assets built as d395b6… against the shared server refreshed to af202e151. This showed an “Update available” toast; the first Escape dismissed that toast, so the Inspector remained open and the detach assertion timed out. I am rebuilding the production web assets with the server's current build ID before rerunning the required flow.
Author
Owner

Follow-up finding: the first identity correction used the shared server's 9-character filename prefix (af202e151), but the production server version uses the full commit SHA. The rerun still showed “Update available” in its failure screenshot, so its first-Escape focus result was not valid. I am rebuilding the client with full origin/dev SHA af202e1514 and will rerun the flow.

Follow-up finding: the first identity correction used the shared server's 9-character filename prefix (af202e151), but the production server version uses the full commit SHA. The rerun still showed “Update available” in its failure screenshot, so its first-Escape focus result was not valid. I am rebuilding the client with full origin/dev SHA af202e1514955ddf0f8b4b3d2503c780d807ed84 and will rerun the flow.
Author
Owner

The full-SHA production build removed the Update notice. In the full Mac-emulated E2E, the tag Undo action completed, its toast left the DOM, focus was in the Inspector, and one Escape closed the Inspector. The run also passed month deep links, burst key-photo Undo, and RAW+JPEG before reaching the later HLS probe. I am keeping the full E2E running through the remaining checks after correcting the HLS probe's direct-source retry behavior.

The full-SHA production build removed the Update notice. In the full Mac-emulated E2E, the tag Undo action completed, its toast left the DOM, focus was in the Inspector, and one Escape closed the Inspector. The run also passed month deep links, burst key-photo Undo, and RAW+JPEG before reaching the later HLS probe. I am keeping the full E2E running through the remaining checks after correcting the HLS probe's direct-source retry behavior.
Author
Owner

The Mac-emulated full E2E now passes the HLS fallback probe: the browser requests the master and variant playlists and consumes the real fixture segment, then direct playback also passes. The fixture rejects every direct-source retry because Chromium issued a second direct request before the prior one settled. The run then found an ambiguous “More actions” locator where the header and selection toolbar both expose that accessible name; I am scoping it to the Photos header action group.

The Mac-emulated full E2E now passes the HLS fallback probe: the browser requests the master and variant playlists and consumes the real fixture segment, then direct playback also passes. The fixture rejects every direct-source retry because Chromium issued a second direct request before the prior one settled. The run then found an ambiguous “More actions” locator where the header and selection toolbar both expose that accessible name; I am scoping it to the Photos header action group.
Author
Owner

The selection-mode E2E now resolves the header's “More actions” button through the named Photos actions group, so the intended Select photos menu opens and the test advances to the selection-mode click check. That later check exposed a pointer intercept during the shared mode-tray exit transition; I am waiting for the outgoing hidden pane to detach before the next pointer action.

The selection-mode E2E now resolves the header's “More actions” button through the named Photos actions group, so the intended Select photos menu opens and the test advances to the selection-mode click check. That later check exposed a pointer intercept during the shared mode-tray exit transition; I am waiting for the outgoing hidden pane to detach before the next pointer action.
Author
Owner

The selection-mode E2E reached the correct Photos menu, then exposed a stale test step: the second click on the selected tile had already cleared selection and restored the mode tray, so the test tried to click a Clear selection action that no longer existed. The failure capture shows the grid and mode tray with no selected tile. I replaced that redundant click with an assertion that the selection count reaches zero and its control leaves before continuing.

The selection-mode E2E reached the correct Photos menu, then exposed a stale test step: the second click on the selected tile had already cleared selection and restored the mode tray, so the test tried to click a Clear selection action that no longer existed. The failure capture shows the grid and mode tray with no selected tile. I replaced that redundant click with an assertion that the selection count reaches zero and its control leaves before continuing.
Author
Owner

E2E update: a full CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs run reached the Photos selection, tag and Trash flows with the production client build ID matching the shared server. The Trash Undo locator matched two buttons because an earlier Undo toast was still exiting. I scoped it to the “Moved 2 items to the Trash” toast. The same run also showed the E2E's redundant “Clear selection” click happened after the selected tile had already cleared itself; the test now waits for the shared capsule exit before continuing. These are harness corrections; no product behavior changed. The full rerun is in progress.

E2E update: a full `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` run reached the Photos selection, tag and Trash flows with the production client build ID matching the shared server. The Trash Undo locator matched two buttons because an earlier Undo toast was still exiting. I scoped it to the “Moved 2 items to the Trash” toast. The same run also showed the E2E's redundant “Clear selection” click happened after the selected tile had already cleared itself; the test now waits for the shared capsule exit before continuing. These are harness corrections; no product behavior changed. The full rerun is in progress.
Author
Owner

E2E finding: Trash Undo restored the server bucket total, but the visible first day stayed at 6 tiles instead of returning to 8 (failure screenshot: target/tmp/photos-e2e-failure-1791469480932.png). PhotosView used the shared trashItems receipt without its existing onrestored hook, so the loaded timeline did not refresh after Undo. I now refresh the Photos buckets on receipt restoration; the existing E2E assertion covers the restored day. I will rerun the full flow.

E2E finding: Trash Undo restored the server bucket total, but the visible first day stayed at 6 tiles instead of returning to 8 (failure screenshot: `target/tmp/photos-e2e-failure-1791469480932.png`). `PhotosView` used the shared `trashItems` receipt without its existing `onrestored` hook, so the loaded timeline did not refresh after Undo. I now refresh the Photos buckets on receipt restoration; the existing E2E assertion covers the restored day. I will rerun the full flow.
Author
Owner

E2E follow-up: forcing the immediate and delayed bucket reads still left the first day at 6 tiles after Undo. subscribeFiles can reschedule the same timers with the normal cache setting after the Undo event, cancelling the forced retry. I am retaining the forced setting across reschedules and using a generation guard so an older timer cannot clear a newer retry. The next full rerun will validate the race fix.

E2E follow-up: forcing the immediate and delayed bucket reads still left the first day at 6 tiles after Undo. `subscribeFiles` can reschedule the same timers with the normal cache setting after the Undo event, cancelling the forced retry. I am retaining the forced setting across reschedules and using a generation guard so an older timer cannot clear a newer retry. The next full rerun will validate the race fix.
Author
Owner

E2E update: Photos selection, Tags, Trash Undo, and the restored-day assertion now pass. The run then reaches Calendar and finds the captured Photo activity, but its accessible button name says “photos at …” while the existing assertion expects “photos saved at …” (the expectation was added in fe5e7203). I will correct the shared Calendar activity label and keep the test assertion unchanged.

E2E update: Photos selection, Tags, Trash Undo, and the restored-day assertion now pass. The run then reaches Calendar and finds the captured Photo activity, but its accessible button name says “photos at …” while the existing assertion expects “photos saved at …” (the expectation was added in `fe5e7203`). I will correct the shared Calendar activity label and keep the test assertion unchanged.
Author
Owner

Full CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs now passes the Photos viewer/deep-link/route-counter checks, selection, tags, Trash Undo restoration, pair deletion, upload and keyboard navigation. It then reaches the unchanged Calendar /p/<id> assertion and fails because a Day activity with one saved photo is left selected without opening its shared preview. The early return was added for the More Items overflow-list second-click flow in #1115, but it also suppresses the normal Day activity preview. I will scope that selection-only behavior to the More Items row so the Day activity keeps its established preview and stable photo link.

Full `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` now passes the Photos viewer/deep-link/route-counter checks, selection, tags, Trash Undo restoration, pair deletion, upload and keyboard navigation. It then reaches the unchanged Calendar `/p/<id>` assertion and fails because a Day activity with one saved photo is left selected without opening its shared preview. The early return was added for the More Items overflow-list second-click flow in #1115, but it also suppresses the normal Day activity preview. I will scope that selection-only behavior to the More Items row so the Day activity keeps its established preview and stable photo link.
Author
Owner

The rerun passed the unchanged Calendar /p/<id> check, Settings, extensionless media, and all requested Photos checks, including the #1357 route/counter assertion. It exits 1 only in verifyExtensionlessStartup: the legacy fixture deletes Files migrations 26 and 27, then asserts the maximum is 25, but the current origin/dev schema also has Files migration 28 (crates/plugins/files/migrations/0028_modified_mtime_lookup.sql). I kept the existing E2E fixture and assertion unchanged per the owner rule against changing test fixtures to pass. This needs an owner-approved fixture update before the full E2E can be green on the current server.

The rerun passed the unchanged Calendar `/p/<id>` check, Settings, extensionless media, and all requested Photos checks, including the #1357 route/counter assertion. It exits 1 only in `verifyExtensionlessStartup`: the legacy fixture deletes Files migrations 26 and 27, then asserts the maximum is 25, but the current `origin/dev` schema also has Files migration 28 (`crates/plugins/files/migrations/0028_modified_mtime_lookup.sql`). I kept the existing E2E fixture and assertion unchanged per the owner rule against changing test fixtures to pass. This needs an owner-approved fixture update before the full E2E can be green on the current server.
Author
Owner

Completed work on job/photoopen-1367

Head: 42231eccd4449231f4debd838f3eef949a044b3a
git fetch origin completed. git merge origin/dev returned Already up to date; origin/dev (af202e1514955ddf0f8b4b3d2503c780d807ed84) was already an ancestor. The #1357 fix from job/bash3-filesphotos (809a1e18a) is in this branch.

Built

  • Photos Quick Look now paints the clicked tile's cached image in its first viewer frame and animates from the tile bounds. Mouse activation and shortcut-driven focus stay quiet; the shared data-input focus behavior remains in place.
  • Photos warming uses a shared, owner-aware queue with at most two active viewer renditions. Selection mode cancels opens-only work. The shared ImageView keeps the grid thumbnail live while viewer-size and full-resolution sources decode offscreen.
  • ArrowRight after opening updates both the stable /p/<id> route and the library counter (#1357). The Calendar Day activity preview opens on the first click; More Items keeps its select-first behavior for a single saved item.
  • Used the shared QuickLook, ImageView, and OverlaySurface; no new visual control was added.
  • Added bench/photoopen-1367.mjs for same-Home ABBA comparisons at 500 and 5,000 Photos.

UX gaps closed

  • The clicked Photo is visible in the first frame at 390, 820, and 1440 px, in light and dark mode. Captures emulate macOS. All 18 actual-size captures are attached below; the click-frame logs report the grid thumbnail as the first source and zero focus rings.
  • Pointer, keyboard shortcut, and ArrowRight route/counter paths were exercised. The requested burst-thumbnail wait no longer times out.

UX gaps left

  • CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs reached and passed the #1357 route/counter assertion and the other Photos flows, then exited at the legacy extensionless-startup fixture. It deletes Files migration versions 26/27 and asserts max version 25, while the origin/dev server contains migration 28. I left the existing fixture and assertion unchanged. The test must be reconciled with the current migration set before this command can be green.
  • Controlled perf-VM A/B metrics at 500 and 5,000 Photos are not available. The first locked attempt exited before sampling because the staged media runtime launcher symlink pointed to the local worktree. I corrected the staged launcher and verified all required executables. The retry stayed queued until the 45-minute wait limit, when mailsel-1449-origin-a held the VM; I canceled it. No local substitute was measured and no benchmark numbers were added to docs/perf/baseline.json. That file's existing 500-Photo high-load entry is from an earlier revision and is not evidence for this head.

Cost and decisions

The benchmark creates and indexes N real JPEG Photos, then runs ten samples per build for opening, full-resolution arrival, next/previous navigation, and selection toggles. The 500-Photo capture fixture indexed in 25,520 ms on the local build host; that is setup time only, not an A/B performance result. The 5,000-Photo profile remains unmeasured.

DESIGN §59 does not specify an intermediate viewer rendition size or the concurrency of its preloader. I chose a 1024 px viewer rendition and one shared queue capped at two active decodes, with owner-based cancellation. The clicked tile thumbnail remains visible until the next source has decoded.

Gates

bun run check (exit 0):

perf-lint: PASS; 0 violations; 22544 scoped exceptions
svelte-check found 0 errors and 4 warnings in 2 files

bun run test --maxWorkers=2 (exit 0):

 Test Files  298 passed (298)
      Tests  2129 passed (2129)
   Start at  17:27:29
   Duration  654.65s (environment 27%, transform 26%, import 24%, tests 18%, setup 5%)

cargo clean output:

     Removed 1 file, 356B total

No Rust crate changed. Local web builds and the origin/dev baseline checkout were removed. git diff --check origin/dev...HEAD passed; the worktree is clean.

Files

  • Photos UI: apps/web/src/lib/photos/PhotoTimeline.svelte, PhotoViewer.svelte, PhotosView.svelte, preload.ts, timeline.svelte.ts
  • Photos tests: apps/web/src/lib/photos/preload.test.ts, timeline.test.ts
  • Shared viewer and overlays: packages/ui/src/components/OverlaySurface.svelte, packages/ui/src/components/menu/FloatingSurface.svelte, packages/ui/src/components/viewer/ImageView.svelte, QuickLook.svelte
  • Shared Calendar activity: packages/ui/src/components/calendar/GridColumn.svelte, apps/web/src/routes/calendar/[view]/[date]/+page.svelte
  • Accessibility and toaster: apps/web/src/lib/a11y/focusTrap.ts, focusTrap.test.ts, inputModality.ts, apps/web/src/lib/components/AppToaster.svelte, AppToaster.svelte.test.ts, OverlaySurface.svelte.test.ts
  • E2E and performance: apps/web/e2e/audiophotos-720.mjs, files.mjs, photo-library.mjs, photos-perf.mjs, photos.mjs, apps/web/package.json, bench/photoopen-1367.mjs
  • Performance data: contracts/perf/adoption-1058.json, contracts/perf/exceptions.json, docs/perf/baseline.json

For the merge round

  • cd apps/web && CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs must prove the #1357 route/counter assertion and extensionless startup after the legacy migration fixture is reconciled.
  • Run bench/photoopen-1367.mjs under perf-run calternal photoopen3-500 20 and perf-run calternal photoopen3-5000 45, each against origin/dev and this branch, with ten samples per build. Record click-to-visible, full-resolution, navigation, selection, CPU/RSS, fixture/index time, and load average in docs/perf/baseline.json.

Attached evidence

## Completed work on `job/photoopen-1367` **Head:** `42231eccd4449231f4debd838f3eef949a044b3a` `git fetch origin` completed. `git merge origin/dev` returned `Already up to date`; origin/dev (`af202e1514955ddf0f8b4b3d2503c780d807ed84`) was already an ancestor. The #1357 fix from `job/bash3-filesphotos` (`809a1e18a`) is in this branch. ### Built - Photos Quick Look now paints the clicked tile's cached image in its first viewer frame and animates from the tile bounds. Mouse activation and shortcut-driven focus stay quiet; the shared `data-input` focus behavior remains in place. - Photos warming uses a shared, owner-aware queue with at most two active viewer renditions. Selection mode cancels opens-only work. The shared ImageView keeps the grid thumbnail live while viewer-size and full-resolution sources decode offscreen. - ArrowRight after opening updates both the stable `/p/<id>` route and the library counter (#1357). The Calendar Day activity preview opens on the first click; More Items keeps its select-first behavior for a single saved item. - Used the shared `QuickLook`, `ImageView`, and `OverlaySurface`; no new visual control was added. - Added `bench/photoopen-1367.mjs` for same-Home ABBA comparisons at 500 and 5,000 Photos. ### UX gaps closed - The clicked Photo is visible in the first frame at 390, 820, and 1440 px, in light and dark mode. Captures emulate macOS. All 18 actual-size captures are attached below; the click-frame logs report the grid thumbnail as the first source and zero focus rings. - Pointer, keyboard shortcut, and ArrowRight route/counter paths were exercised. The requested burst-thumbnail wait no longer times out. ### UX gaps left - `CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` reached and passed the #1357 route/counter assertion and the other Photos flows, then exited at the legacy extensionless-startup fixture. It deletes Files migration versions 26/27 and asserts max version 25, while the origin/dev server contains migration 28. I left the existing fixture and assertion unchanged. The test must be reconciled with the current migration set before this command can be green. - Controlled perf-VM A/B metrics at 500 and 5,000 Photos are not available. The first locked attempt exited before sampling because the staged media runtime launcher symlink pointed to the local worktree. I corrected the staged launcher and verified all required executables. The retry stayed queued until the 45-minute wait limit, when `mailsel-1449-origin-a` held the VM; I canceled it. No local substitute was measured and no benchmark numbers were added to `docs/perf/baseline.json`. That file's existing 500-Photo high-load entry is from an earlier revision and is not evidence for this head. ### Cost and decisions The benchmark creates and indexes N real JPEG Photos, then runs ten samples per build for opening, full-resolution arrival, next/previous navigation, and selection toggles. The 500-Photo capture fixture indexed in 25,520 ms on the local build host; that is setup time only, not an A/B performance result. The 5,000-Photo profile remains unmeasured. DESIGN §59 does not specify an intermediate viewer rendition size or the concurrency of its preloader. I chose a 1024 px viewer rendition and one shared queue capped at two active decodes, with owner-based cancellation. The clicked tile thumbnail remains visible until the next source has decoded. ### Gates `bun run check` (exit 0): ```text perf-lint: PASS; 0 violations; 22544 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files ``` `bun run test --maxWorkers=2` (exit 0): ```text Test Files 298 passed (298) Tests 2129 passed (2129) Start at 17:27:29 Duration 654.65s (environment 27%, transform 26%, import 24%, tests 18%, setup 5%) ``` `cargo clean` output: ```text Removed 1 file, 356B total ``` No Rust crate changed. Local web builds and the origin/dev baseline checkout were removed. `git diff --check origin/dev...HEAD` passed; the worktree is clean. ### Files - Photos UI: `apps/web/src/lib/photos/PhotoTimeline.svelte`, `PhotoViewer.svelte`, `PhotosView.svelte`, `preload.ts`, `timeline.svelte.ts` - Photos tests: `apps/web/src/lib/photos/preload.test.ts`, `timeline.test.ts` - Shared viewer and overlays: `packages/ui/src/components/OverlaySurface.svelte`, `packages/ui/src/components/menu/FloatingSurface.svelte`, `packages/ui/src/components/viewer/ImageView.svelte`, `QuickLook.svelte` - Shared Calendar activity: `packages/ui/src/components/calendar/GridColumn.svelte`, `apps/web/src/routes/calendar/[view]/[date]/+page.svelte` - Accessibility and toaster: `apps/web/src/lib/a11y/focusTrap.ts`, `focusTrap.test.ts`, `inputModality.ts`, `apps/web/src/lib/components/AppToaster.svelte`, `AppToaster.svelte.test.ts`, `OverlaySurface.svelte.test.ts` - E2E and performance: `apps/web/e2e/audiophotos-720.mjs`, `files.mjs`, `photo-library.mjs`, `photos-perf.mjs`, `photos.mjs`, `apps/web/package.json`, `bench/photoopen-1367.mjs` - Performance data: `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json`, `docs/perf/baseline.json` ### For the merge round - `cd apps/web && CALTERNAL_E2E_MAC=1 bun e2e/photos.mjs` must prove the #1357 route/counter assertion and extensionless startup after the legacy migration fixture is reconciled. - Run `bench/photoopen-1367.mjs` under `perf-run calternal photoopen3-500 20` and `perf-run calternal photoopen3-5000 45`, each against origin/dev and this branch, with ten samples per build. Record click-to-visible, full-resolution, navigation, selection, CPU/RSS, fixture/index time, and load average in `docs/perf/baseline.json`. ### Attached evidence - phone 390px, light: [Photos grid](https://git.kayg.org/attachments/8262f460-d0a9-46c6-bec7-94b81eb94c7c), [keyboard open](https://git.kayg.org/attachments/dc91d507-c0de-402a-b2a5-1b3c691a7bdc), [mouse first frame](https://git.kayg.org/attachments/772e28de-7361-4199-9252-cadd93b1ec35) - phone 390px, dark: [Photos grid](https://git.kayg.org/attachments/5629e24f-529c-4bed-acfe-d54e4a8ba69b), [keyboard open](https://git.kayg.org/attachments/59c3080f-4c87-4687-9a9c-78714e8c7da6), [mouse first frame](https://git.kayg.org/attachments/110db688-f665-4f59-96b5-e6fe8ba4cd7f) - tablet 820px, light: [Photos grid](https://git.kayg.org/attachments/04eee535-a9d9-4a7f-89b2-6d17d8df5210), [keyboard open](https://git.kayg.org/attachments/b93951a7-b174-4ace-b5e1-8ede0d8ae95a), [mouse first frame](https://git.kayg.org/attachments/df9b80be-5900-43f5-8631-8ed76d15f6d9) - tablet 820px, dark: [Photos grid](https://git.kayg.org/attachments/43aed620-4b3d-4e5c-b5db-9f69cf9756f6), [keyboard open](https://git.kayg.org/attachments/77e59a1a-998b-4dba-87a8-c57889b85a6c), [mouse first frame](https://git.kayg.org/attachments/8b3213f3-d3c3-40a0-b16f-703c6274016a) - desktop 1440px, light: [Photos grid](https://git.kayg.org/attachments/6aee53ac-ea49-4677-aa11-36d1c23268ae), [keyboard open](https://git.kayg.org/attachments/ff0ee22c-6bbb-4388-a9d9-f78e344bab0d), [mouse first frame](https://git.kayg.org/attachments/f7b95a32-2951-4650-a254-8762d45e8b0d) - desktop 1440px, dark: [Photos grid](https://git.kayg.org/attachments/baa57803-2c6a-4468-9d6d-c6d9e75ab9a7), [keyboard open](https://git.kayg.org/attachments/79957687-ae56-4689-91c5-7593bb11fa3c), [mouse first frame](https://git.kayg.org/attachments/1fce6f8d-4de2-4b9a-9c39-2ca3b11cd68a)
Author
Owner

Starting photoopen4 on job/photoopen-1367. Branch head 91cf6b9bff; base origin/dev af202e1514.

Starting photoopen4 on job/photoopen-1367. Branch head 91cf6b9bff1507327c611c6c421f8bd1033ea072; base origin/dev af202e1514955ddf0f8b4b3d2503c780d807ed84.
Author
Owner

Finding: the current Files migration set reaches 28 (), but modeled production migration 25 while only deleting receipts 26/27. Migration 28 creates without . I updated the copied legacy Index to drop that index and receipts 26/27/28, and kept the upgrade/restart receipt check for Files 26/27/28 plus Search 5.

Finding: the current Files migration set reaches 28 (), but modeled production migration 25 while only deleting receipts 26/27. Migration 28 creates without . I updated the copied legacy Index to drop that index and receipts 26/27/28, and kept the upgrade/restart receipt check for Files 26/27/28 plus Search 5.
Author
Owner

Finding: Files migrations reach 28 (crates/plugins/files/migrations/0028_modified_mtime_lookup.sql), while photos.mjs modeled production migration 25 but removed only receipts 26/27. Migration 28 creates files_index_modified without IF NOT EXISTS. I updated the copied legacy Index to drop that index and receipts 26/27/28, and kept the upgrade and restart receipt check for Files 26/27/28 plus Search 5.

Finding: Files migrations reach 28 (crates/plugins/files/migrations/0028_modified_mtime_lookup.sql), while photos.mjs modeled production migration 25 but removed only receipts 26/27. Migration 28 creates files_index_modified without IF NOT EXISTS. I updated the copied legacy Index to drop that index and receipts 26/27/28, and kept the upgrade and restart receipt check for Files 26/27/28 plus Search 5.
Author
Owner

Finding: the full Photos E2E passed the viewer navigation flows but exited before the migration fixture at apps/web/e2e/photos.mjs:1319. After the Undo toast detached and the API confirmed zero Tags, document.activeElement was outside .inspector-surface; the assertion reported actual false, expected true. The prior photoopen3 run reached the migration fixture, so this may be timing-sensitive. I am keeping the expected focus behavior unchanged while checking the flow.

Finding: the full Photos E2E passed the viewer navigation flows but exited before the migration fixture at apps/web/e2e/photos.mjs:1319. After the Undo toast detached and the API confirmed zero Tags, document.activeElement was outside .inspector-surface; the assertion reported actual false, expected true. The prior photoopen3 run reached the migration fixture, so this may be timing-sensitive. I am keeping the expected focus behavior unchanged while checking the flow.
Author
Owner

The focused content-type E2E now passes the copied production-7c migration and second-restart assertions. It then exits at apps/web/e2e/photos.mjs:468 while looking for the exact HEIC image Kind label under .info after Meta+i. This is after the migration coverage; I am comparing the locator with current Files E2E selectors without changing UI code or weakening the expected label.

The focused content-type E2E now passes the copied production-7c migration and second-restart assertions. It then exits at apps/web/e2e/photos.mjs:468 while looking for the exact HEIC image Kind label under .info after Meta+i. This is after the migration coverage; I am comparing the locator with current Files E2E selectors without changing UI code or weakening the expected label.
Author
Owner

The content-type migration check passed: the copied production-7c Index upgraded Files 26/27/28 and Search 5 once, and the second restart skipped the completed backfill. The later E2E assertion at photos.mjs:468 expects a MIME-specific label inside .info. Current InfoPanel.svelte derives Kind as File for ordinary files (kindName = explicitItemKind ?? ... ?? File); the MIME-specific labels are in the Files list. I am leaving this expectation unchanged under the owner rule and will report the E2E gap.

The content-type migration check passed: the copied production-7c Index upgraded Files 26/27/28 and Search 5 once, and the second restart skipped the completed backfill. The later E2E assertion at photos.mjs:468 expects a MIME-specific label inside .info. Current InfoPanel.svelte derives Kind as File for ordinary files (kindName = explicitItemKind ?? ... ?? File); the MIME-specific labels are in the Files list. I am leaving this expectation unchanged under the owner rule and will report the E2E gap.
Author
Owner

On merged head 5f1d2ef7d5, the full Photos E2E passes the empty state, timeline, zoom, and scrubber checks, then times out at apps/web/e2e/photos.mjs:1239. After ArrowRight and opening the Inspector with “i”, the test cannot find the “Taken” field within 30 seconds. I kept the assertion and UI code unchanged per the job boundary. The full run has not reached the migration fixture; I am running its focused content-type flow separately.

On merged head 5f1d2ef7d56945227678acbeacad2ffc9b4a523c, the full Photos E2E passes the empty state, timeline, zoom, and scrubber checks, then times out at apps/web/e2e/photos.mjs:1239. After ArrowRight and opening the Inspector with “i”, the test cannot find the “Taken” field within 30 seconds. I kept the assertion and UI code unchanged per the job boundary. The full run has not reached the migration fixture; I am running its focused content-type flow separately.
Author
Owner

Focused Photos migration run on the merged branch confirms the legacy fixture now passes without removing the migration check: PASS copied production-7c Files/Search Index upgrades once; second restart skips completed backfill. The run then stops in the existing extensionless Inspector assertion at apps/web/e2e/photos.mjs:468, waiting 30 seconds for HEIC image; I left the expectation and UI untouched per the task boundary. The full Photos E2E also stops at the existing Taken Inspector assertion at line 1239. Evidence is in target/photos-e2e-migration-final.log and target/photos-e2e-final.log in the worktree.

Focused Photos migration run on the merged branch confirms the legacy fixture now passes without removing the migration check: `PASS copied production-7c Files/Search Index upgrades once; second restart skips completed backfill`. The run then stops in the existing extensionless Inspector assertion at `apps/web/e2e/photos.mjs:468`, waiting 30 seconds for `HEIC image`; I left the expectation and UI untouched per the task boundary. The full Photos E2E also stops at the existing `Taken` Inspector assertion at line 1239. Evidence is in `target/photos-e2e-migration-final.log` and `target/photos-e2e-final.log` in the worktree.
Author
Owner

The required Mac Chrome A/B phase is waiting on the shared VM lock. The lock waiter has run for more than two hours, and other SSH sessions on calternal@10.69.69.21 remain active. I have not entered the VM or started a competing browser session. The runner is staged locally and I will continue while the four-hour job window allows; no performance numbers are available yet.

The required Mac Chrome A/B phase is waiting on the shared VM lock. The lock waiter has run for more than two hours, and other SSH sessions on `calternal@10.69.69.21` remain active. I have not entered the VM or started a competing browser session. The runner is staged locally and I will continue while the four-hour job window allows; no performance numbers are available yet.
Author
Owner

Mac Chrome A/B complete under /tmp/mac-bench.lock, using Chrome 155.0.8059.39 on the macOS VM with ANGLE Metal rendering confirmed. All values are p50/p95 milliseconds, origin/dev → branch:

Photos Metric origin/dev branch
500 click → first visible frame 165.0 / 190.6 66.4 / 105.6
500 click → full resolution 1172.9 / 1456.7 1170.2 / 1314.7
500 ArrowRight next Photo visible 928.2 / 1243.4 889.3 / 984.1
5,000 click → first visible frame 149.0 / 196.5 66.8 / 126.2
5,000 click → full resolution 1294.7 / 1469.8 1220.0 / 1414.2
5,000 ArrowRight next Photo visible 916.9 / 1181.9 928.0 / 967.7

The p50 regression rule passes on every metric. The fixed first-frame p95 ceiling fails at both sizes (105.6 ms and 126.2 ms > 100 ms), so the decision is REJECT. No UI tuning was made. End-of-run Mac load average was 3.80 / 4.40 / 4.10. Perf VM measurements are queued behind another holder.

Mac Chrome A/B complete under `/tmp/mac-bench.lock`, using Chrome 155.0.8059.39 on the macOS VM with ANGLE Metal rendering confirmed. All values are p50/p95 milliseconds, origin/dev → branch: | Photos | Metric | origin/dev | branch | |---:|---|---:|---:| | 500 | click → first visible frame | 165.0 / 190.6 | 66.4 / 105.6 | | 500 | click → full resolution | 1172.9 / 1456.7 | 1170.2 / 1314.7 | | 500 | ArrowRight next Photo visible | 928.2 / 1243.4 | 889.3 / 984.1 | | 5,000 | click → first visible frame | 149.0 / 196.5 | 66.8 / 126.2 | | 5,000 | click → full resolution | 1294.7 / 1469.8 | 1220.0 / 1414.2 | | 5,000 | ArrowRight next Photo visible | 916.9 / 1181.9 | 928.0 / 967.7 | The p50 regression rule passes on every metric. The fixed first-frame p95 ceiling fails at both sizes (105.6 ms and 126.2 ms > 100 ms), so the decision is REJECT. No UI tuning was made. End-of-run Mac load average was 3.80 / 4.40 / 4.10. Perf VM measurements are queued behind another holder.
Author
Owner

Finished the #1367 completion pass. The UI implementation remains unchanged. The Photos legacy fixture now models Files 25 / Search 4, drops Files 26–28 and Search 5 receipts, and retains the migration/backfill assertions. The migration scenario passed:

PASS copied production-7c Files/Search Index upgrades once; second restart skips completed backfill

The full Photos E2E then exited at the existing Info shortcut assertion:

TimeoutError: waitFor: Timeout 30000ms exceeded.
waiting for locator('.inspector-body').getByText('Taken') to be visible

The focused migration run also passed that migration scenario, then exited at the later extensionless HEIC Info assertion (photos.mjs:468, waiting for “HEIC image”). No UI code or test expectation was changed for either failure.

Performance A/B used three alternating rounds and 15 samples per metric. Values below are p50/p95 milliseconds, origin/dev → branch:

Host / Photos Metric origin/dev branch
Mac Chrome / 500 Click → first frame 165.0 / 190.6 66.4 / 105.6
Mac Chrome / 500 Click → full resolution 1172.9 / 1456.7 1170.2 / 1314.7
Mac Chrome / 500 ArrowRight next Photo 928.2 / 1243.4 889.3 / 984.1
Mac Chrome / 5,000 Click → first frame 149.0 / 196.5 66.8 / 126.2
Mac Chrome / 5,000 Click → full resolution 1294.7 / 1469.8 1220.0 / 1414.2
Mac Chrome / 5,000 ArrowRight next Photo 916.9 / 1181.9 928.0 / 967.7
Perf VM / 500 Click → first frame 147.8 / 204.0 68.8 / 97.9
Perf VM / 500 Click → full resolution 247.2 / 591.3 261.8 / 370.2
Perf VM / 500 ArrowRight next Photo 49.3 / 380.5 19.3 / 49.1
Perf VM / 5,000 Click → first frame 160.8 / 317.8 66.3 / 95.5
Perf VM / 5,000 Click → full resolution 350.3 / 824.7 299.6 / 415.4
Perf VM / 5,000 ArrowRight next Photo 55.9 / 99.2 25.0 / 83.2

Mac Chrome 155.0.8059.39 used ANGLE Metal rendering. The Mac p50 regression rule passed for every metric, but the fixed first-frame p95 ceiling failed at both sizes: 105.6 ms at 500 and 126.2 ms at 5,000. Decision: REJECT. The Perf VM load5 was 2.83 → 3.48 for 500 and 3.48 → 3.89 for 5,000, sampled inside each perf-run lock. docs/perf/baseline.json was not changed because the fixed Mac rule rejected the result.

Gates (after the origin/dev merge):

perf-lint: PASS; 0 violations; 22544 scoped exceptions
svelte-check found 0 errors and 4 warnings in 2 files

 Test Files  302 passed (302)
      Tests  2179 passed (2179)
     Start at  21:10:46
   Duration  453.00s (environment 29%, transform 25%, import 22%, tests 18%, setup 6%)

cargo clean: Removed 1 file, 356B total. Web build output was removed. No Rust files changed. The benchmark-only --skip-opening-arrow-assertion option is opt-in for headless Perf VM profiling; the default remains strict, and Mac runs used the strict default.

Head: 33621f9e5c3c65b95cc0b36a22de5c5e284a7460.

Finished the #1367 completion pass. The UI implementation remains unchanged. The Photos legacy fixture now models Files 25 / Search 4, drops Files 26–28 and Search 5 receipts, and retains the migration/backfill assertions. The migration scenario passed: `PASS copied production-7c Files/Search Index upgrades once; second restart skips completed backfill` The full Photos E2E then exited at the existing Info shortcut assertion: `TimeoutError: waitFor: Timeout 30000ms exceeded.` `waiting for locator('.inspector-body').getByText('Taken') to be visible` The focused migration run also passed that migration scenario, then exited at the later extensionless HEIC Info assertion (`photos.mjs:468`, waiting for “HEIC image”). No UI code or test expectation was changed for either failure. Performance A/B used three alternating rounds and 15 samples per metric. Values below are p50/p95 milliseconds, origin/dev → branch: | Host / Photos | Metric | origin/dev | branch | |---|---|---:|---:| | Mac Chrome / 500 | Click → first frame | 165.0 / 190.6 | 66.4 / 105.6 | | Mac Chrome / 500 | Click → full resolution | 1172.9 / 1456.7 | 1170.2 / 1314.7 | | Mac Chrome / 500 | ArrowRight next Photo | 928.2 / 1243.4 | 889.3 / 984.1 | | Mac Chrome / 5,000 | Click → first frame | 149.0 / 196.5 | 66.8 / 126.2 | | Mac Chrome / 5,000 | Click → full resolution | 1294.7 / 1469.8 | 1220.0 / 1414.2 | | Mac Chrome / 5,000 | ArrowRight next Photo | 916.9 / 1181.9 | 928.0 / 967.7 | | Perf VM / 500 | Click → first frame | 147.8 / 204.0 | 68.8 / 97.9 | | Perf VM / 500 | Click → full resolution | 247.2 / 591.3 | 261.8 / 370.2 | | Perf VM / 500 | ArrowRight next Photo | 49.3 / 380.5 | 19.3 / 49.1 | | Perf VM / 5,000 | Click → first frame | 160.8 / 317.8 | 66.3 / 95.5 | | Perf VM / 5,000 | Click → full resolution | 350.3 / 824.7 | 299.6 / 415.4 | | Perf VM / 5,000 | ArrowRight next Photo | 55.9 / 99.2 | 25.0 / 83.2 | Mac Chrome 155.0.8059.39 used ANGLE Metal rendering. The Mac p50 regression rule passed for every metric, but the fixed first-frame p95 ceiling failed at both sizes: 105.6 ms at 500 and 126.2 ms at 5,000. Decision: REJECT. The Perf VM load5 was 2.83 → 3.48 for 500 and 3.48 → 3.89 for 5,000, sampled inside each `perf-run` lock. `docs/perf/baseline.json` was not changed because the fixed Mac rule rejected the result. Gates (after the origin/dev merge): ```text perf-lint: PASS; 0 violations; 22544 scoped exceptions svelte-check found 0 errors and 4 warnings in 2 files Test Files 302 passed (302) Tests 2179 passed (2179) Start at 21:10:46 Duration 453.00s (environment 29%, transform 25%, import 22%, tests 18%, setup 6%) ``` `cargo clean`: `Removed 1 file, 356B total`. Web build output was removed. No Rust files changed. The benchmark-only `--skip-opening-arrow-assertion` option is opt-in for headless Perf VM profiling; the default remains strict, and Mac runs used the strict default. Head: `33621f9e5c3c65b95cc0b36a22de5c5e284a7460`.
Author
Owner

Perf VM resource summary, measured by the same three-round profile under perf-run (origin/dev → branch):

Photos Process Mean CPU Mean RSS Peak RSS
500 Chromium process tree 157.33% → 171.31% 705,815,053 → 740,847,302 B 819,425,280 → 819,593,216 B
500 Server 18.06% → 18.84% 302,938,663 → 347,965,490 B 492,429,312 → 492,851,200 B
5,000 Chromium process tree 177.52% → 155.12% 908,135,446 → 872,064,983 B 1,090,617,344 → 1,246,609,408 B
5,000 Server 7.47% → 8.03% 351,017,778 → 334,990,448 B 410,198,016 → 366,977,024 B

The fixture wrote 15,100,696 bytes / 500 Photos and 151,580,646 bytes / 5,000 Photos. Perf VM Index times were 14,234 ms and 26,508 ms. The reported Mac p95 rejection remains unchanged.

Perf VM resource summary, measured by the same three-round profile under `perf-run` (origin/dev → branch): | Photos | Process | Mean CPU | Mean RSS | Peak RSS | |---:|---|---:|---:|---:| | 500 | Chromium process tree | 157.33% → 171.31% | 705,815,053 → 740,847,302 B | 819,425,280 → 819,593,216 B | | 500 | Server | 18.06% → 18.84% | 302,938,663 → 347,965,490 B | 492,429,312 → 492,851,200 B | | 5,000 | Chromium process tree | 177.52% → 155.12% | 908,135,446 → 872,064,983 B | 1,090,617,344 → 1,246,609,408 B | | 5,000 | Server | 7.47% → 8.03% | 351,017,778 → 334,990,448 B | 410,198,016 → 366,977,024 B | The fixture wrote 15,100,696 bytes / 500 Photos and 151,580,646 bytes / 5,000 Photos. Perf VM Index times were 14,234 ms and 26,508 ms. The reported Mac p95 rejection remains unchanged.
Author
Owner

Orchestrator decision: ACCEPT 33621f9e5 despite the pre-set 100 ms absolute p95 ceiling. The branch beats origin/dev on every metric on both hosts (Mac first frame p50 165->66 / 149->67 ms, p95 191->106 / 197->126 ms; ArrowRight p95 1243->984 ms), and the Mac numbers include the ssh-tunnel HTTP/1.1 overhead under investigation in selroot. The ceiling stays the target for a follow-up. Merge round must run the full Photos e2e: the Info 'Taken' / 'HEIC image' assertions timed out on this branch and must be classified against dev before merge.

Orchestrator decision: ACCEPT 33621f9e5 despite the pre-set 100 ms absolute p95 ceiling. The branch beats origin/dev on every metric on both hosts (Mac first frame p50 165->66 / 149->67 ms, p95 191->106 / 197->126 ms; ArrowRight p95 1243->984 ms), and the Mac numbers include the ssh-tunnel HTTP/1.1 overhead under investigation in selroot. The ceiling stays the target for a follow-up. Merge round must run the full Photos e2e: the Info 'Taken' / 'HEIC image' assertions timed out on this branch and must be classified against dev before merge.
Author
Owner

Deployed to calternal.cloud in merge round 35c (build 5a2c02c13, live 2026-10-09 ~16:35 IST). Closing.

Deployed to calternal.cloud in merge round 35c (build 5a2c02c13, live 2026-10-09 ~16:35 IST). Closing.
kayg closed this issue 2026-10-09 11:11:43 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1367
No description provided.