Safari: pull-down rubber-band shows a black band above the app; no document scroll + theme colour everywhere #718

Open
opened 2026-10-02 10:58:05 +00:00 by kayg · 13 comments
Owner

Owner report (2026-10-02, Safari on macOS)

A long pull-down at the top of calternal.cloud rubber-bands the whole page and shows a black band with Safari's reload spinner above the app. The band should not appear. If Safari still allows its gesture, the exposed area must show our own background, not black.

Expected

  1. The app shell is not the document scroller. html and body are fixed to the viewport (height: 100dvh; overflow: hidden), and each mode's content scrolls in its own container with overscroll-behavior: contain (or none at the top level). This is how app-like web UIs avoid page-level rubber-banding and pull-to-refresh. Keep scroll position restoration, keyboard scrolling (Space/PageDown), anchor/deep-link scrolling and focus-into-view working. The iOS Safari address-bar collapse behaviour must be decided and documented: with no document scroll, the bars stay expanded.
  2. Set overscroll-behavior: none on html and body as a second guard (Safari 16+ honours it for the root scroller).
  3. Paint what can still show: html gets the theme's background colour (the same token as the app background, per scheme). The <meta name="theme-color"> values (light and dark, media queries) are updated live when the theme or scheme changes. Then any remaining overscroll, safe-area or bar tint shows our colour instead of black or white. This also covers the earlier iOS status-bar safe-area report.
  4. A picture background is visible behind the content, not only behind the scroller.

Tests

  • Playwright WebKit: the document has no scrollable overflow (scrollingElement.scrollHeight == clientHeight) on every route; mode content still scrolls; deep links scroll to their target.
  • A screenshot test of an overscroll gesture (where WebKit allows simulating it) shows the theme colour, not black.
  • Manual check on Safari macOS and iOS: write exact steps for the owner, because the macOS VM is offline.
## Owner report (2026-10-02, Safari on macOS) A long pull-down at the top of calternal.cloud rubber-bands the whole page and shows a **black band** with Safari's reload spinner above the app. The band should not appear. If Safari still allows its gesture, the exposed area must show our own background, not black. ## Expected 1. **The app shell is not the document scroller.** `html` and `body` are fixed to the viewport (`height: 100dvh; overflow: hidden`), and each mode's content scrolls in its own container with `overscroll-behavior: contain` (or `none` at the top level). This is how app-like web UIs avoid page-level rubber-banding and pull-to-refresh. Keep scroll position restoration, keyboard scrolling (Space/PageDown), anchor/deep-link scrolling and focus-into-view working. The iOS Safari address-bar collapse behaviour must be decided and documented: with no document scroll, the bars stay expanded. 2. Set `overscroll-behavior: none` on `html` and `body` as a second guard (Safari 16+ honours it for the root scroller). 3. **Paint what can still show:** `html` gets the theme's background colour (the same token as the app background, per scheme). The `<meta name="theme-color">` values (light and dark, `media` queries) are updated live when the theme or scheme changes. Then any remaining overscroll, safe-area or bar tint shows our colour instead of black or white. This also covers the earlier iOS status-bar safe-area report. 4. A picture background is visible behind the content, not only behind the scroller. ## Tests - Playwright WebKit: the document has no scrollable overflow (`scrollingElement.scrollHeight == clientHeight`) on every route; mode content still scrolls; deep links scroll to their target. - A screenshot test of an overscroll gesture (where WebKit allows simulating it) shows the theme colour, not black. - Manual check on Safari macOS and iOS: write exact steps for the owner, because the macOS VM is offline.
Author
Owner

Starting #718 on job/overscroll-718, based on c4a61e8cf0 (origin/dev). I’m checking the current shell scroll and theme mechanisms and the shared background-token work before editing.

Starting #718 on job/overscroll-718, based on c4a61e8cf090170f35b1bed3350d9de20c83ecd5 (origin/dev). I’m checking the current shell scroll and theme mechanisms and the shared background-token work before editing.
Author
Owner

Finding: the shared #route-content feed was still the document scroller, and Files/Photos virtualization, the Photos scrubber and Note recovery read or wrote window.scrollY. I’m moving those consumers to the existing feed while keeping the ref-counted OverlaySurface lock API. The shell now keeps html/body fixed and lets #route-content own contained scrolling; this also means iOS Safari’s address bar stays expanded. I’m using the existing --paper token and not changing theme tokens for #588.

Finding: the shared #route-content feed was still the document scroller, and Files/Photos virtualization, the Photos scrubber and Note recovery read or wrote window.scrollY. I’m moving those consumers to the existing feed while keeping the ref-counted OverlaySurface lock API. The shell now keeps html/body fixed and lets #route-content own contained scrolling; this also means iOS Safari’s address bar stays expanded. I’m using the existing --paper token and not changing theme tokens for #588.
Author
Owner

The serial web test gate is slow on the shared host. bun run test -- --maxWorkers=1 had not reached the end of its 152 test files after more than 30 minutes, so I stopped it (exit 130) to continue with the required production WebKit checks. Before stopping, it reported 8 failures in untouched UI tests: modeHeader.svelte.test.ts (1), StatRow.svelte.test.ts (1), RangeBar.svelte.test.ts (1), ThemePicker.svelte.test.ts (2), fontsGroup.svelte.test.ts (1), SettingsCard.svelte.test.ts (1), and SelectionBar.svelte.test.ts (1). Each reported case took 5.2–13.3 seconds against the usual 5-second window. I did not change test expectations.

The serial web test gate is slow on the shared host. `bun run test -- --maxWorkers=1` had not reached the end of its 152 test files after more than 30 minutes, so I stopped it (exit 130) to continue with the required production WebKit checks. Before stopping, it reported 8 failures in untouched UI tests: `modeHeader.svelte.test.ts` (1), `StatRow.svelte.test.ts` (1), `RangeBar.svelte.test.ts` (1), `ThemePicker.svelte.test.ts` (2), `fontsGroup.svelte.test.ts` (1), `SettingsCard.svelte.test.ts` (1), and `SelectionBar.svelte.test.ts` (1). Each reported case took 5.2–13.3 seconds against the usual 5-second window. I did not change test expectations.
Author
Owner

The first WebKit E2E attempt did not reach any route assertions. After the harness accepted /readyz, registerOwner navigated Chromium to http://localhost:6189/readyz and received net::ERR_CONNECTION_REFUSED. The server process was started from the existing debug server binary built from the job's backend base. I am adding server diagnostics to the E2E failure path and will make one bounded rerun; no route behavior has been validated yet.

The first WebKit E2E attempt did not reach any route assertions. After the harness accepted `/readyz`, `registerOwner` navigated Chromium to `http://localhost:6189/readyz` and received `net::ERR_CONNECTION_REFUSED`. The server process was started from the existing debug server binary built from the job's backend base. I am adding server diagnostics to the E2E failure path and will make one bounded rerun; no route behavior has been validated yet.
Author
Owner

The bounded WebKit retry reached the real API but stopped in owner setup, before route assertions or screenshots. The server reported SQLite schema statements taking 10.1–11.4 seconds, connection-pool acquisition taking up to 22.6 seconds (2-second threshold), a background job pool timeout, and then /api/v1/auth/passkeys/login/start returned 503 after registration had returned 200. The harness closed the server. This is a SLOW-only shared-host result; it does not identify a #718 UI failure. The WebKit route/screenshot matrix and local scroll profile therefore remain unverified here. No assertions were changed.

The bounded WebKit retry reached the real API but stopped in owner setup, before route assertions or screenshots. The server reported SQLite schema statements taking 10.1–11.4 seconds, connection-pool acquisition taking up to 22.6 seconds (2-second threshold), a background job pool timeout, and then `/api/v1/auth/passkeys/login/start` returned 503 after registration had returned 200. The harness closed the server. This is a SLOW-only shared-host result; it does not identify a #718 UI failure. The WebKit route/screenshot matrix and local scroll profile therefore remain unverified here. No assertions were changed.
Author
Owner

The IPv4 browser-host workaround was not valid for passkey setup: the server rejects an RP ID of localhost when the browser origin is 127.0.0.1 (rp_id is not an effective_domain of rp_origin). I replaced that change with a test-only IPv6 loopback bind while preserving the http://localhost:<port> origin, so the browser, public URL and WebAuthn RP remain aligned. This only changes the shared E2E harness when bindHost is explicitly supplied.

The IPv4 browser-host workaround was not valid for passkey setup: the server rejects an RP ID of `localhost` when the browser origin is `127.0.0.1` (`rp_id is not an effective_domain of rp_origin`). I replaced that change with a test-only IPv6 loopback bind while preserving the `http://localhost:<port>` origin, so the browser, public URL and WebAuthn RP remain aligned. This only changes the shared E2E harness when `bindHost` is explicitly supplied.
Author
Owner

The final WebKit run preserved the localhost WebAuthn origin and got past owner registration. It then timed out waiting for #route-content on /today after 30 seconds. Server diagnostics from the same run show SQLite pool acquisition up to 27.5 seconds against a 2-second threshold, plus worker/cron timeouts and Index is busy errors. No route assertions or screenshots completed. This is a SLOW-only shared-host failure; the WebKit matrix and the scroll benchmark have no measurements from this host. I am stopping after this time-boxed run rather than retrying against the same overloaded server.

The final WebKit run preserved the `localhost` WebAuthn origin and got past owner registration. It then timed out waiting for `#route-content` on `/today` after 30 seconds. Server diagnostics from the same run show SQLite pool acquisition up to 27.5 seconds against a 2-second threshold, plus worker/cron timeouts and `Index is busy` errors. No route assertions or screenshots completed. This is a SLOW-only shared-host failure; the WebKit matrix and the scroll benchmark have no measurements from this host. I am stopping after this time-boxed run rather than retrying against the same overloaded server.
Author
Owner

Implementation for Forgejo #718 is committed on job/overscroll-718. Base was c4a61e8cf; origin/dev merged cleanly as 3c7541287. No push or deploy was made. Final head: 4d119bcb2417ae4affe34e9486529cbe7ec0d7e4.

Built

  • Fixed html, body and the app frame to the viewport. The existing #route-content feed now owns app scrolling and contains overscroll. The root paints the existing --paper token.
  • Reused the ref-counted overlay scroll lock to freeze and restore that feed. Updated Photos, Files, Notes, Calendar, the mode header and pull gesture to read the same scroller, with Window fallbacks for non-app hosts.
  • Added per-history-entry feed restoration while keeping native hash-anchor navigation. Made the feed keyboard-focusable and named for assistive technology.
  • Kept both theme-color media values live. System mode uses the light and dark shared --paper tokens; a forced mode writes its active token to both.
  • Added a production WebKit route/history/theme/safe-area screenshot matrix and a route-feed scroll performance profile. The shared E2E harness accepts an optional IPv6 loopback bind while retaining the localhost WebAuthn origin.

Files

  • Shell and theme: apps/web/src/app.d.ts, apps/web/src/app.html, apps/web/src/calternal-app.css, apps/web/src/routes/+layout.svelte, apps/web/src/routes/layout.css, apps/web/src/lib/themeColor.ts, apps/web/src/lib/stores/settings-store.ts.
  • Scroll consumers: apps/web/src/lib/overlay/scrollLock.ts, apps/web/src/lib/notes/NoteView.svelte, apps/web/src/lib/photos/PhotoTimeline.svelte, apps/web/src/lib/photos/TimelineScrubber.svelte, apps/web/src/routes/calendar/[view]/[date]/+page.svelte, packages/ui/src/components/ModeHeader.svelte, packages/ui/src/components/files/FileCollection.svelte, packages/ui/src/gestures.ts.
  • Verification and profile: apps/web/e2e/harness.mjs, apps/web/e2e/overscroll-718.mjs, apps/web/e2e/process-perf.mjs, apps/web/package.json, bench/overscroll-718.mjs.

UX gaps closed

  • Route scrolling now works through one accessible feed; PageDown/PageUp can target it. Overlay close restores the same feed position. History traversal restores distinct entries, and hash anchors keep their native target behavior.
  • Photos, Files and Calendar geometry follows the feed rather than window.scrollY. System theme changes update both browser chrome colors.

UX gaps left

  • The WebKit run did not complete route assertions, so it produced no screenshots to attach. Its final run passed owner setup, then timed out waiting for #route-content; server logs showed SQLite pool acquisition up to 27.5s against a 2s threshold, plus background worker/cron timeouts. This is a SLOW-only shared-host result. The scroll profile was also attempted but produced no measurements under the same host pressure.
  • The serial web suite did not finish. After more than 30 minutes it had reported eight failures in untouched UI tests, with each reported test taking 5.2–13.3s against the normal 5s timeout. I stopped it with exit 130; no expectations changed.
  • The owner still needs to perform the manual Safari checks below on macOS and iOS.

Decisions not stated in DESIGN

  • A shallow history-state ID distinguishes entries that share a URL. Feed offsets live in an 80-entry in-memory map to bound memory.
  • The browser fixture binds to IPv6 loopback but keeps the localhost origin so WebAuthn RP validation remains correct.
  • I reused --paper and added no token, coordinated with #588.

Gates and evidence

bun run check completed with:

User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/overscroll-718/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

bun run build completed with:

✓ built in 4m 55s
Run npm run preview to preview your production build locally.
> Using @sveltejs/adapter-static
  Wrote site to "build"
  ✔ done

cargo fmt --check exited 0 with no output. No Rust crate or server contract changed, so Rust clippy/test gates did not apply. The debug server build for browser work was stopped after 21 minutes of shared-host compilation; I used the existing debug server artifact from the unchanged backend base. cargo clean completed:

Removed 1052 files, 529.9MiB total

git diff --check passed. Node syntax checks passed for the new E2E and benchmark scripts. The WebKit E2E, scroll profile and web unit suite remain unverified as described above.

Manual Safari steps for the owner

macOS Safari

  1. Open /today. In Appearance, select Paper, then Tokyo Night, and then System. Check that Safari’s top chrome follows the active paper color in each mode.
  2. Scroll down, return to the top, and pull down; then scroll to the bottom and pull up. Confirm the page feed scrolls, the document does not move, and the exposed edge uses the selected theme color.
  3. Focus Page content and press PageDown and PageUp. Confirm the feed moves and focus remains visible.
  4. Scroll to a Note heading, open its deep link, then use Back and Forward. Confirm the heading and feed position restore.
  5. Open Notifications over a scrolled page, close it with Escape, and confirm the feed returns to the same position.

iOS Safari

  1. Open /today with System appearance. Repeat the top and bottom pull checks in Light and Dark. Check the status area, Safari toolbar and home-indicator safe area against the page paper color; confirm Safari’s address bar stays expanded.
  2. Repeat with Paper and Tokyo Night forced in Appearance.
  3. Open a Note heading deep link, use browser Back, and confirm the heading and feed position restore. Open and close Notifications and confirm the page position remains stable.
Implementation for Forgejo #718 is committed on `job/overscroll-718`. Base was `c4a61e8cf`; `origin/dev` merged cleanly as `3c7541287`. No push or deploy was made. Final head: `4d119bcb2417ae4affe34e9486529cbe7ec0d7e4`. ## Built - Fixed `html`, `body` and the app frame to the viewport. The existing `#route-content` feed now owns app scrolling and contains overscroll. The root paints the existing `--paper` token. - Reused the ref-counted overlay scroll lock to freeze and restore that feed. Updated Photos, Files, Notes, Calendar, the mode header and pull gesture to read the same scroller, with Window fallbacks for non-app hosts. - Added per-history-entry feed restoration while keeping native hash-anchor navigation. Made the feed keyboard-focusable and named for assistive technology. - Kept both `theme-color` media values live. System mode uses the light and dark shared `--paper` tokens; a forced mode writes its active token to both. - Added a production WebKit route/history/theme/safe-area screenshot matrix and a route-feed scroll performance profile. The shared E2E harness accepts an optional IPv6 loopback bind while retaining the `localhost` WebAuthn origin. ## Files - Shell and theme: `apps/web/src/app.d.ts`, `apps/web/src/app.html`, `apps/web/src/calternal-app.css`, `apps/web/src/routes/+layout.svelte`, `apps/web/src/routes/layout.css`, `apps/web/src/lib/themeColor.ts`, `apps/web/src/lib/stores/settings-store.ts`. - Scroll consumers: `apps/web/src/lib/overlay/scrollLock.ts`, `apps/web/src/lib/notes/NoteView.svelte`, `apps/web/src/lib/photos/PhotoTimeline.svelte`, `apps/web/src/lib/photos/TimelineScrubber.svelte`, `apps/web/src/routes/calendar/[view]/[date]/+page.svelte`, `packages/ui/src/components/ModeHeader.svelte`, `packages/ui/src/components/files/FileCollection.svelte`, `packages/ui/src/gestures.ts`. - Verification and profile: `apps/web/e2e/harness.mjs`, `apps/web/e2e/overscroll-718.mjs`, `apps/web/e2e/process-perf.mjs`, `apps/web/package.json`, `bench/overscroll-718.mjs`. ## UX gaps closed - Route scrolling now works through one accessible feed; PageDown/PageUp can target it. Overlay close restores the same feed position. History traversal restores distinct entries, and hash anchors keep their native target behavior. - Photos, Files and Calendar geometry follows the feed rather than `window.scrollY`. System theme changes update both browser chrome colors. ## UX gaps left - The WebKit run did not complete route assertions, so it produced no screenshots to attach. Its final run passed owner setup, then timed out waiting for `#route-content`; server logs showed SQLite pool acquisition up to 27.5s against a 2s threshold, plus background worker/cron timeouts. This is a SLOW-only shared-host result. The scroll profile was also attempted but produced no measurements under the same host pressure. - The serial web suite did not finish. After more than 30 minutes it had reported eight failures in untouched UI tests, with each reported test taking 5.2–13.3s against the normal 5s timeout. I stopped it with exit 130; no expectations changed. - The owner still needs to perform the manual Safari checks below on macOS and iOS. ## Decisions not stated in DESIGN - A shallow history-state ID distinguishes entries that share a URL. Feed offsets live in an 80-entry in-memory map to bound memory. - The browser fixture binds to IPv6 loopback but keeps the `localhost` origin so WebAuthn RP validation remains correct. - I reused `--paper` and added no token, coordinated with #588. ## Gates and evidence `bun run check` completed with: ```text User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/overscroll-718/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run build` completed with: ```text ✓ built in 4m 55s Run npm run preview to preview your production build locally. > Using @sveltejs/adapter-static Wrote site to "build" ✔ done ``` `cargo fmt --check` exited 0 with no output. No Rust crate or server contract changed, so Rust clippy/test gates did not apply. The debug server build for browser work was stopped after 21 minutes of shared-host compilation; I used the existing debug server artifact from the unchanged backend base. `cargo clean` completed: ```text Removed 1052 files, 529.9MiB total ``` `git diff --check` passed. Node syntax checks passed for the new E2E and benchmark scripts. The WebKit E2E, scroll profile and web unit suite remain unverified as described above. ## Manual Safari steps for the owner **macOS Safari** 1. Open `/today`. In Appearance, select Paper, then Tokyo Night, and then System. Check that Safari’s top chrome follows the active paper color in each mode. 2. Scroll down, return to the top, and pull down; then scroll to the bottom and pull up. Confirm the page feed scrolls, the document does not move, and the exposed edge uses the selected theme color. 3. Focus Page content and press PageDown and PageUp. Confirm the feed moves and focus remains visible. 4. Scroll to a Note heading, open its deep link, then use Back and Forward. Confirm the heading and feed position restore. 5. Open Notifications over a scrolled page, close it with Escape, and confirm the feed returns to the same position. **iOS Safari** 1. Open `/today` with System appearance. Repeat the top and bottom pull checks in Light and Dark. Check the status area, Safari toolbar and home-indicator safe area against the page paper color; confirm Safari’s address bar stays expanded. 2. Repeat with Paper and Tokyo Night forced in Appearance. 3. Open a Note heading deep link, use browser Back, and confirm the heading and feed position restore. Open and close Notifications and confirm the page position remains stable.
Author
Owner

Independent read-only review started. Branch: job/rev2-overscroll-718. Base: 440e19dce2. Target: 4d119bcb2, compared with origin/dev. This LIGHT job will read source and tests only. It will not build, run tests, start a server, or use a browser.

Independent read-only review started. Branch: job/rev2-overscroll-718. Base: 440e19dce23040ac8ebaae88f0469b6535b1afcb. Target: 4d119bcb2, compared with origin/dev. This LIGHT job will read source and tests only. It will not build, run tests, start a server, or use a browser.
Author
Owner

Independent review of #718

Review target: 4d119bcb2417ae4affe34e9486529cbe7ec0d7e4.
Comparison base: 440e19dce23040ac8ebaae88f0469b6535b1afcb (origin/dev).
Branch: job/rev2-overscroll-718.

Result

Two P2 defects need a fix in the route feed navigation policy. No P1 defect
was found by source review. This is not a runtime acceptance result.

  1. P2 — Keep the caller's scroll policy.
    apps/web/src/routes/+layout.svelte:230 resets the feed for every
    non-history navigation without a hash. Existing noScroll: true callers
    include the budget month controls at
    apps/web/src/routes/money/[budget]/[month]/+page.svelte:118 and photo
    opening at apps/web/src/lib/photos/PhotosView.svelte:361. Changing the
    month from a lower category now moves the User to the top. Fix the shared
    route feed policy to preserve these offsets. Cancel stale frame callbacks
    on later navigation. Add a visible test with the real month control and a
    scrolled budget. Also test photo open and close below the first screen.

  2. P2 — Restore history after content provides the scroll range.
    apps/web/src/routes/+layout.svelte:227 queues one frame; line 229 assigns
    the saved offset once. The browser clamps it when the returning route is
    still short. apps/web/src/lib/notes/NoteView.svelte:141 replaces content
    with its loading state before the async request at line 153. Returning to
    a long Note with a delayed response therefore loses the saved position.
    Keep an entry-scoped pending restore and use the route's ready state to
    apply it when content exists. Cancel it on navigation or User scrolling.
    Show content immediately. Add a real long-Note Back test with a delayed
    response. The shell fixture in apps/web/e2e/overscroll-718.mjs:321 stays
    mounted across navigation and cannot catch this defect.

Issue searches used scroll restoration, noScroll, and overscroll.
Both findings share the #718 fix. Evidence is posted to that existing issue.
No new issue or separate fix owner is needed.

Scope checks

  • Read CLAUDE.md, CONTEXT.md, issue #718, and the relevant DESIGN §§33–35
    and §57. DESIGN §58 is absent in the reviewed commit and fetched
    origin/dev. No rule was invented for that missing section.
  • Reviewed all 20 changed files. Searched existing scroll helpers and
    navigation callers with rg. The change reuses the route feed, collection
    virtualization, gestures, and theme token selectors. Feed lookup logic is
    repeated across components; this is a reuse concern for the shared policy
    fix, not a separate defect report.
  • No server endpoint, authorization rule, data writer, or User data response
    changes. No new cross-User disclosure, data loss, or corruption was found
    in this diff. The history map holds only entry IDs and numeric offsets.
  • The feed listeners use passive events and frame coalescing. The history
    map is bounded at 80 entries. Theme token probes are cached per palette.
    There is no new per-row scroll observer. Runtime performance was not
    measured. The benchmark uses a synthetic height and 60 fixed rows on Today;
    its numbers cannot prove real Files or Photos virtualization performance.
  • Comments describe the fixed shell, route feed, and expanded iOS address
    bars. The history comment promises restoration without documenting the
    content-ready limit. Update it with the fix.
  • No existing test expectation was changed in the reviewed diff. New shell
    assertions reject the old document scroller. The new test exercises
    PageDown with the feed focused and a synthetic same-page anchor. Real
    async-route restoration and existing no-scroll actions need coverage.

Verification and known gaps

No builds, tests, servers, browsers, performance runs, or screenshots were
run. The LIGHT instruction prohibits them. There is no gate output to quote.
git fetch origin completed with no output. The comparison file list stayed
the same. No merge was made because this job is read-only and has no product
changes. The author's worktree was read only and stayed clean.

Safari rubber-band paint, touch, focus scrolling, all real deep links, and
the phone/tablet/desktop light/dark screenshot matrix need runtime checks.
Source review cannot confirm those behaviours. No product UX gap was fixed
in this review. The two scroll defects are the UX gaps left.

For the merge round

  • cd apps/web && bun run check
  • cd apps/web && bun run test
  • cd apps/web && bun run test:e2e:overscroll-718

The merge round must prove that the document does not scroll, real content
does scroll, no-scroll actions keep position, history survives delayed
content, deep links expose their targets, and overlays restore their feed.
Extend the focused E2E before running it. Keep its macOS platform emulation
and six width/scheme screenshot combinations. Check the reported Safari
gesture on macOS and iOS as required by #718.

Decisions

Record both findings on #718 because they share its route feed fix. Do not
create duplicate issues. Keep the target commit fixed after fetch. Write
review documents only; do not change product code or run gates.

Review head: c147730d4ae8dc263069744286b22b217359f70d.
Files committed: audit-findings.md, review-overscroll-718.md.
Gate output: none; gates are prohibited by the LIGHT instruction.

# Independent review of #718 Review target: `4d119bcb2417ae4affe34e9486529cbe7ec0d7e4`. Comparison base: `440e19dce23040ac8ebaae88f0469b6535b1afcb` (`origin/dev`). Branch: `job/rev2-overscroll-718`. ## Result Two P2 defects need a fix in the route feed navigation policy. No P1 defect was found by source review. This is not a runtime acceptance result. 1. **P2 — Keep the caller's scroll policy.** `apps/web/src/routes/+layout.svelte:230` resets the feed for every non-history navigation without a hash. Existing `noScroll: true` callers include the budget month controls at `apps/web/src/routes/money/[budget]/[month]/+page.svelte:118` and photo opening at `apps/web/src/lib/photos/PhotosView.svelte:361`. Changing the month from a lower category now moves the User to the top. Fix the shared route feed policy to preserve these offsets. Cancel stale frame callbacks on later navigation. Add a visible test with the real month control and a scrolled budget. Also test photo open and close below the first screen. 2. **P2 — Restore history after content provides the scroll range.** `apps/web/src/routes/+layout.svelte:227` queues one frame; line 229 assigns the saved offset once. The browser clamps it when the returning route is still short. `apps/web/src/lib/notes/NoteView.svelte:141` replaces content with its loading state before the async request at line 153. Returning to a long Note with a delayed response therefore loses the saved position. Keep an entry-scoped pending restore and use the route's ready state to apply it when content exists. Cancel it on navigation or User scrolling. Show content immediately. Add a real long-Note Back test with a delayed response. The shell fixture in `apps/web/e2e/overscroll-718.mjs:321` stays mounted across navigation and cannot catch this defect. Issue searches used `scroll restoration`, `noScroll`, and `overscroll`. Both findings share the #718 fix. Evidence is posted to that existing issue. No new issue or separate fix owner is needed. ## Scope checks - Read CLAUDE.md, CONTEXT.md, issue #718, and the relevant DESIGN §§33–35 and §57. DESIGN §58 is absent in the reviewed commit and fetched `origin/dev`. No rule was invented for that missing section. - Reviewed all 20 changed files. Searched existing scroll helpers and navigation callers with `rg`. The change reuses the route feed, collection virtualization, gestures, and theme token selectors. Feed lookup logic is repeated across components; this is a reuse concern for the shared policy fix, not a separate defect report. - No server endpoint, authorization rule, data writer, or User data response changes. No new cross-User disclosure, data loss, or corruption was found in this diff. The history map holds only entry IDs and numeric offsets. - The feed listeners use passive events and frame coalescing. The history map is bounded at 80 entries. Theme token probes are cached per palette. There is no new per-row scroll observer. Runtime performance was not measured. The benchmark uses a synthetic height and 60 fixed rows on Today; its numbers cannot prove real Files or Photos virtualization performance. - Comments describe the fixed shell, route feed, and expanded iOS address bars. The history comment promises restoration without documenting the content-ready limit. Update it with the fix. - No existing test expectation was changed in the reviewed diff. New shell assertions reject the old document scroller. The new test exercises PageDown with the feed focused and a synthetic same-page anchor. Real async-route restoration and existing no-scroll actions need coverage. ## Verification and known gaps No builds, tests, servers, browsers, performance runs, or screenshots were run. The LIGHT instruction prohibits them. There is no gate output to quote. `git fetch origin` completed with no output. The comparison file list stayed the same. No merge was made because this job is read-only and has no product changes. The author's worktree was read only and stayed clean. Safari rubber-band paint, touch, focus scrolling, all real deep links, and the phone/tablet/desktop light/dark screenshot matrix need runtime checks. Source review cannot confirm those behaviours. No product UX gap was fixed in this review. The two scroll defects are the UX gaps left. ## For the merge round - `cd apps/web && bun run check` - `cd apps/web && bun run test` - `cd apps/web && bun run test:e2e:overscroll-718` The merge round must prove that the document does not scroll, real content does scroll, no-scroll actions keep position, history survives delayed content, deep links expose their targets, and overlays restore their feed. Extend the focused E2E before running it. Keep its macOS platform emulation and six width/scheme screenshot combinations. Check the reported Safari gesture on macOS and iOS as required by #718. ## Decisions Record both findings on #718 because they share its route feed fix. Do not create duplicate issues. Keep the target commit fixed after fetch. Write review documents only; do not change product code or run gates. Review head: `c147730d4ae8dc263069744286b22b217359f70d`. Files committed: `audit-findings.md`, `review-overscroll-718.md`. Gate output: none; gates are prohibited by the LIGHT instruction.
Author
Owner

Starting the #718 review follow-up on job/overscroll-718. Base SHA: 440e19dce2; reviewed head: 4d119bcb24.

Starting the #718 review follow-up on job/overscroll-718. Base SHA: 440e19dce23040ac8ebaae88f0469b6535b1afcb; reviewed head: 4d119bcb2417ae4affe34e9486529cbe7ec0d7e4.
Author
Owner

Finding confirmed in source: +layout.svelte applies a single RAF restore and resets every non-popstate route without a hash, while current callers request noScroll for the budget month pager and photo viewer. NoteView.svelte clears note content when it starts an async load, so the one-frame history write can clamp before the long Note returns. I’m adding caller-policy propagation and an entry-scoped restore that waits for Note editor readiness; regression coverage will exercise the real routes.

Finding confirmed in source: +layout.svelte applies a single RAF restore and resets every non-popstate route without a hash, while current callers request noScroll for the budget month pager and photo viewer. NoteView.svelte clears note content when it starts an async load, so the one-frame history write can clamp before the long Note returns. I’m adding caller-policy propagation and an entry-scoped restore that waits for Note editor readiness; regression coverage will exercise the real routes.
Author
Owner

Completed

  • Preserved route-feed scroll for app goto({ noScroll: true }) calls. A shared wrapper carries the choice in one-navigation page state; the shell consumes it and skips its route-feed reset.
  • Made history restoration entry-scoped and pending. Note routes apply the saved offset only after the live or fallback editor has mounted real content. Navigation and scroll input cancel stale work.
  • Added unit coverage and focused E2E cases for Money month navigation, Photos Quick Look open/close, and delayed Note history restoration.

Files

  • Shared navigation and shell: apps/web/src/lib/routeFeedNavigation.ts, apps/web/src/routes/+layout.svelte, apps/web/src/app.d.ts, apps/web/src/lib/notes/NoteView.svelte.
  • Existing goto callers: apps/web/src/lib/components/analytics/RangeBar.svelte, apps/web/src/lib/files/PublicLinkPage.svelte, apps/web/src/lib/mail/MailView.svelte, apps/web/src/lib/photos/PhotosView.svelte, apps/web/src/routes/ai/turns/[id]/+page.svelte, apps/web/src/routes/calendar/[view]/[date]/+page.svelte, apps/web/src/routes/money/[budget]/[month]/+page.svelte, apps/web/src/routes/settings/[...path]/+page.svelte, apps/web/src/routes/settings/admin/JobsGroup.svelte, apps/web/src/routes/settings/jobs/MyJobsGroup.svelte, apps/web/src/routes/settings/mail/MailSection.svelte.
  • Tests: apps/web/src/lib/routeFeedNavigation.test.ts, apps/web/e2e/overscroll-718.mjs.

Verification

cargo fmt --check completed with exit 0 and no output. No Rust source changed, so Rust clippy/test were not run.

bun run check:

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

Focused Vitest:

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/overscroll-718/apps/web

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  23:32:52
   Duration  1.59s (transform 66%, import 15%, tests 13%, worker 4%, environment 1%)

The focused E2E did not reach the new regressions. WebKit returned 401 for /api/v1/auth/me after the Chromium session-cookie transfer. An authenticated Chromium diagnostic then reached the existing waitForRoute('/today') path assertion: /today redirects to /calendar/week/2026-09-27, so the helper expected /today but received the documented canonical Calendar URL. I left that existing expectation unchanged. The first WebKit run also reported an execution-context change during the two-frame wait.

Screenshots

Captured 36 screenshots from the production build with an authenticated local server, real empty states, and macOS platform properties (MacIntel / macOS). The matrix covers phone 390 px, tablet 820 px, and desktop 1440 px in light and dark. Raw PNGs remain in ignored artifacts/overscroll-718/; five full-resolution contact sheets are attached. Each sheet has light on top and dark below, with phone, tablet, and desktop from left to right.

UX gaps

Closed: noScroll controls now preserve the shared feed; late Note content keeps its entry's saved position; user scroll intent cancels delayed restores.

Left: The focused E2E regression scenarios did not run because the browser fixture cannot authenticate WebKit on the HTTP localhost origin and the existing /today assertion rejects its canonical redirect. Real Safari overscroll remains for the merge round.

Decisions not specified in DESIGN

  • Used a one-navigation page-state marker because SvelteKit afterNavigate does not expose the original goto options.
  • Used a layout context callback from NoteView to signal when async note content can accept its saved feed offset.
  • Captured screenshots in Chromium with macOS platform emulation after WebKit rejected the transferred Secure cookie on the local HTTP test origin. This does not replace a real Safari check.

For the merge round

  • Repair the focused E2E fixture so WebKit has a valid session on a secure test origin, then run bun run test:e2e:overscroll-718 to prove Money and Photos noScroll behavior and exact delayed Note restoration. Resolve the /today helper mismatch with the owner before changing that existing assertion; the route intentionally redirects to its canonical Calendar URL.
  • On the macOS VM, use real Safari to pull down at the top of a long route feed in light and dark mode. Confirm no black band or document scrolling appears, the theme color fills any exposed area, and keyboard scrolling, deep-link anchors, and Back restoration still work.

Revision and cleanup

Head: c8db5e6e96f2501aa9564c6b5ab4d71862f7dd8a (fix(web): honor noScroll and late route restores). cargo clean completed: Removed 7237 files, 4.6GiB total. apps/web/build and .svelte-kit/output were removed. The worktree is clean.

## Completed - Preserved route-feed scroll for app `goto({ noScroll: true })` calls. A shared wrapper carries the choice in one-navigation page state; the shell consumes it and skips its route-feed reset. - Made history restoration entry-scoped and pending. Note routes apply the saved offset only after the live or fallback editor has mounted real content. Navigation and scroll input cancel stale work. - Added unit coverage and focused E2E cases for Money month navigation, Photos Quick Look open/close, and delayed Note history restoration. ## Files - Shared navigation and shell: `apps/web/src/lib/routeFeedNavigation.ts`, `apps/web/src/routes/+layout.svelte`, `apps/web/src/app.d.ts`, `apps/web/src/lib/notes/NoteView.svelte`. - Existing `goto` callers: `apps/web/src/lib/components/analytics/RangeBar.svelte`, `apps/web/src/lib/files/PublicLinkPage.svelte`, `apps/web/src/lib/mail/MailView.svelte`, `apps/web/src/lib/photos/PhotosView.svelte`, `apps/web/src/routes/ai/turns/[id]/+page.svelte`, `apps/web/src/routes/calendar/[view]/[date]/+page.svelte`, `apps/web/src/routes/money/[budget]/[month]/+page.svelte`, `apps/web/src/routes/settings/[...path]/+page.svelte`, `apps/web/src/routes/settings/admin/JobsGroup.svelte`, `apps/web/src/routes/settings/jobs/MyJobsGroup.svelte`, `apps/web/src/routes/settings/mail/MailSection.svelte`. - Tests: `apps/web/src/lib/routeFeedNavigation.test.ts`, `apps/web/e2e/overscroll-718.mjs`. ## Verification `cargo fmt --check` completed with exit 0 and no output. No Rust source changed, so Rust clippy/test were not run. `bun run check`: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/overscroll-718/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest: ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/overscroll-718/apps/web Test Files 1 passed (1) Tests 2 passed (2) Start at 23:32:52 Duration 1.59s (transform 66%, import 15%, tests 13%, worker 4%, environment 1%) ``` The focused E2E did not reach the new regressions. WebKit returned 401 for `/api/v1/auth/me` after the Chromium session-cookie transfer. An authenticated Chromium diagnostic then reached the existing `waitForRoute('/today')` path assertion: `/today` redirects to `/calendar/week/2026-09-27`, so the helper expected `/today` but received the documented canonical Calendar URL. I left that existing expectation unchanged. The first WebKit run also reported an execution-context change during the two-frame wait. ## Screenshots Captured 36 screenshots from the production build with an authenticated local server, real empty states, and macOS platform properties (`MacIntel` / `macOS`). The matrix covers phone 390 px, tablet 820 px, and desktop 1440 px in light and dark. Raw PNGs remain in ignored `artifacts/overscroll-718/`; five full-resolution contact sheets are attached. Each sheet has light on top and dark below, with phone, tablet, and desktop from left to right. - [Calendar week matrix](https://git.kayg.org/attachments/7a8a1f58-d140-401b-8909-aa47499ca68f) - [Notes matrix](https://git.kayg.org/attachments/ec61fb6e-cfd2-4ff0-b806-44a364987a8b) - [Files matrix](https://git.kayg.org/attachments/f1e22221-a075-4b2c-98c1-71d063cb3973) - [Photos matrix](https://git.kayg.org/attachments/da77b310-b22a-42c9-9630-273ec920bfaa) - [Appearance matrix](https://git.kayg.org/attachments/ff0ba49e-4638-437a-9f3e-30c3e9cf2818) ## UX gaps **Closed:** `noScroll` controls now preserve the shared feed; late Note content keeps its entry's saved position; user scroll intent cancels delayed restores. **Left:** The focused E2E regression scenarios did not run because the browser fixture cannot authenticate WebKit on the HTTP localhost origin and the existing `/today` assertion rejects its canonical redirect. Real Safari overscroll remains for the merge round. ## Decisions not specified in DESIGN - Used a one-navigation page-state marker because SvelteKit `afterNavigate` does not expose the original `goto` options. - Used a layout context callback from `NoteView` to signal when async note content can accept its saved feed offset. - Captured screenshots in Chromium with macOS platform emulation after WebKit rejected the transferred Secure cookie on the local HTTP test origin. This does not replace a real Safari check. ## For the merge round - Repair the focused E2E fixture so WebKit has a valid session on a secure test origin, then run `bun run test:e2e:overscroll-718` to prove Money and Photos `noScroll` behavior and exact delayed Note restoration. Resolve the `/today` helper mismatch with the owner before changing that existing assertion; the route intentionally redirects to its canonical Calendar URL. - On the macOS VM, use real Safari to pull down at the top of a long route feed in light and dark mode. Confirm no black band or document scrolling appears, the theme color fills any exposed area, and keyboard scrolling, deep-link anchors, and Back restoration still work. ## Revision and cleanup Head: `c8db5e6e96f2501aa9564c6b5ab4d71862f7dd8a` (`fix(web): honor noScroll and late route restores`). `cargo clean` completed: `Removed 7237 files, 4.6GiB total`. `apps/web/build` and `.svelte-kit/output` were removed. The worktree is clean.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#718
No description provided.