docs(audit): 2026-11-11 audit report

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 09:21:57 +01:00
1 parent 0462ab84ce
commit d21eb92bd0
2 files changed
+67

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ Small, specific, independently actionable. Remove only when actually done.
- [ ] Reword the 2021 `portfolio` project summary if wanted ("The 2021 version of this portfolio…", derived from its `info.json`).
## Next
- [ ] Work through `docs/audits/2026-11-11-audit.md` (24 findings from the 2026-11-11 audit; each row carries its own status).
- [ ] Start page: the owner feels the personal half lag on hover (2026-11-10). Findings so far: his browser gets ~70–80 fps
on a 144 Hz monitor even on a blank page (testufo: 70; Firefox `layout.frame_rate` is -1), so the cap is his machine,
possibly load from other programs; recheck with nothing else running. The grain's live filter was a real cost and is
+66
View File
@@ -0,0 +1,66 @@
# Audit — 2026-11-11
A read of the whole app (`src/`, `deploy/`, CI, docs), a build of the real content repo with fixture
sources, and a look at the live site (`https://lucaswinther.info`, behind Cloudflare). Only findings that
were **not** already in `docs/TODO.md` are listed. Baseline on `main` at `0462ab8`: typecheck clean,
`bun test` 186 pass; Playwright was not run.
Status column: **open**, **fixed** (with the branch), or **owner** (not fixable in this repo).
Update the status here when a finding is dealt with; delete the file once every row is fixed or moved to `docs/TODO.md`.
## How things were measured
- Build: `SOURCES_FIXTURES=fixtures/sources bun src/cli.ts build ~/portfolio-next-content <out> <cache>`; sizes with `wc -c` and `gzip -9`.
- Origin headers: `bun src/cli.ts preview <out>` and `curl -I`.
- Live: `curl -I` against `lucaswinther.info` and `www.lucaswinther.info`, and `GET /status`.
## Broken or wrong today
| # | Finding | Evidence | Fix | Status |
|---|---|---|---|---|
| 1 | `www.lucaswinther.info` returns a Cloudflare **525** (TLS handshake with the origin fails). The apex works. | `curl -I https://www.lucaswinther.info/` → `HTTP/2 525` | Homelab config or Cloudflare: a certificate and `VIRTUAL_HOST` for `www`, or a Cloudflare redirect rule to the apex. | owner |
| 2 | The work pages ship ~38 KB of fonts inside render-blocking CSS. Vite inlines assets under 4 KB as base64, so `fonts-work.css` carries six Geist Mono subsets (Cyrillic-ext, Vietnamese, symbols; two weights) that an English page never requests. | `fonts-work.*.css` 42.8 KB raw / 28 KB gzip, 8 `url(data:` faces; the other themes' font CSS is 8–11 KB with none. | `vite.build.assetsInlineLimit: 0` in `astro.config.mjs`. | open |
| 3 | The start page downloads the React runtime (213 KB raw, 66 KB gzip) for almost nothing: `Gate.tsx` returns `null` and only attaches listeners; the only other island is the three-button preference switch. | `index.html` has two `astro-island`s, both with `renderer-url="/_astro/client.*.js"` (212,880 bytes). | Make both plain scripts on the start page (the switch stays a React island on the work pages, where React is loaded anyway). Needs a spec. | open |
| 4 | Replaced media can stay stale for 20 hours. The origin sends `max-age=3600` for `/media/`; Cloudflare raises that to `max-age=72000` for browsers. File names carry no hash, so a new CV PDF or image under the same name keeps showing the old one. | `curl -I https://lucaswinther.info/media/mr-rowing.jpg` → `cache-control: public, max-age=72000`. | Hash media URLs at build (`/media/x.jpg?v=<hash>` or hashed copies), or set Cloudflare's Browser Cache TTL to "respect existing headers". | open |
| 5 | Work pages are dark but never declare `color-scheme: dark` (`/everything/` and `/simple` do declare theirs), so native scrollbars and the search field's built-in parts render light on the dark page. | `grep color-scheme src/site/styles` → only `simple.css`, `everything.css`. | `color-scheme: dark` on `[data-theme='work']`. Visual: owner looks before commit. | open |
| 6 | The Ghost fetch uses `limit=all`. The blog runs Ghost 6.75; Ghost 6 is believed to cap that at 100 posts per request (not confirmed against the docs). Harmless under 100 posts, silent truncation above. | `src/sources/ghost.ts`; `GET blog…/ghost/api/admin/site/` → `"version":"6.75"`. | Page through `meta.pagination` with `limit=100`. | open |
| 7 | `docs/HANDOFF.md` and `docs/TODO.md` say the site is undeployed and that the source clients never ran live. The site is live and `/status` reports all four sources `ok`. | Live `/status` on 2026-11-11: `ghost`, `trakt`, `hardcover`, `listenbrainz` all `"ok": true`. | Bring both files up to date. | open |
## Missing
| # | Finding | Fix | Status |
|---|---|---|---|
| 8 | Shared links have no preview: the head has a title and description only. No Open Graph tags, no canonical URL, no sitemap, and Astro has no `site` set. | `site` in the Astro config, canonical + `og:*` in `Base.astro`, a sitemap. An image per page is a design question. Needs a spec. | open |
| 9 | No feed. At 1–3 new items a month there is no way to follow the wall or the writing. | An Atom feed (wall + writing), linked with `rel="alternate"`; nothing visible changes. Needs a spec. | open |
| 10 | `/simple` has no print styles, though it is the page someone would print or save as a PDF. | `@media print` in `simple.css`. Visual: owner looks. | open |
| 11 | The content repo has no check. A broken commit never goes live and the only signal is `/status`. | A CI job in `portfolio-next-content` that runs the published image's `site validate`. | open |
| 12 | A content push waits up to `POLL_INTERVAL` (1 h) unless someone runs `site refresh` on the host. | A Gitea webhook to a secret-guarded refresh endpoint, or a content-repo CI step. Touches deployment: ask first. | open |
## Ideas
| # | Idea | Notes | Status |
|---|---|---|---|
| 13 | Hover lag on the start page (hypothesis, **not tested**): the grain layer (`.art::before`), the work side's fade (`.tech::after`) and its grid are all sized to their half, so every frame of the `flex-grow` lean re-rasterises them. | Give those layers a fixed width (about 60vw, clipped by the half's `overflow: hidden`) so their size is constant during the lean; try it in the hover lab before any transform rewrite. Also: the crosshair writes `top`/`left` and `innerHTML` on every `mousemove`; a transform is cheaper. | open |
| 14 | Prerender on hover: a speculation rule that loads `/work/` or `/off-the-clock/` while the visitor leans toward a half, so entering is instant in Chromium browsers. | Fires only after the visitor acts; first-party only. Firefox ignores it. | open |
| 15 | Per-space `theme-color`, so phone browser chrome tints warm dark on work and cream off the clock. | One `<meta>` per theme in `Base.astro`. | open |
## Small things
| # | Finding | Status |
|---|---|---|
| 16 | `src/runtime/refresh.ts` ignores the commit `fetchSnapshot` returns and records the earlier `ls-remote` head, so `/status` can name a different commit than the one built when a push lands between the two. | open |
| 17 | A build killed mid-run leaves `data/src/<uuid>` behind; nothing cleans it (`removeStaleStaging` only covers the app's `.tmp/build-*`). | open |
| 18 | `deploy/compose.yml` has no `LISTENBRAINZ_USER`, though `docs/DEPLOY.md` lists it. | open |
| 19 | `src/site/pages/404.astro` hard-codes "Lucas Winther" in its title instead of `site.name`. | open |
| 20 | The analytics script is baked into every build, so local previews and the image smoke test count as visits (e2e aborts the request). An env switch set only in production would stop that. | open |
| 21 | `everything.astro` adds the `js` class in its own inline script; `Base.astro` already does. | open |
| 22 | The origin sends no `ETag` or `Last-Modified`, so `no-cache` responses are never answered with a 304. Small today (HTML is 6–10 KB compressed and Cloudflare caches static files), cheap to add. | open |
| 23 | `/status` is public and shows commit ids and the tail of the last build error. Low risk; say so in `docs/DEPLOY.md` or restrict it at the proxy. | open |
| 24 | The live "Last film" cell read "watched 7 Aug" while the wall had a film dated 10 Nov. That is what the Trakt history says; noted only because visitors see both. | owner |
## Checked and fine
- Path handling in `static-server.ts` (traversal, symlinks, `//host` redirects) and the Ghost HTML allowlist in `src/site/post-html.ts`.
- `CONTENT_REPO_TOKEN` stays out of argv, URLs, logs and the build child.
- A stale source is shown honestly ("synced N d ago"); a broken content commit freezing source refreshes is already documented in `docs/DEPLOY.md`.
- Compression: Cloudflare serves Brotli, so the origin's lack of it does not reach visitors.