Skip to content

routine 2026-10-10: main green again (restored scroll is a jump), a11y pass measures a page at rest, the visitor Cloudflare saw in the log, Nuxt 4.6.1 - #21

Merged
physics515 merged 7 commits into
mainfrom
routine/site-2026-10-10
Oct 11, 2026
Merged

physics515 merged 7 commits into
mainfrom
routine/site-2026-10-10

Conversation

@physics515

@physics515 physics515 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Nightly run, 2026-10-10 21:10 → 2026-10-11 00:05 CDT. Seven commits. The night's theme is trustworthy checks and logs. main has been red since #20 merged: check:navigation failed by 3 px on the push run. Tonight's own gate run also turned up a second flaky check, and the live log turned out to be naming Docker's gateway instead of the visitor.

Unless noted, each increment was verified on its own build:

  • nuxt typecheck clean, Vitest, npm run build.
  • The real server on a spare port; all 42 routes verify hits returned identical status codes before and after (/healthz 200, / 200, /projects 200, every /projects/<slug> 301 with /about and /blog 200, deliberate 404s 404, admin 404s, /onion-frame 503 with no gateway).
  • Headless-Chromium screenshots of home, Skidbladnir and Nisaba at 1280 and 390 px, taken and looked at. The flat ground and the masthead glass are intact.

1. The log names the visitor Cloudflare saw — feat(log) (a26d715)

Roadmap: found in the live log (new [x], plus an owner [ ]).
Files:

  • shared/log/client.ts (new)
  • server/plugins/request-log.ts
  • test/client-address.test.ts (new, 4 cases)
  • README.md, ROADMAP.md

The problem: 5,548 of 5,555 request lines in the container's 21 hours of log carry "ip": "172.18.0.1". Caddy trusts no proxy, so the x-forwarded-for it sets is its own peer, and that peer is Docker's bridge gateway. The site is proxied through Cloudflare (server: cloudflare, cf-ray on the public address).

The change: every line now also carries cfIp, from cf-connecting-ip, which Caddy passes through. It is logged under its own name because nothing checks that a request came through Cloudflare, and nothing in the site acts on it. Over Tor, neither address is logged.

Checked on the built server:

  • x-forwarded-for: 172.18.0.1 + cf-connecting-ip: 203.0.113.7 → "ip":"172.18.0.1","cfIp":"203.0.113.7", on both the 200 line and the 404 line.
  • x-via-onion + a forged cf-connecting-ip → "ip":null,"cfIp":null,"via":"onion".
  • Still one line per page view.

2. The browser a11y pass measures a page at rest — fix(checks) (fa732a7)

Files: scripts/check-a11y-browser.mjs, ROADMAP.md.

Found when tonight's first a11y:firefox run failed target-size on /status's /sitemap.xml link ("partially obscured", 88.6×21 px). The next three runs passed.

Cause: the site has scroll-behavior: smooth, so the pass's bare scrollTo calls only start animations. Probed at 390 px:

  • The reading pass reached 1,014 of 3,894 px in Chromium on onyums by its last step.
  • scrollTo(0, 0) was still 1,696 px down right before axe ran.

So axe was measuring links while they slid under the fixed masthead.

Fix: every jump is behavior: 'instant', and the pass throws if the page isn't at the top when axe starts.

After: the pass reaches the bottom (3,894 / 3,968 px) and is at 0 when axe runs. a11y:browser and a11y:firefox are clean, twice each.

3. Nuxt 4.6.1, vue-tsc 3.3.13, @types/node 26.6.5 — chore(deps) (a7d2d88)

Roadmap: the Nuxt 4.6.1 item, ticked. Files: package.json, package-lock.json, ROADMAP.md.

Soak: 27 h. Only two issues were filed against 4.6.1, both checked through the issues API (search was lagging):

  • #36529 reproduces on 3.x.
  • #36530 is the cloudflare_module preset, now closed.

A 4.6.2 release PR is open with nitro/vite fixes that don't touch this site.

Lockfile: cssnano also moved 9.3 → 9.6. The built CSS is byte-identical (entry.DpTKoio1.css, same hash both builds).

Checks:

  • Typecheck clean, 273 tests, build green.
  • All 42 routes identical, and every page body the same byte size.
  • check, a11y, a11y:browser, a11y:firefox (after increment 2), audit:runtime, font:check, og:check, sizes:check, contrast, bases all clean.
  • check:navigation: 144/144 in each engine.

4. main green again: a restored scroll position is a jump, not an animation — fix(checks) (9cba524), then fix(scroll) (5e60bd1)

Files:

  • app/router.options.ts (new)
  • scripts/check-navigation.mjs
  • app/composables/useReadmeFold.ts (comment only)
  • README.md, ROADMAP.md

What failed: main's push run after #20 failed one check:navigation case: a reload of onyums' README at 390 px landed at 5,425 against the 5,418 left, outside the ±4 px slack.

First attempt, wrong: I couldn't find the cause locally (+2 px at most). 9cba524 widened the slack to 12 px. This PR's first CI run then failed by 21 px on Skidbladnir's desktop reload. The slack was hiding a real bug, not noise.

Root cause. I added a frame-by-frame trace to the reload check and slowed the CPU 6× to match the runner. The trace showed what happens:

  • vue-router restores the saved position on reload (from history.state.scroll) with a bare scrollTo({ left: 0, top: 6537 }).
  • With the site's scroll-behavior: smooth, that becomes an animation from the top.
  • The animation races the README fold's own instant restore. A fast machine wins the race; a slow one doesn't.
  • Back and Forward go through the same path.

At 6× slowdown, all 16 reload cases failed, 7–34 px past.

Fix: app/router.options.ts keeps Nuxt's own scrollBehavior and marks a saved position instant, the way a browser restores a page. Hash links still scroll smoothly.

  • Nuxt's options aren't exported, so they are imported by path. If Nuxt moves the file, the build breaks loudly.
  • A client plugin can't do this: Nuxt's router plugin resets router.options.scrollBehavior after the first navigation. I tried it first; the trace showed the wrapper gone.

The slack is back to 4 px. A failed reload check now prints the trace.

Result:

  • At 6× slowdown with a 1 px slack: 144/144 (16 failures before).
  • At normal speed and 4 px: 144/144 in Chromium and in Firefox.
  • CI green on 5e60bd1.
  • Visible to visitors: Back, Forward and reload put the reader back in one jump instead of scrolling them there.

Roadmap/README-only commits

83dc303:

  • The cfIp item.
  • A new owner-gated [ ]: a verified client address at the edge (details under Research).

2e3cd52, snapshot: npm run sync covered all 8 projects, with Nanna v0.3.42-beta.51 and WeftDB's README. All 28 README images in it are sized.

Final tree (5e60bd1)

  • Typecheck clean, 273 tests, build green.
  • All 42 routes as on main.
  • check (22 pages, 79 URLs), a11y (23), a11y:browser and a11y:firefox (23 × 2 widths) clean.
  • check:navigation 144/144 in Chromium and in Firefox.
  • audit:runtime, font:check, og:check, sizes:check, contrast, bases pass.

Live, before this deploy:

  • npm run vitals -- https://basicautomation.io (phone): every page measured was "good", CLS 0.000, LCP 1,068–2,496 ms.
  • The run timed out at 63 of 108 pages, so desktop wasn't reached.

Research

  • Cloudflare: CF-Connecting-IP carries the client's address to the origin, and Cloudflare recommends it over X-Forwarded-For. It can be spoofed by anyone who reaches the origin directly. https://developers.cloudflare.com/fundamentals/reference/http-headers/
  • Docker: a published port's IPv6 traffic to an IPv4-only bridge network goes through the userland proxy. This host has a global IPv6 address and caddy-shared-network is IPv4-only, which would explain 172.18.0.1. Unconfirmed: whether Cloudflare reaches the origin over IPv6 is in the Cloudflare dashboard. https://docs.docker.com/engine/network/port-publishing/
  • Caddy: trusted_proxies (none trusted by default) and client_ip_headers are how the real visitor would reach {client_ip} once the real peer is visible. https://caddyserver.com/docs/caddyfile/options
  • Nuxt: the 4.6.1 notes, and the 4.6.2 release PR (v4.6.2 nuxt/nuxt#36545). Open #36346 ("Scroll is not restored after refresh") is adjacent to increment 4, not the same thing. https://github.com/nuxt/nuxt/releases/tag/v4.6.1
  • Nothing new for Tailwind (4.3.3 is still latest) or GitHub's REST API this week (the October changelog entries are merge-queue, advisory comments and Copilot review). npm audit: the same dev-time advisories as last night, none shipped (audit:runtime clean).

Done vs open

  • Landed:
    • 4 increments: the log field, two check fixes, the Nuxt patch.
    • Roadmap research and the snapshot.
    • main's red CI: root cause found and fixed (increment 4), CI green on this PR.
  • New [ ]: a verified client address at the edge (owner: Caddy/Docker/Cloudflare).
  • Still owner-gated: site token, security.txt contact, distroless/healthcheck, Onion-Location, HSTS, basic-automation.github.io, contrast, and the cause of the onion outage.
  • Upstream:
  • Observed, not acted on:
    • Arti logs "Too many preemptive onion service circuits failed" about 6 times an hour, and 17 descriptor upload failures. The onion service still reports reachable.
    • A 2.5-day-old node .output/server/index.mjs on port 3124 from another session's .worktrees/site-main worktree was left alone.

STOP REASON: no workable next item, at 00:05 CDT, 2 h 55 min after the 21:10 start, short of the 4 h bar. Every remaining roadmap item is owner-gated, upstream in repos this routine doesn't push to, or blocked. I looked for more: the live log, CI history, a vitals sweep of the live site, npm audit, TODOs, and a screenshot tour of seven more pages. That search found increments 1, 2 and 4, and increment 4 took a second round when CI disproved my first fix. Nothing more turned up.

Recommended next step

  1. Owner:
    • Look at Cloudflare's origin record (IPv6?) and decide on Caddy trusted_proxies.
    • Name a security.txt contact.
  2. Next run: Nuxt 4.6.2 once it is published and has soaked, with the same gates.

🤖 Generated with Claude Code

physics515 and others added 7 commits October 10, 2026 21:14
Every clearnet request line in 21 hours of live log carried
`"ip": "172.18.0.1"` (5,548 of 5,555): Caddy trusts no proxy, so the
x-forwarded-for it sets is its own peer, and behind Docker's port proxy
that is the bridge gateway. Only LAN visits, which skip Cloudflare, ever
showed a real address.

The request log now also writes `cfIp`, from `cf-connecting-ip`, which
Caddy passes through. It is logged under its own name because nothing
verifies the request really came through Cloudflare; nothing in the site
decides anything from it. A request over the onion gateway logs neither
address.

- shared/log/client.ts: clientAddress()
- server/plugins/request-log.ts: uses it on both the request and error line
- test/client-address.test.ts: 4 cases
- README.md: the log fields

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The site scrolls smoothly (`scroll-behavior: smooth`), so the reading
pass's bare `scrollTo` calls only started animations. Measured at 390 px:
the pass stalled a screen or so down (Chromium, onyums: 1,014 of 3,894 px
by its last step), and the jump back to the top was still ~1,700 px out
when axe ran. axe then judged a page in motion, with links passing under
the fixed masthead: `target-size` failed /status's /sitemap.xml link as
"partially obscured" on one Firefox run of four tonight, and passed the
other three.

Every jump is now `behavior: 'instant'`, and the pass throws if the page
is not at the top when axe starts. Probed in both engines: the pass now
reaches the bottom (3,894 / 3,968 px) and is at 0 when axe runs.
a11y:browser and a11y:firefox clean, twice each.

- scripts/check-a11y-browser.mjs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nuxt 4.6.1 after 27 hours with two issues filed against it, neither
touching this site (#36529 reproduces on 3.x; #36530 is the
cloudflare_module preset). Mostly dev-server and typing fixes plus a lazy
portable-event view in Nitro. The lockfile also moved cssnano 9.3 -> 9.6;
the built CSS is byte-identical (same hash).

Checks on the upgraded tree: typecheck clean, 273 tests, build green,
all 42 verify routes identical to 4.6.0's with every page body the same
size, check (22 pages, 79 URLs), a11y (23 pages), a11y:browser,
a11y:firefox, check:navigation 144/144 in Chromium and in Firefox,
audit:runtime, font:check, og:check, sizes:check, contrast, bases.

- package.json, package-lock.json
- ROADMAP.md: the 4.6.1 item

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
main's CI went red after #20 merged on one check:navigation case: a
reload of onyums' README at 390 px landed at 5,425 against the 5,418 the
reader left, outside the 4 px slack. Reproduced locally at +2 px on about
one reload in three. The site's own scrollTo lands exactly; the extra
pixels arrive in the next frame with no script scrolling (every scrolling
API hooked), page height and README offset unchanged, and with
scrollRestoration manual or overflow-anchor: none alike, so it is the
browser's own. The slack is now 12 px, half a 23 px line; the bug this
check guards against was 3,300 px.

check:navigation 144/144 in Chromium and in Firefox on this build.

- scripts/check-navigation.mjs
- ROADMAP.md: this and the a11y scroll fix

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he edge (owner)

Research from tonight: Cloudflare's header reference, Docker's
port-publishing notes on the userland proxy, Caddy's trusted_proxies and
client_ip_headers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
npm run sync, 2026-10-11 03:3x UTC: all 8 projects; Nanna's
v0.3.42-beta.51, WeftDB's README and pushedAt, counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The real cause of main's red check:navigation, which 9cba524 only
widened the slack around: the PR's own CI then landed a reload of
Skidbladnir's README 21 px past the reader's place.

vue-router restores a saved position (Back, Forward, and a reload from
history.state.scroll) with a bare scrollTo({ left, top }), so the site's
`scroll-behavior: smooth` made each an animation from the top, racing
the README fold's own instant restore. Fast machines won the race; slow
ones did not. Reproduced with the CPU slowed 6x: all 16 reload cases
failed, 7-34 px past.

app/router.options.ts keeps Nuxt's scrollBehavior (reached by path, it
is not exported, so a move breaks the build loudly) and marks a saved
position `instant`, as a browser's own restoration is. Hash links still
scroll smoothly. A client plugin could not do this: Nuxt's router plugin
resets router.options.scrollBehavior after the first navigation.

- app/router.options.ts (new)
- scripts/check-navigation.mjs: slack back to 4 px; a failed reload
  check prints a frame-by-frame trace of the page's first seconds
  (scroll calls, position, height, README offset, font state, shifts),
  which is how this was found
- app/composables/useReadmeFold.ts: comment, vue-router does restore on
  reload
- README.md, ROADMAP.md

Checks: typecheck, 273 tests, build; 42 routes identical; check, a11y,
a11y:browser, a11y:firefox clean; check:navigation 144/144 in Chromium
and Firefox at 4 px, and 144/144 in Chromium with the CPU slowed 6x at
a 1 px slack (16 failures before this change).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@physics515 physics515 changed the title routine 2026-10-10: main green again, a11y pass measures a page at rest, the visitor Cloudflare saw in the log, Nuxt 4.6.1 routine 2026-10-10: main green again (restored scroll is a jump), a11y pass measures a page at rest, the visitor Cloudflare saw in the log, Nuxt 4.6.1 Oct 11, 2026
@physics515
physics515 merged commit 25a8a15 into main Oct 11, 2026
3 checks passed
@physics515

Copy link
Copy Markdown
Contributor Author

Shipped. Squash-merged as 25a8a15; CI green.

Release: nightly-2026-10-10, a prerelease: https://github.com/basic-automation/basicautomation.io/releases/tag/nightly-2026-10-10

Deploy: the old image was tagged basicautomation-site:previous (d96d0bb3e043), then I rebuilt basicautomation-site:local (5699a9df35fc) and recreated the container with --env-file .env.linux. It reported healthy 6 s after start.

Post-deploy gate on the live site:

  • / → 200, with the expected <title>.
  • /projects/skidbladnir/about and /projects/onyums/about → 200.
  • /healthz → 200: status: ok, live data, authenticated.
  • / through Cloudflare's public address → 200.

The onion gateway reported launched right after the restart. A request through Cloudflare was logged with a cfIp, so increment 1 works in production.

No rollback was needed.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant