diff --git a/docs/NEXT.md b/docs/NEXT.md --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -2,7 +2,7 @@ # NEXT — the queue for future sessions The flight plan. Each item carries enough context to start cold; update this file whenever an item lands (move it to "Done") or a new one is queued. Decisions made while working an item still go through `decisions/` as usual. -_Last updated: 2026-08-27 (**PD-11 recorded: the site-metrics offering** — free real-7d / paid-30d on Analytics Engine, design fully settled in `research/2026-08-26-analytics-offering.md`, **and now built** — ADR 0016 Proposed, PR open, deploy pending; see item 2 and Done. Also: **PRs #27, #28 and #31 are all merged and deployed.** mooring.page now serves the demo-first landing page (PD-10) — verified live: the apex carries the Open Graph card, `/s/jzweifel.dev` renders with the claim banner and `noindex`, and the domain lock, sign-off band and republished `signOff` lexicon from the earlier PRs are all in place. **PR #33 is merged and deployed too** — the postmark cut-off fix and the landing footer's attribution row (both below), smoke-tested live. **PR #35 is merged and deployed** — the admin and login pages wear the site letterhead (details below). **PR #37 is merged and deployed too** — it fixes the white frame #35 shipped with (the `body { margin: 0 }` reset lived only in SiteLayout's `:global` styles, so routes that never bundle SiteLayout kept the default body margin; `letterhead.css` now resets body margin and paints `html` with background + `color-scheme` via `:has(.letterhead)`, mirroring SiteLayout's own root treatment) — verified live: the served letterhead stylesheet on mooring.page/login carries the reset in both schemes. **PR #39 is merged and deployed too** — the theme's contrast guards for user color overrides (details below); verified live in both schemes. **PR #41 is merged and deployed too** — Bluesky post embeds render instead of the cue (details below); verified live. **PR #43 is open and not yet merged** — trimmed posts/writing sections now close with a cue carrying the count and a link to where the rest lives, and the career log stays whole by decision (details below); this closes the *volume behavior* sub-item of the theme tail. **#43 is in flight; the rest of the queue below is current.** The queue below is otherwise current. Critique trends, two separate targets: the rendered **theme** ran 25 → 31 → 29 → 32, and the **landing page** has one run at 21/40 — snapshots in `.impeccable/critique/`)._ +_Last updated: 2026-08-27 (**PD-11 recorded: the site-metrics offering** — free real-7d / paid-30d on Analytics Engine, design fully settled in `research/2026-08-26-analytics-offering.md`, **and now built** — ADR 0016 Proposed, PR #45 open, deploy pending; see item 2 and Done. Also: **PRs #27, #28 and #31 are all merged and deployed.** mooring.page now serves the demo-first landing page (PD-10) — verified live: the apex carries the Open Graph card, `/s/jzweifel.dev` renders with the claim banner and `noindex`, and the domain lock, sign-off band and republished `signOff` lexicon from the earlier PRs are all in place. **PR #33 is merged and deployed too** — the postmark cut-off fix and the landing footer's attribution row (both below), smoke-tested live. **PR #35 is merged and deployed** — the admin and login pages wear the site letterhead (details below). **PR #37 is merged and deployed too** — it fixes the white frame #35 shipped with (the `body { margin: 0 }` reset lived only in SiteLayout's `:global` styles, so routes that never bundle SiteLayout kept the default body margin; `letterhead.css` now resets body margin and paints `html` with background + `color-scheme` via `:has(.letterhead)`, mirroring SiteLayout's own root treatment) — verified live: the served letterhead stylesheet on mooring.page/login carries the reset in both schemes. **PR #39 is merged and deployed too** — the theme's contrast guards for user color overrides (details below); verified live in both schemes. **PR #41 is merged and deployed too** — Bluesky post embeds render instead of the cue (details below); verified live. **PR #43 is open and not yet merged** — trimmed posts/writing sections now close with a cue carrying the count and a link to where the rest lives, and the career log stays whole by decision (details below); this closes the *volume behavior* sub-item of the theme tail. **#43 is in flight; the rest of the queue below is current.** The queue below is otherwise current. Critique trends, two separate targets: the rendered **theme** ran 25 → 31 → 29 → 32, and the **landing page** has one run at 21/40 — snapshots in `.impeccable/critique/`)._ ## Where things stand @@ -22,7 +22,7 @@ - **Thin deletion-honoring cache** over the direct PDS reads (ADR 0010 §4) — deliberately reordered below hosting on 2026-08-07: rendering reads live per request, which is correct until there is traffic to cache. The demo-first landing page (PD-10) raises the pressure: the apex now invites uncached live reads for arbitrary handles, so abuse or traffic on the lookup form is the trigger. Revisit once sites are actually served; when it comes up, start from Jetstream v2's snapshot+tail replay (`research/2026-08-14-bluesky-protocol-services.md`) rather than a relay consumer. - **Landing-page critique follow-ups, design-decision tier** (queued 2026-08-25; snapshot `.impeccable/critique/2026-08-26T01-52-10Z__apps-web-src-routes-page-svelte.md`, scored 21/40 — the three P1s and the mechanical P2s are fixed; what remains needs product calls): gloss or drop "standard.site"/"sifa" jargon for the non-technical launch audience; a pricing signal ("free subdomain" reads as an unpriced paywall); accept a pasted DID in the lookup (currently lowercased and rejected as a typo); distinguish resolver outages from typos in the error copy; and the big swing — show a real rendered site on the landing page instead of describing one ("you've built a rendering engine and put a text ad in front of it"). - **Explore extracting themeability, and a second theme — possibly nautical** (queued 2026-08-25, Jacob's hunch): "Mooring" reads as boats to plenty of people before it reads as airships, and the professional-presence theme is currently the only theme, its palette and motifs woven through `SiteLayout`/`SiteSections` rather than sitting behind a seam. Two questions to explore together: (1) what a theme contract would look like (tokens? component set? the `theme.colors` override mechanism already hints at one) and whether extracting it is worth the indirection while there is exactly one theme; (2) whether a harbor/nautical variant (same letter-and-postmark bones, different motif and palette) is a cheap second theme that meets boat-minded visitors where they land. Mock on the design canvas before building; ADR 0008 scope discipline applies — this is exploration, not a committed v1 item. -### 2. Site metrics — free 7d dashboard on Analytics Engine (PD-11; **built 2026-08-27, PR open — see Done**; deploy + live verification remain) +### 2. Site metrics — free 7d dashboard on Analytics Engine (PD-11; **built 2026-08-27, PR #45 open — see Done**; deploy + live verification remain) The build landed per the settled design (`research/2026-08-26-analytics-offering.md`) and ADR 0016 (Proposed — ratify on the PR). What remains, in order: (1) **deploy steps** — create an API token scoped Account Analytics:Read and `wrangler secret put ANALYTICS_READ_TOKEN` (the `CLOUDFLARE_ACCOUNT_ID` var is already in wrangler.jsonc; until the secret is set the dashboard names the gap and ingest records regardless), then merge + `npm run deploy -w web`; (2) **smoke-test live** — view a site, then read `/admin/analytics` signed in as the site's owner, and as the `mooring.page` authority account for the apex/`/s/` funnel; ingest-to-readable lag is expected to be minutes; (3) the **30d unlock** still waits on billing — data written from deploy day makes it retroactive. @@ -36,7 +36,7 @@ - **Optional runway**: Skyseed grants ($5–25K) are compatible with the indie model (PD-3) if wanted. ## Done -- 2026-08-27 — **Site metrics built end to end** (PD-11's build session; PR **open, not merged or deployed**). ADR 0016 (Proposed) fixes the specifics: dataset `mooring_site_views` bound as `METRICS`, one data point per rendered HTML page view (GET · 200 · `text/html`) written from `hooks.server.ts` under `waitUntil` — the one place every tenant host has already resolved to a DID — with `index1` = site DID and blobs path / referrer host / country / daily visitor hash. Two calls made in-session (both the recommended option): **referrers are stored host-only** — full URLs would carry query strings into the store against the privacy stance, and hosts are what the dashboard aggregates — and the **salt derives from `SESSION_SECRET`** (`HMAC(secret, 'mooring-visitor:' + UTC date)`, the literal prefix domain-separating it from cookie signing) so self-host stays one-secret and there is nothing to rotate or store. Bots drop at ingest via `isbot` (new dependency; learned in testing: it flags a bare `Mozilla/5.0` as headless, so test fixtures need full UA token chains). App-host traffic records under the new `METRICS_APP_DID` var (the authority DID); tenant traffic under the tenant. The query layer (`lib/server/metrics/query.ts`) is **sampling-aware from day one** — every count is `sum(_sample_interval)`, and a test literally greps the SQL to fail any bare `count()` that sneaks in; DIDs are shape-validated before inlining into SQL. `/admin/analytics` (nav link added) renders tiles, a 7-bar views-by-day chart (peak labeled, zero days as rule-colored stubs), and top pages / referrers / countries with `Intl.DisplayNames` country names — plus the locked "30 days · paid tier" chip, the retroactivity promise, and the three-month horizon stated honestly. Distinct states for unconfigured (names the missing var/secret; says recording still works), store-didn't-answer, and empty. Self-host seam: no `METRICS` binding → ingest no-ops, the route 404s. 36 new tests (260 in `web`); verified in a browser through a temporary preview route (all four states, both schemes, desktop + 375px, no console errors), deleted before commit. **Nothing is live yet**: deploy steps and live smoke-test are item 2 above. +- 2026-08-27 — **Site metrics built end to end** (PD-11's build session; PR #45, **open, not merged or deployed**). ADR 0016 (Proposed) fixes the specifics: dataset `mooring_site_views` bound as `METRICS`, one data point per rendered HTML page view (GET · 200 · `text/html`) written from `hooks.server.ts` under `waitUntil` — the one place every tenant host has already resolved to a DID — with `index1` = site DID and blobs path / referrer host / country / daily visitor hash. Two calls made in-session (both the recommended option): **referrers are stored host-only** — full URLs would carry query strings into the store against the privacy stance, and hosts are what the dashboard aggregates — and the **salt derives from `SESSION_SECRET`** (`HMAC(secret, 'mooring-visitor:' + UTC date)`, the literal prefix domain-separating it from cookie signing) so self-host stays one-secret and there is nothing to rotate or store. Bots drop at ingest via `isbot` (new dependency; learned in testing: it flags a bare `Mozilla/5.0` as headless, so test fixtures need full UA token chains). App-host traffic records under the new `METRICS_APP_DID` var (the authority DID); tenant traffic under the tenant. The query layer (`lib/server/metrics/query.ts`) is **sampling-aware from day one** — every count is `sum(_sample_interval)`, and a test literally greps the SQL to fail any bare `count()` that sneaks in; DIDs are shape-validated before inlining into SQL. `/admin/analytics` (nav link added) renders tiles, a 7-bar views-by-day chart (peak labeled, zero days as rule-colored stubs), and top pages / referrers / countries with `Intl.DisplayNames` country names — plus the locked "30 days · paid tier" chip, the retroactivity promise, and the three-month horizon stated honestly. Distinct states for unconfigured (names the missing var/secret; says recording still works), store-didn't-answer, and empty. Self-host seam: no `METRICS` binding → ingest no-ops, the route 404s. 36 new tests (260 in `web`); verified in a browser through a temporary preview route (all four states, both schemes, desktop + 375px, no console errors), deleted before commit. **Nothing is live yet**: deploy steps and live smoke-test are item 2 above. - 2026-08-27 — **A section that stops short says so, and says where the rest is** (PR #43, **open**). Posts and writing rendered a fixed head of their collections and simply stopped: a reader with 342 posts saw five, with nothing on the page saying so and nowhere to go for the rest, while the career log rendered every record — three inconsistent volume behaviours on one page. Jacob's calls, all three the recommended option: **a stamped cue that links out** rather than an in-place disclosure or an archive route (quote hydration deliberately runs only over the *shown* posts — each quote costs a DID-document resolution plus a post read — so revealing forty more would either pay for forty more of those or render the hidden ones bare); **the career log stays whole** (a résumé's point is showing the log, its lines are compact, and a truncated work history reads as concealment rather than curation — so `positions`/`education` are untouched, and the existing `SKILL_CAP` disclosure stands); and **the default post count moves 5 → 8**, which an honest cue makes affordable. A trimmed list now closes with one line in the theme's system voice — mono, accent, above a dotted rule that replaces the list's own last border: `8 of 342 · the rest on Bluesky ↗`. **The count turned out to be free**: both adapters already page the entire collection (`listAllRecords`, 10 × 100) before slicing, so they now return it — `fetchBlueskyPosts` → `{ posts, total }`, `fetchDocuments` → `{ documents, total, publication? }` (replies count towards neither). **The line only renders when there is somewhere to send the reader** — a count with no destination is a tease, not an affordance. Posts always have one, the owner's Bluesky profile by DID, matching the two links `SiteLayout` already builds. Writing has one when every listed document belongs to a single publication that publishes an address; documents spread across publications, or `pub.leaflet.*` legacy records which name none, get no line, and the label is the publication's name falling back to its host. Resolving that publication costs a read, so it is only attempted once a listing is known to be short, the two things wanting publication records now share one read instead of two, and a failed read costs the cue rather than the listing. No lexicon change, no record migration, no new route. 7 new tests (224 in `web`); verified in a browser through a temporary preview route covering posts-trimmed, posts-whole, writing-with-a-named-publication and writing-falling-back-to-its-host in both schemes, deleted before commit — accent-on-paper measured 6.58:1 light / 5.46:1 dark, 36px tall, no horizontal overflow at 375px. Checked against live PDS data: `jzweifel.dev` has five non-reply posts, so it is trimmed by neither the old limit nor the new one and renders unchanged. **One gap:** no account in hand exercises the cue against live data — it is covered by the tests and the dev preview, not by a deployed site; look the first time a heavier poster's site is rendered.