feat: the redesigned Event Clock

The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-18 01:55:16 +02:00
1 parent ddd9673f86
commit 50c6e91b00
284 files changed
+45100 -1628

No files matched your search

+26 -45
View File
@@ -56,7 +56,7 @@ falling back to checked-in fixtures.
## Three constraints that shape everything
1. **No accounts, no logins, no user records.** Completion state lives in the browser's
`localStorage`, keyed by event ID. There is no user table and no session. Any request implying
`localStorage`, in a v2 envelope keyed by local game/server account and unchanged event ID. There is no user table and no session. Any request implying
"sync across devices" is solved with export/import JSON, not a server-side user.
2. **No LLM in the pipeline.** Event data is extracted by deterministic code-based parsers only.
There is no Anthropic dependency, no API key, and no per-run inference cost. A source that
@@ -133,6 +133,15 @@ cycle:
run `bun run build:feed`, which builds from snapshots and fixtures) before serving. A worktree
also needs its own `bun install`.
**Browser verification must include an ordinary HTTP network hostname, not only localhost.**
Localhost is a secure context even over HTTP, so it conceals API restrictions that affect LAN and
WireGuard addresses. On 2026-10-16 the redesign called `crypto.randomUUID()` during first-visit
migration: it was undefined on HTTP `10.20.20.2`, and startup mislabeled that TypeError as a saved-data
failure. Browser-local IDs now come from `state/reader/ids.ts` using 128-bit `crypto.getRandomValues`,
which works on HTTP too. Use that helper for accounts, operations, clock revisions and backups;
never remint existing identities. A Chrome test hostname mapped to loopback reproduces the HTTP
security boundary without contacting another machine. Test startup and subsequent writes there.
**`build:js` passes `--production`, and that flag is load-bearing.** Without it Bun bundles React's
*development* build: 566 KB rather than 344 KB (164 KB vs 103 KB gzipped), every element creation
validated at runtime, and `StrictMode` double-invoking effects — which in this app means
@@ -167,8 +176,9 @@ src/ingest/ html.ts, dates.ts (sixteen formats), merge.ts, sanitize.ts, ro
parsers/ game8.ts, wikigg.ts, akwiki.ts, fandom.ts, bawiki.ts, holodori.ts, iopwiki.ts,
stellasora.ts, arustats.ts, karendar.ts — keyed by SITE, not game
adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam
src/client/ React app, service worker, manifest
state/ progress, daily log, ignores, prefs, sort — all localStorage
src/client/ ReaderApp (shared interfaces), components/redesign/, service worker, manifest
state/reader/ active v2 envelope, accounts/clocks, migration, archive, backup/restore
state/ legacy v1 helpers, progress validators, prefs normalization, sort
gameOrder.ts — the reader's game order, and the A–Z fallback; pure
useCustom.ts — the reader's own games and events (PRD F13)
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
@@ -1228,48 +1238,19 @@ to an open page). Four things hold it up:
keeps a retired source's slot warm rather than pruning it. `games` itself is left in feed order on
purpose: `adoptNewLanes` diffs it and `knownGames` is seeded from it, so ordering it at source
would let a display preference reach the code that hides a reader's games.
- **Reordering is a settings-only affordance.** The focus bar and the dailies strip are the fastest
tap targets in the app, and a drag target on top of a tick target costs somebody a streak the first
time it misfires — so the live surfaces are never draggable. Both a handle and ↑↓ buttons ship,
because touch fires no drag events at all: the arrows are the mechanism and the drag is the pointer
fast path, and being ordinary buttons is what makes the whole thing reachable by keyboard and
screen reader without a second implementation of the same interaction.
- **The settings panel is groups that state their own answer, and they ship closed.** Six
`<details>` groups — games, server region, appearance, what you see, your own, your progress — each
with its current state on the summary line (`Europe`, `17 of 18 on · A–Z`, `plus finished, not
started`). It was one open block in two columns, which is readable at four games and not at
eighteen, where the game list is eighteen rows of four controls sitting above the checkbox somebody
came to tick. The state line is the half that makes collapsing honest: without it every question
about how the app is set up costs a click, so the closed panel has to be a report and not a menu.
Three rules follow. **Do not default a group open** to make something inside it findable — the empty
states name the group as well as the switch, which is what makes that unnecessary. The summaries are
**derived from `prefs` at render**, never stored, so they cannot drift from the controls they
describe. And **one group answers one question**, which is what the sixth group is: the region and
the theme were a group called `Reading`, grouped on "both are how do I read this?" and summarising
as `Europe · Dark` — two unrelated answers joined by a dot, under a name for neither of them, in a
panel whose whole premise is that a closed line answers *its* group. The region is not a reading
preference: region-scoped ends and every daily reset are cut on that server's clock (§ Domain
rules), so it is the one control here that can make a countdown wrong, and it is now a line of its
own rather than filed behind a word for the theme.
Native `<details>` for the reason the reorder arrows are ordinary buttons: keyboard and screen
reader reach it without a second implementation. `summary` is not an `a`, `button` or `[tabindex]`,
so it needs its own `:focus-visible` rule in `styles.css` — the shared one does not reach it.
- **The panel is one column of groups, and it uses the page's width.** Those are two claims and only
the first survived a desktop read: the rows were held to `max-w-3xl` inside a shell a third wider,
so every rule inside the panel stopped short of the panel's own, and it was the one block on a wide
page not using the width — between a two-column checklist and a three-column footer that both do.
A name at one end of a row and its state at the other is what "running now" already does with a
title and its countdown, so full width is the house pattern rather than a stretch. Prose inside a
group keeps its own measure, and the game list — the one thing tall enough to need it — takes **two
columns of its own past `lg` once there are nine or more games**, flowing down then across so the
numbers still read in a straight line and an arrow still swaps a row with its neighbour. Eighteen
rows in one column was a 1,000px ribbon with two thirds of the width empty beside it.
- **A game's dailies stay together, and grouping never re-sorts inside a group.** `dailyGroups`
(`Dailies.tsx`) emits a game's standing chore followed by that game's repeating events, in the
order those arrived; it replaced `[...chores, ...repeating]`, which put a game's chore and its own
login event at opposite ends of the strip. Collapsed, the grouping is **adjacency only** — no
per-game headings, because that strip is the part of the page answerable in ten seconds and a
heading per game makes it the tallest block on it, pushing "next to expire" down the page.
- **Reordering is a settings-only affordance.** Your games provides ordinary ↑↓ buttons for
keyboard, screen reader and touch. Live focus/daily tick targets are never draggable.
- **Settings uses named pages.** General, Your games, Appearance, Lists, Region & time,
Data & sources, Backup, About Event Clock and Changelog. Keep the complete colophon in About;
Changelog has its own page and no footer link. The footer always states data age and links About.
Reset overrides live per game/server; display timezone is a separate preference.
- **Both interfaces share the reader.** Structured (default, Round 6 B rail) and Relaxed (Round 7)
are CSS/layout choices over one feature tree. Theme, palette and row density are independent.
Do not mount the old v1 persistence hooks alongside ReaderApp's v2 store. See DATA-MODEL for
account IDs, clock revisions, migration recovery, backups and export validation.
- **A game's routines stay together.** The separate daily panel keeps direct ticks; grouped
routines use per-game/server summaries. Collapse policy is configurable and focus can expand a
group without changing that saved policy. Timeline reset strips sit next to their game's events.
- **A day already gone is still tickable, and the window bounds display and never data.** People play
at midnight and tick at breakfast, so `catchUpDays` (`src/shared/daily.ts`) offers the last
`CATCH_UP_DAYS` game-days for a standing chore and for an event whose end was never announced —