Skip to content

feat(serendipity): build the dashboard at deploy, fetch its events as an island - #929

Merged
oddharsh merged 1 commit into
mainfrom
claude/serendipity-island
Sep 25, 2026
Merged

oddharsh merged 1 commit into
mainfrom
claude/serendipity-island

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

What changes

The /serendipity dashboard stops rendering per request. build.ts step 5b bakes the shell: 12 KB of CSS, the toolbar and the filter script. The pool's count line and cards arrive from /serendipity/events.html through lib/island.ts, the same move #924, #926 and #928 made.

today after
document on the wire 15,258 B (edge q4 brotli) 5,260 B shell (q11 twin)
plus the island none ~7,350 B of cards (edge q4)
first visit, total 15,258 B ~12,600 B (−17%)
return visit 15,258 B again 304 on the shell, then the cards
return visit after a deploy 15,258 B 3,671 B dcz delta + cards
script-src 'self' 'unsafe-inline' three sha256 hashes
uid cookie on a plain view minted none

It was the largest page still on the loose script policy, because it composes its own CSP and #925's lunaPage hashing never reached it.

How it fits serendipity's own rules

  • The island is cookie-free and edge-cached for the same 60 s the dashboard took, and every mutation evicts it (sync, enrich, tag, cookies, add-event), the way it evicted the cached dashboard. A contract test pins the eviction.
  • A flash ?msg= view still renders live and whole, since it belongs to the person who just acted, and so does every other /serendipity/* path. Only the plain dashboard GET is built.
  • The CSP is serendipity's own, with the build's hashes. serendipityCsp(scriptSrc) is now the one builder, so the built page keeps img-src 'self' data: https: for the cover proxy's fallback, which the site-wide hashed policy does not allow. It takes 'self' plus three hashes: the filter script, the island loader, and the shared speculation rules.
  • Its headers go in through serveStaticPage, never through withSerendipitySecurityHeaders. That function rebuilds the Response from an object init, which drops encodeBody on a precompressed body and double-encodes it (gotcha 13). Checked on the local built Worker: the body decodes once to HTML. The encodeBody walker's record for serendipity said this day would come; it now says how it was handled.
  • The uid cookie is no longer minted on a plain dashboard view. Only /contribute and the POSTs use it, and they still mint it on first use.

The placeholder

A count line and 8 inert placeholder cards, which are <div>s rather than links to an event that doesn't exist. The live count differs, and it doesn't need to match: nothing follows the island except the hidden no-match note and the cover popover. The toolbar stays in the shell, so a search typed before the swap survives it, and the filter re-applies when the island lands. Over an empty pool, a :has() rule hides the toolbar in the built page, the same way the live render omits it.

Also

  • Bug fix: serendipity's CSS ended with a stray top-level }. Browsers skip it silently. The build's Lightning CSS pass refused it the first time the page was baked.
  • Instrument fix: the layout-shift lab's control went unseen on this page, so the lab refused it. It prepends its control block into .window > .body, which on serendipity's shell is a flex row, so the block is a zero-width column that moves nothing. The lab now tries .window > .body > .content first, and the control is seen here and still on /garage/dyno.

Verification

  • bun run build, bun run lint, bun run typecheck: clean.
  • bun run test: 922 pass. bun run test:node: 912 pass, 10 skipped (HTMLRewriter-only), 0 fail. The new contract-serendipity-dashboard-is-a-built-document covers the shell, the island's caching and eviction, the live flash view, the empty pool and the CSP builder.
  • bun run routes:check: 182 routes, 0 failures. The harness's local D1 carries no serendipity schema, so its fragment row pins only the shape (200 or 503).
  • Browser, against the built Worker with serendipity's migrations applied to local D1 and 40 seeded events: the island goes live with one fetch (initiator link); search, the no-match note and the date chips work across the swap; no cookie is set; no console errors. bun run cls reads the dashboard clean on desktop and phone, with the control seen.

Not in this PR

… an island

The /serendipity dashboard rendered per request behind a 60 s edge
cache. It shipped at the edge's q4 brotli with no dcz delta and no
ETag, and with script-src 'unsafe-inline', since it composes its own
policy and #925's lunaPage hashing never reached it. It was the
largest page left in that state: 15,258 B on the wire, 12 KB of it CSS.

build.ts step 5b now bakes the shell: the CSS, the toolbar and the
filter script. The pool's count line and cards arrive from
/serendipity/events.html through lib/island.ts. The island is
cookie-free and edge-cached for the same 60 s, and every mutation
evicts it the way it evicted the cached dashboard. A flash ?msg= view
still renders live and whole for the person who just acted.

The built page keeps serendipity's own CSP, since its img-src admits
the cover proxy's https fallback, with the build's three hashes in
place of 'unsafe-inline'. Its headers go in through serveStaticPage:
withSerendipitySecurityHeaders rebuilds the Response from an object
init, which would drop encodeBody on the precompressed body (gotcha 13).

Measured against production: shell 5,260 B at q11 plus ~7,350 B of
cards, so a first visit is about 12.6 KB (-17%) and a return visit
pays only the cards.

Also:
- The dashboard no longer mints a uid cookie on a plain view. Only
  /contribute and the POSTs use it, and they still mint it.
- serendipity's CSS ended with a stray top-level "}". Browsers skip it,
  and the build's Lightning CSS pass refused it on the page's first bake.
- The filter script re-reads the cards on every apply and re-runs when
  the island lands; the toolbar stays in the shell, so a search typed
  before the swap survives it.
- The layout-shift lab's control went unseen here, because .window >
  .body is a flex row on this shell. It now tries the content column
  first, and the control is seen on both shells.
@github-actions github-actions Bot added area: docs Runbooks and repository prose area: serendipity The /serendipity event dashboard area: tools Developer tooling and the contract suite area: worker The site Worker: routing, rendering, bindings type: feat New capability or surface labels Sep 25, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh a96b5b3 Sep 25 2026, 06:54 PM

@github-actions

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

3c49ec7efb9d0d68c5d2b7266095bc958aa1fb49 → bdfd90bda1848617de0ef2c20fae68bf6abe4c4e

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 27 files, 91.60 KiB Brotli total.

Pages

Total Brotli: 608.75 KiB → 613.89 KiB (+5.14 KiB, +0.8%) across 61 files, 1 changed.

file base head Δ
serendipity.html — 5.14 KiB new

Worker bundle

257.53 KiB → 258.02 KiB gzip (+0.49 KiB, +0.2%)

Largest module deltas (raw bytes in bundle):

file base head Δ
.build/serendipity/serendipity.ts 90.48 KiB 91.28 KiB +0.80 KiB (+0.9%)
.build/src/worker/index.ts 12.85 KiB 13.17 KiB +0.31 KiB (+2.4%)
.build/src/worker/lib/csp-hashes.ts 6.86 KiB 7.01 KiB +0.15 KiB (+2.3%)

Dictionary transport

Family dictionary acquisition: no change, 14.41 KiB q11.

pd/: 172 → 173 deltas, 507.41 KiB → 510.99 KiB (+3.58 KiB).

@oddharsh
oddharsh enabled auto-merge (squash) September 25, 2026 18:56
@oddharsh
oddharsh merged commit d167751 into main Sep 25, 2026
15 checks passed
@oddharsh
oddharsh deleted the claude/serendipity-island branch September 25, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Runbooks and repository prose area: serendipity The /serendipity event dashboard area: tools Developer tooling and the contract suite area: worker The site Worker: routing, rendering, bindings type: feat New capability or surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant