Add an 18+ age gate and plain-language reader copy #2
- IA
Two lanes that are entangled in `+page.svelte` and `library.ts`, so they ship together rather than as contorted partial commits.
## 18+ age gate
Sexualized and explicit titles are hidden for everyone by default, signed in or not. They can only be revealed by a reader who has confirmed a date of birth showing 18+ **and** has then opted in — the opt-in is off until they turn it on.
**GitHub's OAuth API exposes no date of birth, age, or age bracket in any scope.** `/user` returns identity and profile fields only; GitHub collects a birthday at signup but never surfaces it. So the claim is self-attested. This is an honest record of what the reader told us and a real default-off gate, but it is **not age assurance** — anyone can type a date, and user-facing copy should not call it verification.
Design points worth reviewing:
- **The unlock is never a trusted stored boolean.** It is recomputed from the date in `parseAdultConsent`, in the Svelte `$derived`, and again server-side before every write. A request claiming `allowAdult: true` on a minor's date is persisted as a refusal (covered by test). - **The date is stored rather than a derived flag** so a reader who confirms at 17 gains the option on their birthday without being asked again. It is used for nothing else and is never exported. - `isAdult` computes the 18th birthday as a calendar date rather than elapsed days, so leap days and time zones cannot move it. - **Only sexualized and explicit sit behind the gate.** Harem is a story structure and AI disclosure is a production fact, so both stay ordinary preferences — per the rule in CLAUDE.md that these are separate axes and covers cannot establish story content. - **Unknown signals still show.** Forcing them out would empty the grid at current cover-assessment coverage. - Every consumer reads `effectiveFilters` rather than raw `filters`, so a stale localStorage value, another tab, or an imported settings blob cannot reveal anything on its own. Gated controls are not rendered while locked, and `setFilter` refuses gated keys as a second line of defence. - Attestation lives in D1 for signed-in readers and localStorage for guests; a guest's claim is carried up on sign-in rather than asked for twice.
## Plain-language reader copy
Internal pipeline and rubric vocabulary was surfacing in the UI. Filter labels, catalog-completeness notes, series and book drawer notes, and the Jev/cover assessment note strings now read as plain language — including the tool names that were shipping inside ~2,760 catalog note strings and ~2,540 "Genre needs review" issue strings.
## Also
Fixes `workers/accounts.test.ts`, which only ever executed migration `0001`. It now applies every migration in sorted order with named-column seed inserts, so the next `ALTER TABLE` does not silently break it.
## Deploying — read before merging
`workers/migrations/0002_adult_consent.sql` adds three columns to `users`. It **must be applied before the Worker ships**, or `/api/session` will 503 on the missing columns:
``` npx wrangler d1 migrations apply litrpg-hub-accounts --remote ```
Deployment is manual per CLAUDE.md. This has not been run and nothing has been deployed.
## Verification
Run against an isolated worktree of this commit, so unrelated uncommitted work in the dev tree could not influence the result:
- `npm test` — 53 files / 1272 tests passing - `npm run check` — 354 files, 0 errors, 0 warnings - `npm run check:backend` and `tsc -p tsconfig.worker.json` — clean - `npm run build` — succeeds
Note for anyone comparing numbers: a run in the dev tree reports ~104 files / ~2515 tests because a stale `.worktrees/login-d1/` nested checkout is double-counted. The isolated figures above are the real suite.
## Deliberately excluded
An in-progress quality-ranking lane from another working session is present in the dev tree and is **not** in this PR: `scripts/backend/quality/`, `scripts/backend/migrations/013_quality_index.sql`, `docs/quality-benchmarks.md`, and the `pipeline:quality` line in `package.json`. Nothing in `src/` or `workers/` references any of it.
iamnbutlermerged e4c0a116a995last month on GitHub
iamnbutlerhead-ref-deletedlast month on GitHub
iamnbutlerreferenced this in Move the age gate to a real settings page #3last month on GitHub
github-actions[bot]referenced this in upstream age verification activity detected #1last month on GitHub