diff --git a/.superpowers/brainstorm/.last-port b/.superpowers/brainstorm/.last-port new file mode 100644 index 0000000..496f6db --- /dev/null +++ b/.superpowers/brainstorm/.last-port @@ -0,0 +1 @@ +58245 \ No newline at end of file diff --git a/.superpowers/brainstorm/.last-token b/.superpowers/brainstorm/.last-token new file mode 100644 index 0000000..85a9459 --- /dev/null +++ b/.superpowers/brainstorm/.last-token @@ -0,0 +1 @@ +e13012f97798f2f41fa46ae58204a4bcd50a142b56e7cca42efec09e7633540f \ No newline at end of file diff --git a/.superpowers/brainstorm/1683332-1792128126/content/a-game-setup.html b/.superpowers/brainstorm/1683332-1792128126/content/a-game-setup.html new file mode 100644 index 0000000..6c4949b --- /dev/null +++ b/.superpowers/brainstorm/1683332-1792128126/content/a-game-setup.html @@ -0,0 +1,700 @@ + + + + + +A · Every game has its own setup + + + + + + +
+ +
+ + + + diff --git a/.superpowers/brainstorm/1683332-1792128126/content/b-routines-home.html b/.superpowers/brainstorm/1683332-1792128126/content/b-routines-home.html new file mode 100644 index 0000000..08f41de --- /dev/null +++ b/.superpowers/brainstorm/1683332-1792128126/content/b-routines-home.html @@ -0,0 +1,616 @@ + + + + + +B · Routines get their own home + + + + + + +
+
+
+ + + + diff --git a/.superpowers/brainstorm/1683332-1792128126/content/c-one-due-list.html b/.superpowers/brainstorm/1683332-1792128126/content/c-one-due-list.html new file mode 100644 index 0000000..68b2144 --- /dev/null +++ b/.superpowers/brainstorm/1683332-1792128126/content/c-one-due-list.html @@ -0,0 +1,703 @@ + + + + + +C · One due list + + + + + + +
+
+
+ + + + diff --git a/.superpowers/brainstorm/1683332-1792128126/state/server-stopped b/.superpowers/brainstorm/1683332-1792128126/state/server-stopped new file mode 100644 index 0000000..9a061a4 --- /dev/null +++ b/.superpowers/brainstorm/1683332-1792128126/state/server-stopped @@ -0,0 +1 @@ +{"reason":"stop-server.sh","timestamp":1792128694} diff --git a/AGENTS.md b/AGENTS.md index 8a50c01..4f5a972 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 - `
` 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 `
` 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 — diff --git a/TODO.md b/TODO.md index 0083df6..d5804be 100644 --- a/TODO.md +++ b/TODO.md @@ -10,9 +10,12 @@ note only says where to start. ([spec](docs/superpowers/specs/2026-10-14-built-in-routines-design.md)), every-N-days cycles for tasks and built-ins ([spec](docs/superpowers/specs/2026-10-16-cycle-routines-design.md)), and battle pass windows as feed events ([spec](docs/superpowers/specs/2026-10-16-pass-windows-design.md), - see its § Outcome). Plans are beside them in `docs/superpowers/plans/`. Nothing is in flight: no - open branches or worktrees. -- **No next task has been chosen.** The open items below are the candidates; the owner decides. + see its § Outcome). Plans are beside them in `docs/superpowers/plans/`. The redesign attempt is uncommitted and was rejected for substantial markup divergence. + **Start with [the replacement-agent handoff](docs/handoffs/2026-10-16-redesign-fidelity-handoff.md).** + The existing plan/ledger contain superseded completion claims; no extra branch/worktree. +- **Redesign rejected for markup divergence; presentation correction pending.** See docs/design-reviews/2026-10-16-redesign-implementation.md. + The assigned follow-up is to faithfully port the approved markup and finish the redesign. + Unrelated open items below remain separate. The ones that need the owner in the game first are marked as checks. - **How the owner works** (their standing instructions, also in their personal Claude settings): once they approve a design, carry it through spec → plan → execution with no extra review stops, @@ -30,6 +33,17 @@ note only says where to start. ## Open items +- [ ] **Redesign Settings → Your own events and games** (owner, 2026-10-17: “the split-up from + the mockup is nice, but I guess it needs a bigger design”). Deferred on purpose: it works as well + as it does today, which is enough for now. Direction from the owner: a heading per game you + created, with that game's events listed under it, taking inspiration from the Routines tab — + possibly reusing the routine editor (`components/redesign/RoutineDrawer.tsx`, the right-docked + create/edit shelf) as the base for an event editor. Not in the Round 6/7 mockups, so it needs its + own short design (brainstorm → spec → plan; owner approves once, then carry it through). Keep the + current behaviour (`components/YourOwn.tsx`) working until the replacement is accepted. + - Owner, same day, an alternative to weigh in that design: drop the separate settings page and + put an **Add** button on the Settings sidebar's “Your games” list, opening an event editor + there (a new game, or a new event under a game). - [ ] Make it clear which game each *This week & month* chip belongs to (owner, 2026-10-16: "it can become a bit unclear"). With built-ins for seven games, that row now holds many chips whose titles do not name their game — "Weekly Routine", "Weekly Challenge", "City Stamina", @@ -235,3 +249,20 @@ note only says where to start. `EventForm`, `src/client/components/TaskForm.tsx`). Review them as a reader would — field order, defaults, copy, validation messages, how a cadence and a measure are chosen, phone width — against `docs/DESIGN.md` and `docs/PRODUCT.md`. +- [ ] Follow up the 2026-10-18 pre-ship review of the v1→v2 reader migration. A schema-guardian review + found nothing blocking: event IDs, day keys and the feed contract are unchanged, and v1 keys are + never removed, so a rollback finds them byte-identical. These gaps remain, all low severity: + - Orphaned v1 entries (marks or amounts for a deleted `myevent:`/`mytask:`, or any entry whose game + can't be resolved) are not carried into v2 account data (`state/reader/migration.ts`, + `gameOf`). They survive in `legacyRaw`, `gacha-tracker:v2:migration-recovery` and the untouched v1 + keys. Consider a catch-all bucket so a restored custom item finds them. + - Importing a v1 file whose region differs from the reader's account lands its marks in a new + `account:import-N`, not the visible account (`state/reader/import.ts`, `export.ts`). Kept, but + out of sight. + - `initializeReader` reconciles and writes back outside `withReaderLock`, with no compare-and-set + (`state/reader/store.ts`). A second tab's write in that window can be overwritten. + - Storage pressure: every save carries `legacyRaw`, and every automatic backup embeds the state, + `legacyRaw` and the v1 keys again, with no size cap (only manual backups are capped). Heavy + readers could reach the ~5 MB quota. Failures are reported, not silent. + - After a rollback, anything done in the new build lives only in v2 and isn't shown by the old app. + It's stranded, not lost. diff --git a/design/mockups/round4/converged.html b/design/mockups/round4/converged.html new file mode 100644 index 0000000..bad5560 --- /dev/null +++ b/design/mockups/round4/converged.html @@ -0,0 +1,1636 @@ + + + + + +Round 4 · Round 4 · Combined review + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round5/converged.html b/design/mockups/round5/converged.html new file mode 100644 index 0000000..46dcd56 --- /dev/null +++ b/design/mockups/round5/converged.html @@ -0,0 +1,1813 @@ + + + + + +Round 5 · Event Clock redesign + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round5/refined.html b/design/mockups/round5/refined.html new file mode 100644 index 0000000..d323bd1 --- /dev/null +++ b/design/mockups/round5/refined.html @@ -0,0 +1,1843 @@ + + + + + +Round 5 · Quieter layout + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round6/a-inline.html b/design/mockups/round6/a-inline.html new file mode 100644 index 0000000..9964eaa --- /dev/null +++ b/design/mockups/round6/a-inline.html @@ -0,0 +1,1968 @@ + + + + + +Event Clock · Design A + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round6/b-rail.html b/design/mockups/round6/b-rail.html new file mode 100644 index 0000000..a912fbd --- /dev/null +++ b/design/mockups/round6/b-rail.html @@ -0,0 +1,1968 @@ + + + + + +Event Clock · Design B + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round6/c-open.html b/design/mockups/round6/c-open.html new file mode 100644 index 0000000..4de3a7a --- /dev/null +++ b/design/mockups/round6/c-open.html @@ -0,0 +1,1968 @@ + + + + + +Event Clock · Design C + + + + + + +
+
+
+ + + + diff --git a/design/mockups/round6/index.html b/design/mockups/round6/index.html new file mode 100644 index 0000000..f4de58e --- /dev/null +++ b/design/mockups/round6/index.html @@ -0,0 +1,8 @@ + +Event Clock · Three layouts

Give the events room.

Three complete layouts for Event Clock. Each keeps the agreed routines, Archive, settings and onboarding, while moving focus and freshness controls out of the way of the event list.

Compare the same busy calendar in each. The top review bar can simulate older data, open onboarding and reset the sample state.

+
A

Inline desk

Game focus beside the list heading. A small freshness line with Details. Your one-click dailies and pinned routines stay in the familiar left rail.

The smallest change to the layout you already liked; retains two columns.

Open A
+
B

Game desk

Game focus, daily ticks and source status share the left rail. The event heading stays clear, with more attention on the list itself.

Controls have one predictable home; the rail holds more, and stacks before events on a phone.

Open B
+
C

Open ledger

A wide event list with a quick daily strip. Other pinned routines open above the list, with your choice of collapsed, expanded or always open.

Most space for event titles; daily and routine panels scroll with the list.

Open C
+

Shared changes to try

Routines → Checklist display now controls the daily panel and group expansion. Numeric fields keep their native one-step arrows.

Settings includes Changelog as its own page, custom interface palettes, per-game source links and thanks, reset-time overrides with Automatic, and clearly labelled backup previews. Upcoming events is part of What do you want to track.

Returning user opens a guided tour in your preferred view. Take it or keep browsing; your progress stays intact.

This is a review prototype. Codes, source health and historical data are illustrative. The production app and earlier mockups are unchanged.

Compare with Round 5

diff --git a/design/mockups/round7/converged.html b/design/mockups/round7/converged.html new file mode 100644 index 0000000..624a035 --- /dev/null +++ b/design/mockups/round7/converged.html @@ -0,0 +1,2103 @@ + + + + + +Event Clock · Airier unified design + + + + + + +
+
+
+ + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7a40874..db8c473 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -364,3 +364,56 @@ other, so `test/update.test.tsx` pins both ends of the `skip-waiting` handshake cleanly because state is in SQLite. - **No server-side rendering.** The feed is small and cacheable; a static SPA is enough. - **No websockets.** Events change on a scale of hours; a 5-minute cache is more than adequate. + +## Reader application (2026-10-16 redesign) + +`App.tsx` exports `ReaderApp`; only the v2 reader store writes production progress. Legacy hooks +remain as compatibility helpers, validators and tests, not mounted writers. ReaderContent coordinates +shared Checklist/Timeline/Routines/Archive/Settings, account-aware detail adapters and timed archive +transitions. `components/redesign/` contains the feature pages. `redesign.css` scopes Relaxed and +Structured via data-interface, with one application tree and independent theme/density. + +`state/reader/` owns migration, schema, reducer, store, clocks, accounts, archive, backups, restore, +imports, routines and event selectors. Operations re-read the persisted envelope under Web Locks; +an IndexedDB readwrite transaction serializes browsers without Web Locks. Failed locking/storage +never publishes an optimistic success. Storage events update other tabs. Field timestamps protect +newer edits and tombstones; account writes rebase changed routine choices. Legacy deltas reconcile +on startup into the original migrated accounts; raw keys remain preserved. + +Clock revisions freeze automatic metadata and overrides. Imported historical clocks are excluded +from active clock resolution; history resolves each explicit revision. Display timezone lives in +DisplayTimeContext and never changes the feed. + +Events and redeem codes use separate validated static feeds. `build-codes.ts` reads pinned snapshots +or fixtures offline and preserves firstObservedAt in snapshots/codes-first-seen.json. The refresh +runner combines event adapters and count-only code sources while sharing robots, floors, per-host +spacing and snapshot gates. No fake GachaEvents represent codes. Both feeds are network-first in +the worker; manual refresh bypasses the browser cache. Shell update waiting/activation is unchanged. + +Backups are outside the reader envelope. Restore validates first, writes a recovery export, prepares +a pending transaction, then replaces live state. Startup finishes an interrupted transaction before +opening the reader. Browser UI restore uses the same cross-tab lock as ordinary edits. + + +### Ordinary HTTP / WireGuard hosts + +Reader identity minting uses `state/reader/ids.ts`: 128-bit `crypto.getRandomValues` tokens for +accounts, operations, reset revisions and backups. The redesign's initial `randomUUID` calls were a +startup bug on ordinary HTTP network hosts; unlike localhost, these are not browser secure contexts, +even when reached through a WireGuard tunnel. Existing IDs are unchanged and valid. Initialization +preserves migration recovery before minting, so the failed first visit did not discard legacy bytes. + +Regression verification uses a Chrome test hostname mapped to loopback with isSecureContext false, +randomUUID unavailable and actual getRandomValues/IndexedDB available. Existing-save migration, +new setup, editing, additional servers, reset override/revert, backup and reload all pass there. + + +### Presentation correction (2026-10-17) + +`ChecklistEvent`/`NextDeadline` render the approved list hierarchy; `TimelinePage` owns the shared +axis for event spans and routine periods. `RoutinesPage` groups by reset or account, with native +`RoutineEditor` dialogs. `ArchiveDetail` reads saved archive snapshots and never routes historical +progress through the live event editor. Settings keeps one shared tree for both interfaces, with +separate default-view/custom-content pages and direct game profiles. Structured is the absent-value +default at prepaint, preference normalization, setup and appearance application. No state keys, +event identities or stored clock revisions changed for this presentation correction. diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 6160317..9aab3ab 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -244,7 +244,10 @@ CREATE TABLE snapshots ( `region_ends` and `payload` hold JSON as TEXT; parse them through the Zod schema on read so a malformed row surfaces at the boundary rather than deep in the UI. -## Client-side storage +## Legacy client-side storage (v1 compatibility) + +Production uses the [active v2 reader envelope](#active-reader-state-v2-2026-10-16). The keys and +merge rules in this section describe migration input and retained compatibility helpers. Namespaced, versioned, and small. Nothing here ever goes to the server. @@ -772,3 +775,48 @@ Two timestamp fields distinguish crawler activity from site updates: `lastConfir the crawler last successfully contacted and verified the source (HTTP 200 or 304 Not Modified), and `contentChangedAt` records when the served bytes last changed. Both default to `null` so feeds built before their introduction continue to validate. + +## Active reader state (v2, 2026-10-16) + +The localStorage/export descriptions above document legacy v1 compatibility. Production mounts +only `ReaderApp` and persists `gacha-tracker:v2:reader`, validated by state/reader/schema.ts. +One stable `account:` represents one tracked game/server; it is browser-local, not a login. +Original event/task/daily IDs remain unchanged inside account data. Account data contains progress, +ignored, and daily/amount maps nested by clock revision. A revision freezes effectiveAt, region, +reset override and automaticBase (offset/hour/minute). Changes append revisions; imported clocks +can be historicalOnly and never replace the local active reset. + +The envelope includes accounts, data, archive, seenEvents, prefs, customGames/customEvents/tasks, +codeSeen (per game), codeRedeemed (per account), and eventSeen (per account). Archive/seen keys are +JSON.stringify([accountId,eventId]); archive snapshots retain event, outcome/time, resolved end, +clockRevision, progress, days and amounts. Account/revision cross-references are validated before +restore. Field timestamps and operation IDs rebase writes and preserve deletion tombstones. Unknown +fields pass through exports; progress imports retain opaque root/account extensions in +importedExtensions as well as mergeable known fields. Existing same-server settings remain local. + +Migration preserves raw v1 keys in place and in migration recovery, creates one account per game at +the saved region and maps old follows/pins/logs. Newly offered games remain off. Startup compares +legacyRaw with surviving v1 keys to carry older-tab deltas into the original account without +resurrecting newer v2 deletions. Parser aliases carry progress, ticks, amounts and archive/seen IDs. + +Keys outside the envelope: + +- gacha-tracker:v2:migration-recovery — raw pre-migration bytes. +- gacha-tracker:v2:backups — at most 7 daily + 4 weekly + 6 monthly UTC bucket snapshots. +- gacha-tracker:v2:restore-recovery — the complete pre-restore export. +- gacha-tracker:v2:restore-pending — prepared replacement, resumed before mounting on startup. + +Exports use format gacha-tracker-export, version 2, exportedAt, reader and legacy. Progress exports +mark scope progress; full imports preview replacement, progress imports preview a merge. Backups +are never recursively included. Original v1 export envelopes remain convertible. + +Prefs add interface (structured/default or relaxed), density (standard/default or compact), scheme +(slate/warm/plum/custom), separate light/dark customPalette, per-game gameColors, timezone +(null/Automatic or IANA), checklist daily placement/expansion and redesignTourSeen. Main view remains +soon or timeline; utility navigation is transient. Future/unreadable records remain exportable even +when this build cannot offer editing controls. + + +The fidelity correction adds `prefs.routineGroup` (`reset`, the absent-value default, or `game`) +for the Routines page's grouping. It changes no routine IDs or placement keys. Structured is the +interface default as of the owner's 2026-10-17 instruction; explicit saved Relaxed is preserved. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 707611c..7421917 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2,84 +2,84 @@ name: Event Clock description: Live and upcoming gacha events, sorted by what expires next. colors: - ground: "#12141c" - surface: "#1b1e2a" - raised: "#232739" - hairline: "#2c3145" - ink: "#e9ebf3" + ground: "#1a1c24" + surface: "#22252f" + raised: "#30333c" + hairline: "#43454c" + ink: "#eeeae3" ink-strong: "#ffffff" - muted: "#888fa6" - faint: "#5a6178" + muted: "#b1afad" + faint: "#9c9b9b" scrim: "rgb(18 20 28 / 0.8)" glow: "#1a1e2c" calm: "#55607f" - near: "#5b8def" - soon: "#e8b33c" - critical: "#ff4d6a" - ground-light: "#edf0f7" - surface-light: "#ffffff" - raised-light: "#dfe4f0" - hairline-light: "#c9d1e2" - ink-light: "#151824" + near: "#8eb3f4" + soon: "#e9b95b" + critical: "#ff8598" + ground-light: "#f3f1ec" + surface-light: "#fffdf8" + raised-light: "#f0eeea" + hairline-light: "#dddbd8" + ink-light: "#28272d" ink-strong-light: "#000000" - muted-light: "#535b73" - faint-light: "#767e94" + muted-light: "#69676a" + faint-light: "#6e6d6e" scrim-light: "rgb(26 31 48 / 0.42)" glow-light: "#ffffff" calm-light: "#8a92a8" - near-light: "#2b62c6" - soon-light: "#8a6008" - critical-light: "#c81e3c" + near-light: "#315fa5" + soon-light: "#805a12" + critical-light: "#b12947" typography: numeral: - fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif" fontSize: "2.75rem" fontWeight: 700 lineHeight: 1 letterSpacing: "-0.025em" fontFeature: "\"tnum\" 1" display: - fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif" fontSize: "1.75rem" fontWeight: 600 lineHeight: 1.15 letterSpacing: "-0.025em" wordmark: - fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif" fontSize: "0.9375rem" fontWeight: 700 letterSpacing: "0.02em" reading: - fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif" fontSize: "0.875rem" fontWeight: 600 fontFeature: "\"tnum\" 1" title: - fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif" fontSize: "0.9375rem" fontWeight: 500 lineHeight: 1.375 body: - fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif" fontSize: "0.875rem" fontWeight: 400 lineHeight: 1.625 summary: - fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif" fontSize: "0.8125rem" fontWeight: 400 lineHeight: 1.375 control: - fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif" fontSize: "0.75rem" fontWeight: 500 label: - fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Bricolage Grotesque, ui-sans-serif, system-ui, sans-serif" fontSize: "0.6875rem" fontWeight: 600 letterSpacing: "0.14em" tag: - fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif" + fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif" fontSize: "0.625rem" fontWeight: 500 rounded: @@ -170,7 +170,11 @@ components: # Design System: Event Clock -## Overview +The active Relaxed/Structured layout and palette contract is recorded in +[Active redesign system](#active-redesign-system-2026-10-16). The component rationale below describes +the preceding instrument-panel system; use the active section and live token resolver for new work. + +## Original component rationale **Creative North Star: "The Lit Instrument Panel"** @@ -528,3 +532,105 @@ rather than being toned down. chooses. - **Don't** put a second control inside a row, or a drag target on the focus bar or the dailies strip. + +## Shipped redesign system (2026-10-18) + +The owner accepted this build as the initial redesign on 2026-10-18, after screen-by-screen review +against the approved mockups (`design/mockups/round6/b-rail.html` for Structured, +`design/mockups/round7/converged.html` for Relaxed) and the corrections recorded round by round in +`docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`. This section supersedes the original +shell/type/palette/layout descriptions above. Those older sections remain the rationale for the detail, +timeline and uncertainty components that carried over. Overrides live in `src/client/redesign.css` +and `src/client/state/appearance.ts`. + +### Shell and layout + +- **Interfaces.** Settings → Appearance offers Structured (the Round 6 B rail; the default for new + installs and missing preferences) and Relaxed (Round 7). A saved Relaxed choice stays Relaxed. + Structured uses Barlow Condensed/Barlow and adjoining ruled sections. Relaxed uses Bricolage + Grotesque/Figtree with separated panels. Fonts keep system fallbacks offline. +- **Navigation.** The tabs are Checklist, Timeline, Routines and Archive, with Settings on the right. + The EVENTCLOCK logo returns to the default view. Each page is in the address bar (`#checklist`, + `#settings/games/genshin`, …), and changing tab starts at the top of the page. +- **Width.** The page fills up to 1920px (Relaxed 1840px) and centres beyond that. +- **Two insets on `.reader-ui`.** `--edge` (16px) between content and the page's outer edges. + `--gutter` (24px) where a side column (Checklist rail, Settings nav, Archive sidebar) meets the main + content. At ≤700px, where everything stacks, both are the edge. +- **Checklist rail.** 360px wide (Relaxed 380px; 280/300px at medium widths). Top to bottom: the + focus pills, Next to expire (headline, meter and the next three), Today's dailies, then pinned + routines grouped by game. + +### Urgency and colour + +- **Two axes, never crossed.** Game hue answers whose event: the 3px strip on the left of each + Standard row, the Compact game cell, the pills. The urgency ramp answers how long: countdowns and + meter ticks. +- **The urgency ramp is live's.** Grey for a week or more, blue under a week, yellow under 3 days, red + under a day: `--color-urgency-{calm,near,soon,critical}`, set by `urgencyTokens`. It is separate + from the accent, so a chosen accent never changes urgency. +- **Reader-editable.** Settings → Appearance → Urgency colours sets each step per theme + (`prefs.urgencyPalette`), with any scheme. Values are nudged only as far as 3:1 contrast, the + large-text bar. +- **Fallback for the live UI.** `URGENCY_COLOR` falls back to the old `--color-*` tokens, so the + live UI is unchanged. +- **Schemes.** Slate, Warm, Plum and Custom shift surfaces. Custom stores ground/surface/text/accent + per theme. Text and accent derive at least 4.5:1 contrast on ground and surface. Game colours are + set per game in Your games. + +Default Slate tokens: + +| Role | Structured dark | Structured light | Relaxed dark | Relaxed light | +| --- | --- | --- | --- | --- | +| Ground | #0e1117 | #eef0f5 | #1a1c24 | #f3f1ec | +| Surface | #141923 | #ffffff | #22252f | #fffdf8 | +| Primary text | #eeeae3 | #28272d | #eeeae3 | #28272d | +| Accent | #8eb3f4 | #315fa5 | #8eb3f4 | #315fa5 | + +### Checklist + +- **Heading.** "Running now" sits with the sort and density buttons on one row. Under them is one + line: the count on the left and live's legend on the right ("bars = time left · colour = ends in a + week+ / under a week / under 3 days / under a day"). +- **Standard rows.** Game, title, optional description, then the countdown, date and meter, with the + chevron 10px from the date. Every row has a meter, with or without a description; the mockup hid + it on rows without one, and the owner chose consistency. +- **Compact rows.** A ledger with Game / Event / Time left / Ends in columns. +- **"Not started yet."** It opens a second section: a 48px gap, a 2px accent rule and a raised band + with a "Coming up" eyebrow. In Compact its columns read Duration / Starts in. + +### Focus pills + +They have live's chip feel: 30px tall, 12px text, a 30% hue border unpressed, the full hue with a 14% +wash when pressed. State changes ease over 130ms. Hover adds a hue ring and a 1px lift, a press +scales to 0.96, and games with nothing outstanding dim to 50%. The "Focus" label is a 12px body-face +legend on the same baseline as "Next: …". + +### Ticking things off + +- **Today's dailies.** The whole row is the target. A tick washes the row in its game's hue, pops the + box and draws the check. A pip bar and "N of M done" sit under the heading. A check-in goal (for + example "5/5 wk") counts as done once met, with a dotted check, and shows its goal count instead of + a streak. +- **Pinned routines** use the same card language. A box ticks a check-in routine, or completes an + amount routine in one click. "+1" steps an amount, and the count opens an editor. Typing a value at + or over the target completes it after a 450ms pause. +- **Completion animation.** The bar fills, the row warms, then the box pops. Unticking undoes it: + the amount from before goes back, or the editor opens when that value isn't known. +- **Celebration.** A single completion fires live's small `Fireworks` in the row. Finishing a whole + section (every daily, or every routine in a game's group) fires `Fireworks grand`: seven bursts in + two waves with the game's hue mixed in, a light sweep, and the "All done" line popping. +- **When it fires.** Only an action taken now that completes the item or section, never arriving + complete. Reduced motion removes all of it. + +### Other pages + +- **Routines.** Bands by reset (Dailies, Weeklies, Monthlies, Cycles) or by game. The table has tick + and click-to-edit amount cells. The library and create/edit drawers are shelves. +- **Timeline.** Two looks (new and classic) chosen in Appearance. Dailies are not on the timeline. +- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance + (interface, theme, scheme, urgency colours, default view, game order, timeline design, focus bar, + checklist group), Your own events and games, Backup (tiers plus a manual "Your backups" list), and + per-game Setup / Codes / About. +- **Returning-user tour.** Routines → Checklist → Timeline → Redeem codes → Archive → Settings. + Continue stays in a fixed place, Back sits beside it, and Skip is apart. "Replay the introduction" + starts it at step 1. diff --git a/docs/INGESTION.md b/docs/INGESTION.md index 1bb85c6..cae93a6 100644 --- a/docs/INGESTION.md +++ b/docs/INGESTION.md @@ -640,3 +640,22 @@ fixture is the regression test proving the parser still handles the previous for guessing, impossible calendar dates are rejected, ranges crossing New Year roll the start year back, and abbreviated months parse. `test/merge.test.ts` covers cross-source agreement, disagreement, and rerun disambiguation. These are the last line of defense before a wrong date reaches a user. + + +## Redeem codes source (2026-10-16) + +Genshin codes use https://game8.co/games/Genshin-Impact/archives/304759, separately from events. +One descriptive-agent robots request and one page request returned HTTP 200 during capture; no +access workaround or repeated source fetch was used. The raw page and capture metadata are pinned +as fixtures/genshin/game8-codes-2026-10-16.*, with ten independently counted current codes. The +parser reads actual code inputs/current reward rows and excludes expired programme sections; a +future-dated programme fixture variant verifies that still-current programmes are admitted. +Unannounced expiry remains null. A printed calendar date is expiresOn, never a fabricated exact +expiresAt instant. Attribution is retained on every code. + +src/ingest/codes/registry.ts registers a count-only refresh source. The existing runner shares robots, +six-hour floors, conditional requests, per-host spacing and snapshot gates across event/code sources. +Event adapter schemas are unchanged. scripts/build-codes.ts builds data/codes.v1.json offline from +snapshots or fixtures and preserves observation timestamps in snapshots/codes-first-seen.json. +Code newness is per game; redemption is per local game/server account and never clears newness by +itself. No code row is synthesized into the event feed. diff --git a/docs/PRD.md b/docs/PRD.md index 4f049c7..2e846fb 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -111,14 +111,9 @@ few pixels apart the board is being asked the one question it exists to answer a reader to squint. It also gives a one-day event a bar to read and to tap rather than the 34px minimum every short bar collapses to. -**What has not started yet is shown to a new reader, and either can switch it** — `prefs.showUpcoming`, -which governs **both views**: the board plots them, and the checklist keeps its "Not started yet" -section (F2). One question, asked once, because they are the same events. It was off by default, -because this app answers *what expires next* and a reader with fourteen lanes has a next patch queued -behind every one of them. Since 2026-10-13 (repository owner) first-run setup turns it **on**: a new -reader should see what is coming before deciding they do not want it. The stored default is still -`false`, so an existing install whose saved prefs predate the field keeps today's answer rather than -having it switched on underneath its reader. +**Upcoming events are optional and off initially.** New setup asks “Show events that haven't +started” under “What do you want to track?”. The same saved answer governs Checklist and Timeline; +Checklist separates them under “Upcoming events”. Existing saved choices survive migration. On the board that is structural as well as editorial: the window is drawn from what is plotted, so showing the future stretches the right edge weeks past today and squeezes the running bars the @@ -129,10 +124,8 @@ quiet fortnight, and this app does not leave a reader to infer what it is not sh page header states `N live · N upcoming` whatever the setting says, and either view left with nothing to show says so in words and names the setting rather than rendering an empty column. -The switch is in settings (F4), with the two other answers to *what am I allowed to look at* — -`showCompleted` and `showIgnored` — and **not** in the board's own header beside the stacking and -scale controls. That is the line those two draw: they reshape what is already on the board, which is -why they are reached for while reading it, and this one decides what is on it at all. +The switch lives in Settings → Lists, because it decides which events enter both main views. +Done and ignored events live in Archive; Lists has no redundant Archive link or reveal switches. **Switched on, the board says in words where they begin.** A bar drawn to the right of the "now" rule is only *implicitly* in the future, and implicitly is not a standard this product holds itself @@ -198,42 +191,17 @@ an explicit "show all N". This truncates the *view* only: the order is untouched the cut are still counted in the header, still on the timeline, and one tap away. **F3 — Mark completed.** -A toggle on every event, in both views. State is written to `localStorage` immediately and -optimistically — there is no server round trip and no failure case. Completed events stay visible -but de-emphasized; a filter toggles them out entirely. +Done writes durable progress and a metadata snapshot immediately. A confirmation remains for about +1.5 seconds before the row fades/collapses into Archive. Undo is available for eight seconds; +Archive Restore remains available afterwards. A failed write is disclosed and never presented as +saved. Reduced motion removes the fade. -**F4 — Filters.** -Filter by game (multi-select, persisted) and by event type. Hiding a game hides it from both views. -Preferences persist in `localStorage`. - -The panel holding them is **six collapsed groups, each stating its own answer on the summary line** -— games, server region, appearance, what you see, your own games and events, your progress. It was -one open block of everything, laid out in two columns on a wide screen, which read fine at four games -and stopped working at eighteen: the game list alone is eighteen rows of four controls, and it sat -above the checkbox a reader had come down here to tick. Collapsing on its own would only trade that -friction for another, though — a group that shows nothing but its name turns *what is my region set -to?* into a click — so each summary answers its group's question, and the closed panel is a six-line -report of how the app is configured. Opening one is for changing an answer, not for reading it. - -Three things follow. **The groups ship closed**, including the one holding the filters that other -copy points at: the empty states name both the switch and the group it is in, which is more findable -than a checkbox in a wall was. **The summary states are derived, never stored** — they are a reading -of `prefs`, so they cannot disagree with the controls inside. And **a group answers one question**. -The region and the theme began as one group called *Reading*, on the argument that both are "how do I -read this?" — which summarised as `Europe · Dark`, two unrelated answers joined by a dot, under a -name for neither. The region is not a reading preference: it is which server the reader's account is -on, and region-scoped ends, undated deadlines and every daily streak are read off that server's -clock, so it is the one control in this panel that can make a countdown wrong. It gets its own line, -and says as much when opened. - -**The panel uses the page's width**, like every other block on it. The rows were held to a narrower -measure on the argument that a name and its state stop reading as one line across a wide screen, but -the panel's own rule spans the shell, so the rules inside it stopped a third of the column short and -the panel became the one thing on a desktop page not using the width — sitting between a two-column -checklist and a three-column footer that both do. A title at one end of a full-width row and its -countdown at the other is what "running now" does directly above it. Prose inside a group keeps a -readable measure, and the game list takes two columns of its own on a wide screen once there are nine -or more games, because eighteen rows in one column is a thousand pixels of ribbon down the left. +**F4 — Settings and visibility.** +Settings has named pages: General, Your games, Appearance, Lists, Region & time, Data & sources, +Backup, About Event Clock, and Changelog. Your games controls visibility/order, tracking choices, +servers, reset overrides and colours; Lists controls upcoming events and timeline grouping. +Routines owns follows and independent Checklist/Timeline pins. Utility navigation does not replace +the saved default main view. Empty states name where to change the relevant choice. **F4a — Focus one game at a time.** Switching games on and off says *which games the reader plays*, and is set once. It is the wrong @@ -253,15 +221,26 @@ has left the feed is ignored rather than obeyed, so it can never strand the read whose cause is elsewhere. Each chip carries that game's outstanding count, so a game with nothing waiting says so before it is visited. -**F5 — Region selection.** -A user picks Asia / America / Europe once. For events where `regionScoped` is true, all displayed -end times resolve to that region's server reset. This is stored in `localStorage` and defaults to a -guess from the browser timezone, shown as a dismissible "showing America server times — change". +**F5 — Servers and display timezone.** +Default server is chosen in setup and Region & time. Each locally tracked game/server has its own +stable account identity and progress. “Add another server” is for simultaneously playing multiple +servers: it starts fresh tracking choices and never clones marks. Server/reset changes append clock +revisions so history stays on its original clock. Overrides can revert to Automatic. Display timezone +is Automatic by default or a searchable IANA zone; it changes printed dates and manual date entry, +never a source's exact timestamps or game-day keys. -**F6 — Export / import.** -Because there are no accounts, moving between devices is manual: download a JSON file of completed -IDs and preferences, upload it elsewhere. Import merges rather than replaces, and never removes a -completion the user already has. +**F6 — Export / import and backup.** +No login is involved. Export progress / Import progress merges per game/server, retaining local +preferences and active reset configuration. Day logs union; newer records/amounts win conflicts. +Imported clock revisions remain accessible as historical clocks. Unknown extensions are retained in +the envelope. Export all / Import all restores the complete validated envelope through a preview, +with a recovery copy before replacement. Undo last restore reverses it. Version-1 exports remain +convertible; original v1 browser keys are retained after migration. + +Local backup history keeps seven daily, four weekly and six monthly snapshots while the app is +used. UTC buckets keep their latest snapshot; backups never contain other backups. View backup +opens the preview; Restore this backup is the separate committing action. Storage failures are +visible. Browser deletion removes local history; export is the way to keep an external copy. **F12 — Record your own progress and effort.** Three states, not two: untouched, doing it, done. Plus an optional effort estimate — quick, short, @@ -418,15 +397,16 @@ the one after it — because they answer "what ends soonest". The timeline draws every occurrence in view, because it answers "what is the rhythm". **F8 — First-run setup.** -Before any events are shown, the reader picks which games they play, and how they want to read -them. A calendar full of games they -don't play is worse than an empty one — it buries the thing they came for. Nothing is preselected -and the button stays disabled until something is chosen; guessing on their behalf and hoping they -notice is worse than asking. **The games are listed alphabetically by name**, not in feed order: -everywhere else a lane's position carries meaning, but here the reader is scanning for the two or -three names they already know, and a list ordered by which game happened to hold the first event row -gives them nothing to scan by. The choice is stored as *hidden* games, the inverse — which is a storage -shape, not a policy: what happens to a game added later is decided separately, below. +Before events are shown, a four-step wizard asks for games, default server and preferred appearance, +tracking kinds, and the default main view. Games are alphabetical, nothing selected. Events are the +only initially selected tracking kind. Dailies, weekly/monthly and cycles have short descriptions; +unavailable kinds remain selectable and say nothing is currently detected. Upcoming visibility +belongs to that tracking step and starts off. Server and appearance can be changed later. + +Returning readers get a one-time introduction and optional four-step tour. Skipping or finishing +preserves progress and the saved main view; General can replay it. + + **A game added later arrives switched off.** Adding a source is our decision, not the reader's, and someone who plays two games did not ask for the other twelve; a calendar that fills itself up is the @@ -447,10 +427,13 @@ The view question (F2) sits under it, with each option drawn rather than only de it arrives already answered, and the screen says where to change it afterwards: the tabs are small text in a corner, which is the one control a first-time reader will not find on their own. -**F9 — Ignore an event.** -Distinct from completing one. "Done" keeps an event visible and counted; "not interested" removes it -from both views entirely. Ignored events stay recoverable: a count and a reveal toggle appear in -settings once there is something to reveal. +**F9 — Ignore and Archive.** +Ignore saves immediately and follows the same confirmation/Undo transition as Done. Both move to +Archive with event metadata, notes, goals, logs, resolved end and clock revision. Previously seen +followed events that expire unfinished also enter Archive; unknown-end events never expire by a +guessed date. Per-game history is default, with an alternate chronological history. Story statistics +include completion, check-ins and shortfalls where known; ignored events are excluded from success +rates. Restore recovers saved progress without pretending an expired event is live. **F10 — Works offline.** The reader's question is answered entirely by data already on the device, and countdowns run off the @@ -459,9 +442,9 @@ the last feed it downloaded. Offline is disclosed in the header and above the fo data must never be presented as current. **F11 — Credit and disclaimer.** -The sources that compile these calendars, and the studios that make the games, are named on the same -screen as the data rather than one navigation step away. The page states plainly that it is -unofficial and unaffiliated, and that the source page is the authority when the two disagree. +Settings → About Event Clock preserves the full colophon, source credits, unofficial/unaffiliated +disclaimer and feedback links. The footer links About. Each game's About lists the real data sources, +links and thanks; event detail links its authoritative source. **F14 — Say when a new version of the app is ready.** F10 caches the shell so the app survives losing signal, and the same cache is why a reader who never @@ -483,42 +466,29 @@ arrive without a reload, and calling that a new version would train readers to d unread. **F7 — Freshness disclosure.** -The footer shows when the feed was last updated, per game. If a game's data is more than 48 hours -stale, its lane carries a warning badge. Never present stale data as current — the whole value -proposition is trust in the dates. +The footer always states data age, never build age. Only tracked games with stale data produce a +compact “Event data may be out of date” row in the shared updates area. New events and new codes +remain distinct notices. Data & sources and event detail expose the last successful check, last +content change, source links and recorded reasons. A successful 304 confirms a check without +claiming the wiki changed. -**The reader can ask for the newest data.** A reload is not enough — the browser may keep the feed -for minutes — so *Check for new data* beside the age asks the server past that copy and swaps a newer -feed in without a reload. It says what happened: updated, nothing newer published yet (sources are -read about twice a day, and the button cannot make that sooner), or the server could not be reached. -"Newer" is judged on events and source health, never on the build stamp. Disabled offline; nothing -polls on its own. +Check for new data revalidates event and code feeds, compares their content rather than generatedAt, +and reports updated, unchanged or failure. It does not scrape source sites from the browser. -**The staleness notice names only the games the reader has switched on, and says that it does.** -The headline age is about the feed and stays whole; the notice under it is an *instruction*, and the -only remedy it offers is "go and check that game's source page" — which a reader cannot act on for a -game they turned off and cannot see a single row of. With nineteen lanes and two switched on, naming -eleven they do not play buries the one they do, and a warning about games nobody reads is a warning -nobody reads (F8: a game we add is a game they never asked for, and arrives switched off, so an -untouched install would be warned about the whole catalogue). +**F15 — Appearance.** +Relaxed (Round 7) and Structured (Round 6 B rail) share one component/state tree. Structured is the default (owner decision, 2026-10-17); an explicitly saved Relaxed preference is preserved. +Interface choice, Standard/Compact density, Dark/Light/System theme and Slate/Warm/Plum/Custom +scheme are independent. Custom stores ground/surface/text/accent separately for light and dark; +rendered colours derive readable contrast without overwriting the saved choices. Per-game colours +have a picker, hex field, preview and reset. Checklist dailies can occupy a separate direct-tick panel +or grouped routines, with collapsed/expanded/always-expanded policies; these controls also live in +Routines. Numeric routines use native step-one inputs. -Both halves of that are load-bearing. Scoping it silently would be the worse half: "nothing has -refreshed" is a claim about the whole calendar, so a reader with fourteen of eighteen lanes off would -read a sentence about four as one about all of them. The notice therefore states whose games it -counted, in the same words the empty list uses — *the games you have switched on*. Credit in the -column beside it is owed to every source we read and is not scoped; nor is the "last refreshed" line, -which reports the feed and feeds the bug form. - -**The staleness notice differentiates crawler checks from site updates.** If our crawler successfully -checked the wiki recently (e.g. confirming HTTP 304) but the site's content has not changed in over -two days, the notice explicitly distinguishes the two: "data pulled X ago, site updated Y ago", with -an expandable breakdown detailing each source's check and content update times. - -**F15 — Light mode, with dark still the default.** +The earlier theme rationale below still applies to both interfaces. The app is a lit instrument panel and that is what it should be on first sight, but it is also read on a train in daylight and by people who find a dark UI harder rather than moodier. A public fork worked a theme toggle out first and is credited for the idea in the colophon. So the ground is -the reader's choice: **Dark**, **Light**, or **System** to follow the device, in settings next to the +the reader's choice: **Dark**, **Light**, or **System** to follow the device, in Settings → Appearance, separately from the region — both are "how do I read this?" and neither changes what the page knows. Dark is what it ships as, and deliberately not `System`. A reader whose laptop is in light mode has @@ -571,7 +541,15 @@ placed. It also arrives switched off (F8), so settings is where they meet it, wh would move it anyway. **A game's dailies stay together** — a repeating event the reader marked sits with the standing chore of the game it came from, because the reader thinks in games and not in kinds. -**F17 — Weekly and monthly tasks.** +**F17 — Routines.** +Routines is the library and management page for daily, weekly, monthly and cycle tasks. Follow each +per server; pin independently to Checklist and Timeline. Built-ins offer Make it mine, copying only +that account's logs; custom routines remain editable. Catch up & recorded history exposes logs on +their original reset revisions. A separate Timeline reset strip sits directly after its game's events; +with strips off, Ending soonest remains a single cross-game deadline queue. + +The cadence and goal semantics below remain the domain contract; the redesign moves their controls +from the former Your own section to Routines. Some chores roll weekly or monthly rather than daily — earning ZZZ's weekly activity points, finishing Endfield's weekly routine — and no source publishes them, so they were nowhere in the app. A reader can now set them up per game in *Your own*: a title, every week (from a weekday they pick), @@ -624,7 +602,7 @@ instant on every server rolls on that server's reset for every reader. The app now says what changed, in two tiers, both tied to git. **Short entries** — one line each, tagged New, Fixed, or Games & sources — appear once as *What's new*, above the lists, after a new version loads; *Got it* dismisses them. **Release notes** — a few paragraphs per feature or -milestone, summarised from the history and the design specs — are in the footer's *Changelog*, each +milestone, summarised from the history and the design specs — are in Settings → *Changelog*, each linked to the commits it covers and the specs it draws on. Data refreshes are not changes in this sense; the footer already states the data's age. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index f5e8569..f3dbef9 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -71,7 +71,7 @@ away for more coverage (see Product Principles). ## Capabilities and Constraints -**Capabilities.** The full feature set is specified as F1–F16 in `docs/PRD.md`: +**Capabilities.** The full feature set is specified as F1–F18 in `docs/PRD.md`: - A timeline: one lane per game, or a single queue sorted by soonest end. - A checklist of everything running, sorted by soonest end. @@ -79,16 +79,17 @@ away for more coverage (see Product Principles). - Daily checklists, with past days that can still be filled in. - Ignoring an event, and filters by game or type. - Focusing on one game at a time. -- Region selection. -- JSON export and import. +- Separate game/server tracking, reset corrections and Automatic or named display timezone. +- JSON export/import, recoverable restore and 7 daily / 4 weekly / 6 monthly local backups. - The reader's own games and events, one-off or repeating. -- First-run setup. +- Events-first new setup and a one-time returning-reader introduction. - Offline use. - A notice when a new version of the app is ready. - Dark or light mode, plus a "follow the system" option. -- The reader's own game order. +- The reader's own game order, per-game colours, Relaxed/Structured interfaces and custom schemes. +- A Routines library with independent view pins, an Archive with story statistics, and sourced redeem codes. -The app currently covers twenty games from twenty-two sources. +The app covers twenty games; source registries are the authoritative current count. **Hard constraints** (`AGENTS.md` § Three constraints): @@ -112,7 +113,7 @@ The app currently covers twenty games from twenty-two sources. **Terminology:** - The user is a **reader**. -- The two views are **Checklist** (stored id `"soon"`) and **Timeline**. +- The main views are **Checklist** (stored id `"soon"`) and **Timeline**; Routines, Archive and Settings are utility pages. - An event's states are **untouched**, **doing it** and **done**. - **Ignore** is not the same as done. - A **lane** is one game's row or source. diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 8d5e3a0..808c5ed 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -976,3 +976,22 @@ Facts stated above that a future reader may want to re-check, with how they were | `marisaimpact.com` states no year on any calendar | stripped tags from `/calendar89` and `/calendar90` and grepped the text for `20\d\d` — zero matches on either | | Its calendar URL is per-version, with no stable alias | `/calendar90` is `200` and covers `Aug 20 - Oct 22`; `/valk/calendar90` — the scheme its own `og:url` uses — is `404`; the nav links only `/calendar88` and `/calendar89` | | Its schedule is not behind an API | `GET /calendar90/__data.json` returns 144 bytes: an `ogImage` URL and nothing else | + + +## Redeem codes source (2026-10-16) + +Genshin codes use https://game8.co/games/Genshin-Impact/archives/304759, separately from events. +One descriptive-agent robots request and one page request returned HTTP 200 during capture; no +access workaround or repeated source fetch was used. The raw page and capture metadata are pinned +as fixtures/genshin/game8-codes-2026-10-16.*, with ten independently counted current codes. The +parser reads actual code inputs/current reward rows and excludes expired programme sections; a +future-dated programme fixture variant verifies that still-current programmes are admitted. +Unannounced expiry remains null. A printed calendar date is expiresOn, never a fabricated exact +expiresAt instant. Attribution is retained on every code. + +src/ingest/codes/registry.ts registers a count-only refresh source. The existing runner shares robots, +six-hour floors, conditional requests, per-host spacing and snapshot gates across event/code sources. +Event adapter schemas are unchanged. scripts/build-codes.ts builds data/codes.v1.json offline from +snapshots or fixtures and preserves observation timestamps in snapshots/codes-first-seen.json. +Code newness is per game; redemption is per local game/server account and never clears newness by +itself. No code row is synthesized into the event feed. diff --git a/docs/design-reviews/2026-10-16-redesign-implementation.md b/docs/design-reviews/2026-10-16-redesign-implementation.md new file mode 100644 index 0000000..5455d3d --- /dev/null +++ b/docs/design-reviews/2026-10-16-redesign-implementation.md @@ -0,0 +1,125 @@ +# Approved redesign — implementation review, 2026-10-16 + +**Status: rejected by the owner for substantial divergence from the approved Round 7 markup.** +The completion claims below describe the earlier implementation report and are withdrawn as +evidence of design acceptance. Functional tests and overflow checks did not establish visual or +interaction fidelity. A faithful markup-to-component correction is still required. + +The approved [spec](../superpowers/specs/2026-10-16-ui-redesign-design.md) and +[16-task plan](../superpowers/plans/2026-10-16-ui-redesign.md) are implemented in the working tree. +Execution was Native. Nothing has been committed, pushed or published. + +## Delivered + +Relaxed (Round 7, default) and Structured (Round 6 B rail) share one reader/component tree; Appearance +switches between them independently of theme, palette and Standard/Compact density. The redesign +includes Routines and view pins, direct daily ticks, configurable grouping, multi-server progress, +reset override/revert with accessible clock history, game colours/source credits, custom palettes, +Automatic/IANA timezone, new and returning onboarding, compact source/new-event/code notices, +Archive/story statistics, timed Done/Ignore/Undo, local rolling backups and recoverable restore. +Settings About preserves the colophon; Changelog has its own page and no footer link. + +The codes feed contains ten real Genshin codes from the pinned Game8 capture. Source fetching remains +deterministic and shares the existing conduct gates; no additional community requests were needed +while implementing or reviewing. Unstated expiry stays unknown. Codes newness and per-server +redemption are separate. Manual refresh checks both feeds and discloses partial code-feed failure. + +## Verification + +- **1,587 tests pass**, zero failures, across 74 files. Strict TypeScript and whitespace checks pass. +- The suite also runs without `public/` or `docs/` in the Docker COPY layout: 1,586 pass, one + intentional README-only skip, zero failures. No tests depend on a preceding build. +- Full offline build passes: 209 events across 20 games, ten codes, production JS/CSS/static shell. + Final shell build `f5b25675010f` was loaded on a fresh origin (port 41880). +- Chrome checks cover both interfaces, both themes, 1440/390 widths, Standard/Compact, reduced motion + and an effective 200% CSS viewport. No horizontal page overflow or page errors in that matrix. +- Browser journeys pass for migration, saved preferred Timeline, events-only setup, selectable empty + tracking kinds, upcoming placement, isolation from unselected games, extra server with fresh state, + direct ticks, reset override/revert/history, per-server redemption, Done/Ignore/Undo, unknown-end + custom creation, real export download/upload/restore, backup preview/restore/Undo, custom palette, + timezone selection/search, offline reload and the real service-worker update/reload handshake. +- Two real browser tabs save concurrently with Web Locks explicitly disabled: IndexedDB serialization + preserves both edits. Unit regressions cover stale progress fields and independent routine pins. +- A fresh independent reviewer completed whole-change review and focused re-reviews. All important + findings were fixed with reproductions; final review reports no remaining important findings. + +## Review fixes and durable decisions + +Deleted custom definitions no longer reappear from seen snapshots. Progress imports preserve local +active reset configuration, retain imported clocks as history, and reuse those revisions on repeated +imports. Opaque extensions survive subsequent transfers without duplicate accumulation. Timeline +focus applies to routines as well as events; Ending soonest uses one global queue when reset strips +are off. The browser fallback uses an IndexedDB transaction rather than unlocked whole-envelope +writes. Account rebase preserves independent routine/pin edits and explicit retirement removal. + +Original v1 bytes remain available after migration. Restore validates account/clock references and +saves recovery before replacement. Setup completes before seen-history capture. Source diagnostics +state what the feed records; absent failure reasons are described as unavailable, never guessed. + +## Practical limits + +Backups are browser-local and created while the app is used; they cannot recover storage a reader +clears. Older completion IDs whose metadata was never retained remain recoverable raw data, but +cannot produce invented archive descriptions or historical statistics. Codes currently have one +registered game/source. External fonts have system fallbacks offline. Calendar-date code expiry is +shown as a date without an invented instant. No accounts, remote sync or push notifications added. + +## Captures inspected + +[Relaxed, light, desktop](assets/2026-10-16-redesign/relaxed-light-1440.png) · +[Structured, dark, desktop](assets/2026-10-16-redesign/structured-dark-1440.png) · +[Structured, dark, phone](assets/2026-10-16-redesign/structured-dark-390.png) + +Additional transient browser logs and captures are in `/tmp/redesign-browser-*` and +`/tmp/redesign-final-*`; the facts above and representative images are recorded here for handoff. + + +## Follow-up: first-visit HTTP/WireGuard startup failure + +The owner reported the recovery screen on first visit through WireGuard `10.20.20.2`. The precise +failure reproduced in Chrome at an ordinary HTTP hostname: `crypto.randomUUID` was undefined, +initial account minting threw, and initialization returned the generic unavailable/saved-data error. +Localhost-only verification had missed this because localhost is a browser secure context. + +All reader ID consumers now use 128-bit `crypto.getRandomValues` through `state/reader/ids.ts`. +Three new regressions failed before the fix and pass afterwards. Browser verification on an actual +insecure HTTP origin passes both existing-save migration and fresh setup, ordinary edits, adding a +server, reset override/revert, backups and reload; the original v1 bytes remain unchanged. Focused +independent review found no important issues. The full suite now passes **1,590 tests** across 75 +files. Typecheck, production rebuild and whitespace checks pass. Rebuilt shell: `9752bd6bf1d2`. + +The existing preview serves the rebuilt files; hard-refreshing bypasses the browser's cached JS. +No storage deletion or identity remapping was performed. + + +## Owner rejection: markup fidelity + +Direct comparison confirms substantial divergence despite matching outer gutters/panels/fonts: + +- Round 7's event-entry structure separates event content, a large deadline with supporting text, + and chevron. Production reused EventRow's prior internal structure and decorated it with CSS. +- Round 7's Routines uses game bands and aligned columns for progress, cycle/reset, pins and menus. + Production renders a flattened library with checkbox options and no equivalent table structure. +- Round 7's settings sidebar includes individual game entries with descriptions, leading into their + profiles. Production substitutes a generic Your games page and game selector. +- Round 7's Timeline places routine periods/progress/reset markers on the shared time axis. + Production appends ordinary routine controls below event timelines and uses textual reset strips. +- Round 7's Archive story includes interactive per-game outcome breakdowns. Production substitutes + per-game text totals. +- The approved header has navigation counts and a dedicated Settings placement. Production uses + the simpler generic navigation and a different header summary. + +References: design/mockups/round7/converged.html (final CSS from line 1003 onward and final renderer +overrides); production ReaderApp.tsx, EventRow.tsx, TimelinePage.tsx, RoutinesPage.tsx, +SettingsLayout.tsx, GameSettings.tsx, ArchiveStory.tsx. Earlier render functions/CSS in the mockup +are superseded by later overrides and must not be used in isolation. + +Root delivery error: the implementation preserved/reused existing presentation components and +added selected reference styling instead of faithfully translating the approved rendered markup. +Verification checked functionality and overflow without comparing equivalent reference/production +screens and interactions. The final review concentrated on data preservation and did not close that +design-fidelity gap. The owner should not have had to identify it after a completion claim. + +No implementation edits or data resets were made during this rejection audit. Existing storage and +HTTP fixes remain in place. Required correction: use the approved rendered markup, hierarchy and +interactions as the presentation contract, then connect real reader data and compare matching states. diff --git a/docs/design-reviews/2026-10-16-redesign-round4.md b/docs/design-reviews/2026-10-16-redesign-round4.md new file mode 100644 index 0000000..17db7fc --- /dev/null +++ b/docs/design-reviews/2026-10-16-redesign-round4.md @@ -0,0 +1,167 @@ +# Routine redesign: Round 4 review + +This records the owner's decisions from three mockup rounds and the subsequent review on +2026-10-16. These are proposed product changes, **not shipped behaviour**. The implementation spec +and migration plan come after review of this combined prototype. + +## Review artifact + +Open [the standalone Round 4 prototype](../../design/mockups/round4/converged.html) in a browser. +The review copy is also placed in `~/gacha-mockups/round4/converged.html`, linked from the existing +Samba index. Earlier rounds remain intact there. + +The prototype extends the Round 3 archive variant, keeping its ruled timetable visual language and +routine library. It is deliberately separate from the production app. Its storage namespace is +`event-clock:mockup:round4:v1`, never a production `gacha-tracker` key. Reset demo restores sample +state; reloading otherwise keeps density, choices and progress where browser storage is available. + +## Decisions to preserve + +- Checklist and Timeline retain their names. Routines and Archive have their own tabs, as does + Settings. Settings uses a sidebar with General first. +- Routines owns follows, individual editing and independent Checklist/Timeline pins. Built-ins do + not appear under Your own events and games. The library puts Create your own routine first. +- Checklist keeps compact per-game routine summaries in its rail. Focusing a game expands its + routines; All collapses them. Remove the unrelated Your own events callout from the rail. +- The main event list has two densities: Standard by default, with wrapping titles, breathing room + and supporting copy, and Compact using the dense table. Density is remembered and independent of + routine pins, filters, sort order and Timeline. Both densities open the same event detail. +- Game setup offers composable Events / Dailies / Weeklies & monthlies / Cycles shortcuts; individual + follows remain editable in Routines. Events is a visibility choice, rather than a routine. +- Each game can have multiple servers, with separate routine and daily progress. An additional + server starts with events only and fresh routine choices; it does not copy the first server's + follows, pins or ticks. +- Onboarding begins with events only. Explain each type in plain language, especially cycles: + routines following the game's own cadence, such as a mode resetting every fourteen days. Ask + whether Checklist or Timeline should open first. Defer pin placement until readers have seen the + views. +- Returning readers receive an introduction to the new layout in either main view, with existing + choices and progress preserved. Changelog remains accessible afterward. +- Codes and About live beside Setup in the game's settings area. In-app notices appear in both + main views. New means discovered since the reader last viewed that game's codes, not unredeemed. + Viewing clears the new indicator without marking redemption. Redemption is separate per server. +- Archive defaults to per-game groups, with the ledger's story panel and small statistics above + them. History timeline is an additional view. Records remain in the save, with description, + source and progress available after the live source changes. +- Done and Ignore immediately save an archive record, then retain the active row for about 1.5 + seconds with Completed/Ignored · Moved to Archive before fading and closing the gap. Undo remains + available afterward; Restore also lives in Archive. Reduced motion keeps the confirmation delay + and removes the fade. Unknown end dates stay unknown in archived records. +- Expired unfinished events belong in Archive as unfinished, rather than silently disappearing. +- Timeline uses full rows for routines. It offers Ending soonest and By game. Separate reset strips + are optional and sit directly below their own game's events on the same axis. In separate-strip + mode, Ending soonest orders game groups by their next deadline to preserve this adjacency. + +## What to review + +1. Compare the same 21 events in Standard and Compact; focus Genshin and inspect its routine rail. +2. Complete or ignore an event, observe confirmation/fade, Undo, then open its saved Archive detail + and Restore it. Try an event with no announced end. +3. Follow/unfollow a routine from the library and pin it independently to each view. +4. Add a server in game settings and confirm the other server's choices and ticks stay separate. +5. Open Codes, mark one redeemed on a single server, and simulate a new arrival from the demo bar. +6. Compare the per-game Archive with History timeline, and check both timeline reset layouts. +7. Walk through New user, then Returning user with Timeline as the default. + +## Owner's feedback after Round 4 + +The combined prototype was received positively. Further requirements: + +- Restore the richer statistics from Round 3 H's Ledger with a story. Round 4 reduced that panel + to completed/ignored/unfinished counts, which lost what the owner liked. Keep the per-game + archive as the default body, but restore the completion percentage for the current month with + an overall comparison, current/best completion run, logged event check-in days, and unfinished + events with near-miss detail. Retain the compact per-game outcome breakdown from that panel. + Ignored events must not be counted as failures in the completion percentage. Definitions and + scope labels must be explicit: recorded archive history is not all play history, event check-in + totals are not unique calendar days, and incomplete/unknown progress cannot support a fabricated + near-miss claim. These statistics should use saved progress and dates, not invented telemetry. + +- Keep the colophon, potentially in Settings, and add an app-level About section distinct from a + game's About tab. Preserve source/studio credits, author and contributor acknowledgements and + useful project links. Moving credits must not hide the feed-age and stale-source disclosures + that readers need while looking at deadlines. +- The grouped routine summaries are easier to scan but add friction to ticking daily chores. + Explore a separate daily panel with directly available one-click ticks, selectable as an + alternative to grouping dailies with the other routines. Keep server progress separate and + avoid showing the same tick in both panels. Placement and default are not yet settled. +- Appearance should support colour schemes in addition to dark/light/system. The choice between + curated palettes and reader-edited colours remains open. Any scheme must preserve game identity, + urgency meanings, and readable contrast in both modes. +- Each game's Settings → Setup includes a custom game-colour control, a preview and Reset to + default. Remember the override and apply it consistently wherever that game appears, including + Checklist, Timeline, Routines and Archive. It belongs to the game, shared across its servers, + separately from the global interface colour scheme. Use the existing readable-hue approach to + preserve legibility in dark and light appearances without changing the reader's saved base + colour. Game colour expresses identity; urgency colours retain their meaning. Export/import + must retain these overrides in the eventual implementation. +- Clarify the additional-server action as simultaneous account tracking. Use “Track another + server” with helper text: “Play on more than one server? Add your other server to track both + accounts separately.” Keep changing the existing server as a distinct control. The added-server + setup still starts with fresh routine choices and separate progress, as previously agreed. +- Standard event rows without descriptions currently feel too empty. Size rows to their actual + content: reserve no description line when absent, reduce vertical padding for title-only rows, + and keep the countdown/meter stack from imposing a description-sized minimum height. Preserve + wrapping titles and comfortable tap targets; Standard remains roomier than Compact without + forcing every event to the same tall height. Do not invent fallback description text to fill + the space. +- Region & time must let the reader select their display timezone. Automatic, using the browser's + timezone, is the default; a manual selection is remembered and can be returned to Automatic. + Display named IANA zones with their current UTC offsets, so daylight-saving changes follow the + selected zone rather than a fixed offset. This affects displayed dates/times, not the server + clocks that determine deadlines, reset boundaries and stored game-day keys. Reader-authored + event entry must explicitly identify the timezone used to interpret its input. +- In onboarding's What do you want to track step, all type choices stay selectable even when a + game has no current matching routines or windows. Explain the empty state beside the choice; + distinguish no current active window from no supported built-in routine. An empty result is not + a disabled control. Retain the reader's selected tracking intent rather than deriving it solely + from which routines happen to exist today. The implementation spec must reconcile this intent + with the earlier shortcut-only composition model and define how newly supported routines are + offered without silently expanding what existing readers track. +- Initial setup also asks for default game server, preferred appearance and whether to show + events that have not started. The selected server initializes game setup and individual games + can override it. Appearance includes Dark / Light / System and the agreed colour-scheme choices + once those are settled. Upcoming events remain off initially, consistent with the existing + default, until the reader selects otherwise. The previously agreed Checklist/Timeline default + question remains. Present these as a short preferences step rather than crowding the tracking + matrix with unrelated controls. + +## Prototype boundaries and implementation work still ahead + +- “Now” and reset clocks are fixed for review. The final app must use the existing per-game reset + rules and actual region-specific end times. +- Most event content was carried over from the earlier prototypes. Codes, historical archive + records, version groups and some descriptions are illustrative. No new source fetch runs here. + Live-list source links are matched by title against the existing local feed; unmatched sample + events receive no invented source URL. + Game8's supplied Genshin code page is a source-shape reference, not an implemented adapter. +- Only in-app code notices are proposed. Push, email and background notifications remain outside + the product scope. +- Some inherited controls remain placeholders: backup and portions of own-event/routine editing. +- The archive snapshot works within this prototype's save. Production needs an explicit snapshot + retention/export format and a policy for capturing events before they drop from the feed. +- Production needs a reviewed migration for existing progress into per-server storage; no existing + event ID, daily ID or day key may be changed casually. The archive and code state also need a + documented export/import contract. This artifact is not a storage implementation to copy. +- `docs/PRD.md`, `docs/DATA-MODEL.md`, `docs/ARCHITECTURE.md`, `docs/DESIGN.md` and the matching + AGENTS guidance must be updated with the eventual implementation. They describe the shipped app + today, so this review record does not rewrite them as though the redesign had landed. + +## Verification + +Browser verification and screenshots are performed with scratch-installed puppeteer-core and +Chrome, at 1440px and 390px. The prototype needs no Bun build or production feed changes. The test +journeys cover density parity, navigation, completion/ignore delay and Undo, archived detail and +Restore, separate code visibility/redemption, server setup, reset-strip adjacency, onboarding, +returning users, persistence, reduced motion and unknown end dates. + +Both width passes completed with no JavaScript errors and no page-level horizontal overflow in +the checked screens. The initial mobile pass caught a confirmation notice intercepting Archive +row taps; informational notice surfaces now let pointer events through while Undo remains a real +button. An early verification-script failure also tried to open a Star Rail event after onboarding +had selected only Genshin; the scenario now resets sample state before testing an unknown end. + +Impeccable's mechanical detector reports palette, typography and size differences against the +**shipped** DESIGN.md, plus inherited stripe treatments. The new Barlow timetable appearance is +intentional and selected in earlier rounds; this prototype does not rewrite the shipped design +reference or add detector suppressions. Its visual language remains subject to this review. diff --git a/docs/design-reviews/2026-10-16-redesign-round5.md b/docs/design-reviews/2026-10-16-redesign-round5.md new file mode 100644 index 0000000..43bc139 --- /dev/null +++ b/docs/design-reviews/2026-10-16-redesign-round5.md @@ -0,0 +1,112 @@ +# Routine redesign: Round 5 review + +This is a review prototype, not a production release or an approved implementation spec. It +combines the owner's feedback after Round 4, recorded in +[the preceding review](2026-10-16-redesign-round4.md), and the subsequent freshness, backup and +focus-bar requests. + +Open [Round 5](../../design/mockups/round5/converged.html), or its copy at +`~/gacha-mockups/round5/converged.html` through the shared index. Earlier rounds remain unchanged. + +## What changed + +- **One-click dailies:** an optional panel exposes daily ticks directly, separately per server. + The alternative groups dailies with the routine summaries. A daily appears in only one place; + pins still decide which dailies appear. The separate panel is the prototype's starting choice, + to test the owner's request for immediate access; this default remains reviewable. +- **Focus alternatives:** a labelled game toolbar is the starting choice. The review bar's Focus + selector also offers a compact game switcher and a picker at the top of the Checklist rail. + Each changes the same focus state and counts, and preserves auto-expanding focused routines. + Timeline retains a visible toolbar/switcher rather than acquiring a Checklist rail. +- **Content-sized Standard rows:** events lacking descriptions use less vertical padding and + omit the segmented meter. They retain a wrapping title, countdown and tap target. No filler + descriptions are added. +- **About and colophon:** Settings → About Event Clock retains the author, source communities, + studio credits, acknowledgements, reset-clock reference, disclaimer and project links. The + footer links there. Source credits come from the existing local feed, not the selected games. +- **Freshness:** Checklist and Timeline visibly state data age and warn about older schedules for + the games switched on. Source breakdown distinguishes last successful check from last content + change and explains unchanged pages, overdue pulls and never-fetched data. Unknown causes are + described as unreported, not invented. Settings → Data & sources exposes the same information. + The review bar offers Current / Some stale / All stale / Never fetched / Offline. These are + labelled illustrative states; no real fetch runs. Check for new data demonstrates a no-newer-feed + result and is disabled offline. The headline data timestamp covers the demo feed as a whole; + warnings and named source rows cover selected games. No build timestamp is used. +- **Appearance:** Dark / Light / System plus three curated scheme previews, Slate / Warm / Plum. + This answers the palette request for review, not the unresolved possibility of a fully editable + interface palette. Semantic deadline colours remain separate from game identity. +- **Game colour:** Setup adds a colour picker, hex input, preview and Reset to default. An override + applies to all the game's servers and all its views. The stored base colour remains unchanged + while its rendered colour is adjusted for contrast in the chosen appearance. +- **Timezone:** Automatic is the default; named zones can be selected and searched in Region & + time. Dates use the selected zone, including daylight saving, without changing server reset + boundaries. The prototype's per-game reset clocks remain simplified. +- **Additional servers:** “Track another server” explains simultaneous separate account tracking. + Added servers still start with fresh routine choices and progress. +- **Onboarding:** initial preferences now ask default server, mode/scheme, timezone and whether + upcoming events should appear. Game selection, tracking types and Checklist/Timeline default + follow. All tracking types remain selectable even if no matching built-in exists. Availability + is explained, and selected intent is saved separately from actual follows. The no-matching-types + message does not incorrectly call that selection events only. New supported built-ins are + described as offers for review rather than automatic extra work for existing readers. +- **Archive story:** restores monthly/overall completion percentages, current/best completion + run, logged event check-ins and unfinished/near-miss counts, plus per-game outcome bars, above + the default per-game archive. Ignored events are excluded from completion-rate denominators. + Progress and statistics derive from illustrative saved archive records, and scope is labelled. + History timeline remains an alternate view. +- **Automatic backup history:** Backup shows seven daily, four weekly and six monthly snapshots, + with latest-per-period compression. The retained historical examples are labelled sample; today's + snapshot updates from the demo's own state. Restore previews game/routine/archive/progress counts + and preserves current data as a recovery copy first. Undo last restore is available. Manual + Back up now and demo JSON export/import live alongside the automatic history. + +## Backup semantics used for this review + +The most recent seven days retain one latest snapshot per day. Older snapshots fold into four +consecutive seven-day windows. Older records then fold into six calendar-month buckets, keeping +the latest snapshot in each. This is up to seventeen retained snapshots, plus a recovery copy from +the last restore. No snapshot is invented for a day the reader never opened the app. + +The demo clock is fixed and its bucket calculations use UTC. Production needs to settle and +document the stable calendar basis independently of display-timezone changes, handle storage +capacity/failures, validate restore files, and use the real export contract. Backups are browser +local and are created while the app is used; they cannot be promised for days when it stays closed. +Export remains the way to keep a copy outside that browser or move to another device. + +## How to inspect + +1. Compare focus options in the review bar. Focus Genshin; tick EU and Asia dailies separately. +2. Compare Standard/Compact, especially rows without descriptions, and switch the daily layout. +3. Use the Data selector to inspect every freshness state, then expand Why the data is older. +4. Open About, Data & sources, Region & time and Appearance. Change timezone, scheme and game colour. +5. Open Archive to inspect richer stats and History timeline. +6. Open Backups, review a monthly sample, restore it, then Undo last restore. Demo export files are + separate from the production app's format and storage. +7. Open New user. Choose preferences and Genshin, then select Cycles despite no matching built-in. +8. Set Timeline as the default and use Returning user to check the introduction appears there. + +## Verification and boundaries + +Chrome journeys at 1440px and 390px exercised direct daily ticks, layout choices, focus alternatives, +freshness variants, credits, timezone persistence, light/dark/system and schemes, game-colour reset, +fresh server setup, Archive stats, backup restore/recovery, onboarding with an empty type, +returning-reader introduction and completion/Undo. Both passes had no JavaScript errors and no +page-level horizontal overflow in the checked screens. A follow-up checked latest-per-period +retention across 400 simulated days, all-games empty-type toggles, and desktop sticky toolbar/rail +separation. + +The mechanical design detector reports 30 findings, chiefly differences from the shipped palette, +Barlow typography and size/radius ramp, plus inherited archive stripe treatments. These are review +artifacts in the owner-selected replacement visual direction; no production design reference or +detector suppression was changed to make the prototype appear to match the shipped app. + +An inherited Round 4 Reset handler initially discarded the new state fields. Round 5 now owns +reset initialization and its browser check passes. The active prototype data namespace is +`event-clock:mockup:round5:v1`; production localStorage keys and progress are untouched. + +The existing demo library/create form, independent pins, completion confirmation/fade/Undo, +Archive restore and code-newness/redemption distinctions remain. Some inherited own-event editors +are still placeholders. Codes, old Archive history, backup history and source health are sample +content. Real parsers, source refreshes, app storage migrations and production backup machinery are +not implemented by this artifact. The shipped specification and design documents remain about the +shipped app until the redesign's spec and implementation are reviewed. diff --git a/docs/design-reviews/2026-10-16-redesign-round6.md b/docs/design-reviews/2026-10-16-redesign-round6.md new file mode 100644 index 0000000..08b41b8 --- /dev/null +++ b/docs/design-reviews/2026-10-16-redesign-round6.md @@ -0,0 +1,105 @@ +# Routine redesign: three quieter layouts + +Review artifacts, not a production release. This round responds to the owner's concern that +Round 5's focus toolbar and stale-data notice occupied too much space, and incorporates the +additional feedback received while building the alternatives. + +Open [the comparison index](../../design/mockups/round6/index.html). The shared copy lives at +`~/gacha-mockups/round6/index.html`; earlier rounds stay available. Each design is a complete +standalone HTML prototype with the same sample event data and the shared features below. + +## The alternatives + +- **A — Inline desk:** retains the left routine/daily rail. Game focus is a small selector beside + the event heading, and freshness is a single understated line. Source reasons are one click away. +- **B — Game desk:** puts game focus, daily ticks and a short source-status report in the rail. + There is no full-width freshness or focus band above the event list. It reserves more attention + for the list; its tradeoff is a more populated rail that stacks before events on mobile. +- **C — Open ledger:** replaces the rail with a full-width event list, a brief next-deadline row, + direct daily ticks in a horizontal strip, and a disclosure for other pinned routines. Game focus + and a short freshness report sit with the event heading. It gives titles the most width; the + routine controls scroll with the list. + +The chosen timetable appearance is preserved. These compare structure and interaction rather than +asking the owner to pick a new visual identity again. + +## Shared decisions implemented for review + +- **Changelog is its own Settings page.** The owner explicitly chose this over a main app tab, + and explicitly declined a footer link. Remove the awkward Changelog button from About, remove + it from the footer, and render release notes in the page rather than the old overlay. Existing + returning-reader news links lead to that Settings page. +- **Freshness remains honest and available.** Keep the full data timestamp and age in the footer, + scope warnings to selected games, and expose successful-check/content-change times and reasons + in Data & sources. Older source data is also explained in the event's detail. Unknown causes + remain unreported, not invented. The review bar's Data selector tests the illustrative scenarios. +- **Dailies on Checklist belongs in Routines as well.** Routines → Checklist display controls the + same preference as the daily panel shortcut and Appearance. Choose Separate panel or Grouped + with routines. Independent pins still govern whether a routine appears. +- **Checklist routine groups have three behaviours:** Collapsed by default, Expanded by default + (still collapsible), and Always expanded (no collapse buttons). Focusing a game expands its + routines. The wide layout also honours this for its enclosing routine section. +- **Numeric fields show the native stepper.** Browser increase/decrease controls remain visible + and use step one, as requested. They are not replaced by a separate custom spinner. +- **Appearance includes Custom.** Edit background, surface, text and interface accent separately + for light and dark. Stored choices are retained while preview text/surfaces are adjusted for + contrast. Game identities and deadline urgency remain separate from the custom interface accent. + Individual game colours retain their own control and reset. +- **Reset timing can be overridden per game/server.** Game Setup and Region & time expose Automatic + versus a custom server-local hour/minute, with Revert to Automatic. The prototype applies the + override consistently to daily/routine periods and reset summaries. Reader-authored routines + with their own explicit reset retain it. The automatic source clocks remain simplified here; + this is not a production reset migration. +- **A game's About names its sources.** Links come from that game's entries in the existing local + feed, including separate event/banner/pass sources. Use readable schedule labels and thank the + editors. This differs from the global About's full, unfiltered colophon. +- **Remove Done and ignored events from Lists.** That row only linked to Archive and was not a + setting. Archive keeps its own main navigation tab. +- **Backup rows say View backup.** They open a preview headed Backup from [date], showing the + snapshot's contents. Restore this backup is the explicit action there; the current state is + preserved as a recovery copy. The seven-day/four-week/six-month history is retained. +- **Returning readers get a guided introduction.** The review bar's Returning user control opens + a short welcome in whichever view is preferred, with Take the quick tour and Keep browsing. + Four steps visit Routines, Checklist, Archive and Settings. Back and Skip are available; finishing + returns to the preferred view. Games, follows and progress remain intact. Production will bind + this to a saved release marker, so it is offered once rather than every load. +- **Upcoming events moves to What do you want to track?** It is no longer in the initial preference + step. The initial preferences retain default server, appearance and timezone; all type choices + remain selectable and explained even when no matching built-in exists. + +Standard/Compact density, richer Archive statistics and the history timeline, separate server +progress, code-newness versus redemption, completion confirmation/fade/Undo, timezone and the +existing routine library/create form remain available in every alternative. + +## Boundaries + +Each alternative uses its own `event-clock:mockup:round6::v1` browser namespace. No production +keys, parsers or feed data are changed. Source-health states, codes, archived history and historical +backups are illustrative. Clock overrides affect the demo's period calculations, not any real stored +game-day keys. Production must review migration and the preservation of historical ticks before +introducing reader overrides. These are disposable prototypes, not code to transplant into the app. + +## Verification record + +The browser pass checks all three designs at 1440px and 390px: focus and daily controls, native +numeric stepping, all disclosure modes, source reasons in event detail, the Changelog page and its +absence from About/footer, custom-palette contrast, reset override/revert, per-game source credits, +Lists cleanup, backup labels, the moved upcoming option, and the returning-user tour preserving +progress. Screenshots cover each main layout plus the new shared pages. + +The first mobile pass found a backup-preview smooth scroll continuing after navigation, allowing a +subsequent automated tap to hit the fixed review bar. The preview now scrolls instantly, avoiding a +pending animation against a different screen. + +The confirming pass completed all six combinations of design and viewport with no JavaScript +errors or page-level horizontal overflow in the checked screens. Custom-palette checks included +equal chosen background/text colours and confirmed the rendered text was adjusted to at least +4.5:1. Per-server override checks exercised 05:30, verified the 90-minute change from 04:00, then +returned to Automatic. The tour returned a Timeline-default reader to Timeline with saved games, +follows and progress unchanged. + +Design-hook triage: retain the owner-selected Barlow typography. Narrow value exceptions for that +font are scoped to `design/mockups/round6/index.html` only, with evidence in `.impeccable/config.json`. +No whole-file or whole-rule exclusions are added. The shipped DESIGN.md remains the production +reference; prototype differences in palette/size and inherited archive treatments remain review +findings rather than being suppressed broadly. diff --git a/docs/design-reviews/2026-10-16-redesign-round7.md b/docs/design-reviews/2026-10-16-redesign-round7.md new file mode 100644 index 0000000..c04d9c2 --- /dev/null +++ b/docs/design-reviews/2026-10-16-redesign-round7.md @@ -0,0 +1,80 @@ +# Routine redesign: one airier design + +**Later owner decision:** retain two interfaces after all: Round 7 and Round 6 **B**, selected +in Settings. The earlier single-interface direction below records the mockup review, not the +final implementation decision. Replace “Older schedules” with “Event data may be out of date”. +See the [production redesign specification](../superpowers/specs/2026-10-16-ui-redesign-design.md). + +Review prototype, not shipped behaviour. Open +[the unified mockup](../../design/mockups/round7/converged.html), also copied to +`~/gacha-mockups/round7/converged.html` through the shared index. + +## The owner's correction + +Round 6's three alternatives were rejected. The owner asked for: + +- Older-data notices in the same structure as New since you last looked, appearing only when + games have older data. +- The current production game's focus-pill structure, placed at the top of the left rail as in B. +- A more airy, easy-to-read design informed by the original + `~/gacha-mockups/c-one-due-list.html`. + +The initial idea of a Simple mode or a spacing toggle was then explicitly reconsidered: the owner +does not want two distinct UIs. Carry the spirit of C into one unified interface instead. + +## What this mockup does + +**A shared updates area.** New code notices and Older schedules use two compact rows in one +component above the Checklist's event list and above Timeline. The older row lists affected games +and links to Details & reasons; no full-width health banner or rail status panel remains. It is +absent when the selected games have current sources. If there are no new codes either, the whole +updates area disappears. Source reasons remain available in Data & sources and event detail; the +full feed timestamp stays in the footer on every load. Scope is the games the reader has switched +on, rather than only the temporary focus. Repetitive older-data labels on individual list rows are +removed because the updates row already names those games. + +**The existing focus behaviour.** `src/client/components/GameFocus.tsx` is the reference: All and +game pills, game hues, outstanding counts, and Next progressing through games and then back to All. +Tapping the focused pill returns to All. Pills wrap in their own panel at the top of the Checklist +rail; they are absent for a single-game setup. Timeline uses the same pills above its lanes. Focus +never changes which games are followed. + +**One visual world.** The original C informs separated panels, quieter rules, softened surfaces, +more room around controls and a warmer reading rhythm. Bricolage Grotesque and Figtree replace the +compressed timetable typography consistently across the prototype, including settings and forms. +The rail and event list have a real gutter; focus, deadline, dailies and routines have separated +surfaces. Light mode uses warm paper and dark mode a softer charcoal. No Simple/Detailed mode or +global spacing toggle is added. Standard/Compact remain two row densities of this same appearance. +Rows with no description remain shorter; comfortable layout is not a fixed tall event height. + +Direct daily ticks, routine disclosure choices, independent pins, custom palettes, per-game colours +and reset overrides, source links and thanks, Changelog in Settings, Archive statistics and history, +backup history/recovery, timezone and both onboarding flows remain available. The daily-panel layout +shortcut is removed from the Checklist rail to reduce controls there; its Routines and Appearance +settings continue to edit the same preference. + +## Validation + +Chrome journeys at 1440px and 390px passed with no JavaScript errors and no page-level horizontal +overflow in the checked views. They cover conditional/scope-aware notices, empty updates, focus +toggle/advance, spacing, routine expansion, reset override/revert, source credit links, custom +scheme controls, light-theme game-colour contrast on ground and panel, Changelog location, backup +history, Archive stats, Timeline, completion/fade/Undo and onboarding. Contrast adjustment now +considers both panel and page grounds, while retaining saved base game colours. + +Screenshots were inspected for Checklist in both appearances, Routines, Data & sources, Archive +and Timeline. The first verification script used a nonexistent review-bar Settings shortcut; it +was corrected to use the actual Settings tab. No product change was needed for that script error. + +The mechanical design check flags differences from the **shipped** DESIGN.md in palette, type +size, radius and inherited archive treatments. Those remain review findings, not broad suppressions. +The intentional Bricolage font exception is scoped to this one prototype file, with rationale in +`.impeccable/config.json`. The production visual reference is not rewritten before implementation. + +## Boundaries + +The namespace is `event-clock:mockup:round7:v1`. Earlier mockups and production progress remain +unchanged. Historical records, codes and source health are illustrative; clocks are fixed and +automatic reset defaults simplified. No source requests or production parser/storage changes run +here. This is a disposable visual review artifact; the redesign still needs a reviewed production +specification and migration plan. diff --git a/docs/design-reviews/2026-10-17-redesign-fidelity.md b/docs/design-reviews/2026-10-17-redesign-fidelity.md new file mode 100644 index 0000000..098145f --- /dev/null +++ b/docs/design-reviews/2026-10-17-redesign-fidelity.md @@ -0,0 +1,101 @@ +# Redesign fidelity correction — 2026-10-17 + +> **Owner rejected this implementation after browsing it:** “This is still not even close to the +> mockup.” The completion claim is withdrawn. Screenshots and review verdicts below are historical +> evidence of the rejected attempt, not acceptance. Continue from +> [the second-rejection handoff](../handoffs/2026-10-17-redesign-second-rejection-handoff.md): +> launch the current server, then ask the owner for the biggest differences before further UI edits. + +The owner asked to repair the rejected implementation against Round 7 Relaxed and Round 6 B +Structured, then explicitly made **Structured the default**. Existing saved Relaxed choices remain +valid. This correction is uncommitted in the existing checkout; no branch, publication or data-source +refresh was performed. + +## What changed + +- Checklist uses dedicated reference-shaped event rows and next-deadline panel, with separate copy, + countdown, supporting date, segmented meter and detail chevron. Header counts and Settings placement + follow the reference. Both interfaces retain the owner-approved focus pills. +- Routines has By reset/By game bands, aligned game/routine/progress/view-pin/menu columns, live totals, + direct daily status, amount bars/inputs, library and a native custom-routine drawer. Bulk pins combine + writes per account. Multi-server row identity includes both account and routine. +- Timeline plots event spans and routine periods on one axis with frozen labels, progress fill and + reset markers. In-progress bar labels remain visible after scrolling. Deadline ordering includes + routine resets, and separate reset strips remain available. +- Settings restores separate Default view and Your own events and games pages, game profiles, tracking + summaries and Other games. Profiles follow tracking → servers → colour → reset timing. A server + selector above tracking keeps choices independent without duplicating tall account panels. +- Archive restores the story, interactive outcome bars, game sidebar, saved rows and history timeline. + Its detail dialog reads saved metadata, original clock region, notes, days and amounts. It does not + open the live progress editor or replace historical state with the current clock. +- Structured defaults agree across prepaint, normalization, setup and applied appearance. No IDs, + localStorage key spaces, migrations or ingestion rules were changed by this correction. + +## Reference comparisons + +Images are in [the evidence directory](assets/2026-10-17-fidelity/). Representative matched views: + +| View | App | Approved reference | +| --- | --- | --- | +| Structured Checklist | [Desktop](assets/2026-10-17-fidelity/app-structured-dark-1440.png) | [Desktop](assets/2026-10-17-fidelity/reference-structured-dark-1440.png) | +| Relaxed Checklist | [Desktop](assets/2026-10-17-fidelity/app-relaxed-dark-1440.png) | [Desktop](assets/2026-10-17-fidelity/reference-relaxed-dark-1440.png) | +| Relaxed mobile | [390px](assets/2026-10-17-fidelity/app-relaxed-dark-390.png) | [390px](assets/2026-10-17-fidelity/reference-relaxed-dark-390.png) | +| Routines | [Structured](assets/2026-10-17-fidelity/app-structured-routines.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-routines.png) | +| Timeline | [Structured](assets/2026-10-17-fidelity/app-structured-timeline.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-timeline.png) | +| Game Setup | [Relaxed](assets/2026-10-17-fidelity/app-relaxed-settings.png) | [Relaxed](assets/2026-10-17-fidelity/reference-relaxed-settings.png) | +| Archive | [Structured](assets/2026-10-17-fidelity/app-structured-archive.png) | [Structured](assets/2026-10-17-fidelity/reference-structured-archive.png) | + +Additional captures cover both themes at 1440px/390px, populated mobile utility pages, compact rows, +event/archive details, custom-routine drawers, archive history and the Settings pages. + +The comparison harness freezes time at 2026-10-16 12:00 UTC and intercepts the event-feed request +with the prototype's **synthetic demonstration events**. It seeds disposable browser storage with +matching routine choices and 38 archive snapshots. These fixtures are review evidence only: they +are not imported by production, copied into public, or written into source snapshots. Actual setup, +HTTP migration and offline checks use the built app and its real feed separately. + +Expected semantic differences: real account rows include Asia events, so totals differ from the +prototype's game-only event list. One prototype custom task was not seeded. Production has no +version metadata for archive events, so it groups saved records without inventing version names. +Real reset clocks and available routines govern progress. Code/release notices reflect actual data; +Checklist comparisons hide these notices to align the first viewport. The Structured prototype's +filter dropdown is intentionally superseded by the owner's focus-pill decision. + +Reproduce with [fidelity-capture.mjs](fidelity-capture.mjs), from the repository root, setting +`PUPPETEER_MODULE` to the scratch installation's ESM entry and `APP_URL` / `MOCKUP_URL` to fresh +running origins. Chrome is `/usr/bin/google-chrome-stable`; the harness maps event-clock.test to +loopback. Rebuild CSS/JS/static without fetching the feed, and use a new app port after each rebuild. + +## Independent review + +The first reviewer invocation hit a session usage limit; a retry succeeded. Its initial material +findings were invisible scrolled timeline labels, substituted Settings navigation, routine column/ +status hierarchy, multi-account row-key collisions, and insufficient populated Archive evidence. +One follow-up also required correcting the game-profile hierarchy. Each finding was fixed and the +same screenshot paths recaptured. Final independent verdict: **“ship the reviewed corrections.”** +All reported material findings were resolved. This verdict is scoped to the compared screens and +reported fixes; it is not owner acceptance or blanket proof of every interaction. + +The Impeccable detector ran once and returned `[]` (no findings). + +## Verification + +Final browser build: `a28021bbe1a8`, production React/JSX transform, 117 bundled modules. + +- TypeScript passes. Full offline suite: **1,594 passed, 0 failed**, 5,410 assertions. +- Docker-equivalent copied file layout, without docs/design/public: 1,593 passed, one intentional + docs-dependent skip, no failures. This validates the COPY-layout assumption; no Docker image was built. +- Eight interface/theme/width combinations: no document overflow or page exceptions. Both densities, + reduced motion/effective 200% viewport, custom palette and timezone selection checked. +- Ordinary insecure HTTP hostname: new setup, existing v1 migration, daily writes, second server, + reset override/revert, backup and reload pass; original legacy bytes retained. Structured is the + missing-choice default and saved Relaxed survives reload. +- Real two-tab simultaneous writes with Web Locks disabled pass using the existing IndexedDB lock. + Offline reload passes on localhost's service-worker-capable origin. +- Final-build interaction checks: multi-server bulk pins preserve the other view's placement; numeric + edits and custom-routine creation persist; completion, Undo, archive detail, Restore and Escape pass. + Final profile's server/reset controls were rechecked on insecure HTTP after layout changes. +- `git diff --check` passes. No new runtime dependency, source fetch, commit or publication. + +The earlier rejection is preserved in the 2026-10-16 report. Passing tests did not establish its +visual fidelity; the side-by-side evidence and scoped review above are the correction's design record. diff --git a/docs/design-reviews/assets/2026-10-16-redesign/relaxed-light-1440.png b/docs/design-reviews/assets/2026-10-16-redesign/relaxed-light-1440.png new file mode 100644 index 0000000..9a14723 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-16-redesign/relaxed-light-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-1440.png b/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-1440.png new file mode 100644 index 0000000..e6c119e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-390.png b/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-390.png new file mode 100644 index 0000000..851fc0f Binary files /dev/null and b/docs/design-reviews/assets/2026-10-16-redesign/structured-dark-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-1440.png new file mode 100644 index 0000000..7c4b7ad Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-390.png new file mode 100644 index 0000000..b23003d Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-detail-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-1440.png new file mode 100644 index 0000000..88614bc Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-390.png new file mode 100644 index 0000000..e698ba1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-history-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-mobile.png new file mode 100644 index 0000000..bf1aa69 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive.png new file mode 100644 index 0000000..f5f3374 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-archive.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-1440.png new file mode 100644 index 0000000..b36fd13 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-390.png new file mode 100644 index 0000000..c8d23df Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-compact-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-1440.png new file mode 100644 index 0000000..79ea5f8 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-390.png new file mode 100644 index 0000000..0a270d7 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-dark-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-1440.png new file mode 100644 index 0000000..26d9771 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-390.png new file mode 100644 index 0000000..ace6977 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-event-detail-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-1440.png new file mode 100644 index 0000000..b93d5c4 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-390.png new file mode 100644 index 0000000..5443e66 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-light-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-1440.png new file mode 100644 index 0000000..7a8aa09 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-390.png new file mode 100644 index 0000000..c7a0d1d Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routine-editor-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines-mobile.png new file mode 100644 index 0000000..cbccdae Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines.png new file mode 100644 index 0000000..d838cad Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-routines.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-1440.png new file mode 100644 index 0000000..0962800 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-390.png new file mode 100644 index 0000000..896b6a1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-About-Event-Clock-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-1440.png new file mode 100644 index 0000000..5b84fbc Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-390.png new file mode 100644 index 0000000..dc46df6 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Appearance-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-1440.png new file mode 100644 index 0000000..75d91f1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-390.png new file mode 100644 index 0000000..a0b8495 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Backup-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-1440.png new file mode 100644 index 0000000..fb7de33 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-390.png new file mode 100644 index 0000000..910c52f Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Changelog-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-1440.png new file mode 100644 index 0000000..59a7aee Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-390.png new file mode 100644 index 0000000..cdbf3aa Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Default-view-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-1440.png new file mode 100644 index 0000000..d13ee9e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-390.png new file mode 100644 index 0000000..612fbb2 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Region-&-time-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-1440.png new file mode 100644 index 0000000..fd312ab Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-390.png new file mode 100644 index 0000000..924fe3d Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-Your-own-events-and-games-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-mobile.png new file mode 100644 index 0000000..024370b Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings.png new file mode 100644 index 0000000..cbf824f Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-settings.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline-mobile.png new file mode 100644 index 0000000..3ecba2e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline.png new file mode 100644 index 0000000..83f5a49 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-relaxed-timeline.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-1440.png new file mode 100644 index 0000000..dcc5894 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-390.png new file mode 100644 index 0000000..a7cdd35 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-detail-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-1440.png new file mode 100644 index 0000000..794d73e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-390.png new file mode 100644 index 0000000..edf6655 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-history-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-mobile.png new file mode 100644 index 0000000..e37b401 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive.png new file mode 100644 index 0000000..174b1fa Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-archive.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-1440.png new file mode 100644 index 0000000..94678af Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-390.png new file mode 100644 index 0000000..8ba9d6c Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-compact-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-1440.png new file mode 100644 index 0000000..2674f8d Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-390.png new file mode 100644 index 0000000..e176da5 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-dark-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-1440.png new file mode 100644 index 0000000..c888168 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-390.png new file mode 100644 index 0000000..c6d14ab Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-event-detail-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-1440.png new file mode 100644 index 0000000..e2b389b Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-390.png new file mode 100644 index 0000000..7fec5b1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-light-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-1440.png new file mode 100644 index 0000000..5e6a4df Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-390.png new file mode 100644 index 0000000..a9b4ab4 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routine-editor-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines-mobile.png new file mode 100644 index 0000000..28446f8 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines.png new file mode 100644 index 0000000..1b0c7a1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-routines.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-1440.png new file mode 100644 index 0000000..ec16347 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-390.png new file mode 100644 index 0000000..b64c6bf Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-About-Event-Clock-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-1440.png new file mode 100644 index 0000000..4dc91fd Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-390.png new file mode 100644 index 0000000..ea51b52 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Appearance-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-1440.png new file mode 100644 index 0000000..68cbf9b Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-390.png new file mode 100644 index 0000000..afc3193 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Backup-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-1440.png new file mode 100644 index 0000000..9d108cd Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-390.png new file mode 100644 index 0000000..4322477 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Changelog-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-1440.png new file mode 100644 index 0000000..357d882 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-390.png new file mode 100644 index 0000000..97f059e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Default-view-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-1440.png new file mode 100644 index 0000000..b572331 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-390.png new file mode 100644 index 0000000..b71b55e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Region-&-time-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-1440.png new file mode 100644 index 0000000..6d0d23f Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-390.png new file mode 100644 index 0000000..7c2f439 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-Your-own-events-and-games-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-mobile.png new file mode 100644 index 0000000..cc5ade6 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings.png new file mode 100644 index 0000000..6ba61c1 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-settings.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline-mobile.png new file mode 100644 index 0000000..1abbc6c Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline.png b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline.png new file mode 100644 index 0000000..65667c0 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/app-structured-timeline.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-events.json b/docs/design-reviews/assets/2026-10-17-fidelity/reference-events.json new file mode 100644 index 0000000..6c45876 --- /dev/null +++ b/docs/design-reviews/assets/2026-10-17-fidelity/reference-events.json @@ -0,0 +1 @@ +[{"id":"e0","game":"genshin","title":"Anniversary Memories Album","desc":"Look back through the year's moments in an album and claim Primogems for each page you fill.","start":1790564400000,"end":1792292400000,"src":"https://genshin-impact.fandom.com/wiki/Anniversary_Memories_Album"},{"id":"e1","game":"zzz","title":"Clink, Clank, Pinball Knight!","desc":"A pinball minigame with daily stages; clear them for Polychromes and upgrade materials.","start":1789009200000,"end":1792378800000,"src":"https://game8.co/games/Zenless-Zone-Zero/archives/457176"},{"id":"e2","game":"zzz","title":"Diary of an Orbie Parent","desc":"Raise an Orbie through daily check-ins and small tasks.","start":1790737200000,"end":1792378800000,"src":"https://game8.co/games/Zenless-Zone-Zero/archives/457176"},{"id":"e3","game":"nte","title":"Stamina Recharge","desc":"Spend City Stamina twice as fast in select Hethereau Hobby activities.","start":1791169200000,"end":1792378800000,"src":"https://game8.co/games/Neverness-to-Everness/archives/592073"},{"id":"e4","game":"zzz","title":"Data Bounty - Combat Simulation","desc":"Short combat simulations with bonus rewards for the first clear of each.","start":1791946800000,"end":1792378800000,"src":"https://game8.co/games/Zenless-Zone-Zero/archives/457176"},{"id":"e5","game":"zzz","title":"All-New Program","desc":"","start":1788922800000,"end":1792465200000,"src":"https://game8.co/games/Zenless-Zone-Zero/archives/457176"},{"id":"e6","game":"genshin","title":"Predictive Victory Dynamics","desc":"A combat event in stages; each unlocks a day after the last.","start":1791774000000,"end":1792465200000,"src":"https://game8.co/games/Genshin-Impact/archives/301601"},{"id":"e7","game":"wuwa","title":"Bountiful Crescendo","desc":"","start":1792033200000,"end":1792638000000,"src":"https://game8.co/games/Wuthering-Waves/archives/453473"},{"id":"e8","game":"hsr","title":"4.6 Planar Fissure","desc":"Double rewards from Planar Ornament Extraction, a few times a day.","start":1791774000000,"end":1792897200000,"src":"https://game8.co/games/Honkai-Star-Rail/archives/408749"},{"id":"e9","game":"wuwa","title":"Artisan's Search","desc":"","start":1791428400000,"end":1792983600000,"src":"https://game8.co/games/Wuthering-Waves/archives/453473"},{"id":"e10","game":"wuwa","title":"Waking Moon Fishing","desc":"","start":1792033200000,"end":1793502000000,"src":"https://game8.co/games/Wuthering-Waves/archives/453473"},{"id":"e11","game":"genshin","title":"Battle Pass - Moontrace","desc":"","start":1790132400000,"end":1793588400000,"src":"https://game8.co/games/Genshin-Impact/archives/301601"},{"id":"e12","game":"genshin","title":"To Temper Thyself and Journey Far","desc":"","start":1786330800000,"end":1793588400000,"src":"https://game8.co/games/Genshin-Impact/archives/301601"},{"id":"e13","game":"hsr","title":"Love, Ghosts & Robots","desc":"","start":1790564400000,"end":1794366000000,"src":"https://game8.co/games/Honkai-Star-Rail/archives/408749"},{"id":"e14","game":"hsr","title":"Astral Imagea","desc":"","start":1790564400000,"end":1794366000000,"src":"https://game8.co/games/Honkai-Star-Rail/archives/408749"},{"id":"e15","game":"nte","title":"Pukaland Travelogue","desc":"","start":1790737200000,"end":1794366000000,"src":"https://game8.co/games/Neverness-to-Everness/archives/592073"},{"id":"e16","game":"wuwa","title":"3.7 Moonlit Path","desc":"","start":1790737200000,"end":1794366000000,"src":"https://game8.co/games/Wuthering-Waves/archives/453473"},{"id":"e17","game":"nte","title":"Coal Lumps Treasure","desc":"","start":1791428400000,"end":1794366000000,"src":"https://game8.co/games/Neverness-to-Everness/archives/592073"},{"id":"e18","game":"endfield","title":"Originium Supply Run","desc":"Mockup event so Endfield has a row.","start":1791514800000,"end":1792724400000,"src":null},{"id":"e19","game":"hsr","title":"Free Gilgamesh or Archer","desc":"","start":1784862000000,"end":null,"src":"https://game8.co/games/Honkai-Star-Rail/archives/408749"}] \ No newline at end of file diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive-mobile.png new file mode 100644 index 0000000..3980b8e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive.png new file mode 100644 index 0000000..e638422 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-archive.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-1440.png new file mode 100644 index 0000000..eb6c659 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-390.png new file mode 100644 index 0000000..6ea3648 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-dark-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-1440.png new file mode 100644 index 0000000..7ee5976 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-390.png new file mode 100644 index 0000000..18dff2f Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-light-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines-mobile.png new file mode 100644 index 0000000..066e769 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines.png new file mode 100644 index 0000000..dd642cd Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-routines.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings-mobile.png new file mode 100644 index 0000000..6812bfb Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings.png new file mode 100644 index 0000000..808a15d Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-settings.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline-mobile.png new file mode 100644 index 0000000..6d4420a Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline.png new file mode 100644 index 0000000..88854d4 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-relaxed-timeline.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-state.json b/docs/design-reviews/assets/2026-10-17-fidelity/reference-state.json new file mode 100644 index 0000000..b2475d5 --- /dev/null +++ b/docs/design-reviews/assets/2026-10-17-fidelity/reference-state.json @@ -0,0 +1 @@ +{"archive":[{"id":"g1","game":"genshin","ver":"6.0","title":"Thunder Gauntlet Trials","status":"done","start":"2026-08-27","end":"2026-09-08","at":"2026-09-07","desc":"A combat challenge in six stages, one unlocking each day. Each first clear pays Primogems.","moved":false,"prog":{"t":"stages","got":6,"need":6,"unit":"stages","days":[]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g4","game":"genshin","ver":"6.0","title":"Fontinalia Film Festival","status":"done","start":"2026-09-03","end":"2026-09-17","at":"2026-09-16","desc":"Shoot short scenes around Fontaine and trade Film Points in the event shop.","moved":false,"prog":{"t":"amount","got":1200,"need":1200,"unit":"Film Points","days":[]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g5","game":"genshin","ver":"6.0","title":"Mystic Melody Concert","status":"ignored","start":"2026-09-10","end":"2026-09-24","at":"2026-09-11","desc":"A rhythm minigame with daily songs.","moved":false,"prog":{"t":"days","got":0,"need":5,"unit":"days","days":[]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g7","game":"genshin","ver":"6.0","title":"Gilded Winds Exploration","status":"expired","start":"2026-09-12","end":"2026-10-06","at":"2026-10-06","desc":"Explore five wind-swept ruins; each area opens a few days after the last.","moved":false,"prog":{"t":"stages","got":4,"need":5,"unit":"stages","days":[]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g3","game":"genshin","ver":"6.0","title":"Ley Line Overflow","status":"expired","start":"2026-09-15","end":"2026-09-22","at":"2026-09-22","desc":"Double Ley Line rewards, three times a day.","moved":false,"prog":{"t":"days","got":1,"need":7,"unit":"days","days":[6]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g6","game":"genshin","ver":"6.0","title":"Primordial Echoes Web Event","status":"done","start":"2026-09-20","end":"2026-09-27","at":"2026-09-21","desc":"A browser event: check in on three days for Primogems.","moved":true,"prog":{"t":"days","got":3,"need":3,"unit":"days","days":[0,4,6]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g2","game":"genshin","ver":"6.0","title":"Overflowing Mastery","status":"done","start":"2026-10-02","end":"2026-10-09","at":"2026-10-08","desc":"Double Talent Domain drops, three times a day.","moved":false,"prog":{"t":"days","got":6,"need":7,"unit":"days","days":[0,1,2,3,4,5]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"g8","game":"genshin","ver":"6.1","title":"Moonlit Feast Login","status":"done","start":"2026-10-08","end":"2026-10-15","at":"2026-10-14","desc":"Log in on seven days for Intertwined Fates.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[0,1,2,3,4,5,6]},"src":"https://game8.co/games/Genshin-Impact/archives/297500"},{"id":"h2","game":"hsr","ver":"4.5","title":"Starlight Rhapsody","status":"done","start":"2026-08-13","end":"2026-09-02","at":"2026-09-01","desc":"A story event in eight chapters.","moved":false,"prog":{"t":"stages","got":8,"need":8,"unit":"stages","days":[]},"src":null},{"id":"h3","game":"hsr","ver":"4.5","title":"Gift of Odyssey","status":"done","start":"2026-08-13","end":"2026-09-24","at":"2026-08-20","desc":"Log in on seven days for Star Rail Special Passes.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[11,12,15,21,25,32,35]},"src":null},{"id":"h4","game":"hsr","ver":"4.5","title":"Aetherium Wars: Final Bout","status":"ignored","start":"2026-08-20","end":"2026-09-23","at":"2026-08-21","desc":"A creature-battling event with a separate shop.","moved":true,"prog":{"t":"days","got":0,"need":10,"unit":"days","days":[]},"src":null},{"id":"h5","game":"hsr","ver":"4.5","title":"Realm of the Strange","status":"done","start":"2026-09-01","end":"2026-09-08","at":"2026-09-07","desc":"Triple relic drops from Caverns of Corrosion, a few times a day.","moved":false,"prog":{"t":"days","got":3,"need":3,"unit":"days","days":[0,2,5]},"src":null},{"id":"h6","game":"hsr","ver":"4.5","title":"Fate's Forge Trial","status":"done","start":"2026-09-05","end":"2026-09-19","at":"2026-09-18","desc":"Trial characters in staged fights; points unlock rewards.","moved":false,"prog":{"t":"amount","got":3000,"need":3000,"unit":"points","days":[]},"src":null},{"id":"h7","game":"hsr","ver":"4.5","title":"Boulder Town Market","status":"expired","start":"2026-09-09","end":"2026-09-23","at":"2026-09-23","desc":"Run a market stall; coins buy materials in the event shop.","moved":false,"prog":{"t":"amount","got":1800,"need":2400,"unit":"coins","days":[]},"src":null},{"id":"h8","game":"hsr","ver":"4.6","title":"Planar Fissure","status":"done","start":"2026-09-26","end":"2026-10-03","at":"2026-10-02","desc":"Double rewards from Planar Ornament Extraction.","moved":false,"prog":{"t":"days","got":5,"need":5,"unit":"days","days":[0,1,2,3,4]},"src":null},{"id":"h1","game":"hsr","ver":"4.6","title":"Garden of Plenty","status":"expired","start":"2026-09-29","end":"2026-10-06","at":"2026-10-06","desc":"Double rewards from Calyx, until the daily limit.","moved":false,"prog":{"t":"days","got":3,"need":7,"unit":"days","days":[1,2,5]},"src":null},{"id":"z3","game":"zzz","ver":"2.2","title":"Sweet Sweet Sweets","status":"done","start":"2026-08-20","end":"2026-09-03","at":"2026-09-02","desc":"Bake with Bangboos on seven days for Polychromes.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[0,1,2,3,4,5,6]},"src":null},{"id":"z6","game":"zzz","ver":"2.2","title":"Ether Tide Login","status":"done","start":"2026-08-20","end":"2026-09-02","at":"2026-08-27","desc":"Log in on seven days for Encrypted Master Tapes.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[0,1,2,3,4,5,6]},"src":null},{"id":"z4","game":"zzz","ver":"2.2","title":"Rally Racer Remix","status":"ignored","start":"2026-08-27","end":"2026-09-10","at":"2026-08-27","desc":"A racing minigame in six tracks.","moved":false,"prog":{"t":"stages","got":0,"need":6,"unit":"stages","days":[]},"src":null},{"id":"z1","game":"zzz","ver":"2.2","title":"Hollow Zero: Lost Void – Season 2","status":"done","start":"2026-08-29","end":"2026-09-28","at":"2026-09-27","desc":"A roguelite season with weekly score tiers; each tier pays out once.","moved":false,"prog":{"t":"stages","got":9,"need":9,"unit":"tiers","days":[]},"src":null},{"id":"z5","game":"zzz","ver":"2.2","title":"Bangboo Brawl","status":"expired","start":"2026-09-03","end":"2026-09-17","at":"2026-09-17","desc":"Team Bangboos up in auto-battles for event points.","moved":false,"prog":{"t":"amount","got":1450,"need":1500,"unit":"points","days":[]},"src":null},{"id":"z2","game":"zzz","ver":"2.2","title":"Hollow Zero: Lost Void – Season 3","status":"done","start":"2026-09-12","end":"2026-10-12","at":"2026-10-11","desc":"A roguelite season with weekly score tiers; each tier pays out once.","moved":false,"prog":{"t":"stages","got":9,"need":9,"unit":"tiers","days":[]},"src":null},{"id":"z7","game":"zzz","ver":"2.3","title":"Phaethon's Errands","status":"done","start":"2026-10-01","end":"2026-10-13","at":"2026-10-12","desc":"Run five errands across New Eridu, one a day.","moved":false,"prog":{"t":"stages","got":5,"need":5,"unit":"stages","days":[]},"src":null},{"id":"w2","game":"wuwa","ver":"3.6","title":"Tidebound Regatta","status":"done","start":"2026-08-21","end":"2026-09-04","at":"2026-09-03","desc":"Boat races along the coast; first clears pay Astrite.","moved":false,"prog":{"t":"stages","got":7,"need":7,"unit":"races","days":[]},"src":null},{"id":"w3","game":"wuwa","ver":"3.6","title":"Echoing Marsh Patrol","status":"expired","start":"2026-08-28","end":"2026-09-11","at":"2026-09-11","desc":"Daily patrol missions through the marsh.","moved":false,"prog":{"t":"days","got":4,"need":7,"unit":"days","days":[2,4,11,12]},"src":null},{"id":"w4","game":"wuwa","ver":"3.6","title":"Lollo Logistics","status":"done","start":"2026-09-04","end":"2026-09-18","at":"2026-09-15","desc":"Deliver parcels around Lahai-Roi for Credits.","moved":false,"prog":{"t":"amount","got":2000,"need":2000,"unit":"Credits","days":[]},"src":null},{"id":"w6","game":"wuwa","ver":"3.6","title":"Waveplate Bonus Week","status":"ignored","start":"2026-09-15","end":"2026-09-22","at":"2026-09-15","desc":"Double drops from Simulation Challenges.","moved":false,"prog":{"t":"days","got":0,"need":7,"unit":"days","days":[]},"src":null},{"id":"w5","game":"wuwa","ver":"3.6","title":"Thousand Gateways","status":"done","start":"2026-09-11","end":"2026-09-25","at":"2026-09-24","desc":"A ten-floor challenge tower with set teams.","moved":false,"prog":{"t":"stages","got":10,"need":10,"unit":"floors","days":[]},"src":null},{"id":"w1","game":"wuwa","ver":"3.7","title":"Ascendant Aces","status":"ignored","start":"2026-10-01","end":"2026-10-29","at":"2026-10-03","desc":"A card battle event with a separate shop.","moved":false,"prog":{"t":"stages","got":0,"need":8,"unit":"rounds","days":[]},"src":null},{"id":"e2","game":"endfield","ver":"1.2","title":"Talos-II Survey Grant","status":"done","start":"2026-08-14","end":"2026-08-28","at":"2026-08-27","desc":"Log in on seven days for Originium.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[0,1,2,3,4,5,6]},"src":null},{"id":"e3","game":"endfield","ver":"1.2","title":"Wuling Night Market","status":"expired","start":"2026-08-21","end":"2026-09-11","at":"2026-09-11","desc":"Trade at the night market stalls for event points.","moved":true,"prog":{"t":"amount","got":4200,"need":6000,"unit":"points","days":[]},"src":null},{"id":"e1","game":"endfield","ver":"1.2","title":"Frontier Logistics Drill","status":"done","start":"2026-09-04","end":"2026-09-18","at":"2026-09-16","desc":"A logistics event in daily stages.","moved":false,"prog":{"t":"stages","got":6,"need":6,"unit":"stages","days":[]},"src":null},{"id":"e4","game":"endfield","ver":"1.3","title":"Protocol Overload Exercise","status":"ignored","start":"2026-09-25","end":"2026-10-09","at":"2026-09-26","desc":"Combat drills with rotating modifiers.","moved":false,"prog":{"t":"stages","got":0,"need":5,"unit":"stages","days":[]},"src":null},{"id":"n2","game":"nte","ver":"1.1","title":"Hethereau Cup Kart","status":"done","start":"2026-08-28","end":"2026-09-11","at":"2026-09-10","desc":"Kart races through the city's districts.","moved":false,"prog":{"t":"stages","got":5,"need":5,"unit":"races","days":[]},"src":null},{"id":"n3","game":"nte","ver":"1.1","title":"City Pulse Sign-in","status":"done","start":"2026-08-28","end":"2026-09-10","at":"2026-09-05","desc":"Sign in on seven days for Annulith.","moved":false,"prog":{"t":"days","got":7,"need":7,"unit":"days","days":[0,1,2,3,4,5,6]},"src":null},{"id":"n4","game":"nte","ver":"1.1","title":"Anomaly Night Shift","status":"expired","start":"2026-09-17","end":"2026-10-01","at":"2026-10-01","desc":"Night-shift anomaly hunts, one a day.","moved":false,"prog":{"t":"days","got":6,"need":7,"unit":"days","days":[0,6,9,10,11,12]},"src":null},{"id":"n1","game":"nte","ver":"1.1","title":"Neon Nights Photo Hunt","status":"ignored","start":"2026-09-20","end":"2026-10-04","at":"2026-09-22","desc":"Photograph neon signs around the city for event currency.","moved":false,"prog":{"t":"days","got":1,"need":5,"unit":"days","days":[1]},"src":null},{"id":"n5","game":"nte","ver":"1.2","title":"Pukaland Pre-show","status":"done","start":"2026-10-08","end":"2026-10-14","at":"2026-10-13","desc":"A three-day warm-up for Pukaland.","moved":false,"prog":{"t":"days","got":3,"need":3,"unit":"days","days":[0,1,2]},"src":null}],"followed":["routine:genshin:daily","routine:genshin:daily@asia","routine:hsr:daily","routine:zzz:daily","routine:wuwa:daily","routine:genshin:weekly-bosses","routine:genshin:weekly-bosses@asia","routine:genshin:battle-pass-exp","routine:genshin:spiral-abyss","routine:hsr:echo-of-war","routine:hsr:points","routine:hsr:pure-fiction","routine:hsr:memory-of-chaos","routine:zzz:ridu-weekly","routine:zzz:notorious-hunt","routine:zzz:shiyu-defense","routine:wuwa:weekly-activities","routine:wuwa:tower-of-adversity","routine:endfield:weekly-routine","routine:nte:beyond-the-rails","mytask:a1"],"pins":{"routine:genshin:daily":{"c":1},"routine:genshin:daily@asia":{"c":1},"routine:hsr:daily":{"c":1},"routine:zzz:daily":{"c":1},"routine:wuwa:daily":{"c":1},"routine:genshin:weekly-bosses":{"c":1,"t":1},"routine:genshin:weekly-bosses@asia":{"c":1},"routine:genshin:spiral-abyss":{"t":1},"routine:hsr:echo-of-war":{"c":1,"t":1},"routine:hsr:points":{"c":1},"routine:hsr:pure-fiction":{"c":1,"t":1},"routine:hsr:memory-of-chaos":{"t":1},"routine:zzz:ridu-weekly":{"c":1,"t":1},"routine:zzz:shiyu-defense":{"c":1,"t":1},"routine:wuwa:tower-of-adversity":{"t":1},"routine:endfield:weekly-routine":{"c":1},"routine:nte:beyond-the-rails":{"t":1},"mytask:a1":{"c":1}},"servers":{"genshin":["europe","asia"],"hsr":["europe"],"zzz":["europe"],"wuwa":["europe"],"arknights":["europe"],"endfield":["europe"],"nte":["europe"],"nikki":["europe"],"p5x":["europe"],"r1999":["europe"],"ba":["europe"],"fgo":["europe"],"gfl2":["europe"],"stellasora":["europe"],"czn":["europe"],"nikke":["europe"],"uma":["europe"],"hi3":["europe"],"pgr":["europe"]}} \ No newline at end of file diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive-mobile.png new file mode 100644 index 0000000..39fcfe9 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive.png new file mode 100644 index 0000000..71f9e79 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-archive.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-1440.png new file mode 100644 index 0000000..17e69bf Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-390.png new file mode 100644 index 0000000..3e33459 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-dark-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-1440.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-1440.png new file mode 100644 index 0000000..2b2dc82 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-1440.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-390.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-390.png new file mode 100644 index 0000000..f99406a Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-light-390.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines-mobile.png new file mode 100644 index 0000000..6b2d2ca Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines.png new file mode 100644 index 0000000..c4fda44 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-routines.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings-mobile.png new file mode 100644 index 0000000..d05e71e Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings.png new file mode 100644 index 0000000..6d64d1a Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-settings.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline-mobile.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline-mobile.png new file mode 100644 index 0000000..14e1d74 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline-mobile.png differ diff --git a/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline.png b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline.png new file mode 100644 index 0000000..1834b25 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-fidelity/reference-structured-timeline.png differ diff --git a/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-checklist.png b/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-checklist.png new file mode 100644 index 0000000..1ab5d33 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-checklist.png differ diff --git a/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-setup.png b/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-setup.png new file mode 100644 index 0000000..a661830 Binary files /dev/null and b/docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-setup.png differ diff --git a/docs/design-reviews/fidelity-capture.mjs b/docs/design-reviews/fidelity-capture.mjs new file mode 100644 index 0000000..750cc99 --- /dev/null +++ b/docs/design-reviews/fidelity-capture.mjs @@ -0,0 +1,349 @@ +// Run from the repository root. Puppeteer stays in a scratch installation. +const { default: puppeteer } = await import(process.env.PUPPETEER_MODULE); +const APP_URL = process.env.APP_URL || "http://event-clock.test:41907"; +const MOCKUP_URL = process.env.MOCKUP_URL || "http://localhost:41901"; +import fs from "node:fs/promises"; +const out = "docs/design-reviews/assets/2026-10-17-fidelity"; +await fs.mkdir(out, { recursive: true }); +const base = JSON.parse( + await fs.readFile("public/data/events.v1.json", "utf8"), + ), + events = JSON.parse( + await fs.readFile(`${out}/reference-events.json`, "utf8"), + ); +const at = "2026-10-16T12:00:00.000Z"; +const fixture = { + ...base, + events: events.map((e) => ({ + ...base.events[0], + id: `${e.game}:visual-${e.id}:2026-10-01`, + game: e.game, + title: e.title, + summary: e.desc || null, + startsAt: new Date(e.start).toISOString(), + endsAt: e.end ? new Date(e.end).toISOString() : null, + startPrecision: "exact", + endPrecision: e.end ? "exact" : "unknown", + regionScoped: false, + regionEnds: null, + sourceUrl: e.src || base.events[0].sourceUrl, + })), + sources: base.sources.map((s) => ({ + ...s, + lastSuccessAt: at, + lastConfirmedAt: at, + })), +}; +const sampleState = JSON.parse( + await fs.readFile(`${out}/reference-state.json`, "utf8"), +); +const b = await puppeteer.launch({ + executablePath: "/usr/bin/google-chrome-stable", + args: [ + "--no-sandbox", + "--no-proxy-server", + "--host-resolver-rules=MAP event-clock.test 127.0.0.1", + ], +}); +let errors = []; +for (const variant of ["structured", "relaxed"]) + for (const width of [1440, 390]) + for (const theme of ["dark", "light"]) { + for (const mode of ["reference", "app"]) { + const ctx = await b.createBrowserContext(), + p = await ctx.newPage(); + p.on("pageerror", (e) => errors.push(e.message)); + await p.setViewport({ width, height: 1000 }); + if (mode === "app") { + await p.setRequestInterception(true); + p.on("request", (r) => + r.url().endsWith("events.v1.json") + ? r.respond({ + status: 200, + contentType: "application/json", + body: JSON.stringify(fixture), + }) + : r.continue(), + ); + await p.evaluateOnNewDocument((at) => { + const D = Date; + class FixedDate extends D { + constructor(...args) { + super(...(args.length ? args : [at])); + } + static now() { + return D.parse(at); + } + } + window.Date = FixedDate; + if (!localStorage.getItem("seeded")) { + localStorage.setItem("seeded", "yes"); + localStorage.setItem( + "gacha-tracker:v1:prefs", + JSON.stringify({ + onboarded: true, + knownGames: [ + "genshin", + "hsr", + "zzz", + "wuwa", + "endfield", + "nte", + ], + view: "soon", + region: "europe", + }), + ); + } + }, at); + await p.goto(APP_URL); + await p.waitForSelector(".app-navigation"); + await p.evaluate( + ({ variant, theme, sampleState, template, at }) => { + let s = JSON.parse( + localStorage.getItem("gacha-tracker:v2:reader"), + ); + s.prefs.redesignTourSeen = "2026-10-redesign"; + s.prefs.interface = variant; + s.prefs.theme = theme; + s.prefs.changelogSeen = "9999-12-31"; + const genshin = Object.values(s.accounts).find( + (a) => a.game === "genshin", + ); + const asia = structuredClone(genshin); + asia.id = "account:visual-asia"; + asia.region = "asia"; + asia.clocks[0].region = "asia"; + asia.clocks[0].automaticBase.offsetUtc = 8; + s.accounts[asia.id] = asia; + s.data[asia.id] = { + progress: {}, + ignored: {}, + daily: {}, + amounts: {}, + }; + for (const a of Object.values(s.accounts)) { + a.routines = {}; + a.intent = { + events: true, + dailies: true, + weeklyMonthly: true, + cycles: true, + }; + for (const key of sampleState.followed) { + const [id, region = "europe"] = key.split("@"); + if (!id.includes(`:${a.game}:`) || region !== a.region) + continue; + const rid = id.endsWith(":daily") ? `dailies:${a.game}` : id; + const pin = sampleState.pins[key] || {}; + a.routines[rid] = { + followed: true, + checklist: !!pin.c, + timeline: !!pin.t, + }; + } + if ( + (a.game === "genshin" && a.region === "europe") || + a.game === "hsr" + ) + s.data[a.id].daily.initial = { + [`dailies:${a.game}`]: { days: ["2026-10-16"], at }, + }; + s.data[a.id].amounts.initial = { + "routine:genshin:weekly-bosses": { + "2026-10-12": { value: a.region === "asia" ? 3 : 2, at }, + }, + "routine:hsr:points": { "2026-10-12": { value: 11000, at } }, + }; + } + for (const r of sampleState.archive) { + const a = Object.values(s.accounts).find( + (a) => a.game === r.game && a.region === "europe", + ); + if (!a) continue; + const event = { + ...template, + id: `${r.game}:archive-${r.id}:${r.start}`, + game: r.game, + title: r.title, + summary: r.desc || null, + startsAt: new Date(r.start).toISOString(), + endsAt: new Date(r.end).toISOString(), + startPrecision: "day", + endPrecision: "day", + sourceUrl: r.src || template.sourceUrl, + }; + const key = JSON.stringify([a.id, event.id]); + s.archive[key] = { + accountId: a.id, + event, + outcome: r.status === "expired" ? "unfinished" : r.status, + outcomeAt: new Date(r.at).toISOString(), + resolvedEnd: Date.parse(r.end), + clockRevision: "initial", + progress: { note: "Saved note from this event." }, + days: r.prog.days.map((d) => + new Date(Date.parse(r.start) + d * 86400000) + .toISOString() + .slice(0, 10), + ), + amounts: + r.prog.t === "amount" + ? { [r.start]: { value: r.prog.got, at } } + : {}, + }; + } + localStorage.setItem( + "gacha-tracker:v2:reader", + JSON.stringify(s), + ); + }, + { variant, theme, sampleState, template: fixture.events[0], at }, + ); + await p.reload(); + await p.waitForSelector(".event-entry"); + await p.evaluate(() => { + for (const e of document.querySelectorAll( + ".updates-panel,.release-update", + )) + e.style.display = "none"; + }); + } else { + await p.goto( + `${MOCKUP_URL}/${variant === "structured" ? "round6/b-rail.html" : "round7/converged.html"}`, + ); + await p.evaluate((theme) => { + S.theme = theme; + S.sourceScenario = "current"; + S.wnSeen = true; + S.codeSeen = CODE_DEMO.map((c) => c.code); + render(); + }, theme); + await p.addStyleTag({ + content: + "#chrome{display:none}body{padding-top:0}.rail{top:0!important;max-height:100vh!important}.source-status-label{display:none}", + }); + } + await p.evaluate(() => document.fonts.ready); + await p.screenshot({ + path: `${out}/${mode}-${variant}-${theme}-${width}.png`, + }); + if ( + await p.evaluate( + () => document.documentElement.scrollWidth > innerWidth, + ) + ) + errors.push(`${mode} ${variant} ${width} ${theme} overflow`); + if (theme === "dark") { + for (const page of ["routines", "archive", "timeline", "settings"]) { + await p.evaluate( + ({ page, mode }) => { + if (mode === "reference") { + S.tab = page; + if (page === "settings") S.sel = "genshin"; + render(); + } else { + [...document.querySelectorAll(".app-navigation button")] + .find((e) => e.textContent.toLowerCase().startsWith(page)) + .click(); + } + }, + { page, mode }, + ); + await new Promise((r) => setTimeout(r, 100)); + if (page === "settings" && mode === "app") { + await p.evaluate(() => + [...document.querySelectorAll(".settings-row")] + .find((e) => e.textContent.startsWith("Genshin Impact")) + .click(), + ); + await new Promise((r) => setTimeout(r, 100)); + } + await p.screenshot({ + path: `${out}/${mode}-${variant}-${page}${width === 1440 ? "" : "-mobile"}.png`, + }); + } + if (mode === "app") { + const nav = async (name) => { + await p.evaluate( + (name) => + [...document.querySelectorAll(".app-navigation button")] + .find((e) => e.textContent.startsWith(name)) + .click(), + name, + ); + await new Promise((r) => setTimeout(r, 120)); + }; + await nav("Archive"); + await p.click(".archive-open"); + await p.waitForSelector("dialog[open]"); + await p.screenshot({ + path: `${out}/app-${variant}-archive-detail-${width}.png`, + }); + await p.keyboard.press("Escape"); + await p.evaluate(() => + [...document.querySelectorAll("button")] + .find((e) => e.textContent === "History timeline") + .click(), + ); + await p.screenshot({ + path: `${out}/app-${variant}-archive-history-${width}.png`, + }); + await nav("Routines"); + await p.evaluate(() => + [...document.querySelectorAll("button")] + .find((e) => e.textContent === "Create your own routine") + .click(), + ); + await p.waitForSelector("dialog[open]"); + await p.screenshot({ + path: `${out}/app-${variant}-routine-editor-${width}.png`, + }); + await p.keyboard.press("Escape"); + await nav("Settings"); + for (const label of [ + "Appearance", + "Default view", + "Your own events and games", + "Region & time", + "Backup", + "About Event Clock", + "Changelog", + ]) { + await p.evaluate( + (label) => + [...document.querySelectorAll(".settings-row")] + .find( + (e) => + e.querySelector(".settings-copy > span") + ?.textContent === label, + ) + .click(), + label, + ); + await new Promise((r) => setTimeout(r, 100)); + await p.screenshot({ + path: `${out}/app-${variant}-settings-${label.replaceAll(" ", "-")}-${width}.png`, + }); + } + await nav("Checklist"); + await p.evaluate(() => + [...document.querySelectorAll("button")] + .find((e) => e.textContent === "Compact") + .click(), + ); + await p.screenshot({ + path: `${out}/app-${variant}-compact-${width}.png`, + }); + await p.click(".event-entry"); + await p.screenshot({ + path: `${out}/app-${variant}-event-detail-${width}.png`, + }); + await p.keyboard.press("Escape"); + } + } + await ctx.close(); + } + } +console.log(JSON.stringify(errors)); +await b.close(); diff --git a/docs/handoffs/2026-10-16-redesign-fidelity-handoff.md b/docs/handoffs/2026-10-16-redesign-fidelity-handoff.md new file mode 100644 index 0000000..d4cc333 --- /dev/null +++ b/docs/handoffs/2026-10-16-redesign-fidelity-handoff.md @@ -0,0 +1,464 @@ +# Event Clock redesign: handoff to the replacement agent + +> **Newer handoff:** the replacement implementation was also rejected. Read +> [2026-10-17 second-rejection handoff](2026-10-17-redesign-second-rejection-handoff.md) first. +> The owner's next-step instruction is to launch the current server, then ask them for the biggest +> differences from the mockup before making more UI changes. + +Date: 2026-10-16. Repository: `/home/user/gacha-event-tracker`. + +**2026-10-17 owner amendment:** Structured is now the default. Preserve any explicitly saved +Relaxed choice. The references and the historical rejection described below remain unchanged. +Correction evidence is recorded in `docs/design-reviews/2026-10-17-redesign-fidelity.md`. + +## 1. Assignment and current acceptance status + +**Finish the already-approved redesign by faithfully translating the approved mockup markup into +production React and wiring in real data. The current implementation was rejected by the owner.** + +The owner approved Round 7, then explicitly chose to retain two interfaces with a Settings toggle: + +- **Relaxed (default):** `design/mockups/round7/converged.html`. +- **Structured:** `design/mockups/round6/b-rail.html` — specifically Round 6 **B**, not A or C. + +The owner subsequently reported that the implementation differed substantially from Round 7 and +asked why the previous agent had invented a design instead of using the markup and wiring data. +The previous agent acknowledged that this is what happened: it reused old visual components and +selected mockup CSS, built substitute pages, then verified functionality rather than fidelity. + +**Do not repeat that approach.** Preserve the approved rendered structure, hierarchy, proportions, +spacing, typography, control arrangements, information architecture, responsive behavior and +interactions. Translate HTML to JSX, break it into maintainable components and connect real state. +The existing presentation is not the reference. Matching fonts and outer panel sizes is insufficient. + +Do not transplant the prototype's fake data, fixed clocks, demo toolbar, localStorage namespace or +monolithic demo JavaScript into production. That restriction was previously misinterpreted as +permission to redesign its presentation. It is not. Real-data integration should retain the markup. + +The owner selected **Native execution** and had already approved the spec/plan before implementation. +The task is to finish that approved design, not restart brainstorming or request permission for each +routine correction. Resolve actual conflicts explicitly; do not make new product choices silently. +This handoff-writing turn makes no implementation changes and does not complete the correction. + +## 2. Read these records, in this order + +1. `AGENTS.md` — complete repository working agreement, not just the excerpt in a chat. +2. This document and the approved reference HTML files above, **rendered in a browser**. +3. `docs/design-reviews/2026-10-16-redesign-round7.md` and corresponding round4/round5/round6 records. +4. `docs/superpowers/specs/2026-10-16-ui-redesign-design.md` — approved functional/state requirements. +5. `docs/superpowers/plans/2026-10-16-ui-redesign.md` — implementation inventory, not evidence of acceptance. +6. `docs/PRD.md`, `PRODUCT.md`, `DESIGN.md`, `DATA-MODEL.md`, `ARCHITECTURE.md`; source work additionally + requires `INGESTION.md` and `SOURCES.md`. +7. `docs/design-reviews/2026-10-16-redesign-implementation.md` — prior verification, rejection audit, + HTTP follow-up and representative **rejected implementation** screenshots. +8. `.superpowers/sdd/2026-10-16-ui-redesign/progress.md` and `review-packet.md` — historical ledger. + +Important record caveats: + +- The plan's checkboxes were all marked complete before the owner rejected the result. Presentation + and fidelity gates must be reopened; do not treat those checks as acceptance. +- The spec's opening “written for owner review; not implemented” status is stale. Its requirements + were approved; implementation exists but is rejected. Distinguish intent from implementation status. +- Most product/design docs were rewritten to describe the rejected implementation. Their design + descriptions cannot override the approved mockup or the owner's later correction. Repair this + documentation drift as the faithful implementation lands. +- The Round 7 review initially discussed one UI; its later owner-decision note supersedes that: + retain **two**, Round 7 and Round 6 B. +- `2026-10-16-110223-command-messagesuperpowersusing-superpowersc.txt` is an earlier transcript supplied + by the owner. It predates later work; it is supplemental, not the only record of current decisions. + +## 3. Workspace and authorization + +Last inspected HEAD: `ee41389` (`docs: a handoff note for the next agent, and the worktree feed trap`). +Implementation was made in place on `main`. **All redesign changes remain uncommitted.** No push, +PR, publication or deployment was performed by this agent. The owner has separately built/used the +app through their network, so browser data may already contain v2 state. + +Run `git status --short` before work. There are many modified tracked files and many untracked new +files; `git diff` alone omits most new implementation code. Preserve them. In particular, do not +`git clean`, reset to HEAD, delete all untracked files, or discard the state work to obtain a clean +starting point. Earlier untracked `.impeccable/`, design artifacts, transcripts and review documents +also existed and must not be treated as disposable agent scratch. + +The approved plan says to use the current checkout; do not create a branch/worktree, commit, push, +or publish without a separate owner instruction. The owner's standing workflow is to carry approved +work through execution without repeated approval stops. Keep progress communication concise and +report genuine problems directly. + +## 4. What went wrong, concretely + +The previous implementation created `ReaderApp.tsx`, reused old `EventRow`, `NextUp`, `Timeline`, +`EventDetail` and `Colophon` presentation, and added `redesign.css` plus new simplified pages. +That produced functional features with materially different presentation and interactions. + +Confirmed divergence examples — this is a starting inventory, not an exhaustive audit: + +| Area | Approved reference | Rejected implementation | +| --- | --- | --- | +| Header | Navigation counts; intentional Settings placement; mockup header hierarchy | Generic navigation and a different live/upcoming summary | +| Standard events | Event content, prominent separate deadline/supporting text, chevron, deliberate meter placement | Old EventRow internals decorated with CSS; different countdown hierarchy and layout | +| Routines | Per-game bands, aligned routine/progress/cycle/pin/menu columns and appropriate library/create flows | Flattened library rows with stacked checkbox options | +| Settings | Sidebar includes individual game entries/descriptions leading into game profiles | Generic Your games page with a game dropdown | +| Timeline | Routine periods/progress and reset markers plotted on the same time axis as events | Event timelines followed by ordinary routine controls; textual reset strips | +| Archive | Story panel plus interactive per-game outcome bars/filtering and the approved per-game/history bodies | Simplified statistics and per-game text totals | +| Forms/overlays | Approved drawers, sheets, field groupings and hierarchy | Several older production forms and modal layouts reused | +| Mobile | Mockup-specific compact rail, hierarchy and responsive adaptations | Generic stacking; screenshots showing no overflow were accepted without reference comparison | + +The earlier independent review concentrated on storage safety and functional correctness. It found +important real bugs, which were fixed, but it did not establish design fidelity. **1,590 passing +tests is not evidence that this is the approved design.** Screenshots in the earlier implementation +report document the rejected result; do not use them as visual goldens. + +## 5. How to read the mockup correctly + +The reference HTML is layered: it contains earlier CSS/render functions, then later overrides. +Reading its first stylesheet or first definition of a renderer in isolation gives the wrong target. +Render the complete file and exercise its actual controls. + +Useful anchors in Round 7 (line numbers are approximate; search the names): + +- Final airy system begins around line 1003: `/* A single airier world, informed by the original C planner. */`. +- Header `tabs()` around 1336; initial `baseRoutinesScreen()` around 1392; `settingsScreen()` around 1480. +- Later `round4Checklist()` around 1894; final timeline renderer around 1912. +- Final `checklistScreen()` around 1976; `archiveStory()` / `renderArchive()` around 1989. +- Final `routinesScreen()` around 2051; later render/tour/number-input adjustments around 2065. +- Final `generalPane()` around 2073, `onboarding()` around 2085, `returningTour()` around 2087. + +The reference has its own review toolbar, shortcuts, fixed `NOW`, synthetic histories and namespace +`event-clock:mockup:round7:v1`. Those are tools for inspecting states, not product UI/state to ship. +Capture all relevant states using that toolbar and the product's controls. Do the equivalent for +Round 6 B; do not assume it is merely Round 7 with a border-radius change. + +Some demo behavior is explicitly superseded by owner requirements: e.g. adding a server must start +fresh choices, not copy follows/pins just because an old prototype handler does. The stale-data label +must be **Event data may be out of date**, replacing the mockup's **Older schedules**. The two-interface +selector is an approved addition to Appearance. Keep these necessary adaptations narrow and explicit. + +## 6. Approved requirements to retain + +The detailed spec remains necessary. This summary preserves the accumulated owner feedback: + +- Two visual interfaces over shared real state/components; Relaxed default. Interface, theme, + colour scheme and Standard/Compact row density are independent. +- Standard rows without descriptive text are shorter, with no reserved empty description area. +- Focus uses the existing All/game pills, counts, toggle-to-All and Next behavior, first in the + Checklist rail as in B. It hides for one tracked game and never changes followed games. +- Checklist, Timeline, Routines, Archive and Settings are the main navigation. Utility navigation + must not overwrite the saved preferred main view (`soon` / `timeline`). +- Source age belongs in the same compact updates structure as new-since-last-look notices, only + when tracked games have stale data. Source scope is independent of temporary focus. Codes, + event newness, release news and source age are distinct states. Keep reasons/timestamps/source + links in detail and Data & sources; never invent a failure cause. Footer always states data age. +- Routines are rows. Manage built-ins, custom routines, follow/unfollow, Make it mine, and independent + Checklist/Timeline pins there. Stopping a routine preserves history. Keep daily ticks one action. +- Dailies can have their own Checklist panel or be grouped with routines; the same setting appears + in Routines and Appearance. Group policy: collapsed, expanded by default, or always expanded. + Focus temporarily expands groups without rewriting the policy. Numeric controls have visible + native steppers and step one. +- Timeline routines belong on the time axis. Separate reset strips, when enabled, sit beside their + game's events. Preserve the cross-game Ending soonest queue when strips are off; focus applies + to events, routines and reset strips together. +- Add another server explains simultaneous accounts and asks all tracking questions again. It copies + no progress, follows or pins. Changing an existing server preserves that account's identity/history. + Prevent duplicate active game/server setup; retired histories remain recoverable. +- Each game's settings includes Setup, Codes and About. About names all relevant real sources, + links them and thanks editors, including sources contributing zero events. Game colour has picker, + hex, preview and reset, shared across that game's servers. +- Per-server reset timing has an override and Revert to Automatic; preserve historical clock revisions. + Display timezone defaults to Automatic, permits named IANA zones/search and revert, and affects + display/manual date entry rather than server periods or source instants. +- Appearance includes Slate/Warm/Plum/Custom; custom light/dark palettes, game identity and urgency + remain legible and distinct. Preserve offline font fallback. +- About retains the full colophon, acknowledgements, sources, studios, disclaimer and project links. + Changelog is its own Settings page, **not a footer link or an About button**. +- Lists must contain actual settings, not a pointless Done/ignored row linking to Archive. +- New setup initially selects **events only**. Ask default server, appearance, timezone, games, + tracking types and preferred main view. Types explain cycles and remain selectable even when + nothing is available; explain the absence. Upcoming visibility belongs in “What do you want to + track?”, initially off. Do not change existing readers to new-reader defaults. +- Returning users get a one-time welcome/tour with Skip/Back; preserve progress and preferred view. +- Done/Ignore saves immediately, explains movement to Archive for about 1.5 seconds, then fades/ + collapses. Undo lasts about eight seconds; Archive Restore remains available. Honor reduced motion. +- Archive retains real metadata/logs/outcomes, defaults to per-game history, offers alternate history + timeline and the approved story statistics/outcome bars. Exclude ignored/unresolved entries from + completion-rate denominators; unknown ends stay unknown. Explain incomplete pre-redesign history. +- Backups retain seven daily, four weekly and six monthly snapshots, latest per disjoint UTC bucket, + plus recovery. Created during app use, not magically on unused days. View backup → preview → + Restore this backup; support recovery/Undo. Keep progress merge distinct from full restore. +- Real codes notify in-app. Viewing clears per-game newness separately from per-server redemption; + codes and event newness do not clear one another. No sample codes/history/health as production data. + +## 7. Existing implementation map: preserve useful machinery, replace presentation faithfully + +Production entry is `main.tsx` → `App.tsx` (stable re-export) → `ReaderApp.tsx`. +`ReaderContent` coordinates pages, focus, feed/state, archive transitions, settings and detail adapters. +Only the **v2 reader store** is mounted for production writes. Older hooks still supply validators, +types and pure helpers; **do not mount their v1 persistence effects alongside the v2 store**. + +| Files | Existing responsibilities / cautions | +| --- | --- | +| `src/client/state/reader/schema.ts` | Zod v2 envelope, stable local account IDs, clocks, placements, archive/seen snapshots; passthrough unknown fields; account/clock reference validation | +| `migration.ts`, `legacy.ts` | v1 migration, original-account mapping, retained raw bytes, startup old-tab deltas and deletion handling | +| `store.ts`, `reducer.ts`, `lock.ts`, `useReader.ts` | Serialized read-latest writes, operation replay, field timestamps/tombstones, account/pin rebase, storage-event updates, visible save errors | +| `accounts.ts`, `clock.ts`, `src/shared/readerClock.ts` | Account lifecycle, automatic clock metadata revisions, override/revert, active versus historical-only imported clocks | +| `eventRows.ts`, `routines.ts` | Account-aware real selectors and custom recurrence projection; useful data sources for faithful new markup | +| `archive.ts`, `archiveStats.ts`, `aliases.ts` | Durable snapshots/outcomes/restore/expiry, real statistics, retired/merged ID carry | +| `backups.ts`, `restore.ts`, `export.ts`, `import.ts` | UTC retention, validated versioned envelopes, recovery/pending restore, legacy imports, progress-only merge | +| `ids.ts`, `actions.ts` | HTTP-compatible identity generation, action timestamps/operation IDs | +| `src/client/state/appearance.ts` | Saved palette plus readable derived tokens against surfaces; check reference fidelity when applying | +| `src/client/state/displayTime.ts`, `CustomForms.tsx`, `useCustom.ts` | IANA display formatting, manual wall-time parsing, DST behavior and interpreted-zone context | +| `src/shared/time.ts`, `daily.ts`, `goal.ts`, `task.ts` | Existing domain rules extended with optional injected reset timing; defaults preserve old behavior | +| `src/client/components/redesign/` | Current replacement pages — functional starting points, **not accepted presentation** | +| `src/client/redesign.css`, `styles.css`, `index.html` | New overrides/fonts/prepaint; existing old CSS/components still influence internals | +| `api.ts`, `sw.js`, `useAppUpdate.ts` | Separate event/code feeds; network-first data; unchanged waiting-worker update handshake | + +Existing UI files include AppNavigation, SettingsLayout, GameSettings, RoutinesPage, DailyPanel, +RoutineGroup, RoutineHistory, TimelinePage, ArchivePage/ArchiveStory, BackupPage, AppearancePage, +TimeSettings, UpdatesPanel, DataSourcesPage, FeedFooter, SetupWizard, ReturningTour, TrackingChoices, +CodesPage and ChangelogPage. Reuse their real operations where sound; change their markup/layout to +match the approved reference. Do not preserve a rejected arrangement just to keep a component name. + +The prior wrapper-file substitutions are recorded in the ledger. They do not justify retaining +presentation mismatches. Likewise, prior DOM/render tests may assert the rejected structure; update +those intentionally while retaining meaningful state, accessibility and data-preservation coverage. + +## 8. Data that must survive this correction + +Browser-local accounts are game/server tracking identities, **not authentication accounts**. +There is no server copy of user progress. UI work must preserve existing v1 and already-created v2 +state, account IDs, original event/task/daily IDs and day/period labels. + +Active storage keys: + +``` +gacha-tracker:v2:reader +gacha-tracker:v2:migration-recovery +gacha-tracker:v2:backups +gacha-tracker:v2:restore-pending +gacha-tracker:v2:restore-recovery +``` + +Do not delete `gacha-tracker:v1:*`. `ReaderState` contains `accounts`, per-account `data` with progress/ +ignored/daily/amount maps, `archive`, `seenEvents`, prefs, custom games/events/tasks, `codeSeen`, +`codeRedeemed` and `eventSeen`. Daily/amount maps are nested under clock revision. Archive/seen keys +are `JSON.stringify([accountId,eventId])`; those are distinct from immutable domain IDs. + +Actions carry timestamps and operation IDs. Ordinary writes use Web Locks; browsers without Web +Locks use an IndexedDB readwrite transaction mutex. Failure is disclosed without publishing a +successful in-memory save. Account updates capture a base and rebase changed routine/pin fields. + +Exports: `format: "gacha-tracker-export"`, `version: 2`, `exportedAt`, `reader`, `legacy`; progress +exports mark `scope: "progress"`. Full restore validates first and saves recovery plus a pending +transaction before replacing live state; bootstrap resolves pending restore before mounting writers. +Progress imports merge by game/server, preserve local prefs/active reset, retain imported clock +history and preserve unknown extensions. Backups are outside the envelope to avoid recursive growth. + +Important fixes already made and regression-tested — retain them: + +- Initial event-newness seeding is batched; per-account effect writes previously created an + O(accounts²) queue that made settings appear unresponsive. +- Capture/reconciliation waits for finished onboarding; otherwise unchosen catalogue games enter + new readers' seen history before they select games. +- Deleted custom events/occurrences must not reappear from seen-event fallback. A vanished source + event with an unpassed/unknown end remains accessible; distinguish deletion from source absence. +- Imported clock revisions retain historical logs without changing the local active reset. + Equality ignores import-only `historicalOnly` so repeated imports do not duplicate revisions/logs. +- Incoming opaque root/account extensions survive subsequent exports/transfers and deduplicate. + Imported seen markers remain distinct from redemption and are merged appropriately. +- Retired accounts Resume by actually removing `retiredAt`; changed-field rebase must handle deletion. + Independent tabs editing pins on the same routine must preserve both fields. +- Reset override/revert and automatic metadata correction append revisions, never rewrite old ticks. + History resolves the explicit original revision, not today's reset. +- Alias carry includes amount history and archive/seen snapshots as well as progress/daily/ignore. +- v2 preferences are read before initial paint; user choices must not be replaced by old v1 defaults. +- Manual refresh fetches both feeds, ignores build-only timestamp movement and discloses partial + code-feed failure. Codes page discloses stale data. Source diagnostics never invent missing causes. +- Native numeric controls keep a local draft so async persistence does not swallow rapid typing. + +These foundations have substantial tests, but are not an assurance that every integration is correct. +Review real consumers while changing markup; do not label the state layer or presentation complete +without verifying the resulting flows. + +## 9. Critical HTTP/WireGuard regression: do not reintroduce + +The owner uses an HTTP network address through WireGuard: **10.20.20.2**. +They encountered on first visit: + +> Event Clock could not open. Your saved data could not be opened safely… + +Exact reproduction: plain HTTP hostname in Chrome → `isSecureContext === false`, +`crypto.randomUUID` undefined, `crypto.getRandomValues` available. Startup account minting threw; +`initializeReader` caught it and returned generic `unavailable`, misleadingly blaming saved data. +Localhost-only checks missed it because localhost is treated as a secure context. + +Fixed in `state/reader/ids.ts` using 128-bit `crypto.getRandomValues` tokens. Use `mintAccountId()` for +new accounts and `mintLocalId()` for operations, reset revisions and backups. **Never reintroduce +direct client `crypto.randomUUID()` or remint existing IDs.** There are three new regressions in +`test/reader-http.test.ts` that failed before the fix. Existing legacy bytes were preserved. + +Actual HTTP-origin browser checks pass: old-progress migration, new setup, editing, extra server, +reset override/revert, backups and reload. IndexedDB locking works on this origin too. Secure-only +features must degrade honestly (e.g. clipboard may require manual copy; a service worker is not +available on ordinary HTTP). Do not claim HTTP offline support just because localhost SW tests pass. + +## 10. Codes ingestion is real and already implemented + +Do not replace it with prototype codes or repeat external fetching just to work on presentation. + +- Source: `https://game8.co/games/Genshin-Impact/archives/304759`. +- One descriptive-aggregator robots request and one page request returned 200 during capture. + No header-shaping/access workaround or repeated community requests were used. +- Pinned: `fixtures/genshin/game8-codes-2026-10-16.html`, expected JSON and capture metadata. +- Snapshots: `snapshots/genshin-game8-codes.html`, meta, `snapshots/codes-first-seen.json`. +- Parser/registry: `src/ingest/codes/game8.ts`, `registry.ts`, `build.ts`. +- Offline builder: `scripts/build-codes.ts`; published file `data/codes.v1.json`. +- `src/shared/codes.ts` validates codes/feed. Source-stated calendar expiry is `expiresOn`, not a + fabricated `expiresAt` instant. Unstated expiry remains null. First observation survives rebuilds. +- The captured current section has ten actual codes; the three expired programme codes are fenced + out. Tests include a future-expiry variant that admits those programme codes appropriately. +- `scripts/refresh-sources.ts` combines event adapters and count-only code sources under the original + robots, six-hour floor, host spacing, conditional requests and snapshot gates. Event adapters + still return GachaEvent arrays; codes are not fake events. +- `package.json` build includes `build:codes`; the service worker caches the separate feed correctly. + +Use the fixtures/snapshots offline. Existing parser/date rules and source conduct remain binding. +No SQLite, remote sync, push service, unrelated parser changes or new runtime dependencies are needed. + +## 11. Verification already available — and its limits + +Latest completed verification after the HTTP fix: + +- `bun test`: **1,590 pass, zero failures, 75 files**. +- `bun run typecheck` and `git diff --check`: pass. +- Full offline build before the HTTP follow-up: 209 events across 20 games, ten codes. +- Latest JS/static rebuild after HTTP fix: shell `9752bd6bf1d2`, 119 bundled modules. +- Earlier Docker COPY-layout suite, without docs/public: 1,586 pass, one intentional README-only + skip, zero failures (before the three HTTP tests were added). Recheck after your changes. +- Independent read-only reviews found/fixed several storage/integration issues; focused HTTP review + found no important issues. These reviews did **not** establish visual fidelity. + +Prior browser checks covered migration, default view, both interfaces/themes at 1440/390, densities, +reduced motion, an effective 200% CSS viewport, onboarding, direct ticks, extra servers, reset history, +code redemption, Done/Ignore/Undo, custom unknown-end events, real JSON download/upload/restore, +backup restore/Undo, timezone search, offline reload and the actual waiting-service-worker prompt. +These are behavioral evidence and regression starting points; the owner still rejected the UI. + +Tests that pin state, clocks, source honesty, archive denominators, retention, quota/recovery and +imports remain valuable. Some late render tests passed immediately because code already existed; +not every historical checkbox is evidence of a genuine RED test run. + +## 12. Local tools and traps + +Commands (Bun, React 19, strict TypeScript, Tailwind, Zod; use existing dependencies): + +```bash +bun test +bun run typecheck +bun run build:css +bun run build:js +bun run build:static +PORT= bun run serve.ts +bun run build # offline feed + codes + shell, no wiki fetch required +git diff --check +``` + +- Use `--production` exactly as the existing JS build does. `NODE_ENV=production` or a define alone + can produce a cleanly built bundle that crashes with jsxDEV errors; real-browser loading is required. +- **Fresh port after each rebuild:** a localhost/HTTPS service worker caches the shell cache-first. + Do not diagnose old cached bundles as new-code behavior. Ordinary HTTP also caches JS/CSS in-browser; + use fresh origins/cache bypass for verification and a hard refresh for the owner. +- Stop only your own servers by exact PID/port; never `pkill -f serve.ts`. +- To inspect the reference directly, serve a separate scratch origin with + `PUBLIC_DIR=design/mockups PORT= bun run serve.ts`, then open + `/round7/converged.html` or `/round6/b-rail.html`. Keep mockup and production browser contexts + separate. Never point prototype handlers at production storage. +- Last known preview servers were 41880 and 41881, serving rebuilt `public/`; recheck before use. + Earlier verification servers 41873–41879 were stopped. Do not assume temporary processes persist. +- `public/data/` is ignored. A fresh checkout/worktree has no feed until built/copied. No worktree is + requested here, but this trap matters if the environment is reconstructed. +- Browser storage seed is now v2 after initialization. `gacha-tracker:v1:prefs` with `onboarded:true` + works only before first migration. Use disposable contexts and reload after intentional seeding; + do not let mounted effects race test setup or touch the owner's real storage. +- Wait for rendered content, not `networkidle0`. Use `textContent`, because CSS uppercase changes + `innerText`. Keep capture scale about 3× or less to avoid blank sticky panes. +- Tests must not require build output or docs. `test/serve.test.ts` creates its own tree. +- In this tool sandbox, localhost listening misleadingly reported EADDRINUSE; unrestricted runs + passed. Tests/Chrome/server calls required the appropriate tool escalation. Do not confuse that + restriction with an actual application failure. + +Chrome: `/usr/bin/google-chrome-stable`. Scratch Puppeteer import used successfully: + +``` +/tmp/claude-1000/-home-user-gacha-event-tracker/06f9b760-325c-487b-bd28-0493ad38d1b2/scratchpad/pp/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js +``` + +Scratch Prettier: `/tmp/gacha-redesign-format/node_modules/prettier/bin/prettier.cjs`. +No browser/format dependency was installed into the project. Discover/recreate scratch tooling if +these paths do not survive; do not add runtime dependencies for it. + +Temporary browser scripts (inspect their origins/selectors and adapt; these assert rejected markup): +`/tmp/redesign-browser-journeys.mjs`, `redesign-browser-matrix.mjs`, `redesign-browser-custom.mjs`, +`redesign-browser-update.mjs`, `redesign-browser-release-matrix.mjs`, `/tmp/check-reader-release.mjs`. +HTTP reproduction/fix verification: `/tmp/reproduce-reader-first-visit.mjs`, `/tmp/verify-reader-http.mjs`. +The latter launches Chrome with `--no-proxy-server` and +`--host-resolver-rules=MAP event-clock.test 127.0.0.1` and uses `http://event-clock.test:`. +This exercises the HTTP security boundary locally; no request to the owner's WireGuard machine is +necessary. Temporary artifacts are not the durable source of requirements or acceptance. + +## 13. Recommended correction sequence + +1. Inspect the complete approved Round 7 and Round 6 B in a browser. Inventory screens, controls, + overlays and states. Capture the target before editing. Start with the real rendered markup. +2. Reopen the plan's presentation/fidelity gates. Record a concrete component mapping from reference + elements to JSX and existing selectors/actions. Keep already-tested domain/state machinery where + correct. Do not create another design or hide differences behind new CSS overrides. +3. Port the shared shell and Checklist faithfully, including exact event structure, header/counts, + rail hierarchy, daily tick treatment, grouped routines, update rows, Standard/Compact and mobile. +4. Port Routines/library/create/edit, per-game settings/sidebar/profiles, appearance/timezone and + source/backup/about/changelog pages. Preserve the approved information architecture and controls. +5. Correct Timeline as a genuine shared time-axis view, with routine periods/progress/reset markers, + optional adjacent reset strips and the correct global queue. Preserve account-aware identities + and event opening for multiple servers/custom occurrences. +6. Port Archive story/outcome bars/filtering, per-game/history layouts, details and both onboarding + flows. Connect actual empty, unavailable, stale, unknown-end and legacy-history states honestly. +7. Verify each screen against equivalent reference states while developing, then repeat the full + matrix and behavior/data-preservation suite. Update docs to the accepted result, not to a newly + invented substitute. Leave integration/commit/publication for explicit owner instruction. + +This is suggested sequencing, not permission to defer required screens. Complete the approved scope. +If production facts require a genuine departure, identify the exact reference behavior and reason; +do not silently substitute a different interaction. + +## 14. Completion criteria for the next agent + +- Side-by-side target/production comparison for both references in equivalent states, using matched + or deliberately representative content. Explain data-dependent differences rather than dismissing + markup/layout mismatches as real-data effects. Inspect DOM structure and computed styles where + useful; screenshots alone do not verify hidden interactions. +- Check Checklist, Timeline, Routines, library/create/edit, game Setup/Codes/About, every Settings + page, Archive/history/details, new onboarding and returning tour, including empty/error states. +- Desktop and phone, light/dark, Standard/Compact, multi-game and focused/single-game, two servers, + long titles, missing descriptions, unknown ends, stale/fresh sources, custom colours/timezone, + keyboard/focus, reduced motion and 200% effective viewport. No clipped controls or page overflow. +- All existing meaningful regressions plus updated presentation tests, strict typecheck, production + build and real-browser load pass. Tests remain offline and independent of public/docs. +- Ordinary HTTP network-origin startup and subsequent writes remain working. Preserve all migration, + recovery, cross-tab, reset-history, code-newness/redemption and import/export protections above. +- A final reviewer must explicitly compare against the approved rendered mockups as well as audit + correctness. Review “looks similar” or “no overflow” is insufficient. Do not report completion + based on counts of passing tests while known design differences remain. +- Update AGENTS/product/design/data/architecture/source docs and handoff/status records honestly. + No false completed checkboxes; record material limits and no invented data. No commit/push/publish + without authorization. + +## 15. Prompt to give the replacement agent + +> Read `AGENTS.md` and `docs/handoffs/2026-10-16-redesign-fidelity-handoff.md`. Finish the approved +> redesign in the current working tree. The current implementation was rejected because it +> substituted its own presentation for the approved markup. Faithfully translate +> `design/mockups/round7/converged.html` (Relaxed/default) and +> `design/mockups/round6/b-rail.html` (Structured) into React and wire real state/data. Preserve the +> existing tested migration, storage, reset, backup, codes and HTTP/WireGuard fixes. Compare actual +> reference and production screens/interactions throughout; do not treat the old completed plan, +> passing tests or rejected screenshots as design acceptance. Complete all required screens and +> validation. Leave changes uncommitted and unpublished unless I separately authorize integration. diff --git a/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md b/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md new file mode 100644 index 0000000..b72c360 --- /dev/null +++ b/docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md @@ -0,0 +1,926 @@ +# Redesign handoff: second implementation rejected + +Date: 2026-10-17. Repository: `/home/user/gacha-event-tracker`. + +## Start here: launch the current server, then ask the owner + +**Explicit owner instruction for the next agent:** launch the current server, make it accessible +on `0.0.0.0`, give the owner its browse URL, **then ask the owner for the biggest differences from +the mockup**. Get that feedback before making more UI changes. Do not begin another broad visual +rewrite or substitute your own audit for this conversation. + +Suggested question after the server is reachable: + +> The current build is running at [URL]. What are the biggest differences you see compared with +> the approved mockup? Which screen and interface should we fix first? + +The owner has not yet identified the specific screen, viewport or visual differences behind this +second rejection. Do not guess that it is Checklist, Structured, a font-loading problem, or stale +cache. An optional screen/layout question was sent, but the owner instead requested this handoff. + +At the handoff, the current build was running at **http://10.20.20.2:41908**, listening on all +interfaces (`*:41908`), Bun PID **2536610**. `/api/health` responded successfully. The process may +not survive the session; check rather than assuming it is running. `serve.ts` uses `Bun.serve`'s +all-interface default even though its log prints `localhost`. Verify the actual listener. + +If it needs restarting, from the repository root: + +```bash +PORT=41908 bun run serve.ts +``` + +If that port is occupied by the current server, reuse it; do not kill unknown processes. If the +shell is rebuilt, use a fresh port to avoid a previous service worker serving an old shell. +For a missing/stale build, rebuild only the shell, preserving the existing feed: + +```bash +bun run build:css && bun run build:js && bun run build:static +PORT= bun run serve.ts +``` + +Use the machine's reachable network address in the URL, not `0.0.0.0` as a destination. The owner +uses WireGuard `10.20.20.2`. Binding may require command escalation in the agent sandbox. Do not +fetch community sources just to start the UI. + +## Current acceptance and scope + +The owner asked for a faithful implementation of the already-approved mockups, then explicitly +said: **“Also make the structure layout the default.”** Structured is now the absent-preference +and new-setup default; an explicitly saved Relaxed choice is preserved. + +After this agent claimed the correction was complete and started the build for the owner, the owner +said: + +> “This is still not even close to the mockup.” + +The owner then stopped further implementation and requested a handover specification. **The current +implementation is rejected. The previous completion claim and visual-review verdict do not establish +acceptance.** This handoff turn makes documentation changes only, retaining two audit screenshots +captured by a browser inspection that had already started when the owner asked to stop. + +The unrelated request about sorting PDF-export examples was explicitly withdrawn as a message meant +for another agent. It is not part of this repository's task. + +## Approved visual authority + +1. Latest owner feedback, including the launch-and-ask instruction above. +2. `design/mockups/round7/converged.html` — Relaxed. +3. `design/mockups/round6/b-rail.html` — Structured, specifically B. +4. Approved functional/state requirements in + `docs/superpowers/specs/2026-10-16-ui-redesign-design.md`. + +Read `AGENTS.md`, relevant product/design/domain docs, and the previous handoff +`docs/handoffs/2026-10-16-redesign-fidelity-handoff.md`. The earlier handoff remains useful for the +first failure, reference-file anchors and invariants. This document supersedes its current-status +and next-action instructions. Historical statements that Relaxed is the default are superseded by +the owner's Structured-default instruction. + +The reference HTML files contain multiple generations of render functions and CSS overrides. The +fully rendered final file is the reference, not the first function with a matching name. Preserve +composition, proportions, hierarchy, typography, controls, density, responsive behavior and page +organization when translating HTML into React. Do not treat the reference as inspiration. + +Necessary authorized adaptations remain narrow: focus pills in both interfaces, real data and +account-aware state, independent placements and servers, and the copy “Event data may be out of +date.” Never ship the prototype's fixed clock, synthetic events/history, demo toolbar or demo state. + +## What this attempt changed + +This is an inventory, **not a list of accepted presentation decisions**: + +- `src/client/ReaderApp.tsx`: central shared UI composition, navigation counts, release notice, + settings routing, dedicated checklist components, archive-detail routing. +- `components/redesign/ChecklistEvent.tsx`: new event rows and `NextDeadline`, replacing old + `EventRow`/`NextUp` usage in the active Checklist. +- `FocusPicker.tsx`, `DailyPanel.tsx`, `RoutineGroup.tsx`: rail and direct progress controls. +- `RoutinesPage.tsx`, `RoutineEditor.tsx`: reset/game grouping, table columns, progress bars, + library, independent pin controls and native custom-routine drawer. +- `TimelinePage.tsx`: shared time grid for events and routine periods, sticky names, progress fill, + resets, zoom and mixed deadline queue. Sticky bar labels were repaired after review. +- `ArchivePage.tsx`, `ArchiveStory.tsx`, `ArchiveDetail.tsx`: outcome story/filter/sidebar/list/history + and a saved-record dialog. The archive dialog reads recorded progress rather than today's live + event state. +- `SettingsLayout.tsx`, `GameSettings.tsx`, `AppearancePage.tsx`, `TrackingChoices.tsx`: individual + game navigation, separate settings pages, interface/scheme controls, tracking/server/colour/reset + groups. The final profile uses tabs to choose which server's tracking choices are edited. +- `SetupWizard.tsx`, `EventDetail.tsx`: modified setup/detail presentation, retaining much existing + form content. +- `src/client/redesign.css`: large stylesheet, with initial rules plus later overrides. It is about + 2,900 lines and contains repeated selectors and superseded layout rules. +- `state/usePrefs.ts`, `state/appearance.ts`, `index.html`: Structured default and interface grounds. +- Tests and docs were updated. No change in this attempt intentionally reminted event/account IDs. + +Component shorthand above resolves to `src/client/components/redesign/`, except `EventDetail.tsx`, +which is `src/client/components/EventDetail.tsx`. `state/` shorthand resolves under `src/client/`. +`AppNavigation.tsx` was also changed: live counts and Settings placement. The new component files +introduced by this attempt were `ChecklistEvent.tsx`, `FocusPicker.tsx`, `RoutineEditor.tsx` and +`ArchiveDetail.tsx`; many other untracked files were already present from the preceding agent. + +Additional implementation details worth retaining: + +- `prefs.routineGroup` is a new optional stored preference: absent/`reset` means By reset; `game` + means By game. It uses the existing permissive preferences envelope, with no new storage key. +- `appearance.ts` applies Structured Slate grounds/surfaces separately from Relaxed, while keeping + contrast adjustment and existing custom palettes. The CSS and runtime tokens must be examined + together: runtime variables can override apparent reference values in the stylesheet. +- `TimelinePage` exports `routinePeriod`, also used for Routines reset countdowns. It uses resolved + reader clocks, not guessed UTC boundaries. Custom timeline occurrences respect account event intent. +- `placementAccounts` in `RoutinesPage` combines bulk pin changes per account. Reverting to one stale + full-account write per row can silently undo an earlier row's placement. +- `ArchiveDetail` displays the stored outcome/date/note/check-ins/amounts; Restore uses the existing + reducer. Already-ended records can be reconciled back into history. It is intentionally separate + from the live `ReaderDetail` adapter. +- `RoutineControl` keeps a numeric draft during editing so a transient empty input is usable. + Game colour previews use the rendered, contrast-adjusted game metadata. +- The profile server tabs are an implementation adaptation, not a separately approved owner design. + The reviewer accepted them within its narrow follow-up; the owner's overall rejection supersedes + that. Revisit them if they are among the differences the owner identifies. + +Tests changed during this attempt: + +| File under `test/` | Change | +| --- | --- | +| `appearance.test.ts` | Structured default and retained explicit Relaxed distinction | +| `prefs.test.ts` | Default/fallback expectations and saved Relaxed normalization | +| `reader-store.test.ts`, `reader-restore.test.ts` | Test choices adjusted so writes/restores still change the now-Structured default | +| `redesign-timeline.test.tsx` | Routine resets participate in mixed Ending soonest order | +| `redesign-routines.test.tsx` | Bulk placement combines per account and preserves the other view's pin | +| `game-settings.test.tsx` | Updated expected guidance for independent server progress | + +Documentation touched by this attempt includes `AGENTS.md`, `docs/PRD.md`, `DATA-MODEL.md`, +`DESIGN.md`, `ARCHITECTURE.md`, the approved spec/plan, the first handoff, the correction report, +and `.superpowers/sdd/2026-10-16-ui-redesign/progress.md`. Other dirty docs and ingestion files may +belong to the earlier agent; do not attribute all of `git diff HEAD` to this attempt. There is no +clean per-agent commit boundary. The real changed code remains in the working tree. + +## What went wrong + +### 1. Translation remained approximate + +The first rejected agent had wrapped old presentation with new CSS. This agent replaced more actual +markup, but still recreated the reference piecemeal instead of establishing a complete translation +of the final reference structure. It retained old form internals and filled gaps with implementation +choices, then added CSS overrides until selected screenshots looked closer. + +Concrete examples caught during this attempt's review were substituted Settings navigation, a +routine Cycle column and missing live summary, invisible timeline bar text, and game colour above +tracking with repeated tall account blocks. Those particular defects were subsequently corrected. +**Their correction is not proof that the remaining screens or overall design are faithful.** The +owner's latest rejection requires a fresh understanding of the gaps, starting with their feedback. + +### 2. Comparison fixtures made the evidence narrower than the claim + +`docs/design-reviews/fidelity-capture.mjs` freezes the clock, intercepts the event-feed request with +mockup events, and seeds disposable routine/archive state. That can isolate visual differences, but +it is not the app the owner browsed. For Checklist it explicitly hides `.updates-panel` and +`.release-update` to align the first viewport. The comparison's event/account counts also differ +from the prototype, and the custom prototype routine was not seeded. + +A real-feed capture after the rejection shows two prominent notice bands above Running now, many +short title-only events, and much different content density. A fixture-based image with hidden +notices did not validate that real first impression. Do not suppress production UI in an acceptance +capture. If fixture comparison remains useful, label it as a separate diagnostic and also capture +the exact normal-use state the owner is judging. + +### 3. Functional checks became a proxy for the visual task again + +Tests, no-overflow checks, correct IDs, working locks and successful actions are necessary. They +cannot certify fidelity. This attempt's final response led with rebuilt screens and test success +and reported that review cleared the issues. That was too strong for the evidence and the owner's +actual experience. + +### 4. The independent review's scope was stretched + +The reviewer found a finite list of material issues, then scored fixes to that list. Its final +verdict was **“ship the reviewed corrections”**, explicitly limited to those findings, not blanket +acceptance. Subsequent passes were instructed not to reopen a wider hunt. That process could verify +local repairs while leaving broader divergence undiscovered. The owner rejected the result anyway. + +Do not continue from the review's ship verdict. Treat the supplied user feedback as the next primary +input, then review the whole affected composition against the rendered reference. + +### 5. Documentation presented implementation as the design truth + +`docs/DESIGN.md` and the 2026-10-17 correction report describe the implemented design confidently. +They are records of what was built, not evidence that it is approved. Earlier failure had the same +pattern. Keep requirements, current implementation, functional verification and owner acceptance +separate. Do not rewrite the spec to make the implementation look compliant. + +## Evidence and limits + +- Historical correction report and staged comparison images: + `docs/design-reviews/2026-10-17-redesign-fidelity.md` and its linked assets. + These now carry a rejection warning. They document a rejected attempt, not new visual goldens. +- Fresh real-feed audit image: + `docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-checklist.png`. +- Fresh first-visit setup image: + `docs/design-reviews/assets/2026-10-17-second-rejection/current-structured-setup.png`. + +The two fresh images used Chrome at 1440×1000 on `http://10.20.20.2:41908`. The setup was a clean +browser context. For Checklist, that disposable context was marked onboarded and limited to +Genshin/HSR/ZZZ/Wuwa; the actual feed and notices were retained, with no request interception. +**These are not screenshots of the owner's browser or stored preferences.** The browser reported +Structured and loaded Barlow/Barlow Condensed. This rules out missing fonts in that test context +only; it does not identify the owner's complaint or prove their environment matches it. + +One observable item for later inspection: the actual audit showed a Checklist header count of 46 +and Focus All count of 60. They currently count different row sets. Do not claim this is the owner's +main concern without asking. Likewise, correct shorter rows without descriptions are an approved +requirement, so do not fabricate summaries merely to match fixture density. + +## Preserve the working state and domain work + +HEAD remained `ee41389`. The checkout is dirty on `main`; the last status had 89 modified/untracked +entries including work that predates this agent. No commit, push, PR or deployment was made. Running +a local preview server was explicitly authorized. Do not reset, clean untracked files, discard the +reader implementation or create a new branch/worktree without a new reason/owner instruction. + +Preserve, or carefully test any necessary changes to: + +- v2 reader envelope, migration/recovery, unchanged IDs and legacy bytes; +- account-specific progress, saved clocks and archive snapshots; +- Web Locks plus IndexedDB serialization fallback, save failures and cross-tab synchronization; +- backup/export/import/recovery and independent code seen/redemption state; +- unknown-end/date-precision semantics and community-source conduct; +- insecure HTTP identity generation using `crypto.getRandomValues`, not `crypto.randomUUID`; +- new Structured default and explicit saved Relaxed preference; +- useful fixes from this attempt: bulk routine placements saved together per account, row keys + containing account+routine, and archived details using saved snapshots. + +The most recent successful checks before handoff were 1,594 tests passing, TypeScript passing, +production build `a28021bbe1a8`, ordinary HTTP migration/setup/server/reset/backup checks, numeric +progress and routine creation, completion/Undo/archive Restore, and separate concurrent/offline +browser checks. The Docker-equivalent file layout passed 1,593 tests with one intentional skip; +no Docker image was built. This is functional evidence only, not visual acceptance. + +## Recommended continuation after owner feedback + +1. Write down the owner's biggest differences in their words, with the relevant interface, screen + and viewport. Launch-and-ask comes before implementation. +2. Render that exact approved reference state and current app state. Compare actual content, + visible notices and first-visit/returning conditions as appropriate. Record differences in + topology, dimensions, type hierarchy and controls, not just a list of CSS values. +3. Repair that bounded surface by translating the final reference markup and styles into the real + component tree. Replace rejected structure when needed; avoid another stylesheet of corrective + overrides over structurally different elements. +4. Keep an explicit list of necessary real-data/accessibility adaptations, with reasons. Ask the + owner when an adaptation changes a visible product decision instead of silently choosing it. +5. Show the normal running build to the owner at the chosen checkpoint. A fixture capture, passing + detector or subagent approval cannot substitute for checking their reported differences. +6. Then carry accepted treatment across the remaining screens, checking both interfaces and mobile, + with functional regression checks proportional to the change. Correct docs only to reflect what + has actually been established. + +No further UI implementation was performed after the owner requested this handoff. + + +## New-session continuity checklist + +- Start the new session in **this same checkout**. All implementation files, reference HTML, + screenshots, plans and handoffs are on disk, including untracked files. A fresh clone of HEAD + will NOT contain this work; no commit/push was authorized or performed. +- This handoff plus the linked spec, previous handoff and review records contain the material + decisions and continuation context. They are not a verbatim chat transcript. The owner's exact + visual objections beyond “not even close” are deliberately marked unknown because they were + never supplied; the next session must ask, not reconstruct them from guesses. +- The latest user instruction is to pause implementation for handoff, then have the next agent + launch the current server and ask about the biggest differences. No new design was approved + after either rejection. No permission to commit/publish was given. +- `public/` and `public/data/` are ignored build artifacts. Keep the current feed when starting; + if it is absent, use the documented offline feed build, not a network refresh. A changed port + is a different browser origin, so it will not automatically carry the owner's saved progress. + Do not clear or migrate the owner's browser storage merely to obtain a clean screenshot. +- Durable reproduction entry: `docs/design-reviews/fidelity-capture.mjs`, with reference fixtures + beside its images. Its staged-state limitations are described above. Chrome is at + `/usr/bin/google-chrome-stable`; install/use Puppeteer in scratch, not project dependencies. +- `/tmp/fidelity-http-final.mjs`, `/tmp/fidelity-journeys.mjs`, `/tmp/fidelity-matrix.mjs` and logs + were scratch functional checks, not durable requirements. They may still exist, but have fixed + ports and scratch imports; do not assume they survive or run unchanged. Standard test commands, + checked-in test sources and the recorded browser scenarios are the durable verification contract. +- Preview/mockup processes may survive: this run used app ports 41902–41908 and mockup41901. + Inspect before starting/stopping; never `pkill -f serve.ts`. PID2536610 was the owner preview at + recording time, not a permanent identity. Do not treat old port numbers or tool session IDs as + proof of the build currently being served. +- No UI edits were made in the handoff/continuity-check turns. The second-rejection warnings in + the older report, plan and design documentation exist to prevent another agent mistaking their + earlier positive wording for owner acceptance. + +## Session 3 (2026-10-17): owner feedback on new-user setup, and the rebuild + +The owner chose to start with new-user setup, against Round 6 (`b-rail.html`, final `onboarding()`). +Their differences, in their words, summarised: + +1. Screen 1: “The margin/padding, sizing and placement of buttons is off.” +2. Screen 2: “The games are way too big, and not grouped together, not shown with colors, no + choose all games instead. The back button does work better though.” +3. Screen 3: no table to choose events/dailies/weeklies; instead the per-game subscription block + was “copy pasted” for every game. “Show events that haven't started” was “a random, flowing + button”; in the mockup it sits between dividers with a description. +4. Screen 4: “a weird checklist-timeline switch, instead of the drawing and descriptions the + mockup has.” + +What changed: `SetupWizard.tsx` was rewritten as a translation of the rendered mockup markup +(`.ob`, `.preference-grid`, `.gpick`, `.type-guide`, `.availability`, `.mtx`/`.tg` with a +“Same for all” row and mixed state, `.upcoming-choice`, `.vcard` previews, `.ob-foot`), and the +scattered `.setup-wizard` overrides in `redesign.css` were replaced by one block at the end of the +file translated from the mockup's rules. Game order follows `GAMES`; colours use the app's +contrast-adjusted `meta`. + +Deliberate deviations, for the owner to confirm: + +- The Interface select and the “Everything stays in this browser” line were removed from step 1: + neither is in the mockup or the spec's setup list. New installs still get Structured, and + Interface remains in Settings → Appearance. +- The back link reads “Back” on every step (the mockup says “Preferences” on step 2), because the + owner said the current back button works better. +- The tracking table's row headers are `` rather than ``, for screen readers. + +Evidence is functional and side-by-side capture only (1440 and 390, dark, fresh browser, real +feed, no interception). **Owner acceptance of these screens is still pending.** + +### Setup accepted as “much better”; Checklist requests (same session) + +The owner then asked, for the Checklist: “Add the time left bar to the compact screen. Make the +pill menu smaller. Let users sleep/dismiss the event data may be out of date for up to 7 days.” + +- Compact rows now show the time-left meter on every row, in a fixed 120px column left of the + time (under the time on phones). Standard rows are unchanged: the meter only accompanies a + description. +- Focus pills are smaller (24px high, 11.5px text, tighter gaps), kept at 30px on coarse pointers. +- The source-age notice offers “Hide for 1 day / 3 days / 7 days”. It stores + `prefs.staleSnooze = { until, games }` in the existing permissive prefs envelope (no new storage + key). It returns when the time passes, when a game outside the snoozed set goes stale, or if the + saved value is malformed or more than seven days ahead. Settings → Source status and the + per-event detail notice are not snoozed. + +### Checklist follow-up (same session) + +Owner: “In compact view, there needs to be some more air, like there is in the mockup compact +view. In standard view, the space between the bottom of the Running now [heading] and the first +event in the list is too big. The Next to expire is too small, compared to the easier to read one +in the mockup, and it doesn't show the next 3 ones to expire, like it does in the mockup.” + +- Compact is now a translation of the Round 6 B ledger (`table.led`): column headings Game / + Event / Time left / Ends in, a 3px game stripe with the short name, 15px titles, a 34%-wide + meter and a 20px condensed time, in 51px rows. It is drawn as a CSS grid over the existing + `
  • ` rows (`LedgerColumns`, `ChecklistEvent compact`) so archive fade/confirm transitions keep + working. Under 860px the game column hides, the game name moves above the title and the meter + spans the row. This replaced the earlier compact `.event-entry` rules entirely. +- Note on references: Round 6 **B** and Round 7 render but hide the rail's “then” list + (`.rail .then{display:none}`); Round 6 **A** shows it with a 30px title and 64px countdown. The + owner asked for the mockup's larger rail with the next three, so the rail head now follows A's + proportions (52px under 1050px; the phone sizes and hidden list at 700px are unchanged). +- `.list-heading` bottom padding 22px → 8px (Relaxed 18px → 8px). +- Relaxed was not visually captured for these changes. +- Rail routines heading (owner): title is “Routines” with “Showing only pinned” as its subtext; + the “Focus on a game to open its routines.” hint was removed as not worth the text (groups still + auto-open on focus). + +### Timeline (same session) + +Owner: the implemented Timeline resembles the mockup well, but lost the liked resemblance to the +paimon.moe-style board on the live site; keep the new design and add a switch between the looks. +The scroll-following bar text of the new look is liked. Dailies should not be on the timeline. Add +a checkbox for time left vs end date. Add a divider where routines start when they have their own +section, and rename “Separate reset strips” to “Show routines in own section”. Later: in new-user +setup, when Timeline is chosen, show both designs and ask. + +Owner's answers to clarifying questions: default look **New**; switch in the **Timeline controls +bar**; classic **reproduced as production** (no added scroll-following text); routines in classic +go in **their own section** under a divider. + +What was built: + +- `prefs.timelineLook` (`"new"` default | `"classic"`) and `prefs.timelineLabel` (`"left"` default + | `"end"`), both in the permissive prefs envelope. The classic look reuses the production board + (`components/Timeline.tsx`) with its own By game / Ending soonest, zoom and Jump to today + controls; the new look keeps its controls. The look switch sits first in the options bar. +- Classic gained an optional `routines` section (`ClassicRoutine`): a divider labelled Routines, + then per-game lanes of dashed-edge bars in the board's style, clamped to the board's window and + with names pinned like its event bars. +- Dailies (`cadence === "day"`) never appear on either look (`onTimeline`). The Routines page shows + a dash instead of a Timeline pin for dailies, and hides bulk “pin to Timeline” on daily-only bands. +- “Show end dates instead of time left” switches new-look bar text to “Ends 20 Oct” and routine + bars to “resets ”. It does not change the classic look, which shows titles only. +- New-look divider: a dashed rule with a sticky ROUTINES label before each game's routine rows + when “Show routines in own section” is on. +- Setup step 4: choosing Timeline reveals “Which timeline design?” with drawn New / Classic cards; + `SetupDraft.timelineLook` is saved only when the reader starts on Timeline. + +### Timeline follow-up: shared pill header, look in Settings, focus bar setting + +Owner: the options (By game / Ending soonest, routines in own section, end dates) must work in the +classic look too; the classic board's small pill toggles look better than the big options bar; the +New/Classic switch belongs in Settings. Then: the focus bar moves under the pill header on the +Timeline, with a Settings option to show it on the Checklist and/or Timeline. + +- `BoardHeader` and `TogglePill` (exported from `components/Timeline.tsx`) are the one header for + both looks: stacking pills, “Show routines in own section” and “Show end dates” toggle pills, + small −/+ and Jump to today. The old `.timeline-options` bar and its CSS are gone. +- Classic now honours both options: `routinesInline` (off → routines at the end of each game's + lane; on → own section under the divider) and `label` (time left / end date shown inside the + pinned name, after the title, shrinking before the title does; omitted on bars under 150px). +- Settings → Appearance: “Timeline design” (New / Classic) and “Focus bar” with “Show on the + Checklist” / “Show on the Timeline” (`prefs.focusBar = { checklist, timeline }`, absent = both). + A view whose focus bar is hidden ignores `focusGame`, so a focus chosen elsewhere can never + filter a page with no visible way to clear it. `TimelinePage` now receives `focus` and + `focusBar` from `ReaderApp` instead of reading `focusGame` itself. +- The classic board keeps its header and focus bar when it has nothing to plot, and draws the + board when only routines exist. +- Setup copy now points to Settings → Appearance for switching designs. + +### Timeline polish (same session) + +Owner: focus bar needs one row on large screens and no “Focus” text; By game / Ending soonest +should share an outline; the two options need an off affordance (checkboxes); routine items could +not be opened, so progress could not be tracked; the classic NOW chip was almost unreadable; open +far enough back / zoomed out so most text shows; always prefer the title over time left / end date. + +- Header: the stacking pair is one rounded outline with the chosen side filled; the options are + small checkboxes (`ToggleOption`, replacing `TogglePill`). +- Timeline focus row (`.timeline-focus`): pills and Next on one row, heading hidden; wraps on + phones. +- Routines open a modal dialog (`RoutineDetail` in `TimelinePage.tsx`) from both looks: game and + server, title, reset time, and the shared `RoutineControl` for progress. It replaced the + off-screen `.timeline-routine-detail` section. +- Opening position (`openingLeft`, both looks; also Jump to today): back to the median running + start, with now kept between 25% and 50% across. The default zoom (32px/day) is unchanged; + readers who already zoomed keep their saved width. +- Classic NOW chip: inline critical background and ground-coloured bold text (`.eyebrow` had been + overriding the text colour with faint ink). +- Classic labels: shown only when `labelFits` estimates room for the whole title and the label. +- **Deferred by the owner:** an option, per routine or for all routines, to show routine dates + only without tracking progress. +- Footer (owner: “sometimes sticky… should not stick and take up space unless you've scrolled all + the way down”): the footer was never sticky. The Timeline boards scrolled inside a pane capped at + 72vh, so the page bottomed out early and left the footer on screen while the board scrolled. + Both panes (`.timeline-scroller`, classic `.scroll-pane`) are now capped at `100dvh`: with the + board filling the window the footer is below the fold, and appears only past the board's end. + +### NOW chip, server names, labels (same session) + +Owner: make “now” even more readable; drop “europe” unless a game has two regions; if title and +end date cannot both be shown in full, drop the end date rather than truncating it. + +- Classic NOW chip: 12px bold display caps, white on a fixed `#d42a47` (≈5:1), 2px ground ring. +- `state/reader/servers.ts`: `manyServers` / `serverSuffix`. Reading views (Checklist rows, Next to + expire, dailies, rail routine groups, Routines table, both Timeline looks, routine dialog, + updates, archive list/detail) name a server only when the game has more than one active account + (archives also count retired ones). Settings views still name servers, where they are the subject. + The “New since you last looked” row now names the game instead of the server. +- Classic bar names are one-line wrapping rows with overflow hidden: the title truncates only when + it alone is too long; labels (and routine progress) never shrink and simply wrap out of view + when they do not fit whole. `labelFits` was removed. +- Header options are now three either/or pill pairs (`PillChoice`, which also draws the stacking + pair): By game / Ending soonest, Show time left / Show end dates, Group routines and events / Show + routines in own section. `ToggleOption` (checkboxes) was removed. + +### Routines view (same session) + +Owner: Timeline accepted (“looking good now”). Routines: the Checklist/Timeline pin icons are not +centred and look odd against the mockup; text does not sit together as in the mockup; keep the +game colour bars; an always-open editor adds clutter — match the mockup, but show that progress +is editable and let the reader enter their own. + +- The table is now a translation of Round 6 B `table.rt`: quiet 11px headings with the list/bars + icons on the pin columns, `.gcell` colour bar + short name (server only when several), 15px/500 + titles muted when met, `.pin` 34×28 icon buttons (plain when off; near-tinted outline when on), + and a `.kebab` “⋯” summary for the existing menu. +- `RoutineProgress.tsx` replaces `RoutineControl` in this table only: `.tick` (Done / To do, or + “n of m days”) and `.amt` (bar + “**2** / 3 runs”, unit from the task or built-in). Amounts read + as text; a pencil (brightens, number gets a dashed underline on hover/focus) signals they are + editable. Click opens an inline number field + Save; Enter saves, Escape or leaving cancels. + `RoutineControl` still serves the Checklist daily panel and the Timeline routine dialog. +- All `.routine-table`/`.routine-pin`/`.routine-progress`/`.progress-track`/`.rtitle` rules + (51, including phone variants) were removed rather than overridden; a single `.rt` block holds + the replacement. On phones, hidden Routine/Progress headings keep their cells so pin headings + stay aligned. +- Owner follow-up: pins fade (150ms colour/background/border, skipped under reduced motion), and a + pinned pin previews “unpin” on hover (thinner tint, dashed outline). Ticks no longer carry + “To do / Done” text; multi-day routines keep “n of m days”, and the aria-label keeps the state. + +### Library and create shelves (same session) + +Owner: Routines accepted. Library and Create “lack a lot from the mockup… they are shelves in the +mockup, where they are weird in the live”. Owner chose to add all four missing create fields now +(Every day, Another hour, Note, Link) rather than defer the data changes. + +- `RoutineDrawer.tsx`: one right-docked modal `` (580px, dimmed backdrop, Escape) with two + panes translated from Round 6 B `libraryDrawer` / `createDrawer`: + - Library: Create-your-own card, intro, search, “New follows also show in Checklist/Timeline”, + per-game sections (“n of m followed · k servers”, Follow all weeklies/monthlies/cycles/ + everything), rows with “Every Monday at 04:00 · 3 runs · Community wiki, checked 14 Oct 2026” + and a Follow/Following button, or one button per server (EU/NA/Asia) when several. + - Create/Edit: examples (mockup's five), What is it, Game, Server (select when several, note + otherwise), Repeats (day/week/month/N days + its detail row), Resets at (game reset / another + hour), Done when (once / several days / a number), Note, Link, Also show in (Timeline disabled + for dailies), live Preview, Create and follow / Cancel with a “Still needed” line. +- The library page mode and `RoutineEditor.tsx` (+ `.routine-editor` CSS) were removed. New + routines now follow on the chosen server only, with the chosen pins; edits keep placements. +- Data: `PeriodSchema` + `{unit:"day"}`; `TaskSchema` + optional `resetHour`, `note`, `link` + (http/https only); `GoalStatus.scope` + "day"; a task's `resetHour` swaps only the hour on its + row clock (same revision key). Notes show under the title in the table; links feed the menu's + Source item. Page summary no longer counts dailies as timeline pins. +- schema-guardian review of the routine format change: no blocker. Nothing stored is dropped or + re-keyed; the HEAD build keeps unreadable tasks raw and carries unknown fields through edits, so + new-format routines survive (a `day` routine is just invisible there). Fixed its two should-fix + items: the edit form warns when a reset-hour change will move which day logged ticks count for, + and `RoutineHistory` computes “today” and the catch-up window from a routine's own reset hour. + Notes left as-is: legacy `Dailies.tsx`/`migration.ts` paths cannot meet `day` routines today + (no caller for the legacy strip filter; legacy data cannot contain `day`). +- Owner accepted the create shelf. Library Follow/Following buttons now fade (150ms, skipped under + reduced motion), matching the Routines pins. + +### Top bar (same session) + +Owner: the logo and the tabs sit at different heights, so the bar feels off-centre. Measured: the +logo's text baseline was 3px above the tab labels' (38 vs 41px; the mockup bottom-aligns boxes, +which reads the same way with a 20px logo beside 15px tabs). Structured `.reader-ui .top` now uses +`align-items: baseline` and `.mark` has no bottom padding, so logo, tabs and Settings share one +baseline at the same 57px header height. Relaxed keeps `center` (its tabs are rounded pills). +- Owner: “the logo still looks higher than the tabs”. A shared baseline still left the logo's 15px + capitals centred 2px above the tabs' 11px capitals. `.mark` now also has `position: relative; + top: 0.1em` (half the cap-height difference), so both sets of capitals centre at the same line + (measured 35.5px). Reset to 0 in Relaxed and on phones, where the logo is not beside the tabs. + +### Settings: About and Data & sources (same session) + +Owner: dates and event-data refresh do not belong in About; move them to Data & sources, and make +the implemented pages look more like the mockup. + +- `AboutPage.tsx` (replaces the `Colophon` render in Settings): the Round 6 B single-column + `.about-prose` — intro, Made by (author, GitHub, source code / report a problem / request a + feature / support), source editors (derived from every feed source via Colophon's `siteFor`), + Game-Time-Master, studios, idea credits, Unofficial and unaffiliated, and a “See data freshness + and source status” button. No dates or freshness. +- `DataSourcesPage.tsx`: intro (incl. which timezone dates use), Check for new data (shares the + footer's handler, now `refreshFeed` in ReaderApp), “Event data last refreshed … · ago”, a stale + warning naming the affected games, and one `.source-row` per game with each source's last + successful check, last content change, a status sentence in the mockup's four cases + (`sourceReason`) and Check source page. `.source-record` CSS removed. +- Every settings title now sits on the mockup's `.ptitle` rule. +- `Colophon.tsx` is no longer rendered by the reader app; its exported constants and `siteFor` + are reused. +- Owner follow-up: About's “The studios and games” lists every game again in its own (contrast- + adjusted) colour after the studios; the “See data freshness” button was removed from About. + +### Changelog (same session) + +Owner: make the changelog look like the mockup, keeping the commit history, spec links and the +New / Games & sources / Fixed kinds. `ChangelogPage.tsx` now renders its own Round 6 B +`.changelog-page .rel` markup (the sheet still uses `ChangelogContent`): an uppercase date line +with the note's kinds, a 26px condensed title, summary paragraphs, the covered commits as a bullet +list each ending in a coloured kind tag and its hash link, then “Commits a…b” and “Spec: …” links; +loose commits follow under “Other changes” by month. + +### Reload keeps your place (same session) + +Owner: refreshing always returned to the default view. The page was React state only +(`useState(prefs.view)`). `state/route.ts` now maps the page, the Settings section and a game's +settings page to the address hash (`#checklist`, `#timeline`, `#routines`, `#archive`, +`#settings/
    `, `#settings/games/`). ReaderApp starts from a valid hash, keeps it in +sync with `history.replaceState` (no history entry per click) once onboarded, follows manual hash +edits, and rewrites an unknown hash to the current page. No hash still opens the default view. +Browser-checked: Timeline, Settings → Changelog, a game page and Archive all survive a reload. + +### Region & time (same session) + +Owner: timezone search did not seem to work; is it needed, or should the full list just appear? +Cause: the search box filtered the options of a closed select (no visible effect until opened) +and matched raw IANA ids only. Decision: removed the search. `TimezoneSelect.tsx` is one native +select — Automatic (device zone + offset) first, then every zone grouped by region (`optgroup`), +named by city (“Buenos Aires, Argentina · GMT-3”); typing in the open list jumps to a city. Used by +Settings and new-user setup. `TimeSettings.tsx` now follows Round 6 B: intro, Default game server +as a segmented control, Your timezone with a “Currently displaying …” note, and Reset timing for +your games — one row per server with its automatic/custom reset, your local time and countdown, +and a Reset timing select (Automatic or a whole hour) that appends a clock revision like the game +page's override (earlier progress kept). Rows resolve the clock at max(now, real time) so a just- +saved revision shows immediately. + +### Default view moved into Appearance (same session) + +Owner: move Default view under Appearance and match the mockup's iconography. The separate +“general” settings section is gone; Appearance has a Default view group with the Round 6 B drawn +view cards (`ViewCards.tsx`, shared with new-user setup), the mockup's copy, and Replay the +introduction. `#settings/general` and a bare `#settings` now open Appearance. The view-card CSS was +un-scoped from `.setup-wizard` (with its border token defined on `.viewpick`). +- Owner: Lists options move into Data & sources. The “lists” settings section is gone; Data & + sources ends with an “In your lists” group (Show events that haven't started; Detect daily + events from source descriptions), each with a one-line explanation. `#settings/lists` opens Data + & sources. Appearance's menu subtitle is now “Theme, colours & default view”. +- Owner: list options belong before the long source list. “In your lists” now sits directly under + the refresh line and stale warning, above the per-game source rows. +- Owner: “In your lists” goes even before the source explanation. It is now the first group + under the title; everything about sources follows under a matching “Sources” heading. + +### Backup (same session) + +Owner: Backup must match the mockup much more closely. `BackupPage.tsx` keeps every action and its +logic (back up now, export/import all, progress-only export/import, undo last restore, restore +preview) in the Round 6 B layout: intro, a 7 days / 4 weeks / 6 months policy strip, the note, +outlined actions (Back up now solid; file inputs hidden behind their labels, still focusable), +then Daily / Weekly / Monthly sections (“n snapshots · span”) whose rows show the date, a summary +of that snapshot's tracked games, followed routines and archived events, and View backup. +`backupTier` (state/reader/backups.ts) classifies snapshots with the same windows +`retainBackups` keeps, tested against it. Old `.backup-*`/`.file-button` CSS was replaced. +Restore itself was not exercised in the browser (it reloads); its logic is unchanged. +- Owner: Back up now must not overwrite the day's automatic snapshot, and manual backups must be + deletable. New key `READER_KEYS.manualBackups` (`gacha-tracker:v2:manual-backups`), with + `readManualBackups` / `saveManualBackup` / `deleteManualBackup`; never pruned by + `retainBackups`, outside exports (only v1 keys are captured) and untouched by restore/undo. + Capped at 20 copies and ~1.5MB so the shared origin quota keeps room for the reader's own save; + entries are parsed one by one so a damaged entry does not discard the rest. The page shows a + “Your backups” list (View backup, Delete with inline confirm) above “Automatic history”. + schema-guardian: no blockers; its quota should-fix is the size cap above. Left as notes: the + error screen's raw recovery download includes every `gacha-tracker:` key, so it now also carries + manual backups (large but read-only). + +### Your games: tracking saves on click (2026-10-18) + +Owner: “Apply tracking choices” should not exist; choices apply when the panels are clicked. The +button is gone; `AccountSetup` in GameSettings saves the account on each change. It now changes +only the routines of the kind that was toggled (dailies, weeklies & monthlies, or cycles; Events +touches no routines), so a click never resets pins the reader set on the Routines tab — the old +Apply rewrote every kind's placements from the draft. A failed save reverts the panel. Browser- +checked, including a Checklist unpin on Weekly Bosses surviving an Events toggle. +- Owner: the rest of Your games should follow the mockup more closely (padding, margins, dividers, + structure). The Setup tab is now the Round 6 B structure: `.ptitle` (game name in its hue, + studio, Stop following game → hides the game), Codes tab “n new” badge, `.grp` sections with + inline descriptions on a rule — What to track (`.comp` cards with status lines: Shown/Hidden, + “n built in”, dashed “Some, fine-tuned” when partly followed — a click follows the rest — and + dimmed “none built in” that stays selectable per the spec), “Fine-tune in Routines →”; Servers + (`.srvrow` MAIN/ALSO with reset, local time and countdown; Remove only on extra servers); Game + colour (swatch, hex, Reset, preview chip, contrast note); Reset timing (one `ResetTimingRow` per + server, shared with Region & time, replacing the hour/minute editor). `TrackingChoices.tsx` and + 30 unused CSS rules were removed; its test now checks the cards. Browser-checked. +- Owner: a game's About tab should match the mockup. It now follows Round 6 B `aboutPage`: “About + ”, fact rows (Studio; each server's next reset in your time · automatic/custom override), + then Data sources — each source linked by host with “Event / Banner / Battle pass schedule”, + the editors' thanks, and a “Source status and freshness” button to Data & sources. +- Owner: games under “Other games” should be greyed out like the mockup. Unfollowed rows carry + `data-off`; the name is muted and the colour bar faded to 25% (the mockup's `.srow.off`). +- Owner: “Manage games & order” opened a legacy-looking page that also showed the first game's + full configuration. Then: reorder by drag and drop in the sidebar instead. Now: + - Followed games in Settings → Your games are draggable (HTML5 drag; drop indicator), and + Alt+↑/↓ moves a focused row. Saved as the existing `prefs.gameOrder` (followed games in the new + order, then hidden ones), via `SettingsLayout`'s new `onReorder`. A “Sort A–Z” link replaces + “Manage games & order” and appears only when a custom order exists. + - A game page's title button is “Follow game” for an unfollowed game (switching its events on if + nothing was tracked), “Stop following game” otherwise. + - `#settings/games` without a game now shows only a games-and-order list (follow checkbox, name + linking to the game's page, ↑/↓, Reset to A–Z), never a game's configuration. It is no longer + linked from the sidebar. + - Known gap: HTML5 drag does not work on touch screens; phones can reorder only via that list. +- Owner reversed the drag-and-drop idea (weak affordance, hard to see). Removed: sidebar drag and + Alt+↑/↓, its CSS, the sidebar “Sort A–Z”, and the games-and-order list page. Settings → + Appearance now has a “Game order” section listing only tracked games (not hidden; events or a + followed routine) with 40×36 ↑/↓ buttons (disabled at the ends), working for mouse, touch and + keyboard, and “Sort A–Z” when a custom order exists. Saves `prefs.gameOrder` as tracked games + in the new order followed by the rest in their previous order. `#settings/games` without a game + opens Appearance. Browser-checked at 1440 (click) and 390 (tap). +- Owner: in Appearance, “Dailies on Checklist” and “Routine groups” looked odd on their own + background. `ChecklistDisplay` gained a `plain` variant (just the fields); Appearance now has a + “Checklist” settings group with Event rows, Dailies on Checklist and Routine groups side by side + (stacked on phones) and the note below. The Routines page keeps the mockup's shaded band. +- The “Offer standard-banner pulls in the library” switch (`prefs.standardPulls`) had no control + in the redesigned app (only the unmounted legacy `Controls.tsx`). Owner: put it in Data & + sources with the other list options. It is now the third “In your lists” checkbox, with the + mockup's note “Premium pulls are always offered.” Browser-checked: ticking it makes the library + offer Genshin's “Paimon's Bargains: Acquaint Fates”. +- Owner: the settings sidebar needs the mockup's hover. Unselected rows now take the surface + colour and ink text on hover (the mockup's `.srow:hover`), a step lighter than the selected + row's raised background, with a 120ms fade (skipped under reduced motion). Plain `:hover`, as in + the mockup; headless Chrome cannot emulate `(hover: hover)`, so a guarded rule was unverifiable. + +### Round: logo, upcoming heading, routine band names (2026-10-18) + +- The EVENTCLOCK logo is a button once the reader is onboarded; it opens the default view + (`changePage(prefs.view)`). Hover shows only the pointer cursor; the owner rejected an underline. During setup it stays plain text. +- The Checklist's "Upcoming events" `h2` had no styles. It is now a second `.list-heading`, "Not started yet", + with an "N events in G games" sub-line. That mirrors "Running now". The sub-line doesn't say "sorted by…", + because the Doing-first sort orders upcoming rows differently. In compact, the ledger header for this + section reads "Duration / Starts in" (`LedgerColumns upcoming`), not "Time left / Ends in". +- Routines bands (By reset) are named Dailies, Weeklies, Monthlies and Cycles, not Today, This week and + This month. A test covers this in `test/redesign-routines.test.tsx`. The checklist rail's `DailyPanel` + still says "Today · …", because it describes the current period rather than naming a group. +- Follow-up: the owner found "Not started yet" easy to miss. The section now has a 48px gap above it and a + 2px rule in the accent colour (`--color-near`). Its heading sits on a full-width raised band + (`--color-raised`) with a "Coming up" eyebrow, and the heading is the same 28px as "Running now". + +### Round: ticking today's dailies should feel rewarding (2026-10-18) + +The owner found ticking a daily in the redesign "way less satisfying" than the live strip. They asked to keep +the live fireworks. `DailyPanel` (the Checklist rail) now works like this: + +- **Tap target:** the whole row is the button (`.daily-hit`, `aria-pressed`), as the live pill is, not a + small checkbox at the edge. Rows that measure an amount keep their number field. +- **Done state:** a done row washes in the game's hue (`--g`), and its box fills with the hue. A streak + ("3-day streak", from `streakOf`) shows from day 2, as live shows "3d". +- **Tick animation:** only on a tick made in this session (`data-fresh`), so a reload doesn't replay it. + The box pops (an overshoot scale), the check draws in, and a ring glows out from the row. + All of it is off under reduced motion. +- **Progress:** a pip bar in each game's colour and "N of M done" sit under the heading. The last tick + turns them to "All done for today" and flashes the pips (`data-burst`). +- **Fireworks:** the live `Fireworks` component, under the live rule. They fire only when a tick made now + completes the list. A shorter list (focusing a game) or arriving already done never fires them. +- **Tests:** `test/redesign-daily-panel.test.tsx`. +- Follow-up: events with a check-in goal, such as "To Temper Thyself and Journey Far" at 5 days a week, + looked unticked and had no count. The panel read only today's tick, so a week whose goal was met + without today's tick read as not done. `DailyPanel` now counts these as live does: + - **Done:** a row is done when it's ticked today or its goal is met (`hasGoal` and `status.met`). + A met row that isn't ticked draws a dotted check (`data-met`), as the live chip does. Nothing is + ticked on its behalf, so `aria-pressed` stays on today's tick. + - **Counter:** a goal row shows its goal count (`goalCount`: "5/5 wk", "3/5", "12/20 · 6d") in place of + the streak, one counter per row as on live. Its spoken label comes from live's `chipLabel`. + - **Checked:** in the browser with the real Genshin event, a met goal reads done, a tick makes it + "3/3 wk", and the tick survives a reload. Tests are in `test/redesign-daily-panel.test.tsx`. + +### Round: pinned routines complete like dailies (2026-10-18) + +The owner asked that routines with a target be marked complete like dailies, with fireworks, and that +the completion can be edited or undone. The Checklist rail's pinned routines now use +`redesign/PinnedRoutine.tsx` instead of `RoutineControl`, which still serves the Timeline routine dialog. + +- **Look:** the same card language as the daily rows. A met routine washes in the game's colour and reads + "Done this week · resets in 1d 3h". +- **Check-ins** (Realm Depot, Spiral Abyss): the box ticks today. Unticking a met routine removes the + latest tick in the current period (`lastDayInPeriod`), which may be from an earlier day. +- **Amounts** (Weekly Bosses 3 runs, Battle Pass 10,000 EXP), when not met: + - the box completes the routine in one click by setting the amount to the target; + - "+1" steps the amount; + - the count, with a small bar and a pencil, opens an editor. +- **Amounts, auto-complete:** typing a value at or over the target completes the routine without Save, + once typing pauses for `AUTO_COMPLETE_MS` (450ms), so "10000" isn't cut short. +- **Undo for amounts:** unticking puts back the amount from before the completing action, when this + session saw it. After a reload that value isn't known, so the box opens the editor instead. + Nothing is guessed. +- **Completion animation:** only for an action taken in the row; arriving complete never fires. It is + staged: the bar fills from its previous value (`--from`), the row warms, the box pops at about 280ms + and the check draws, then the live `Fireworks` sparks fire at about 300ms. Reduced motion drops all of + it. +- **Tests:** `test/redesign-pinned-routine.test.tsx`. The auto-complete timer and the undo paths were + checked in the browser, not in unit tests, because the suite renders static markup only. + +### Round: time-left bars on every standard row (2026-10-18) + +Round 6 B hid the meter on Standard rows without a description +(`.event-entry.no-description .meter{display:none}`, b-rail.html:837). The owner asked why +those rows had no timing bar and chose to show it on every row, a deliberate departure from the mockup. +`ChecklistEvent` now always renders the meter. Rows without a description keep their tighter padding and +grow by the meter's height, about 20px. + +### Round: the live urgency ramp (2026-10-18) + +The owner asked for live's urgency colours: grey for a week or more, blue under a week, yellow under 3 +days, red under a day. The redesign had derived them from the theme: "under a week" was the accent, and +"a week+" was the general muted text grey, which read nearly white. The ramp is now its own token set, +`--color-urgency-{calm,near,soon,critical}`: + +- **Values:** `urgencyTokens` in `state/appearance.ts` sets live's dark or light values. Each is nudged + only to 3:1 contrast on the scheme's surfaces, the large-text bar; the default dark grey and light grey + move slightly. A custom accent no longer changes urgency. +- **Wiring:** `URGENCY_COLOR` (Meter.tsx) reads `var(--color-urgency-X, var(--color-X))`. The live UI never + sets the urgency tokens, so it falls back to its own unchanged values. +- **No CSS defaults:** none are declared in redesign.css, because that file is imported into the live + stylesheet too. +- **Scope:** `--color-near`, `--color-soon` and `--color-critical` keep their other jobs (accent, + warnings, NOW chip). + +### Round: urgency colours editable, game strips, legend, scroll on tab change (2026-10-18) + +- **Urgency colours in Settings → Appearance.** A new group sits under Colour scheme with the four steps + for the dark and light themes, a small bar sample per step, and "Use the default colours". + - It works with every scheme, not only Custom. + - Choices are stored in a new pref, `prefs.urgencyPalette`, shaped `{ dark: {calm, near, soon, + critical}, light: {…} }` with hex values. + - `urgencyChoice` reads it; unset or invalid steps keep live's values. `urgencyTokens` applies the + 3:1 readability nudge. + - **Schema-guardian:** no risk. Prefs are `z.record(z.unknown())`, patches are shallow merges, and + the pref survives export, backup and restore. One existing behaviour applies to every pref: a v2 + import merge keeps the current prefs. +- **Game colour strip on Standard event rows.** `.event-entry::before`, 3px, in the game hue, sits in the + list's side padding, as live's row rail does. It's faded to 55% and comes to full strength with a + glow on hover. Compact rows already had the `.gcell` bar. +- **Live's legend under "Running now"** (`UrgencyLegend` in ChecklistEvent.tsx): "bars = time left · + colour = ends in a week+ / under a week / under 3 days / under a day". It reuses live's `STEPS`, now + exported from Legend.tsx, and `URGENCY_COLOR`, so it follows the reader's urgency colours. +- **Bug fixed:** changing tab kept the previous page's scroll, so leaving a scrolled Settings page opened + the Checklist part-way down and its rail looked empty. ReaderApp now scrolls to the top when `page` + changes. It skips the first render, so a reload's restored scroll is kept. +- Follow-up: the legend first sat inside the title block. That widened the block, so the sort and density + buttons wrapped under it and the heading looked cluttered. The owner wants one row. The legend is now + the last child of `.list-heading` (`flex-basis: 100%`, a 12px row gap), on its own line under the title + and the buttons. Title and buttons share a row down to 700px. At phone width the buttons wrap as they + always did. +- Follow-up 2: the owner wanted the legend on the same line as "35 events in 4 games · sorted by what + ends first". The Running now heading (`.running-heading`) is now a two-row grid: + - **Row 1:** the title, with the sort and density buttons on the right. + - **Row 2:** `.heading-meta` spans the full width, with the count on the left and the legend on the + right (`space-between`). + - **Narrower screens:** the legend wraps onto its own line under the count only when the line can't + fit, around 1100px. Below 700px the grid is one column, with the buttons left-aligned. + +### Round: a grand celebration for finishing a section (2026-10-18) + +The owner asked for a bigger celebration when a whole section is done: all of today's dailies, or every +routine in a game's group in the Checklist rail. + +- **`Fireworks grand`:** seven bursts in two waves across the section, 14 sparks each, wider reach, 4px + sparks, 1.4s. With `hue` the game's colour joins the sparks. Called without props, it is unchanged for + the live strip and the single-row bursts. +- **`useSectionCelebration(total, done)`** (redesign/useSectionCelebration.ts) is the live strip's rule, + shared by `DailyPanel` and `RoutineGroup`. A section celebrates only when it becomes complete with more + done than a moment ago and the same total. A shorter list or arriving already complete never fires it. +- **Extras on a celebrating section:** + - a sweep of light across it (`section-sweep`, in the group's colour for routines); + - the "All done" line pops (`section-pop`): "All done for today" for dailies, and for routine groups + the summary, which now reads "All N done" when every routine is met (set in ReaderApp), not "N of N + completed". + - A complete group's summary stays bold in the game's colour. +- **Reduced motion and tests:** all motion is off under reduced motion. Tests are in + `test/redesign-routines.test.tsx`. + +### Round: the returning-user tour (2026-10-18) + +The owner walked through the tour on the reachable build (Settings → Appearance → Replay the +introduction) and asked for three things: + +- **Back no longer takes Continue's place.** It used to appear where Continue had been, so clicking + through could land on it. Continue is now always first, in the same spot on every step. Back follows + it from step 2, and "Skip tour" sits apart at the far right. There's also a "2 of 6" counter. +- **The Timeline comes right after the Checklist.** +- **A codes step.** It opens the Codes tab of the first tracked game that has codes, or the Checklist + when none do. + +The order is Routines → Checklist → Timeline → Redeem codes → Archive → Settings, which now opens on +Appearance. The welcome reads "Welcome back. A little has moved." Steps live in `TOUR_STEPS`, and +`ReturningTour` reports `onStep(stop)` to ReaderApp, which opens the page. Tests are in +`test/redesign-onboarding.test.tsx`. +- Follow-up: "Replay the introduction" now starts the tour itself. It used to only bring back the welcome + banner on the Settings page. `AppearancePage` takes `onReplayTour`, and ReaderApp sets `tourAutoStart`, + so `ReturningTour autoStart` opens on step 1 (Routines). Finishing or skipping clears it. A returning + reader seeing the tour for the first time still gets the welcome first. Checked twice in a row in + the browser. + +### Round: focus pills with live's feel (2026-10-18) + +The owner found the redesign's focus pills harsh ("scissors on a blackboard") next to live's chips. +They were cramped: 24px tall, 11.5px text, a 9.5px count and 5px gaps. They also snapped between states +with no easing, and hover only tinted the background. + +`.focus-pill` now follows live's `.hue-chip` / GameFocus `Chip`: +- **Size:** 30px tall, 6px 12px padding, 12px text, a 10px tabular count at 70%, 6px gaps. +- **Colour:** an unpressed border in the game's hue at 30%; pressed, it's the full hue with a 14% wash. +- **Motion:** 130ms eased colour, border, background and shadow changes. Hover adds a 1px ring in the + hue and, where motion is allowed, a 1px lift, and an unpressed label brightens to ink. A press + scales to 0.96. +- **Empty games:** a game with nothing outstanding dims to 50% (`data-empty`). + +The same pills appear in the Timeline's focus row, where they still fit on one line. +- Follow-up (Checklist rows): the owner found the "›" arrow too far from the time and date. It inherited + the row's 24px column gap (22px in Relaxed, 16px under 1050px). A negative margin on `.event-entry > svg` + now leaves 10px at every width and in both interfaces, measured in the browser. + +### Round: closer to the screen edge, a wider rail (2026-10-18) + +The owner found the content "weirdly far away" from the screen edge. The page was capped and centred at +1480px (1400px in Relaxed), which left 220px empty bands each side at 1920px. + +- **Page width:** `.reader-ui` max-width is now 1920px; Relaxed is 1840px with 20px side padding instead + of 28px. At 2560px the page still centres, with 320px bands. +- **Checklist edges:** the list and its heading use 24px side padding instead of 32px, matching the + rail. +- **The rail** (Next to expire, dailies, routines) is wider, as the owner suggested: 300→360px in + Structured, 320→380px in Relaxed, and at medium widths 240→280px and 280→300px. +- **Checked:** Checklist in both interfaces at 1440, 1920 and 2560px, plus Timeline, Routines, Archive + and Settings at 1920px. Settings keeps its readable column width. +- Follow-up: the owner meant the inset between the content and the page's outer edge, about 24px + (32px on the update rows). It's now two tokens on `.reader-ui`: `--edge: 16px` for outer edges and + `--gutter: 24px` for the gap where a side column meets the content, which the owner asked to keep. + - **Full-width parts** use the edge on both sides: header, footer, Timeline and Routines headers, + the Routines table, Archive. + - **Side columns** use the edge outside and the gutter inside: the Checklist rail, the Settings nav, + the Archive sidebar. + - **The main Checklist column** mirrors that: updates, headings, list, ledger, foot. + - **At ≤700px**, where the columns stack, everything uses the edge, and the game strip moves to + `left: -9px` so it doesn't touch the screen edge. + - **Checked:** measured in the browser at 1920px and 390px, with no horizontal scroll. +- Follow-up (focus bar): the "Focus" label above the pills looked off-centre and off in weight. Cause: + `.reader-ui h2` comes later than `.focus-heading h2` with the same specificity, so it swapped in the + condensed display face at 13px. It also sat beside an 11px regular "Next: …" button and was centred + by box, not text. It's now Barlow 500 12px in muted grey, like the rail's "Next to expire" + (`.game-focus .focus-heading h2`), and `.focus-heading` aligns to the text baseline. + +### Shipped as the initial redesign (2026-10-18) + +The owner accepted the build: "I think this is ready to be shipped as the initial redesign." Before +committing: +- **Migration review:** a full schema-guardian review of the v1→v2 migration found no blockers. Its + low-severity gaps are in TODO.md. +- **Image check:** the suite ran as the image runs it, without docs (typecheck clean, 1642 pass, + build OK). +- **Docs:** `docs/DESIGN.md`'s redesign section now describes the shipped system. + +The owner chose to commit and push, including `design/`, the redesign docs, `docs/design-reviews/` and +`.superpowers/`. The changelog entry and note follow in their own commit, because they must name the +redesign commit. diff --git a/docs/superpowers/plans/2026-10-16-ui-redesign.md b/docs/superpowers/plans/2026-10-16-ui-redesign.md new file mode 100644 index 0000000..ef7ff19 --- /dev/null +++ b/docs/superpowers/plans/2026-10-16-ui-redesign.md @@ -0,0 +1,360 @@ +# Event Clock Redesign Implementation Plan + +> **2026-10-17: second implementation rejected by owner.** The later correction/review statements +> below do not mean acceptance. Resume from `docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`. +> Launch the current server and ask the owner for the biggest differences before further UI changes. + +> **Acceptance withdrawn:** the owner rejected the implementation for substantial divergence from +> the approved markup. Completed checkboxes below are historical, not design acceptance. +> Presentation/fidelity gates reopen. Read +> [the replacement-agent handoff](../../handoffs/2026-10-16-redesign-fidelity-handoff.md) first. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Execution method: **Native**, selected by the owner. Steps use checkbox (`- [x]`) syntax for tracking. + +**Goal:** Deliver the approved redesign with Relaxed (Round 7) and Structured (Round 6 B) interfaces, shared functionality, and preserved reader data. + +**Architecture:** Keep one React application and one set of domain selectors; interface tokens change presentation. Introduce an account-aware reader-state envelope with explicit migration and reset revisions before connecting new tracking controls. Separate source ingestion from presentation and keep all user state browser-local. + +**Tech Stack:** Bun 1.3, React 19, strict TypeScript, Tailwind, Zod; existing dependencies only. Bun tests for domain/storage and server-rendered semantics; scratch Puppeteer and Chrome for built-app interaction and appearance. + +**Spec:** [Approved redesign specification](../specs/2026-10-16-ui-redesign-design.md). + +## Global Constraints + +- Two interfaces: **Relaxed / Structured**, using Round 7 / Round 6 B respectively; Relaxed default. +- Interface, Dark/Light/System, colour scheme and Standard/Compact event rows are independent preferences. +- Keep existing event IDs, daily IDs, task IDs and day-key formats. +- No added runtime dependencies, accounts, push service, SQLite work or prototype JavaScript transplanted into production. +- Unknown end dates remain null. Explicit timestamps, `regionEnds` and reader-entered instants remain authoritative. +- **Event data may be out of date** replaces **Older schedules**; warnings cover followed games, independently of focus. +- Backup retention: seven daily, four weekly, six monthly snapshots, plus one recovery copy; stable UTC basis. +- Changelog is a Settings page, with no footer link or About button. +- Small text must clear 4.5:1 in both modes and UIs. +- Work in the current checkout on `main`, per AGENTS.md. Do not create a branch, commit, push or publish without a separate owner instruction. This overrides skill worktree/commit defaults. +- Update shipped docs alongside implemented behaviour; preserve earlier review artifacts and unrelated untracked files. + +## Review Focus + +1. Two open tabs edit different progress fields: preserve both edits rather than last whole-envelope write winning (Task 3). +2. An interrupted migration/restore or quota failure: retain recoverable data and prevent startup effects overwriting it (Tasks 3, 10). +3. Reset changes move today's date backward: keep old ticks reachable without marking the new period completed by accident (Tasks 4, 6). +4. A source disappears while a future/unknown-end event remains outstanding: do not declare it expired or lose its detail (Task 8). +5. Old exports and unreadable future fields: preserve what this build cannot interpret; never claim a partial import fully restored data (Tasks 2, 10). + +## Structure and shared contracts + +Existing entry points stay `App.tsx`, `main.tsx`, `api.ts` and the current feed. New UI lives under +`src/client/components/redesign/`; reader state under `src/client/state/reader/`. Keep shared date +logic in `src/shared/`, without importing browser storage into shared modules. + +The following contracts belong to `src/client/state/reader/schema.ts` and are consumed by later +tasks. Implement Zod schemas as `.passthrough()` for records with forward-compatible fields; +validate known fields without stripping unknown ones. + +- `AccountId`: opaque `account:` identity, minted once, unrelated to mutable region. +- `ClockRevision`: `{ id: string; effectiveAt: string; region: Region; reset: null | { hour: number; minute: number }; automaticBase: { offsetUtc: number; hour: number; minute: number } }`. + Capture automaticBase when creating a revision; later game metadata updates start a new revision + for current Automatic clocks rather than changing the historical one. +- `TrackingIntent`: `{ events: boolean; dailies: boolean; weeklyMonthly: boolean; cycles: boolean }`. +- `RoutinePlacement`: `{ followed: boolean; checklist: boolean; timeline: boolean }`. +- `Account`: `{ id: AccountId; game: LaneId; region: Region; intent: TrackingIntent; routines: Record; clocks: ClockRevision[]; retiredAt?: string }`. +- `AccountData`: `{ progress: Record; ignored: Record; daily: Record>; amounts: Record> }`. + `daily` and `amounts` are keyed first by revision id, then by the original item id. Inside each + item retain existing day/period log shapes. Revision ids never become event ids. +- `ArchiveRecord`: `{ accountId: AccountId; event: DisplayEvent; outcome: "done" | "ignored" | "unfinished"; outcomeAt: string; resolvedEnd: number | null; clockRevision: string; progress: unknown; days: string[]; amounts: Record }`. +- `SeenEvent`: `{ accountId: AccountId; event: DisplayEvent; firstSeenAt: string; lastSeenAt: string; clockRevision: string }`. +- `ReaderState`: `{ version: 2; revision: number; migratedAt: string; legacyDigest: string; accounts: Record; data: Record; archive: Record; seenEvents: Record; prefs: Record; customGames: Record; customEvents: Record; tasks: Record; codeSeen: Record; codeRedeemed: Record>; eventSeen: Record }`. + Keys of archive/seenEvents are `JSON.stringify([accountId, event.id])`, preventing collisions + without altering the domain id. `codeSeen` stores known ids per game; redemption stores code ids + per account. `eventSeen` records event ids per account after the relevant main view is visited. +- `ReaderAction`: discriminated operations to edit a specific account, progress field, day tick, + amount, follow/pin, preference, archive outcome or seen marker. Actions carry `at` and an opaque + operation id for conflict/replay handling; do not expose arbitrary whole-state replacement. +- `ReaderStore`: `{ getSnapshot(): ReaderState; subscribe(listener: () => void): () => void; dispatch(action: ReaderAction): Promise }`. + Async dispatch permits Web Lock serialization; UI event callbacks await or explicitly void it, + and the store publishes save failures through its hook rather than dropping rejected writes. +- `SaveResult`: `{ ok: true } | { ok: false; reason: "unavailable" | "quota" | "invalid" | "conflict" }`. + +Browser keys: `gacha-tracker:v2:reader`, `gacha-tracker:v2:migration-recovery`, +`gacha-tracker:v2:restore-pending`, `gacha-tracker:v2:backups`, `gacha-tracker:v2:restore-recovery`. +Never delete the existing `gacha-tracker:v1:*` keys. A backup contains reader state and legacy raw +values, excluding these backup/recovery keys and service-worker caches. + +## Tasks + +### Task 1: Shared navigation and the two interface shells + +**Files:** Create `src/client/components/redesign/AppNavigation.tsx`, `SettingsLayout.tsx`, +`ChangelogPage.tsx`, `src/client/redesign.css`; modify `App.tsx`, `styles.css`, +`state/usePrefs.ts`, `components/ChangelogSheet.tsx`, `scripts/build-static.ts`; test +`test/redesign-shell.test.tsx`, `test/prefs.test.ts`. + +**Interfaces:** Export `AppPage = "soon" | "timeline" | "routines" | "archive" | "settings"` +and `SettingsPage = "general" | "games" | "appearance" | "lists" | "time" | "sources" | "backup" | "about" | "changelog"`. +`AppNavigation({ page, onPage })` and `SettingsLayout({ page, onPage, children })` use native buttons. +Add normalized optional prefs `interface: "relaxed" | "structured"` and `density: "standard" | "compact"`. + +- [x] Add failing semantic/prefs tests: missing/invalid interface reads Relaxed, stored Structured survives; navigating to Settings does not overwrite `prefs.view`; Changelog renders as a page without modal semantics. +- [x] Run `bun test test/redesign-shell.test.tsx test/prefs.test.ts`; expect failure for new behaviour. +- [x] Implement shared navigation/settings shell. Extract changelog content for page and retained legacy-sheet test compatibility. Scope layout/type tokens by `data-interface`; carry actual Round 7/6 B proportions and offline font fallbacks. Show settings only on its page, not below every list. +- [x] Run named tests and `bun run typecheck`; expect pass. Browser-check both shells at 390/1440px before later features. Update DESIGN/ARCHITECTURE for landed shell only. + +### Task 2: Reader schemas, legacy migration and full-state export + +**Files:** Create `state/reader/schema.ts`, `migration.ts`, `export.ts`; modify `state/export.ts`; +test `test/reader-migration.test.ts`, `test/reader-export.test.ts`. + +**Interfaces:** `migrateLegacy(raw: Record, games: readonly LaneId[], at: string, mint: () => AccountId): ReaderState`; +`parseReaderExport(value: unknown): ReaderState | null`; `buildReaderExport(state: ReaderState, legacy: Record, at: string): object`. +Export format `gacha-tracker-export`, version `2`, full-state field `reader`, raw preservation field +`legacy`; existing version-1 exports remain accepted by a legacy conversion path. + +- [x] Add failing tests from representative v1 saves: completion-only migration, daily/routine/task amounts, hidden games/order/region/theme/view, custom recurrence, unknown goal/task fields and orphan ids. Assert no id/day label changes and no routines enabled beyond the old effective rules. +- [x] Run `bun test test/reader-migration.test.ts test/reader-export.test.ts`; expect missing-feature failure. +- [x] Implement schemas/contracts above and deterministic mapping of each game's old state to one initial account in saved region. Default pins reproduce existing visible routines. Migrate all game identities referenced by logs/tasks/custom records, including games missing from today's feed. Retain unattributable records in legacy raw storage, with no guessed game. +- [x] Test repeat startup uses saved migration result without minting identities again; v2 export round-trip retains unreadable extra fields; malformed known fields reject without writes. Run named tests and typecheck; expect pass. Document schemas/keys in DATA-MODEL. + +### Task 3: Persisted reader store and concurrency + +**Files:** Create `state/reader/reducer.ts`, `store.ts`, `useReader.ts`; modify `state/storage.ts`, +`main.tsx`, `App.tsx`; test `test/reader-store.test.ts`. + +**Interfaces:** `reduceReader(state: ReaderState, action: ReaderAction): ReaderState`; +`createReaderStore(storage: Storage, initial: ReaderState): ReaderStore`; +`useReader(): { state: ReaderState; dispatch: ReaderStore["dispatch"]; saveError: SaveResult | null }`. +Add `tryWriteJson(storage: Storage, key: string, value: unknown): SaveResult` without breaking old `writeJson` callers. + +- [x] Write failing tests for two stores sharing storage: different progress-field edits both survive; stale same-field edit reports conflict; repeated operation is idempotent; removal is a timestamped operation, not a missing-key merge. Inject throwing/quota storage. +- [x] Run `bun test test/reader-store.test.ts`; expect failure. +- [x] Implement read-latest/rebase operations under a Web Lock when available, a revision check/retry fallback otherwise, and storage-event synchronization. Expose conflict instead of silently overwriting. Persist migration recovery before the first reader write. Bootstrap recovery before mounting React; prevent old hook effects writing migrated data. Reconcile post-migration v1 changes by recorded timestamps on next startup, retain deletions in new-state tombstones and preserve raw old values. +- [x] Bind App to the store with `useSyncExternalStore`; retain domain helpers from legacy hooks, remove their mounted persistence effects when v2 is active. Test failed migration write leaves legacy bytes identical and does not mark migration complete. Run named tests, all existing store tests and typecheck; expect pass. + +### Task 4: Revision-aware reset clocks and timezone formatting + +**Files:** Create `src/shared/readerClock.ts`, `state/reader/clock.ts`, +`state/displayTime.ts`; modify `shared/time.ts`, `daily.ts`, `goal.ts`, `task.ts`; +test `test/reader-clock.test.ts`, `test/display-time.test.ts`. + +**Interfaces:** `resolveReaderClock(account: Account, at: number): { revision: string; region: Region; resetHour: number; resetMinute: number; offsetUtc: number }`; +`appendClockRevision(account: Account, reset: ClockRevision["reset"], region: Region, at: string, id: string): Account`; +`formatInZone(instant: number | string, withTime: boolean, zone: string | null): string`. +Pass an optional resolved clock through existing date helpers; absent preserves every old result. + +- [x] Write failing tests for Automatic parity across all registered game/server clocks, 05:30 override, revert, explicit `regionEnds`, day-precision starts/ends, custom instants, built-in `clock` and custom task timing. Check two ticks with the same day label in different revisions remain independent/reachable when reset moves backward. +- [x] Run `bun test test/reader-clock.test.ts test/display-time.test.ts`; expect failure. +- [x] Implement injected clock context, leaving stored ids and historical labels unchanged. Revision selection uses action timestamp for logs and the current clock for current deadlines. Capture resolved offsets in history if automatic metadata changes, so past readings stay reproducible. Formatting uses Intl IANA zones, invalid stored zone falls back to Automatic without altering day keys. +- [x] Run named tests plus `test/time.test.ts`, `daily.test.ts`, `goal.test.ts`, `task.test.ts`, `period.test.ts`; expect pass. Document reset revision semantics and displayed-time separation. + +### Task 5: Account-aware game settings and real source credits + +**Files:** Create `components/redesign/GameSettings.tsx`, `TrackingChoices.tsx`, +`state/reader/accounts.ts`; modify `App.tsx`; test `test/reader-accounts.test.ts`, `test/game-settings.test.tsx`. + +**Interfaces:** `createAccount(game: LaneId, region: Region, at: string, id: AccountId): Account`; +`GameSettings({ accounts, sources, onAction, gameMeta })`; `TrackingChoices({ intent, availability, onChange })`. +Source credits consume `EventFeed.sources`, not event rows; never import ingestion adapters into the client bundle. + +- [x] Add failing tests: added server begins events-only with empty logs/pins; changing an account region retains its identity/history; duplicate same game/server setup is explained; retiring a server preserves its history. All type choices stay enabled when unavailable and explain why. +- [x] Run `bun test test/reader-accounts.test.ts test/game-settings.test.tsx`; expect failure. +- [x] Implement Setup/Codes/About tabs, default-server selection, fresh server tracking flow and reset hour/minute/Automatic/revert. Colour control edits the game, not an account. Source links include every matching health entry with thanks, including zero-event sources. Codes tab initially shows an honest unavailable state. +- [x] Run named tests and typecheck; expect pass. Browser-check adding a second server without copying the first account's marks. + +### Task 6: Routine library, independent pins and direct dailies + +**Files:** Create `components/redesign/RoutinesPage.tsx`, `RoutineGroup.tsx`, +`DailyPanel.tsx`, `state/reader/routines.ts`; modify `Dailies.tsx`, `EditPanel.tsx`, +`YourOwn.tsx`, `TaskForm.tsx`, `AmountEditor.tsx`, `App.tsx`; test `test/reader-routines.test.ts`, +`test/redesign-routines.test.tsx`. + +**Interfaces:** `routineRows(state: ReaderState, accountId: AccountId, now: number): RoutineRow[]`; +`RoutineRow` contains original item id, account id, title, cadence, current GoalStatus, source, +placement and resolved clock revision. Follow/pin changes dispatch ReaderAction. + +- [x] Add failing tests for stop/re-follow preserving logs; independent Checklist/Timeline pins; Make it mine copies the selected account's history only; selected type intent survives empty availability; new built-ins are offered, never automatically followed. +- [x] Run `bun test test/reader-routines.test.ts test/redesign-routines.test.tsx`; expect failure. +- [x] Implement library/Create your own, active routine rows, daily panel/grouped choice and three disclosure policies. Share one display preference between Routines and Appearance. Focus expands groups without saving a different policy. Native numeric inputs use step one and visible browser steppers. Keep custom event/game editing separate from built-in routine management. +- [x] Test the same daily appears exactly once; Always expanded has no collapse button; focus-All restores policy; old-revision history remains accessible in catch-up. Run named tests, existing tasks/dailies/forms tests and typecheck; expect pass. Browser-check one-action daily ticks for two accounts. + +### Task 7: Account-aware Checklist and Timeline + +**Files:** Create `state/reader/eventRows.ts`, `components/redesign/ChecklistPage.tsx`, +`TimelinePage.tsx`, `RoutineTimelineRows.tsx`; modify `EventRow.tsx`, `EventDetail.tsx`, +`Timeline.tsx`, `NextUp.tsx`, `App.tsx`; test `test/reader-event-rows.test.ts`, `test/redesign-timeline.test.tsx`. + +**Interfaces:** `ReaderEventRow = RowEvent & { accountId: AccountId; region: Region; clockRevision: string }`; +`readerEventRows(state: ReaderState, events: readonly DisplayEvent[], now: number): ReaderEventRow[]`. +Use composite React keys/open-selection `{ accountId, eventId }`, preserving the original event id for domain state. + +- [x] Add failing tests for simultaneous regions with different end times, per-account Done/Ignore and goals, custom repeated occurrences, outstanding headline/counts and focus. Standard/Compact contain identical event ids/order. Title-only Standard rows omit description-sized decoration. +- [x] Run `bun test test/reader-event-rows.test.ts test/redesign-timeline.test.tsx`; expect failure. +- [x] Extract view coordination from App; retain existing sort/zoom/recurrence semantics. Implement routine full rows and optional reset strips immediately beneath their game's events. With strips and Ending soonest, order game groups by next deadline, maintaining adjacency; without strips retain existing ending queue. +- [x] Run named tests, `test/views.test.tsx`, `lens.test.ts`, `sort.test.ts`, `zoom.test.ts`, `recurrence.test.ts`; expect pass. Browser-check both interfaces, densities, long titles, empty scope, unknown ends and Timeline scrolling. + +### Task 8: Durable archive, completion confirmation and restoration + +**Files:** Create `state/reader/archive.ts`, `components/redesign/ArchivePage.tsx`, +`ArchiveDetail.tsx`, `state/useArchiveTransition.ts`; modify view/detail wiring; +test `test/reader-archive.test.ts`, `test/archive-ui.test.tsx`. + +**Interfaces:** `captureSeen(state: ReaderState, rows: readonly ReaderEventRow[], at: string): ReaderState`; +`reconcileArchive(state: ReaderState, events: readonly DisplayEvent[], now: number): ReaderState`; +`archiveOutcome(state: ReaderState, accountId: AccountId, eventId: string, outcome: ArchiveRecord["outcome"], at: string): ReaderState`. + +- [x] Add failing tests: save Done/Ignore immediately; Undo restores prior progress/ignore state; expired unfinished snapshots retain descriptions/source/logs; missing-but-unexpired and unknown-end events remain outstanding; hidden/unfollowed catalogue events do not produce history. Retired accounts remain inspectable. +- [x] Run `bun test test/reader-archive.test.ts test/archive-ui.test.tsx`; expect failure. +- [x] Capture feed events on successful reads before feed replacement, reconcile known expiry, and resolve missing live details from seen snapshots. Implement 1.5-second row confirmation then fade/collapse and eight-second Undo, using cancellable timers. Reduced motion removes animation. Archive Restore does not make an expired event live. +- [x] Test legacy marks with no metadata are preserved and labelled incomplete, unknown ends remain null, repeated Done is idempotent, repeated custom occurrences archive separately. Run named tests and typecheck; expect pass. Browser-verify timing, navigation during delay and Undo. + +### Task 9: Archive story statistics and history timeline + +**Files:** Create `state/reader/archiveStats.ts`, `components/redesign/ArchiveStory.tsx`, +`ArchiveTimeline.tsx`; modify ArchivePage; test `test/archive-stats.test.ts`. + +**Interfaces:** `archiveStats(records: readonly ArchiveRecord[], now: number): ArchiveStats`; +`ArchiveStats` includes monthly/overall eligible/done/rate, current/best completion run, check-ins, +unfinished, known target shortfalls and per-game outcome totals. + +- [x] Add failing tests with done/ignored/unfinished/unresolved records, equal timestamps, absent goals, multi-account same dates and empty history. Assert done/(done+unfinished), ignored excluded, no NaN or invented near-miss values. +- [x] Run `bun test test/archive-stats.test.ts`; expect failure. +- [x] Implement pure totals with stable account/event tie-breaks for equal timestamps. Group done by outcomeAt and unfinished by resolved end; future/unknown ends are ineligible. Sum distinct check-ins per event/account; do not label that unique play days. Default body per-game; only real source versions get version labels. Add optional history timeline. +- [x] Run named tests and typecheck; expect pass. Browser-check compact story panel in both interfaces/themes with small and empty archives. + +### Task 10: Backup retention, validated restore and recovery + +**Files:** Create `state/reader/backups.ts`, `restore.ts`, `components/redesign/BackupPage.tsx`; +modify bootstrap/export wiring; test `test/reader-backups.test.ts`, `test/reader-restore.test.ts`. + +**Interfaces:** `retainBackups(snapshots: readonly BackupSnapshot[], now: number): BackupSnapshot[]`; +`BackupSnapshot = { id: string; at: string; payload: object }`; +`prepareRestore(storage: Storage, payload: unknown, at: string): SaveResult`; +`finishPendingRestore(storage: Storage): SaveResult`; `undoRestore(storage: Storage): SaveResult`. +Use Task 2 export validation. Recovery and pending transaction keys are excluded from payload recursion. + +- [x] Add failing retention tests spanning 400 simulated days: disjoint seven daily/four seven-day/six calendar-month buckets, month/year/leap boundaries, latest-per-bucket, missing days and timezone changes. Assert at most seventeen retained snapshots. +- [x] Add failing restore tests for malformed/unknown-version exports, quota at each write, interrupted startup, current-state recovery, undo and reader state with unreadable preserved fields. Run both named files; expect failure. +- [x] Implement rolling retention on app use and debounced successful data changes; no unchanged-state snapshot loop. Write recovery and prepared envelope before replacing live reader state. Bootstrap completes validated pending restore before mounting stores, and preserves pending data on failure. Never claim success after failed persistence. +- [x] Implement View backup → contents preview → Restore this backup; manual Back up now, export/import and Undo last restore. Preserve progress-only versus Import all semantics, including account identity mapping and imported custom dependencies; warn before overwriting ambiguous account mappings. +- [x] Run both named files, export tests and typecheck; expect pass. Browser-test real download/upload, restore and storage-failure feedback. Document local-only lifetime and no backup on unused days. + +### Task 11: Appearance, custom colours and Region & time pages + +**Files:** Create `state/appearance.ts`, `components/redesign/AppearancePage.tsx`, +`TimeSettings.tsx`; modify `state/theme.ts`, `state/gameMeta.tsx`, `scripts/build-static.ts`, +`redesign.css`; test `test/appearance.test.ts`, `test/appearance-ui.test.tsx`. + +**Interfaces:** `appearanceTokens(prefs: Record, theme: Theme): AppearanceTokens`; +`readableOnSurfaces(hue: string, backgrounds: readonly string[], minimum: number): string`. +Token values cover both interface styles, schemes and themes; minimum is 4.5 for small text. + +- [x] Add failing tests for interface/scheme/density independence, malformed saved palette, same background/text, colours legible on page and panels, game overrides shared across accounts and Reset to default. Include System appearance changes without losing custom selections. +- [x] Run `bun test test/appearance.test.ts test/appearance-ui.test.tsx`; expect failure. +- [x] Implement Slate/Warm/Plum/Custom, separate light/dark base edits and derived contrast-safe rendered colours. Apply pre-paint appearance using the reader envelope so reload does not flash a different UI. Reuse Task 4 timezone formatting throughout details/archive/forms; custom-entry copy names its interpretation zone. +- [x] Run named tests, existing theme tests and typecheck; expect pass. Browser-check custom equal colours, semantic urgency, native colour/number controls, IANA search and daylight saving. + +### Task 12: Shared updates, data sources and About/Changelog + +**Files:** Create `state/reader/updates.ts`, `components/redesign/UpdatesPanel.tsx`, +`DataSourcesPage.tsx`, `AboutPage.tsx`, `FeedFooter.tsx`; modify `Colophon.tsx`, +`WhatsNew.tsx`, `App.tsx`; test `test/redesign-updates.test.tsx`, `test/reader-updates.test.ts`. + +**Interfaces:** `updateItems(state: ReaderState, feed: EventFeed, now: number): UpdateItem[]`; +`UpdateItem` is a discriminated union of event-newness, code-newness, release and source-age rows. +`FeedFooter({ sources, now, online, onRefresh, onAbout })` always retains feed-age disclosure. + +- [x] Add failing tests: followed-games source scope independent of focus; one fresh sibling does not conceal old source; never-fetched/unchanged/overdue/unknown reasons; code/events/release markers clear independently; no empty updates panel. +- [x] Run `bun test test/redesign-updates.test.tsx test/reader-updates.test.ts`; expect failure. +- [x] Move notices into shared compact rows above Checklist/Timeline, using exact source-age copy from spec. Show source detail timestamps honestly; do not invent failure causes absent from feed. Preserve Check for new data's result/offline semantics. Mark seen event ids only after the relevant content has been presented; initial migration seeds already-known event ids to avoid announcing the whole calendar. +- [x] Extract full colophon to About, retain all credits unfiltered, and keep short age/About footer without Changelog. Lists contains actual list settings, no Done/ignored archive-link row. Run named tests, existing refresh/changelog tests and typecheck; expect pass. + +### Task 13: New and returning reader onboarding + +**Files:** Create `components/redesign/SetupWizard.tsx`, `ReturningTour.tsx`, +`state/reader/onboarding.ts`; modify `Welcome.tsx`, `App.tsx`; +test `test/redesign-onboarding.test.tsx`, `test/reader-onboarding.test.ts`. + +**Interfaces:** `setupReader(draft: SetupDraft, existing: ReaderState, at: string, mint: () => AccountId): ReaderState`; +`SetupDraft` contains default region, interface/theme/scheme/timezone, selected games, per-game +TrackingIntent, showUpcoming and preferred `View`. Saved tour marker `redesignTourSeen: "2026-10-redesign"` is separate from changelog markers. + +- [x] Add failing tests: initial events-only intent and upcoming off; unavailable cycles selectable; initial supported selections opt in deliberately; additional server uses fresh setup; new-user defaults never overwrite migrated answers. Skip/Back/finish preserve state and return to original main view. +- [x] Run `bun test test/redesign-onboarding.test.tsx test/reader-onboarding.test.ts`; expect failure. +- [x] Implement short preference/game/tracking/view steps and inline type explanations. Upcoming control appears only in tracking step. Persist completed setup once. Returning welcome/tour is offered once on either preferred main view; skipping records dismissal, with a replay link in General. +- [x] Run named tests, existing welcome/prefs tests and typecheck; expect pass. Browser-check both entry paths, keyboard, backtracking and reload after completion. + +### Task 14: Deterministic Genshin codes source and published contract + +**Files:** Create `src/shared/codes.ts`, `src/ingest/codes/game8.ts`, `registry.ts`, +`build.ts`, `scripts/build-codes.ts`, `fixtures/genshin/game8-codes-2026-10-16.html` and +matching expected JSON; modify refresh orchestration, build scripts, package scripts, static server +and service-worker feed handling; test `test/codes.test.ts`, `test/adapters/game8-codes.test.ts`, +`test/codes-refresh.test.ts`. + +**Interfaces:** `RedeemCode = { id: string; game: GameId; code: string; reward: string | null; expiresAt: string | null; firstObservedAt: string; sourceUrl: string }`; +`CodesFeed = { schemaVersion: 1; codes: RedeemCode[]; sources: SourceHealth[] }`; +`parseGame8Codes(html: string, ctx: ParseContext): Omit[]`; +`buildCodesFeed(previous: CodesFeed | null, parsed: readonly Omit[], at: string): CodesFeed`. +Use code identity `JSON.stringify([game, code])`, preserving literal code case/punctuation from the source. + +- [x] Read INGESTION/SOURCES and the real Game8 page/robots through the approved source workflow. Fetch at most once after checking existing cache and six-hour floor; pin actual bytes and independently count the page. If access is refused, do not bypass it; record that condition and seek owner-provided bytes for the fixture rather than fabricate data. +- [x] Add failing tests from pinned bytes: active versus expired sections, unknown reward/expiry, duplicate codes, HTML entities, changed page shape, stable first-observed date across unchanged rebuilds, and failed fetch retaining the previous feed. +- [x] Run `bun test test/codes.test.ts test/adapters/game8-codes.test.ts test/codes-refresh.test.ts`; expect failure. +- [x] Implement pure parser plus a separate `data/codes.v1.json` feed. Reuse robots, conditional-fetch, SnapshotStore, host spacing and failure reporting; do not weaken event Adapter's GachaEvent return contract. The unified refresh scheduler must share host spacing across both source kinds. Offline builds use real fixtures only. Codes do not alter existing event feed ids or event counts. +- [x] Run named tests and offline build; expect pass. Update INGESTION/SOURCES/ARCHITECTURE and workflow copy/build paths so the feed is deployed and cached. Network fetch is not part of tests. + +### Task 15: Code pages, discovery notices and redemption + +**Files:** Create `components/redesign/CodesPage.tsx`, `state/reader/codes.ts`; +modify `api.ts`, GameSettings and UpdatesPanel; test `test/reader-codes.test.ts`, `test/codes-ui.test.tsx`. + +**Interfaces:** `newCodes(feed: CodesFeed, seen: readonly string[], game: GameId): RedeemCode[]`; +`fetchCodesFeed(signal?: AbortSignal): Promise`. +Viewing writes `codeSeen[game]`; redemption writes only `codeRedeemed[accountId][codeId]`. + +- [x] Add failing tests: viewing clears newness without redemption; redeeming one account leaves another unredeemed; expired known code does not become new again; unavailable codes feed leaves events usable; code arrival does not clear new events/release notices. +- [x] Run `bun test test/reader-codes.test.ts test/codes-ui.test.tsx`; expect failure. +- [x] Bind game Codes page and in-app notices to the real feed, with copy control, source/reward/known-expiry fields and per-account redeemed state. Show honest empty/unavailable/stale states, never sample codes. Update refresh comparison to account for code changes separately from event-feed build stamps. +- [x] Run named tests and typecheck; expect pass. Browser-check feed update, Copy, view-seen behaviour and independent two-server redemption; verify exports/backups include state. + +### Task 16: Built-app verification, documentation and final independent review + +**Files:** Update `docs/PRD.md`, `DATA-MODEL.md`, `ARCHITECTURE.md`, `DESIGN.md`, +`PRODUCT.md`, affected AGENTS guidance, `TODO.md`; record evidence in +`docs/design-reviews/2026-10-16-redesign-implementation.md`. Keep browser tooling in `/tmp`. + +**Interfaces:** All previous tasks' contracts; no new domain API. + +- [x] Run `bun run typecheck`, `bun test`, `git diff --check`; expect all pass. Read full failures, not only the command exit status. Confirm tests run without docs/public build output, using the repository's Docker-layout check when needed. +- [x] Run `bun run build:css`, `bun run build:js`, `bun run build:static`; serve on a fresh distinctive port with an existing/copied real feed. Inspect Chrome screenshots for 390/1440px, both interfaces, both themes, Standard/Compact, 200% zoom and reduced motion. No horizontal page overflow, clipped panels or console errors. +- [x] Browser journeys: legacy-save migration; first and returning onboarding; two servers; reset override/backward-day/revert; direct dailies; routine pin/disclosure; event detail/edit; completion/Ignore/Undo; archive/history/stats; source-age scenarios; custom palettes/timezone; real code discovery/redemption; export/import/restore/undo; offline reopening and update prompt. +- [x] Perform a fresh whole-change review using the native execution skill's final reviewer. Review data-loss paths, source honesty, storage failures, shared contracts and all five Review Focus items. Fix critical/important findings with reproducing tests and rerun the suite; record remaining minor findings explicitly. +- [x] Finalize shipped docs to match actual behaviour and both visual systems. Report changes, evidence and limitations. Leave work uncommitted and unpublished until the owner requests those actions; no merge/branch cleanup is needed on main. + +## Execution record and handoff + +Use `.superpowers/sdd/2026-10-16-ui-redesign/progress.md` for task status, verification evidence +and rulings. A task is complete only after its named checks pass. User selected Native execution; +do not ask for the method again. The writing-plans skill requires owner review of this concrete +plan before implementation begins. After that review, execute all tasks continuously, with status +updates and no further per-task approval prompts. The final review is the only planned delegated +work; implementation stays native. + +Self-review: all approved spec sections map to Tasks 1–15; migration, failure recovery and source +integration have explicit gates. Shared contracts are declared above and used consistently. +Task 14 has an honest external prerequisite: real source bytes and policy validation. A refused +source is not permission to invent data or declare the codes feature finished. + + +Completion evidence: [implementation review](../../design-reviews/2026-10-16-redesign-implementation.md). +All 16 feature/verification tasks are complete, uncommitted. The ledger records staged activation, +wrapper-file substitutions, review fixes and the stronger IndexedDB fallback in place of an unlocked +revision retry. Some expanded render checks passed immediately after their already-implemented +behaviour; they are coverage evidence, not claimed RED runs. No commit/push/publication performed. + +## Fidelity correction follow-through — 2026-10-17 + +The original completion statement above describes the rejected first implementation. The subsequent +correction follows the approved reference composition and the owner's new Structured-default ruling. +See `docs/design-reviews/2026-10-17-redesign-fidelity.md` for matched screenshots, independent findings, +resolved verdicts and final functional verification. Final review cleared the reported corrections; +owner acceptance is not inferred from that review. All changes remain uncommitted and unpublished. +Settings now places the replay control under Default view, replacing the previous General page. diff --git a/docs/superpowers/specs/2026-10-16-ui-redesign-design.md b/docs/superpowers/specs/2026-10-16-ui-redesign-design.md new file mode 100644 index 0000000..4b7e787 --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-ui-redesign-design.md @@ -0,0 +1,267 @@ +# Event Clock redesign: Relaxed and Structured + +Date: 2026-10-16. Status: approved requirements; both the initial implementation and the 2026-10-17 fidelity correction were rejected by the owner. Current continuation instructions: `docs/handoffs/2026-10-17-redesign-second-rejection-handoff.md`. Launch the current server, then ask the owner for the biggest differences before further UI changes. + +Owner amendment, 2026-10-17: Structured is the default; preserve explicit saved Relaxed. + +## Intent and approval record + +Make several games' events and routines easier to scan and act on, while retaining trustworthy +deadlines and every existing reader choice. The owner approved the Round 7 visual direction, +then explicitly chose **two UIs**, reversing the preceding single-UI decision. The second is +**Round 6 B**, confirmed in conversation. Both have the same capabilities and data. + +Visual references: + +- [Relaxed: Round 7](../../../design/mockups/round7/converged.html) +- [Structured: Round 6 B](../../../design/mockups/round6/b-rail.html) +- [Accumulated review decisions](../../design-reviews/2026-10-16-redesign-round4.md), + [Round 5](../../design-reviews/2026-10-16-redesign-round5.md), + [Round 6](../../design-reviews/2026-10-16-redesign-round6.md), + [Round 7](../../design-reviews/2026-10-16-redesign-round7.md) + +The prototypes are visual/interaction references, not production code. Sample codes, archive +history, source failures and backups must never become real reader data. + +## Product choices + +Settings → Appearance offers **Interface: Relaxed / Structured**, with preview thumbnails and +short descriptions. Relaxed is the proposed default for new and upgraded installs; existing +Dark/Light/System choices remain intact. Relaxed uses Round 7's separated panels, softer surfaces, +Bricolage Grotesque/Figtree typography and generous gutters. Structured uses Round 6 B's ruled +layout and Barlow typography. Both put the current-style game focus pills first in the left rail. +Switch immediately, remember the choice, and preserve the current page, focus and progress. + +Interface, Dark/Light/System, colour scheme and Standard/Compact event rows are independent +preferences. Standard is the default row density. Content determines row height: missing +descriptions reserve no blank line and receive no fabricated replacement text. + +Use a shared component hierarchy with interface-specific tokens and narrowly scoped styles. +Duplicating the app's React tree or maintaining two separate stores would double behavioural +maintenance. A spacing-only switch would not reproduce the two approved visual directions. + +## Navigation, settings and updates + +Main pages: Checklist, Timeline, Routines, Archive, Settings. Preserve the existing stored +Checklist view id `soon` and Timeline id `timeline`. Visiting a utility page must not overwrite +the preferred main view. Settings has its own navigation, beginning with General, and includes +Your games, Appearance, Lists, Region & time, Data & sources, Backup, About and Changelog. + +Keep game focus behaviour from `GameFocus.tsx`: All/game pills, outstanding counts, tap the +selected game to return to All, Next through games and back to All. Hide the picker for one game. +Focus never changes followed games. Game ordering remains the reader's order, otherwise A–Z. + +Checklist and Timeline share a compact updates area. New code notices, events new since the +reader's last visit, release news and source-age notices have distinct labels and state. Do not +clear one by acknowledging another. Omit empty update kinds and the entire area when empty. + +Replace **Older schedules** with **Event data may be out of date**. Supporting copy names the +affected followed games: “For games you track: Genshin Impact, …”. **Details & reasons** opens +Data & sources. Show this row only when `freshness()` reports an affected followed game; +temporary focus does not narrow it. Do not add a large warning banner or repeat it on every row. + +Reuse the real 48-hour freshness rule and oldest-source-per-game semantics. The footer always +states feed age from source data, never `generatedAt`, with Check for new data. The button requests +the published feed; it cannot force wiki scraping. Source details retain last successful data, +last confirmed check, content-change time, source links and known reasons. A recent check with +unchanged content is different from an overdue check. If no failure reason is recorded, say +“The reason for the delay is unavailable”; do not infer blocking or network failure. Offline +status is distinct from source age. + +About retains the colophon, Lucas Winther attribution, contributor acknowledgements, all source +communities and studios, reset-clock reference, disclaimer and project links. Credits are not +filtered by followed games. Changelog is a Settings page, with no footer link or About button. +Existing release notices link to that page. + +## Routines and game setup + +Routines owns follow/unfollow, built-ins, Create your own routine, Make it mine, editing and +independent Checklist/Timeline pins. A follow controls tracking; a pin controls placement. +Stopping a routine preserves its history. Reader-authored games, events and task forms remain +fully functional; prototype placeholders do not satisfy this requirement. + +Routines → Checklist display controls Separate daily panel / Grouped with routines, also exposed +in Appearance as the same preference. Separate is the proposed default. A daily tick appears in +one place, is reachable with one action, and remains distinct per tracked server. + +Routine group choices: Collapsed by default, Expanded by default, Always expanded. Default is +Collapsed. A single-game focus expands its groups; returning to All restores the chosen policy. +Always expanded removes collapse controls. Numeric inputs retain native steppers and `step=1`. + +Game settings contains Setup, Codes and About. Setup offers Events / Dailies / Weeklies & +monthlies / Cycles, plus individual routine choices. These types compose; events are visibility, +not a routine. Save type intent separately from current follows. Unsupported or currently empty +types remain selectable with an explanation. New built-ins matching saved intent are offered +for review, never silently followed on an established install. + +“Track another server” explains: “Play on more than one server? Add your other server to track +both accounts separately.” It creates fresh choices with Events selected initially, and asks +all tracking questions again. It copies no ticks, follows or pins. Changing an existing server +is a different action and preserves that account's recorded history. + +Game colour is shared across its servers, editable with picker/hex/preview/Reset to default. +About lists every registered data source for that game with links and thanks to its editors, +including event, banner and pass sources even when a source currently contributes no events. + +## Appearance and clocks + +Slate, Warm, Plum and Custom schemes are available in each interface. Custom edits background, +surface, text and interface accent separately for light and dark. Retain chosen base colours; +derive readable rendered text, game hues and semantic colours against their actual surfaces. +Keep game identity distinct from urgency. Small text must clear 4.5:1 in both modes and UIs. +Fonts must have usable offline fallback; do not make rendering depend on Google Fonts loading. + +Display timezone defaults to Automatic, resolved from the browser. Allow searchable named IANA +zones and Revert to Automatic. Display timezone changes formatting, never server periods or +stored day keys. Custom event entry states the timezone in which its date/time is interpreted. + +Per tracked game/server, reset timing defaults to Automatic and permits a custom server-local +hour/minute, with Revert to Automatic. Reuse real game/server offsets, regional hours and +built-in clock overrides. Explicit timestamps, `regionEnds` and reader-entered instants remain +authoritative; only reset-based/day-precision boundaries use the resolved reset clock. A routine +with its own explicit clock keeps it. No change to the global games table is needed for reader +overrides. + +Reset changes are effective from their saved timestamp. Preserve a clock revision history per +tracked account and attach the revision to new logs. Previously stored day/period labels are +never re-keyed or silently interpreted under a different clock. Legacy logs belong to the initial +automatic-clock revision. Historical summaries must retain access to all revisions; transition +overlap is displayed as recorded history rather than merging or dropping ticks. Reverting to +Automatic starts another revision and preserves earlier records. Test the boundary immediately +before and after a change, including a change that moves the current game-day backward. + +## Onboarding + +New-user setup starts with events only. Ask default game server, appearance and timezone, then +games and What do you want to track?, then preferred Checklist/Timeline. **Show events that +haven't started** belongs in the tracking step and begins off. Explain types inline, especially +cycles: routines repeating on a game's cadence, such as every fourteen days. Explain “no active +window” separately from “no supported built-in”. Initial setup may follow currently supported +items deliberately selected by type; subsequent additions require review. Defer pin decisions +until readers can see the views. Do not change old installs to the new-install defaults. + +Returning readers receive a one-time release-specific welcome, available in either main view, +with Take the quick tour / Keep browsing. The tour visits Routines, Checklist, Archive and +Settings, with Back and Skip, then returns to the reader's preferred main view. Persist a marker +separate from changelogSeen. Opening or skipping the tour changes no games, routines or progress. + +## Archive and statistics + +On Done or Ignore, persist the outcome and event snapshot immediately. Show “Completed · Moved +to Archive” or “Ignored · Moved to Archive” in the row for about 1.5 seconds, then fade/collapse. +Keep Undo for about eight seconds. Reduced motion removes the fade, retaining the confirmation. +Archive Restore is always available; restoring an ended event restores its recorded status, +but does not pretend it is running again. + +Retain event metadata, description, source, dates/precision, region-specific ends, account, +progress and outcome timestamp. Unknown end dates remain null. Cache snapshots of seen events +before feed replacement so expired unfinished events can be recorded when they leave the live +feed. Missing from a feed is not proof of expiry: retain an outstanding snapshot until its known +deadline passes; unknown-end events remain unresolved and must not become unfinished by guess. +Only archive events for tracking periods the reader actually followed, not hidden catalogue games. + +Default Archive body is per-game, with version groups only where the source states a version. +History timeline is an optional alternate body. The story panel shows monthly and overall +completion rate, current/best completion run, event check-ins, unfinished counts and per-game +outcome bars. Rates use done / (done + ended unfinished), excluding ignored and unresolved. +Use outcome time for done and resolved end time for unfinished when grouping by month. Completion +runs order those eligible outcomes by that time; ignored entries neither extend nor break them. +Event check-ins sum recorded distinct days per event/account, not unique calendar days of play. +Near-miss detail reports an actually recorded goal shortfall, only where a known target exists; +no invented threshold or estimated play progress. Label all statistics as recorded archive history. + +Old marks whose events are no longer available remain preserved but cannot acquire fabricated +titles, deadlines or descriptions. Explain that earlier history may be incomplete. + +## Persistent state, migration and backup + +Keep existing event IDs, daily IDs, task IDs and day-key formats. Introduce stable tracked-account +identities around game/server choices; do not turn a region change into a different event ID. +Legacy progress, daily logs, ignores, task amounts and routine choices are associated once with +each game's initial account at the reader's saved region. A migration marker records the mapping. +Retain legacy key spaces and unknown/unreadable records; migration is idempotent and does not +duplicate logs on reload. Additional accounts get independent state. Existing global preferences, +game visibility/order, custom content and view survive. Migration must work for readers returning +after months and for old exports, not only the current developer browser. + +New state includes account configurations/logs, routine follows/pins and tracking intent, archive +snapshots, reset revisions, appearance/timezone preferences, update-seen markers and code +redemptions. Document concrete schemas and keys in DATA-MODEL with the storage implementation. +Use a validated versioned full-state envelope for backup and restore; accept existing version-1 +exports through a tested legacy reader. Preserve the existing distinction between progress import +and Import all. A new export must carry all new reader-owned state. Do not recursively include +backup history inside each backup. + +Automatic backups run on app use and after settled data changes. Use a stable UTC calendar basis, +independent of display timezone. Keep latest snapshot per UTC day for today and the six previous +days; older snapshots fold into four consecutive seven-day windows; older snapshots then fold +into six calendar-month buckets measured backward from the oldest weekly window. Buckets are +disjoint; retain the newest snapshot in each and discard anything older. A partially covered +calendar month at the weekly boundary retains only eligible older snapshots. Maximum seventeen +retained snapshots, plus one pre-restore recovery copy. Never invent snapshots for unused days. + +Backup rows say **View backup**. Preview shows date and contents; **Restore this backup** is the +explicit action. Validate the entire payload before changing live state, save current data as a +recovery copy, and support Undo last restore. Make multi-key restore recoverable using a prepared +transaction envelope; startup must resolve an interrupted restore before normal store writes. +Storage writes must report success/failure rather than relying on today's silently failing +`writeJson`. If persistence/quota prevents a recovery copy or complete restore, leave current +data intact and offer export. Failed backup retention writes must not delete the last good copy. +Explain that browser-local backups disappear if browser storage is cleared; export is the copy +outside this browser. No accounts or remote sync. + +## Codes and source boundaries + +Codes belong in game settings and notify in-app in both main views. Discovery/newness is per game +and independent from redemption per tracked account. Viewing a game's codes clears its new +indicator, never redemption. Only declare a code new when present in a successfully read feed, +not because its redemption checkbox is clear. Preserve first-observed metadata across builds. + +The supplied Genshin Game8 codes URL is a source candidate, not a shipped feed. A deterministic +codes source is a separate ingestion sub-project with its own source/robots review, fixture, +parser/schema and refresh integration. It must follow existing scraping rules and publish only +source-stated code/reward/expiry facts; unknown expiry stays unknown. The UI implementation +includes a real empty/unavailable state and never ships sample codes as real. Integrating this +source is required to finish the codes feature, but must not delay validating the shared UI shell. + +## Implementation boundaries and delivery order + +Use production React components, pure state/selectors and existing Bun tooling. Extract page +coordination from App and settings from Controls as needed for the actual new page boundaries; +avoid unrelated refactors. No added runtime dependencies, accounts, push service, SQLite work, +parser rewrites or prototype JavaScript transplanted into production. + +Deliver in independently verifiable stages: + +1. Shared shell, both interfaces, settings/navigation, typography and responsive layouts. +2. Validated state/envelope, account migration and reset revision machinery with recovery tests. +3. Routines, game setup, independent pins, direct dailies and clock-aware views. +4. Archive retention/outcomes/statistics and automatic backup/restore. +5. Onboarding, shared updates/source details, credits/Changelog, themes/timezone/custom colours. +6. Deterministic code ingestion and real code notices/redemption, then complete end-to-end review. + +The detailed implementation plan must define file-level tasks and concrete storage schemas before +stage 2. Each stage preserves a runnable app and documents what has actually landed. No staged +delivery is a claim that the complete redesign is finished. + +## Verification and documentation + +Test migrations from realistic legacy saves and exports, idempotence, independent account progress, +reset changes/reverts at boundaries, null ends, vanished feed rows, archive denominator/ordering, +backup folding across calendar boundaries, quota failures and interrupted restores. Test code +newness independently from redemption and source-age scope independently from focus. Existing +date/parser/ID tests must remain green. Tests create their own fixtures and never require public +build output, repository docs or network. + +Run typecheck and the full offline suite; build CSS/JS/static shell with production JSX settings. +Check actual built app in Chrome on a new port for each rebuild at phone and desktop widths, +both interfaces, both themes, Standard/Compact, keyboard/focus, reduced motion and 200% zoom. +Exercise custom palette contrast on all used surfaces, no horizontal page overflow, one-action +dailies, timeline reset adjacency, both onboarding paths, archive Undo/Restore and backup recovery. +Verify offline reopening and the real update flow without losing progress. + +Update PRD, DATA-MODEL, ARCHITECTURE, DESIGN, PRODUCT and affected AGENTS guidance with each +implemented stage. Source integration also updates INGESTION/SOURCES. The shipped documents +continue to describe shipped behaviour until that implementation exists. Add changelog/release +notes under the repository's normal commit workflow when authorized; do not commit or push merely +because this specification has been written. diff --git a/fixtures/genshin/game8-codes-2026-10-16.expected.json b/fixtures/genshin/game8-codes-2026-10-16.expected.json new file mode 100644 index 0000000..0ade5c5 --- /dev/null +++ b/fixtures/genshin/game8-codes-2026-10-16.expected.json @@ -0,0 +1,82 @@ +[ + { + "id": "[\"genshin\",\"NEEIJB764CA3\"]", + "game": "genshin", + "code": "NEEIJB764CA3", + "reward": "Primogem x60 Adventurer's Experience x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"6LS3F3LS5K87\"]", + "game": "genshin", + "code": "6LS3F3LS5K87", + "reward": "Primogem x20 Geode of Replication x60", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"OPERACOLLAB\"]", + "game": "genshin", + "code": "OPERACOLLAB", + "reward": "Primogem x30 Mora x20,000 Hero's Wit x2", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"VesnaOnPatrol\"]", + "game": "genshin", + "code": "VesnaOnPatrol", + "reward": "Primogem x40 Mora x20,000 Hero's Wit x3", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"EPIC2026\"]", + "game": "genshin", + "code": "EPIC2026", + "reward": "Primogem x40 Mora x20,000 Hero's Wit x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"Y6JYMKV6JKSL\"]", + "game": "genshin", + "code": "Y6JYMKV6JKSL", + "reward": "Primogem x60 Adventurer's Experience x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"Y1INABQB6DEX\"]", + "game": "genshin", + "code": "Y1INABQB6DEX", + "reward": "Primogem x60 Adventurer's Experience x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"2BJ64QRZ7RT8\"]", + "game": "genshin", + "code": "2BJ64QRZ7RT8", + "reward": "Primogem x60 Adventurer's Experience x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"PLDXIB886Y1S\"]", + "game": "genshin", + "code": "PLDXIB886Y1S", + "reward": "Mora x10000 Adventurer's Experience x10 Fine Enhancement Ore x5 Stir-Fried Fish Noodles x5 Jueyun Chili Chicken x5", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + }, + { + "id": "[\"genshin\",\"GENSHINGIFT\"]", + "game": "genshin", + "code": "GENSHINGIFT", + "reward": "Primogem x50 Hero's Wit x3", + "expiresAt": null, + "sourceUrl": "https://game8.co/games/Genshin-Impact/archives/304759" + } +] diff --git a/fixtures/genshin/game8-codes-2026-10-16.html b/fixtures/genshin/game8-codes-2026-10-16.html new file mode 100644 index 0000000..e7750a7 --- /dev/null +++ b/fixtures/genshin/game8-codes-2026-10-16.html @@ -0,0 +1,2609 @@ +All Redeem Codes in October 2026 | Genshin Impact|Game8 + + + + + + + + +

    What can you do as a free member?

    Member benefits illustration

    Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

    Member benefits illustration

    Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

    Member benefits illustration

    Create your free account today and save your favorite games for quick access later, synced across all your devices.

    Member benefits illustration

    By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

    Comment rating feature illustration

    By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

    Premium archive feature illustration

    By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

    Map tool feature illustration

    By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

    Site Interface

    Guest
    Free Member
    Article Watchlist
    Game Bookmarks
    Cross-device Sync
    Light/Dark Theme Toggle
    User Profiles
    Direct Feedback
    Comment Rating
    Comment Notification
    Premium Articles

    Game Tools

    Guest
    Free Member
    Interactive Map Access
    Interactive Map Pins
    Interactive Map Comments
    Interactive Map Pins Cross-Device
    Check List
    Event Choice Checker
    Deck Builder Cross-Device
    Message Board Notification
    Message Board Cross-Device
    Build Planner
    Stat Calculator
    Diagnostic Tool
    Weapon/Armor Wishlist
    Team Builder
    Character Scoring
    Tier List Maker

    Want more information?Learn more

    Find your next guide

    Choose a game to explore its walkthrough and guides.

    +
    Genshin Impact

    All Redeem Codes in October 2026

    + + +Add to Watchlist (651)
    Genshin Impact - All Redeem Codes in October 2026
    + +

    Here is a list of Redeem Codes for Genshin Impact in October 2026. See all of the active and new codes, as well as instructions on how to redeem them!

    + + + + + + +
    Paimon
    +Sign up or Log in and use Add to Watchlist under the page title to get new code updates!
    + + +

    Genshin Impact Codes

    Latest Redeem Codes

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Redeem CodesRewards
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 10/05
    +
    + +Primogem Primogem + + + + + x60
    +
    + +Adventurer Adventurer's Experience + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 10/03
    +
    + +Primogem Primogem + + + + + x20
    +
    + +Geode of Replication Geode of Replication + + + + + x60
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/29
    +
    + +Primogem Primogem + + + + + x30
    +
    + +Mora Mora + + + + + x20,000
    +
    + +Hero Hero's Wit + + + + + x2
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/24
    +
    + +Primogem Primogem + + + + + x40
    +
    + +Mora Mora + + + + + x20,000
    +
    + +Hero Hero's Wit + + + + + x3
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/24
    +
    + +Primogem Primogem + + + + + x40
    +
    + +Mora Mora + + + + + x20,000
    +
    + +Hero Hero's Wit + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/14
    +
    + +Primogem Primogem + + + + + x60
    +
    + +Adventurer Adventurer's Experience + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/01
    +
    + +Primogem Primogem + + + + + x60
    +
    + +Adventurer Adventurer's Experience + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 08/03
    +
    + +Primogem Primogem + + + + + x60
    +
    + +Adventurer Adventurer's Experience + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link
    Date Added: 09/24
    +
    + +Mora Mora + + + + + x10000
    +
    + +Adventurer Adventurer's Experience + + + + + x10
    +
    + +Fine Enhancement Ore Fine Enhancement Ore + + + + + x5
    +
    + +Stir-Fried Fish Noodles Stir-Fried Fish Noodles + + + + + x5
    +
    + +Jueyun Chili Chicken Jueyun Chili Chicken + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link +
    +
    + +Primogem Primogem + + + + + x50
    +
    + +Hero Hero's Wit + + + + + x3
    +
    + +

    Obtain free Primogems by redeeming the Genshin Impact codes NEEIJB764CA3, 6LS3F3LS5K87, OPERACOLLAB, VesnaOnPatrol, EPIC2026, Y6JYMKV6JKSL, Y1INABQB6DEX, and 2BJ64QRZ7RT8 to pull for your next character.

    + +

    7.1 Redeem Codes List and Reward

    +

    7.1 Special Program Codes

    7.1 Promo Codes

    +

    As with any special program, you can obtain three redemption codes that, once redeemed, will grant you a total of 300 primogems.

    + +

    These livestream codes will expire and cannot be redeemed after September 15, 2026.

    +

    Global-Exclusive Codes

    + + + + + + + + + + + + + + + + + +
    Global CodesRewards
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link +
    +
    + +Primogem Primogem + + + + + x100
    +
    + +Mystic Enhancement Ore Mystic Enhancement Ore + + + + + x10
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link +
    +
    + +Primogem Primogem + + + + + x100
    +
    + +Hero Hero's Wit + + + + + x5
    +
    +
    + + +
    Copied
    +
    +
    + ▶︎ Redeem Code Link +
    +
    + +Primogem Primogem + + + + + x100
    +
    + +Mora Mora + + + + + x50,000
    +
    + +

    Version 7.1 Livestream Summary

    +

    How to Redeem Genshin Codes

    +

    You can redeem your promotional codes by either claiming it online from the official Redeem Center or from the game's Account Settings menu. You can only use them once your account has reached Adventure Rank 10 or higher.

    +

    Claim from the Redeem Center Site

    +
    Genshin - Redeem Centre Tutorial
    + +
      +
    1. Go to the official redemption site.
    2. +
    3. Log into your account.
    4. +
    5. Select your account's region.
    6. +
    7. Input the redeem code and click ''Redeem''.
    8. +
    9. Claim rewards fromyour in-game mail.
    10. +
    + +

    The easiest way to redeem codes is through Genshin Impact's official code redemption website. You just have to sign up and enter active codes to redeem your rewards!

    + +

    Official Redemption Site

    +

    Redeem Codes from the Account Settings

    +
    Genshin - How to Redeem Codes from In Game
    + +
      +
    1. Navigate to Paimon's menu and choose Settings.
    2. +
    3. From the Settings menu, click on Account.
    4. +
    5. Click on the ''Redeem Code''.
    6. +
    7. Input your Redeem Code and click ''Exchange''
    8. +
    + +

    If you wish to get your rewards in-game, you can do so by opening the Settings Menu and going to Account. You can then input them via the redemption button! Rewards should be immediately given to you via the in-game mail.

    + +

    This function isn't available for the iOS version.

    +

    Expired Genshin Impact Redeem Codes

    List of Expired Codes

    +

    Below is a list of expired codes from previous versions. If you try to use an expired code, you will get an invalid message.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CodesRewards
    YOAL3V36XHS7・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    BALLETCOLLAB・Primogem x30
    ・Mora x10,000
    ・Herp's Wit x3
    ・Mystic Enhancement Ore x3
    GS71YTAICB・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    MySnezhnayaCareer・Primogem x160
    ・Mora x20,000
    ・Adventurer's Experience x3
    LEGEDILJKSGM・Primogem x60
    ・Adventurer's Experience x5
    DUGODWKRHAKDNJ・Primogem x30
    ・Hero's Wit x3
    ・Mora x10,000
    ・Mystic Enhancement Ore x3
    XVIZDH2B9WGX・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GS70XINGOJ・Mora x30,000
    ・Hero's Wit x3
    Mystic Enhancement Ore x5
    EHVE2TEAFY6O・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    UIVIBUQM6Q8A・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    EZSB8889C2BZ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    PFY1S40I88T9・Primogem x60
    ・Adventurer's Experience x5
    LCXGEA5SX65L・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NMI20MAJGIBP・Primogem x30
    ・Geode of Replication x150
    MageNicolesPuzzle・Primogem x30
    ・Chenyu Adeptea x5
    ・Fruit Tandem Turnovers x1
    ・Midsommar Torte x1
    YMYD76U85Z1U・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    PSCA8NL4ZSPD・Primogem x60
    ・Adventurer's Experience x5
    OHOHONICOLE・Primogem x30
    ・Mora x20,000
    ・Tattered Warrant x3
    A0NBWRZZI3XJ・Primogem x20
    ・Geode of Replication x160
    FEOMBCVX6UO7・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    8BHA0KFRG94K・Primogem x60
    ・Adventurer's Experience x5
    FAIRYGIRLLINNEA・Primogem x30
    ・Tattered Warrant x3
    ・Mora x20000
    INUH43366326・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    Snezhnaya20260812・Primogem x200
    RFVEZO83458Q・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    MIXF44DAP7HW・Primogem x60
    ・Adventurer's Experience x5
    8X73KH58KDHN・Primogem x60
    ・Adventurer's Experience x5
    LTT3DVKVLUQZ・Primogem x30
    ・Mora x20,000
    ・Broken Drive Shaft x3
    GS64YTW65O・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Vayuda Turquoise Sliver x3
    ・Teachings of Freedom x3
    JTSHBHPPKUSV・Primogem x20
    ・Geode of Replication x160
    HUEK357YFYN3・Mora x10,000nl]・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    TSUKINOARIKAEMoonfall Silver x5
    ・Mora x20,000
    ・Hero's Wit x2
    ・Mystic Enhancement Ore x2
    MoonInvitationLunaIV・Primogem x40
    ・Mora x20,000
    ・Hero's Wit x3
    GS63YTOO2X・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Varunada Lazurite Sliver x3
    ・Teachings of Moonlight x3
    DABHTY763DAZ・Mora x20,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    BLKS76MP2B6C・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    P3GXX56W3VG9・Primogem x60
    ・Adventurer's Experience x5
    XXOJ2Z7F8GJI・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    0FQBAJNXCUFU・Primogem x60
    ・Adventurer's Experience x5
    MBEYNDLU3WGZ・Mora x20,000
    ・Hero's Wit x2
    ・Geode of Replication x200
    ・Mystic Enhancement Ore x2
    XTFPHSEJCFYM・Geode of Replication x320
    NTIF4NP4DO89・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GS61RD26W966・Mora x20,000
    ・Hero's Wit x2
    ・Geode of Replication x80
    ・Mystic Enhancement Ore x2
    R4SCAU7CR1G9・Primogem x60
    ・Adventurer's Experience x5
    EKLP57EFE4G4・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    AOZJ2TD5FZPV・Primogem x60
    ・Adventurer's Experience x5
    GenshinHigh5Fight・Primogem x30
    ・Hero's Wit x3
    ・Ile flottante x6
    ・Fine Enhancement Ore x5
    EPIC2025・Primogem x40
    ・Mora x20,000
    ・Adventurer's Experience x5
    CLD6ZWU35GTH・Primogem x60
    ・Adventurer's Experience x5
    WUSXOI92X54U・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    SWIP9F497X18・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GS60YTM4NW・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NODKRAISP0909・Mora x20,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    TOUGA75TO1I3・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ITSTARTSIN60NODKRAI・Primogem x40
    ・Mora x20,000
    ・Hero's Wit x3
    8QD412QXCEU1・Primogem x60
    ・Adventurer's Experience x5
    GS58YTPGFY・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Teachings of Conflict x3
    ・Vajrada Amethyst Sliver x3
    G9VY57AWQACW・Primogem x60
    ・Mystic Enhancement Ore x10
    Vamosacorrer25・Mora x20,000
    ・Puff Pop x1
    ・Blazed Meat Stew x1
    ・Grainfruit Chips x1
    ZT5RJ4AX4WYV・Primogem x60
    ・Adventurer's Experience x5
    GIFTFROMSKIRK57・Mora x30,000
    ・Shivada Jade Fragment x3
    ・Fine Enhancement Ore x3
    ・Meshing Gear x3
    FIGHTLIKESKIRK57・Primogem x40
    ・Hero's Wit x3
    ・Abyssal Bounty x1
    GS57YTSNLJ・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Shivada Jade Sliver x3
    ・Teachings of Contention x3
    HS4NM7YDGC99・Primogem x60
    ・Adventurer's Experience x5
    LWKC7L75S544・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ZXUYP2X2EF14・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    VSL93LTE5WY5・Beryl Conch x5
    ・Lasagna x2
    ・Fish and Chips x2
    ・Tomates Narbonnaises x2
    ・Bulle Sauce Duck Breast x2
    ・Bulle Sauce Duck Breast x2
    ・Barbeque Ribs x2
    ・Meshing Gear x3
    56CacucuVSAjaw・Primogem x30
    ・Mora x20,000
    ・Hero's Wit x3
    OSOIDWA2R5H3・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NODKRAI0910・Primogem x200
    FATUI・Primogem x30
    FROSTMOONSCIONS・Primogem x30
    LIGHTKEEPERS・Primogem x30
    THEWILDHUNT・Primogem x30
    VOYNICHGUILD・Primogem x30
    5AM75NYUGTTR・Primogem x30
    ・Hero's Wit x3
    ・Mora x20,000
    ・Blazed Meat Stew x5
    11KU0MNDK2RG・Primogem x60
    ・Adventurer's Experience x5
    UAL6MXW8BUFH・Mora x20,000
    ・Secret Art x2
    ・Honey-Glazed Ceviche x2
    GS55YTONVQ・Mora x30,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Teachings of Conflict x3
    ・Vajrada Amethyst Sliver x3
    JCWJFZG57YFW・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    XA4N66LAC349・Mora x20,000
    ・Fruit-Flavored Milk Candies x2
    ・Dreams of Healing x2
    ・Impeccably Organized x2
    HNYM49IX9PDS・Primogem x60
    ・Adventurer's Experience x5
    7B476N4TDJ6V・Mora x20,000
    ・Crepes Suzette x1
    ・Cold Cut Platter x1
    ・Tea Break Pancake x1
    ・Matsutake Meat Rolls x1
    ・Sparkling Berry Juice x1
    ・Teyvat Fried Egg x2
    RB5PPN4BV355・Primogem x30
    ・Sea Ganoderma x3
    ・Hero's Wit x3
    ・Mora x20,000
    ・Fine Enhancement Ore x5
    WEAV365PI953・Withering Purpurbloom x3
    ・Quenepa Berry x3
    ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    GS54MTX685・Mora x30,000
    ・Hero's Wit x3
    ・Guide to Transience x3
    ・Mystic Enhancement Ore x5
    ・Vayuda Turquoise Sliver x3
    WAM5UWWU28NV・Primogem x60
    ・Adventurer's Experience x5
    FORNATLAN・Primogem x40
    ・Withering Purpurbloom x5
    ・Warrior's Metal Whistle x3
    MAVUIKAWISHES・Mora x10000
    ・Hymn of Gathered Flame x1
    KAM7UKZGVRXZ・Primogem x30
    ・Stuffed N' Mashed Potatoes x1
    ・Fruity Trio x1
    ・Chocolate x2
    ・Goulash x1
    ・Sweet Madame x1
    ・Chenyu Brew x1
    FIBE54PL5995・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ACUI96V85FCK・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Seasond Fang x3
    SDKLSK67TZ1X・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Seasond Fang x3
    LA5E77CLZX5V・Primogem x60
    ・Adventurer's Experience x5
    MTLF6DKA7PQZ・Mora x30000
    ・Tatacos x3
    ・Grainfruitchip Fruit x3
    ・Cup O' Grainfruit x3
    WTXV36M1J6CO・Brilliant Chrysanthemum x3
    ・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    DEOB21CK9Y6N・Brilliant Chrysanthemum x3
    ・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    SBUEMJB449D9・Brilliant Chrysanthemum x3
    ・Mora x10000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    Genshin51YT・Brilliant Chrysanthemum x5
    ・Mora x30000
    ・Hero's Wit x3
    ・Guide to Kindling x3
    ・Fine Enhancement Ore x5
    DT4BZD5RT5U9・Primogem x60
    ・Adventurer's Experience x5
    HHFP3517D3UN・Mora x10000
    Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Varunada Lazurite Sliver x3
    KEQES9R79R49・Mora x10000
    Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Varunada Lazurite Sliver x3
    EPICGENSHIN2024・Mora x30000
    Hero's Wit x10
    ・Glittering Gemstone x5
    ・Hot Spring O'Clock x5
    ・Puff Pops x5
    ・Mystic Enhancement Ore x5
    BlazeToNatlan・Primogem x100
    Mora x30000
    ・Hero's Wit x3
    ・Adventirer's Experience x5
    EFCI18V69I74・Mora x10000
    Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    +
    5SKXWYSCPEVR・Hero's Wit x5
    5A2WEGTVPXFV・Mystic Enhancement Ore x10
    TTKEXYBCNWFH・Mora x30000
    ・Pile 'Em Up x3
    NAJEWZTD7XD5・Primogem x60
    ・Adventurer's Experience x5
    USDA9H94ZHSE・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ZOYWFIGI3NJ2・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ARQS5391FY79・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    KKFDOUU3GCJP・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    KKFD1J97LTJ2・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    QIDD46418U6Q・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GENSHINSUMMERNIGHT・Mora x20,000
    ・Hero's Wit x2
    ・Biryani x1
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    FBJCMYSXCBT5・Primogem x60
    ・Adventurer's Experience x5
    VAJWEZAC7WCZ・Mora x20,000
    ・Mystic Enhancement Ore x5
    LUURRWZ23243・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    BZPU67488O5V・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    BCPCGY9HGB3H・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    RTJUNRSHTAL9・Primogem x60
    ・Adventurer's Experience x5
    ULEP5WZ36B27・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    XQECMR9IIJ46・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    PZFUH5GIP9UF・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    DSXY15NL247E・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    MLEA572X2UHK・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    RERBFT39544D・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    XEZAXRS88R5G・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    VFNS1OGXYOYU・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    CA3BLTURGH9D・Primogem x60
    ・Adventurer's Experience x5
    KAVI75ET63II・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    HOFK9PBK481G・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NQXAZLX1943G・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GGXG9VO6967I・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    OLQH7G62I7EU・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    BNYP1WBMU61F・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    YKHC29118G54・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    IREQ1FTFRYBX・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    8SJSDSX68UF9・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    TGEFFQY7J6V2・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    FBYQ36SHY279・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    MEIR79527PDC・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    QOTR3M8X535N・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NA88ANTJL5SD・Primogem x60
    ・Adventurer's Experience x5
    JB95D2V5XGJ5・Mora x20,000
    ・Hero's Wit x2
    ・Adventurer's Experience x5
    ・Charcoal-Baked Ajilenakh Cake x5
    ・Curry Shrimp x5
    NNDFKSKEX6TH・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    HJGDJA2FW7FH・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    PAIMONSEAYOUIN2024・Mora x20,000
    ・Hero's Wit x5
    ・Mystic Enhancement Ore x5
    EA8RWDMBVRTR・Primogem x60
    ・Adventurer's Experience x5
    3SRM3VWBHWYH・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    WA8MJCETGXLR・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    ETR5KVFSYFMZ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    YT9L3UFSZXF9・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    WA8MJCETGXLR・Primogem x50
    ・Hero's Wit x3
    6T8NWE3HD7PD・Primogem x60
    ・Adventurer's Experience x5
    9A97KJNX2NZ9・Primogem x30
    AAQNVELTVSXV・Mora x20,000
    ・Mystic Enhancement Ore x5
    TTQNCFMADSWH・Primogem x30
    AS97CE45EUER・Mora x10,000
    ・Mora x20,000
    ・Hero's Wit x2
    ・Mystic Enhancement Ore x3
    GDAH2VWGT8AT・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    WEQP3D5ZHRD9・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    RQWP3ULZZ8VD・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    EAFVKD4GZQBZ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GENSHINIMPACT
    RIVALS23
    ・Mora x20,000
    ・Hero's Wit x3
    ・Mystic Enhancement Ore x5
    ・Sweet Madame x3
    2S84JS839T8R・Primogem x60
    ・Adventurer's Experience x5
    NA9NDHHCZKTD・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    NS92PG6DB52M・Primogem x60
    ・Adventurer's Experience x5
    RS9MLR5553UH・Mora x30,000
    ・Crab Roe Kourayaki x1
    ・Mint Jelly x1
    WTQ2E83WS869・Primogem x60
    ・Adventurer's Experience x5
    9S83F34ZFDVV・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    GA9FPD42SJ4V・Primogem x60
    ・Adventurer's Experience x5
    LAQZMTPKNTYH・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Stir-Fried Fish Noodles x5
    ・Jueyun Chili Chicken x5
    4BQYGWQ9HTR5・Shimi Chazuke x1
    ・Once Upon a Time in Mondstadt x1
    ・Omurice Waltz x1
    ・Survival Grilled Fish x1
    +SSRCJ8HSV7UM
    +
    ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Fisherman's Toast x5
    ・Goulash x5
    LA9C3RHPPHQH・Primogem x60
    ・Adventurer's Experience x5
    DAQS9FPX2U35・Primogem x60
    ・Adventurer's Experience x5
    +FTRUFT7AT5SV
    +
    ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Fried Radish Balls x5
    ・Satisfying Salad x5
    +NT8SU92DKFRZ
    +
    ・Primogem x60
    ・Adventurer's Experience x5
    +WARBDRR9MCQ9
    +
    ・Mora x10,000
    ・Adventurer's Experience x10
    ・Fine Enhancement Ore x5
    ・Northern Smoked Chicken x5
    ・Flaming Red Bolognes x5
    +MA6RPW8GGJAM
    +
    ・Primogem x60
    ・Adventurer's Experience x5
    6A6VJTWGCPYV・Primogem x60
    ・Adventurer's Experience x5
    MTNUJBXDD72R・Primogem x60
    ・Adventurer's Experience x5
    MS7C3SV8DMZH・Primogem x60
    ・Adventurer's Experience x5
    ZSPDKSC3V8V5・Primogem x60
    ・Adventurer's Experience x5
    9BPCJCQGHAWZ・Primogem x60
    ・Adventurer's Experience x5
    FANRONG
    CHANGSHENG
    ・Mora x18,000
    ・Adventurer's Experience x8・
    Fine Enhancement Ore x10
    BSPD3ZRXU985・Primogem x60
    ・Mora x10,000
    GENSHINGALAXY・Mora x10,000
    ・Adventurer's Experience x10
    ・Mondstadt Hash Brown x3
    ・Northern Smoked Chicken x3
    ・Fine Enhancement Ore x5
    GENSHINEPIC・Mora x10,000
    ・Adventurer's Experience x10
    ・Squirrel Fish x3
    ・Northern Apple Stew x3
    ・Fine Enhancement Ore x5
    SBNBUK67M37Z・Primogem x60
    ・Hero's Wit x30
    GS6ACJ775KNV・Primogem x60
    ・Mora x10,000
    TT7BVJNPL249・Primogem x60
    GenshinZHB30・Primogem x160
    GSIMPTQ125・Primogem x60
    ・Mora x10,000
    +GENSHIN0928A
    (Asia Only) +
    ・Primogem x60
    ・Mora x10,000
    +GENSHIN0928N
    (America Only) +
    ・Primogem x60
    ・Mora x10,000
    +GENSHIN0928E
    (Europe Only) +
    ・Primogem x60
    ・Mora x10,000
    +GENSHIN1006S
    (Asia Only) +
    ・Primogem x60
    ・Adventurer's Experience x5
    +GENSHIN0928A
    (America Only) +
    ・Primogem x60
    ・Adventurer's Experience x5
    +GENSHIN0928U
    (Europe Only) +
    ・Primogem x60
    ・Adventurer's Experience x5
    +GENSHINMHY0I
    (Asia Only) +
    ・Primogem x30
    +GENSHINMHY0M
    (America Only) +
    ・Primogem x30
    +GENSHINMHY0O
    (Europe Only) +
    ・Primogem x30
    +

    Genshin Impact Related Guides

    Genshin Impact Wiki Guide

    +
    Genshin Impact - Wiki and Guide Top
    + +

    Genshin Impact Wiki Front Page

    +

    Guides by Category

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    All Categories
    Genshin Impact - Story WalkthroughStory WalkthroughGenshin - News and Game InfoNews & Game Info
    Genshin - List of CharactersCharactersGenshin - GachasGacha (Wishes)
    Genshin - Rerolling GuidesRerollingGenshin - All Tier ListsTier Lists
    +Genshin Impact - Redeem CodesRedeem Codes +Genshin Impact - VersionsVersions
    Genshin - Tips and TricksTips & Tricks +Genshin Impact - List of All Quest WalkthroughsQuests +
    Genshin Impact - List of WeaponsWeaponsGenshin Impact - List of ArtifactsArtifacts
    Genshin Impact - List of Bosses and Boss Battle GuidesBossesGenshin - List of EnemiesEnemies
    Genshin - List of DomainsDomainsGenshin - Items and MaterialsItems & Materials
    Genshin - List of Events and Event GuidesEventsGenshin - List of AchievementsAchievements
    Genshin Impact - Message BoardsMessage BoardsGenshin - Spiral Abyss GuidesSpiral Abyss
    Genshin - Areas and MapsAreas and Maps +Genshin - ArchiveArchive
    +Genshin Impact - Locations and LoreLocations & Lore +Genshin - HousingHousing
    Genshin Impact - Fishing System Slim BannerFishingGenshin Impact - Genius Invokation TCGGenius Invokation
    +Genshin Impact - Imaginarium TheaterImaginarium Theater Genshin - UGCMiliastra Wonderland
    Genshin Impact ReviewGame Review-
    +

    Exclusive Guides

    +
    Genshin - Premium Articles
    + +

    Premium Articles

    +

    Premium Articles

    +
    Genshin - Premium Articles
    + +

    Premium Articles

    + +
    +

    Comment

    167 Anonymous Anonymous about 10 hours

    30 rs redeem codw

    166 Anonymous Anonymous about 10 hours

    Please 30 rs redeem code

      +

      All rights reserved

      + + + +
      Guide Index
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      \ No newline at end of file diff --git a/fixtures/genshin/game8-codes-2026-10-16.meta.json b/fixtures/genshin/game8-codes-2026-10-16.meta.json new file mode 100644 index 0000000..0bbba50 --- /dev/null +++ b/fixtures/genshin/game8-codes-2026-10-16.meta.json @@ -0,0 +1,12 @@ +{ + "sourceId": "genshin-game8-codes", + "url": "https://game8.co/games/Genshin-Impact/archives/304759", + "contentHash": "14e97c995dbad078c2b9b10f10251f4272f06d3428cbf68977894db03aecfea1", + "bytes": 373038, + "etag": null, + "lastModified": null, + "contentChangedAt": "2026-10-16T11:48:08.968Z", + "lastConfirmedAt": "2026-10-16T11:48:08.968Z", + "eventCount": 10, + "charset": "utf-8" +} diff --git a/index.html b/index.html index 4ba08af..946dba1 100644 --- a/index.html +++ b/index.html @@ -49,6 +49,10 @@ var prefs = JSON.parse( localStorage.getItem("gacha-tracker:v1:prefs") || "{}", ); + var reader = JSON.parse(localStorage.getItem("gacha-tracker:v2:reader") || "null"); + if (reader && reader.version === 2 && reader.prefs) prefs = reader.prefs; + document.documentElement.dataset.interface = prefs.interface === "relaxed" ? "relaxed" : "structured"; + document.documentElement.dataset.density = prefs.density === "compact" ? "compact" : "standard"; var light = prefs.theme === "light" || (prefs.theme === "system" && @@ -83,7 +87,7 @@ diff --git a/package.json b/package.json index bd790c5..d6f7cba 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "refresh": "bun run scripts/refresh-sources.ts", "build:css": "bunx @tailwindcss/cli -i src/client/styles.css -o public/styles.css --minify", "build:js": "bun build src/client/main.tsx --outfile public/main.js --minify --production", - "build": "bun run build:feed && bun run build:css && bun run build:js && bun run build:static", + "build": "bun run build:feed && bun run build:codes && bun run build:css && bun run build:js && bun run build:static", "dev": "bun run build && bun run serve.ts", "build:static": "bun run scripts/build-static.ts", - "serve": "bun run serve.ts" + "serve": "bun run serve.ts", + "build:codes": "bun run scripts/build-codes.ts" }, "dependencies": { "react": "^19.2.8", diff --git a/scripts/build-codes.ts b/scripts/build-codes.ts new file mode 100644 index 0000000..b10c1bf --- /dev/null +++ b/scripts/build-codes.ts @@ -0,0 +1,65 @@ +import { mkdir, rename } from "node:fs/promises"; +import { CODE_SOURCES } from "../src/ingest/codes/registry.ts"; +import { buildCodesFeed } from "../src/ingest/codes/build.ts"; +import { CodesFeedSchema, type CodesFeed } from "../src/shared/codes.ts"; +import { SnapshotStore, freshnessAt } from "../src/ingest/snapshots.ts"; +import type { SourceHealth } from "../src/shared/feed.ts"; +export async function buildCodes() { + const root = process.env.SNAPSHOT_DIR ?? "snapshots", + out = process.env.PUBLIC_DIR ?? "public", + store = new SnapshotStore(root); + const firstPath = `${root}/codes-first-seen.json`; + let first: Record = {}; + try { + first = await Bun.file(firstPath).json(); + } catch { + /* First offline build uses the fixture's capture date. */ + } + const previous: CodesFeed = { schemaVersion: 1, codes: [], sources: [] }; + const codes: CodesFeed["codes"] = []; + const sources: SourceHealth[] = []; + for (const source of CODE_SOURCES) { + const snapshot = await store.read(source.id), + at = snapshot + ? freshnessAt(snapshot) + : (( + await Bun.file( + "fixtures/genshin/game8-codes-2026-10-16.meta.json", + ).json() + ).contentChangedAt as string), + html = + snapshot?.html ?? + (await Bun.file("fixtures/genshin/game8-codes-2026-10-16.html").text()); + const parsed = source.parse(html, { + now: new Date().toISOString(), + sourceId: source.id, + sourceUrl: source.url, + game: source.game, + }); + const built = buildCodesFeed(previous, parsed, at); + for (const code of built.codes) { + code.firstObservedAt = first[code.id] ?? code.firstObservedAt; + first[code.id] = code.firstObservedAt; + codes.push(code); + } + sources.push({ + sourceId: source.id, + game: source.game, + url: source.url, + lastSuccessAt: at, + lastConfirmedAt: snapshot?.meta.lastConfirmedAt ?? null, + contentChangedAt: snapshot?.meta.contentChangedAt ?? at, + eventCount: parsed.length, + parsedCount: parsed.length, + statesNoEvents: false, + }); + } + const feed = CodesFeedSchema.parse({ schemaVersion: 1, codes, sources }); + await mkdir(`${out}/data`, { recursive: true }); + await Bun.write(`${out}/data/codes.v1.json`, JSON.stringify(feed)); + await mkdir(root, { recursive: true }); + await Bun.write(firstPath + ".tmp", JSON.stringify(first, null, 2) + "\n"); + await rename(firstPath + ".tmp", firstPath); + console.log(`build-codes: ${codes.length} current codes`); +} +if (import.meta.main) await buildCodes(); diff --git a/scripts/refresh-sources.ts b/scripts/refresh-sources.ts index 7339157..bfc6803 100644 --- a/scripts/refresh-sources.ts +++ b/scripts/refresh-sources.ts @@ -47,10 +47,10 @@ import { appendFile } from "node:fs/promises"; import { ADAPTERS, - adapterById, } from "../src/ingest/adapters/index.ts"; import { SIX_HOURS_MS } from "../src/ingest/adapters/types.ts"; -import type { Adapter } from "../src/ingest/adapters/types.ts"; +import { CODE_SOURCES, type RefreshSource } from "../src/ingest/codes/registry.ts"; +const REFRESH_SOURCES: readonly RefreshSource[] = [...ADAPTERS, ...CODE_SOURCES]; import { RobotsCache, type FetchLike } from "../src/ingest/robots.ts"; import { decodeBody, SnapshotStore } from "../src/ingest/snapshots.ts"; @@ -129,7 +129,7 @@ export interface RobotsGate { } export interface RefreshOptions { - adapters: readonly Adapter[]; + adapters: readonly RefreshSource[]; store: SnapshotStore; robots: RobotsGate; fetchImpl: FetchLike; @@ -335,7 +335,7 @@ export async function runRefresh( } async function refreshOne( - adapter: Adapter, + adapter: RefreshSource, options: RefreshOptions, cycle: Cycle, ): Promise { @@ -715,6 +715,8 @@ export async function rebuildFeedViaScript(): Promise { }); const code = await proc.exited; if (code !== 0) throw new Error(`build-feed exited ${code}`); + const codes = Bun.spawn(["bun", "run", "scripts/build-codes.ts"], { stdout: "inherit", stderr: "inherit" }); + if (await codes.exited !== 0) throw new Error("build-codes failed"); } /** Whether a person authorised this run, and who, for the override gates. */ @@ -847,7 +849,7 @@ const USAGE = `usage: bun run refresh [--dry-run] [--only ] [--snapsho [--force] --dry-run report what each source would do; no requests, no writes - --only refresh a single source (${ADAPTERS.map((a) => a.id).join(", ")}) + --only refresh a single source (${REFRESH_SOURCES.map((a) => a.id).join(", ")}) --snapshots snapshot cache directory (default: snapshots, env SNAPSHOT_DIR) --user-agent override the User-Agent (env REFRESH_USER_AGENT) --no-feed skip regenerating public/data/events.v1.json @@ -878,7 +880,7 @@ async function main(): Promise { return 0; } - if (args.only !== null && adapterById(args.only) === undefined) { + if (args.only !== null && !REFRESH_SOURCES.some(source => source.id === args.only)) { console.error(`unknown source '${args.only}'`); console.error(USAGE); return 2; @@ -939,7 +941,7 @@ async function main(): Promise { }); if (args.force) { - const n = args.only === null ? ADAPTERS.length : 1; + const n = args.only === null ? REFRESH_SOURCES.length : 1; console.warn( ` ! --force: ignoring the 6h floor for ${n} source${n === 1 ? "" : "s"}. ` + `Conditional headers still apply, so an unchanged page costs a 304.`, @@ -955,14 +957,14 @@ async function main(): Promise { } console.log( - `refresh: ${args.only ?? `${ADAPTERS.length} sources`}${args.dryRun ? " (dry run)" : ""}`, + `refresh: ${args.only ?? `${REFRESH_SOURCES.length} sources`}${args.dryRun ? " (dry run)" : ""}`, ); console.log(` user-agent: ${args.userAgent}`); console.log(` snapshots: ${args.root}`); console.log(` interval: ${SIX_HOURS_MS / 3_600_000}h minimum per source\n`); const summary = await runRefresh({ - adapters: ADAPTERS, + adapters: REFRESH_SOURCES, store, robots, fetchImpl: (input, init) => fetch(input, init), diff --git a/snapshots/codes-first-seen.json b/snapshots/codes-first-seen.json new file mode 100644 index 0000000..bcf2e42 --- /dev/null +++ b/snapshots/codes-first-seen.json @@ -0,0 +1,12 @@ +{ + "[\"genshin\",\"NEEIJB764CA3\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"6LS3F3LS5K87\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"OPERACOLLAB\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"VesnaOnPatrol\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"EPIC2026\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"Y6JYMKV6JKSL\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"Y1INABQB6DEX\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"2BJ64QRZ7RT8\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"PLDXIB886Y1S\"]": "2026-10-16T11:48:08.968Z", + "[\"genshin\",\"GENSHINGIFT\"]": "2026-10-16T11:48:08.968Z" +} diff --git a/snapshots/genshin-game8-codes.html b/snapshots/genshin-game8-codes.html new file mode 100644 index 0000000..e7750a7 --- /dev/null +++ b/snapshots/genshin-game8-codes.html @@ -0,0 +1,2609 @@ +All Redeem Codes in October 2026 | Genshin Impact|Game8 + + + + + + + + +

      What can you do as a free member?

      Member benefits illustration

      Create your free account today and unlock all our premium features and tools to enhance your gaming experience.

      Member benefits illustration

      Create your free account today and save articles to your watchlist and get notified when they're updated with new information.

      Member benefits illustration

      Create your free account today and save your favorite games for quick access later, synced across all your devices.

      Member benefits illustration

      By creating a Game8 account and logging in, you'll receive instant notifications when someone replies to your posts.

      Comment rating feature illustration

      By creating a Game8 account and logging in, you can make use of convenient features in the comments section, such as rating and sorting comments.

      Premium archive feature illustration

      By creating a Game8 account and logging in, you can access Premium articles that are exclusively available to members.

      Map tool feature illustration

      By creating a Game8 account and logging in, the items you checked on the map tool will be saved.

      Site Interface

      Guest
      Free Member
      Article Watchlist
      Game Bookmarks
      Cross-device Sync
      Light/Dark Theme Toggle
      User Profiles
      Direct Feedback
      Comment Rating
      Comment Notification
      Premium Articles

      Game Tools

      Guest
      Free Member
      Interactive Map Access
      Interactive Map Pins
      Interactive Map Comments
      Interactive Map Pins Cross-Device
      Check List
      Event Choice Checker
      Deck Builder Cross-Device
      Message Board Notification
      Message Board Cross-Device
      Build Planner
      Stat Calculator
      Diagnostic Tool
      Weapon/Armor Wishlist
      Team Builder
      Character Scoring
      Tier List Maker

      Want more information?Learn more

      Find your next guide

      Choose a game to explore its walkthrough and guides.

      +
      Genshin Impact

      All Redeem Codes in October 2026

      + + +Add to Watchlist (651)
      Genshin Impact - All Redeem Codes in October 2026
      + +

      Here is a list of Redeem Codes for Genshin Impact in October 2026. See all of the active and new codes, as well as instructions on how to redeem them!

      + + + + + + +
      Paimon
      +Sign up or Log in and use Add to Watchlist under the page title to get new code updates!
      + + +

      Genshin Impact Codes

      Latest Redeem Codes

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Redeem CodesRewards
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 10/05
      +
      + +Primogem Primogem + + + + + x60
      +
      + +Adventurer Adventurer's Experience + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 10/03
      +
      + +Primogem Primogem + + + + + x20
      +
      + +Geode of Replication Geode of Replication + + + + + x60
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/29
      +
      + +Primogem Primogem + + + + + x30
      +
      + +Mora Mora + + + + + x20,000
      +
      + +Hero Hero's Wit + + + + + x2
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/24
      +
      + +Primogem Primogem + + + + + x40
      +
      + +Mora Mora + + + + + x20,000
      +
      + +Hero Hero's Wit + + + + + x3
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/24
      +
      + +Primogem Primogem + + + + + x40
      +
      + +Mora Mora + + + + + x20,000
      +
      + +Hero Hero's Wit + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/14
      +
      + +Primogem Primogem + + + + + x60
      +
      + +Adventurer Adventurer's Experience + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/01
      +
      + +Primogem Primogem + + + + + x60
      +
      + +Adventurer Adventurer's Experience + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 08/03
      +
      + +Primogem Primogem + + + + + x60
      +
      + +Adventurer Adventurer's Experience + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link
      Date Added: 09/24
      +
      + +Mora Mora + + + + + x10000
      +
      + +Adventurer Adventurer's Experience + + + + + x10
      +
      + +Fine Enhancement Ore Fine Enhancement Ore + + + + + x5
      +
      + +Stir-Fried Fish Noodles Stir-Fried Fish Noodles + + + + + x5
      +
      + +Jueyun Chili Chicken Jueyun Chili Chicken + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link +
      +
      + +Primogem Primogem + + + + + x50
      +
      + +Hero Hero's Wit + + + + + x3
      +
      + +

      Obtain free Primogems by redeeming the Genshin Impact codes NEEIJB764CA3, 6LS3F3LS5K87, OPERACOLLAB, VesnaOnPatrol, EPIC2026, Y6JYMKV6JKSL, Y1INABQB6DEX, and 2BJ64QRZ7RT8 to pull for your next character.

      + +

      7.1 Redeem Codes List and Reward

      +

      7.1 Special Program Codes

      7.1 Promo Codes

      +

      As with any special program, you can obtain three redemption codes that, once redeemed, will grant you a total of 300 primogems.

      + +

      These livestream codes will expire and cannot be redeemed after September 15, 2026.

      +

      Global-Exclusive Codes

      + + + + + + + + + + + + + + + + + +
      Global CodesRewards
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link +
      +
      + +Primogem Primogem + + + + + x100
      +
      + +Mystic Enhancement Ore Mystic Enhancement Ore + + + + + x10
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link +
      +
      + +Primogem Primogem + + + + + x100
      +
      + +Hero Hero's Wit + + + + + x5
      +
      +
      + + +
      Copied
      +
      +
      + ▶︎ Redeem Code Link +
      +
      + +Primogem Primogem + + + + + x100
      +
      + +Mora Mora + + + + + x50,000
      +
      + +

      Version 7.1 Livestream Summary

      +

      How to Redeem Genshin Codes

      +

      You can redeem your promotional codes by either claiming it online from the official Redeem Center or from the game's Account Settings menu. You can only use them once your account has reached Adventure Rank 10 or higher.

      +

      Claim from the Redeem Center Site

      +
      Genshin - Redeem Centre Tutorial
      + +
        +
      1. Go to the official redemption site.
      2. +
      3. Log into your account.
      4. +
      5. Select your account's region.
      6. +
      7. Input the redeem code and click ''Redeem''.
      8. +
      9. Claim rewards fromyour in-game mail.
      10. +
      + +

      The easiest way to redeem codes is through Genshin Impact's official code redemption website. You just have to sign up and enter active codes to redeem your rewards!

      + +

      Official Redemption Site

      +

      Redeem Codes from the Account Settings

      +
      Genshin - How to Redeem Codes from In Game
      + +
        +
      1. Navigate to Paimon's menu and choose Settings.
      2. +
      3. From the Settings menu, click on Account.
      4. +
      5. Click on the ''Redeem Code''.
      6. +
      7. Input your Redeem Code and click ''Exchange''
      8. +
      + +

      If you wish to get your rewards in-game, you can do so by opening the Settings Menu and going to Account. You can then input them via the redemption button! Rewards should be immediately given to you via the in-game mail.

      + +

      This function isn't available for the iOS version.

      +

      Expired Genshin Impact Redeem Codes

      List of Expired Codes

      +

      Below is a list of expired codes from previous versions. If you try to use an expired code, you will get an invalid message.

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      CodesRewards
      YOAL3V36XHS7・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      BALLETCOLLAB・Primogem x30
      ・Mora x10,000
      ・Herp's Wit x3
      ・Mystic Enhancement Ore x3
      GS71YTAICB・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      MySnezhnayaCareer・Primogem x160
      ・Mora x20,000
      ・Adventurer's Experience x3
      LEGEDILJKSGM・Primogem x60
      ・Adventurer's Experience x5
      DUGODWKRHAKDNJ・Primogem x30
      ・Hero's Wit x3
      ・Mora x10,000
      ・Mystic Enhancement Ore x3
      XVIZDH2B9WGX・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GS70XINGOJ・Mora x30,000
      ・Hero's Wit x3
      Mystic Enhancement Ore x5
      EHVE2TEAFY6O・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      UIVIBUQM6Q8A・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      EZSB8889C2BZ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      PFY1S40I88T9・Primogem x60
      ・Adventurer's Experience x5
      LCXGEA5SX65L・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NMI20MAJGIBP・Primogem x30
      ・Geode of Replication x150
      MageNicolesPuzzle・Primogem x30
      ・Chenyu Adeptea x5
      ・Fruit Tandem Turnovers x1
      ・Midsommar Torte x1
      YMYD76U85Z1U・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      PSCA8NL4ZSPD・Primogem x60
      ・Adventurer's Experience x5
      OHOHONICOLE・Primogem x30
      ・Mora x20,000
      ・Tattered Warrant x3
      A0NBWRZZI3XJ・Primogem x20
      ・Geode of Replication x160
      FEOMBCVX6UO7・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      8BHA0KFRG94K・Primogem x60
      ・Adventurer's Experience x5
      FAIRYGIRLLINNEA・Primogem x30
      ・Tattered Warrant x3
      ・Mora x20000
      INUH43366326・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      Snezhnaya20260812・Primogem x200
      RFVEZO83458Q・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      MIXF44DAP7HW・Primogem x60
      ・Adventurer's Experience x5
      8X73KH58KDHN・Primogem x60
      ・Adventurer's Experience x5
      LTT3DVKVLUQZ・Primogem x30
      ・Mora x20,000
      ・Broken Drive Shaft x3
      GS64YTW65O・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Vayuda Turquoise Sliver x3
      ・Teachings of Freedom x3
      JTSHBHPPKUSV・Primogem x20
      ・Geode of Replication x160
      HUEK357YFYN3・Mora x10,000nl]・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      TSUKINOARIKAEMoonfall Silver x5
      ・Mora x20,000
      ・Hero's Wit x2
      ・Mystic Enhancement Ore x2
      MoonInvitationLunaIV・Primogem x40
      ・Mora x20,000
      ・Hero's Wit x3
      GS63YTOO2X・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Varunada Lazurite Sliver x3
      ・Teachings of Moonlight x3
      DABHTY763DAZ・Mora x20,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      BLKS76MP2B6C・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      P3GXX56W3VG9・Primogem x60
      ・Adventurer's Experience x5
      XXOJ2Z7F8GJI・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      0FQBAJNXCUFU・Primogem x60
      ・Adventurer's Experience x5
      MBEYNDLU3WGZ・Mora x20,000
      ・Hero's Wit x2
      ・Geode of Replication x200
      ・Mystic Enhancement Ore x2
      XTFPHSEJCFYM・Geode of Replication x320
      NTIF4NP4DO89・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GS61RD26W966・Mora x20,000
      ・Hero's Wit x2
      ・Geode of Replication x80
      ・Mystic Enhancement Ore x2
      R4SCAU7CR1G9・Primogem x60
      ・Adventurer's Experience x5
      EKLP57EFE4G4・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      AOZJ2TD5FZPV・Primogem x60
      ・Adventurer's Experience x5
      GenshinHigh5Fight・Primogem x30
      ・Hero's Wit x3
      ・Ile flottante x6
      ・Fine Enhancement Ore x5
      EPIC2025・Primogem x40
      ・Mora x20,000
      ・Adventurer's Experience x5
      CLD6ZWU35GTH・Primogem x60
      ・Adventurer's Experience x5
      WUSXOI92X54U・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      SWIP9F497X18・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GS60YTM4NW・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NODKRAISP0909・Mora x20,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      TOUGA75TO1I3・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ITSTARTSIN60NODKRAI・Primogem x40
      ・Mora x20,000
      ・Hero's Wit x3
      8QD412QXCEU1・Primogem x60
      ・Adventurer's Experience x5
      GS58YTPGFY・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Teachings of Conflict x3
      ・Vajrada Amethyst Sliver x3
      G9VY57AWQACW・Primogem x60
      ・Mystic Enhancement Ore x10
      Vamosacorrer25・Mora x20,000
      ・Puff Pop x1
      ・Blazed Meat Stew x1
      ・Grainfruit Chips x1
      ZT5RJ4AX4WYV・Primogem x60
      ・Adventurer's Experience x5
      GIFTFROMSKIRK57・Mora x30,000
      ・Shivada Jade Fragment x3
      ・Fine Enhancement Ore x3
      ・Meshing Gear x3
      FIGHTLIKESKIRK57・Primogem x40
      ・Hero's Wit x3
      ・Abyssal Bounty x1
      GS57YTSNLJ・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Shivada Jade Sliver x3
      ・Teachings of Contention x3
      HS4NM7YDGC99・Primogem x60
      ・Adventurer's Experience x5
      LWKC7L75S544・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ZXUYP2X2EF14・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      VSL93LTE5WY5・Beryl Conch x5
      ・Lasagna x2
      ・Fish and Chips x2
      ・Tomates Narbonnaises x2
      ・Bulle Sauce Duck Breast x2
      ・Bulle Sauce Duck Breast x2
      ・Barbeque Ribs x2
      ・Meshing Gear x3
      56CacucuVSAjaw・Primogem x30
      ・Mora x20,000
      ・Hero's Wit x3
      OSOIDWA2R5H3・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NODKRAI0910・Primogem x200
      FATUI・Primogem x30
      FROSTMOONSCIONS・Primogem x30
      LIGHTKEEPERS・Primogem x30
      THEWILDHUNT・Primogem x30
      VOYNICHGUILD・Primogem x30
      5AM75NYUGTTR・Primogem x30
      ・Hero's Wit x3
      ・Mora x20,000
      ・Blazed Meat Stew x5
      11KU0MNDK2RG・Primogem x60
      ・Adventurer's Experience x5
      UAL6MXW8BUFH・Mora x20,000
      ・Secret Art x2
      ・Honey-Glazed Ceviche x2
      GS55YTONVQ・Mora x30,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Teachings of Conflict x3
      ・Vajrada Amethyst Sliver x3
      JCWJFZG57YFW・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      XA4N66LAC349・Mora x20,000
      ・Fruit-Flavored Milk Candies x2
      ・Dreams of Healing x2
      ・Impeccably Organized x2
      HNYM49IX9PDS・Primogem x60
      ・Adventurer's Experience x5
      7B476N4TDJ6V・Mora x20,000
      ・Crepes Suzette x1
      ・Cold Cut Platter x1
      ・Tea Break Pancake x1
      ・Matsutake Meat Rolls x1
      ・Sparkling Berry Juice x1
      ・Teyvat Fried Egg x2
      RB5PPN4BV355・Primogem x30
      ・Sea Ganoderma x3
      ・Hero's Wit x3
      ・Mora x20,000
      ・Fine Enhancement Ore x5
      WEAV365PI953・Withering Purpurbloom x3
      ・Quenepa Berry x3
      ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      GS54MTX685・Mora x30,000
      ・Hero's Wit x3
      ・Guide to Transience x3
      ・Mystic Enhancement Ore x5
      ・Vayuda Turquoise Sliver x3
      WAM5UWWU28NV・Primogem x60
      ・Adventurer's Experience x5
      FORNATLAN・Primogem x40
      ・Withering Purpurbloom x5
      ・Warrior's Metal Whistle x3
      MAVUIKAWISHES・Mora x10000
      ・Hymn of Gathered Flame x1
      KAM7UKZGVRXZ・Primogem x30
      ・Stuffed N' Mashed Potatoes x1
      ・Fruity Trio x1
      ・Chocolate x2
      ・Goulash x1
      ・Sweet Madame x1
      ・Chenyu Brew x1
      FIBE54PL5995・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ACUI96V85FCK・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Seasond Fang x3
      SDKLSK67TZ1X・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Seasond Fang x3
      LA5E77CLZX5V・Primogem x60
      ・Adventurer's Experience x5
      MTLF6DKA7PQZ・Mora x30000
      ・Tatacos x3
      ・Grainfruitchip Fruit x3
      ・Cup O' Grainfruit x3
      WTXV36M1J6CO・Brilliant Chrysanthemum x3
      ・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      DEOB21CK9Y6N・Brilliant Chrysanthemum x3
      ・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      SBUEMJB449D9・Brilliant Chrysanthemum x3
      ・Mora x10000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      Genshin51YT・Brilliant Chrysanthemum x5
      ・Mora x30000
      ・Hero's Wit x3
      ・Guide to Kindling x3
      ・Fine Enhancement Ore x5
      DT4BZD5RT5U9・Primogem x60
      ・Adventurer's Experience x5
      HHFP3517D3UN・Mora x10000
      Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Varunada Lazurite Sliver x3
      KEQES9R79R49・Mora x10000
      Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Varunada Lazurite Sliver x3
      EPICGENSHIN2024・Mora x30000
      Hero's Wit x10
      ・Glittering Gemstone x5
      ・Hot Spring O'Clock x5
      ・Puff Pops x5
      ・Mystic Enhancement Ore x5
      BlazeToNatlan・Primogem x100
      Mora x30000
      ・Hero's Wit x3
      ・Adventirer's Experience x5
      EFCI18V69I74・Mora x10000
      Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      +
      5SKXWYSCPEVR・Hero's Wit x5
      5A2WEGTVPXFV・Mystic Enhancement Ore x10
      TTKEXYBCNWFH・Mora x30000
      ・Pile 'Em Up x3
      NAJEWZTD7XD5・Primogem x60
      ・Adventurer's Experience x5
      USDA9H94ZHSE・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ZOYWFIGI3NJ2・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ARQS5391FY79・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      KKFDOUU3GCJP・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      KKFD1J97LTJ2・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      QIDD46418U6Q・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GENSHINSUMMERNIGHT・Mora x20,000
      ・Hero's Wit x2
      ・Biryani x1
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      FBJCMYSXCBT5・Primogem x60
      ・Adventurer's Experience x5
      VAJWEZAC7WCZ・Mora x20,000
      ・Mystic Enhancement Ore x5
      LUURRWZ23243・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      BZPU67488O5V・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      BCPCGY9HGB3H・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      RTJUNRSHTAL9・Primogem x60
      ・Adventurer's Experience x5
      ULEP5WZ36B27・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      XQECMR9IIJ46・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      PZFUH5GIP9UF・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      DSXY15NL247E・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      MLEA572X2UHK・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      RERBFT39544D・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      XEZAXRS88R5G・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      VFNS1OGXYOYU・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      CA3BLTURGH9D・Primogem x60
      ・Adventurer's Experience x5
      KAVI75ET63II・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      HOFK9PBK481G・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NQXAZLX1943G・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GGXG9VO6967I・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      OLQH7G62I7EU・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      BNYP1WBMU61F・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      YKHC29118G54・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      IREQ1FTFRYBX・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      8SJSDSX68UF9・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      TGEFFQY7J6V2・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      FBYQ36SHY279・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      MEIR79527PDC・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      QOTR3M8X535N・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NA88ANTJL5SD・Primogem x60
      ・Adventurer's Experience x5
      JB95D2V5XGJ5・Mora x20,000
      ・Hero's Wit x2
      ・Adventurer's Experience x5
      ・Charcoal-Baked Ajilenakh Cake x5
      ・Curry Shrimp x5
      NNDFKSKEX6TH・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      HJGDJA2FW7FH・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      PAIMONSEAYOUIN2024・Mora x20,000
      ・Hero's Wit x5
      ・Mystic Enhancement Ore x5
      EA8RWDMBVRTR・Primogem x60
      ・Adventurer's Experience x5
      3SRM3VWBHWYH・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      WA8MJCETGXLR・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      ETR5KVFSYFMZ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      YT9L3UFSZXF9・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      WA8MJCETGXLR・Primogem x50
      ・Hero's Wit x3
      6T8NWE3HD7PD・Primogem x60
      ・Adventurer's Experience x5
      9A97KJNX2NZ9・Primogem x30
      AAQNVELTVSXV・Mora x20,000
      ・Mystic Enhancement Ore x5
      TTQNCFMADSWH・Primogem x30
      AS97CE45EUER・Mora x10,000
      ・Mora x20,000
      ・Hero's Wit x2
      ・Mystic Enhancement Ore x3
      GDAH2VWGT8AT・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      WEQP3D5ZHRD9・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      RQWP3ULZZ8VD・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      EAFVKD4GZQBZ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GENSHINIMPACT
      RIVALS23
      ・Mora x20,000
      ・Hero's Wit x3
      ・Mystic Enhancement Ore x5
      ・Sweet Madame x3
      2S84JS839T8R・Primogem x60
      ・Adventurer's Experience x5
      NA9NDHHCZKTD・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      NS92PG6DB52M・Primogem x60
      ・Adventurer's Experience x5
      RS9MLR5553UH・Mora x30,000
      ・Crab Roe Kourayaki x1
      ・Mint Jelly x1
      WTQ2E83WS869・Primogem x60
      ・Adventurer's Experience x5
      9S83F34ZFDVV・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      GA9FPD42SJ4V・Primogem x60
      ・Adventurer's Experience x5
      LAQZMTPKNTYH・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Stir-Fried Fish Noodles x5
      ・Jueyun Chili Chicken x5
      4BQYGWQ9HTR5・Shimi Chazuke x1
      ・Once Upon a Time in Mondstadt x1
      ・Omurice Waltz x1
      ・Survival Grilled Fish x1
      +SSRCJ8HSV7UM
      +
      ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Fisherman's Toast x5
      ・Goulash x5
      LA9C3RHPPHQH・Primogem x60
      ・Adventurer's Experience x5
      DAQS9FPX2U35・Primogem x60
      ・Adventurer's Experience x5
      +FTRUFT7AT5SV
      +
      ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Fried Radish Balls x5
      ・Satisfying Salad x5
      +NT8SU92DKFRZ
      +
      ・Primogem x60
      ・Adventurer's Experience x5
      +WARBDRR9MCQ9
      +
      ・Mora x10,000
      ・Adventurer's Experience x10
      ・Fine Enhancement Ore x5
      ・Northern Smoked Chicken x5
      ・Flaming Red Bolognes x5
      +MA6RPW8GGJAM
      +
      ・Primogem x60
      ・Adventurer's Experience x5
      6A6VJTWGCPYV・Primogem x60
      ・Adventurer's Experience x5
      MTNUJBXDD72R・Primogem x60
      ・Adventurer's Experience x5
      MS7C3SV8DMZH・Primogem x60
      ・Adventurer's Experience x5
      ZSPDKSC3V8V5・Primogem x60
      ・Adventurer's Experience x5
      9BPCJCQGHAWZ・Primogem x60
      ・Adventurer's Experience x5
      FANRONG
      CHANGSHENG
      ・Mora x18,000
      ・Adventurer's Experience x8・
      Fine Enhancement Ore x10
      BSPD3ZRXU985・Primogem x60
      ・Mora x10,000
      GENSHINGALAXY・Mora x10,000
      ・Adventurer's Experience x10
      ・Mondstadt Hash Brown x3
      ・Northern Smoked Chicken x3
      ・Fine Enhancement Ore x5
      GENSHINEPIC・Mora x10,000
      ・Adventurer's Experience x10
      ・Squirrel Fish x3
      ・Northern Apple Stew x3
      ・Fine Enhancement Ore x5
      SBNBUK67M37Z・Primogem x60
      ・Hero's Wit x30
      GS6ACJ775KNV・Primogem x60
      ・Mora x10,000
      TT7BVJNPL249・Primogem x60
      GenshinZHB30・Primogem x160
      GSIMPTQ125・Primogem x60
      ・Mora x10,000
      +GENSHIN0928A
      (Asia Only) +
      ・Primogem x60
      ・Mora x10,000
      +GENSHIN0928N
      (America Only) +
      ・Primogem x60
      ・Mora x10,000
      +GENSHIN0928E
      (Europe Only) +
      ・Primogem x60
      ・Mora x10,000
      +GENSHIN1006S
      (Asia Only) +
      ・Primogem x60
      ・Adventurer's Experience x5
      +GENSHIN0928A
      (America Only) +
      ・Primogem x60
      ・Adventurer's Experience x5
      +GENSHIN0928U
      (Europe Only) +
      ・Primogem x60
      ・Adventurer's Experience x5
      +GENSHINMHY0I
      (Asia Only) +
      ・Primogem x30
      +GENSHINMHY0M
      (America Only) +
      ・Primogem x30
      +GENSHINMHY0O
      (Europe Only) +
      ・Primogem x30
      +

      Genshin Impact Related Guides

      Genshin Impact Wiki Guide

      +
      Genshin Impact - Wiki and Guide Top
      + +

      Genshin Impact Wiki Front Page

      +

      Guides by Category

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      All Categories
      Genshin Impact - Story WalkthroughStory WalkthroughGenshin - News and Game InfoNews & Game Info
      Genshin - List of CharactersCharactersGenshin - GachasGacha (Wishes)
      Genshin - Rerolling GuidesRerollingGenshin - All Tier ListsTier Lists
      +Genshin Impact - Redeem CodesRedeem Codes +Genshin Impact - VersionsVersions
      Genshin - Tips and TricksTips & Tricks +Genshin Impact - List of All Quest WalkthroughsQuests +
      Genshin Impact - List of WeaponsWeaponsGenshin Impact - List of ArtifactsArtifacts
      Genshin Impact - List of Bosses and Boss Battle GuidesBossesGenshin - List of EnemiesEnemies
      Genshin - List of DomainsDomainsGenshin - Items and MaterialsItems & Materials
      Genshin - List of Events and Event GuidesEventsGenshin - List of AchievementsAchievements
      Genshin Impact - Message BoardsMessage BoardsGenshin - Spiral Abyss GuidesSpiral Abyss
      Genshin - Areas and MapsAreas and Maps +Genshin - ArchiveArchive
      +Genshin Impact - Locations and LoreLocations & Lore +Genshin - HousingHousing
      Genshin Impact - Fishing System Slim BannerFishingGenshin Impact - Genius Invokation TCGGenius Invokation
      +Genshin Impact - Imaginarium TheaterImaginarium Theater Genshin - UGCMiliastra Wonderland
      Genshin Impact ReviewGame Review-
      +

      Exclusive Guides

      +
      Genshin - Premium Articles
      + +

      Premium Articles

      +

      Premium Articles

      +
      Genshin - Premium Articles
      + +

      Premium Articles

      + +
      +

      Comment

      167 Anonymous Anonymous about 10 hours

      30 rs redeem codw

      166 Anonymous Anonymous about 10 hours

      Please 30 rs redeem code

        +

        All rights reserved

        + + + +
        Guide Index
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        \ No newline at end of file diff --git a/snapshots/genshin-game8-codes.meta.json b/snapshots/genshin-game8-codes.meta.json new file mode 100644 index 0000000..0bbba50 --- /dev/null +++ b/snapshots/genshin-game8-codes.meta.json @@ -0,0 +1,12 @@ +{ + "sourceId": "genshin-game8-codes", + "url": "https://game8.co/games/Genshin-Impact/archives/304759", + "contentHash": "14e97c995dbad078c2b9b10f10251f4272f06d3428cbf68977894db03aecfea1", + "bytes": 373038, + "etag": null, + "lastModified": null, + "contentChangedAt": "2026-10-16T11:48:08.968Z", + "lastConfirmedAt": "2026-10-16T11:48:08.968Z", + "eventCount": 10, + "charset": "utf-8" +} diff --git a/src/client/App.tsx b/src/client/App.tsx index d5e1317..de1c376 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,1258 +1,2 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { fetchFeed, refreshOutcome, type FeedState, type RefreshOutcome } from "./api.ts"; -import { Controls } from "./components/Controls.tsx"; -import { Dailies } from "./components/Dailies.tsx"; -import { GameFocus } from "./components/GameFocus.tsx"; -import { EventDetail } from "./components/EventDetail.tsx"; -import { EventRow, type DailyBadge, type RowEvent } from "./components/EventRow.tsx"; -import { NextUp } from "./components/NextUp.tsx"; -import { Timeline } from "./components/Timeline.tsx"; -import { Welcome } from "./components/Welcome.tsx"; -import { Colophon } from "./components/Colophon.tsx"; -import { Legend } from "./components/Legend.tsx"; -import { Toast } from "./components/Toast.tsx"; -import { UpdateNotice } from "./components/UpdateNotice.tsx"; -import { - aliasesFrom, - carryDaily, - carryMarks, - carryProgress, -} from "./state/retiredIds.ts"; -import { KEYS } from "./state/storage.ts"; -import { useAppUpdate } from "./state/useAppUpdate.ts"; -import { useMarkSet, type Marks } from "./state/useMarkSet.ts"; -import { useProgress, type ProgressMap } from "./state/useProgress.ts"; -import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts"; -import { useTasks } from "./state/useTasks.ts"; -import { useAmounts } from "./state/useAmounts.ts"; -import { adoptNewLanes, choreRule, firstRunPrefs, routineRule, trackRoutinePatch, usePrefs, type Prefs } from "./state/usePrefs.ts"; -import { buildExportData, parseImportData, triggerDownload } from "./state/export.ts"; -import { snapDayWidth } from "./state/zoom.ts"; -import { useCustom, type EventDraft } from "./state/useCustom.ts"; -import { compareRows, SORT_MODES, type Activity, type SortMode } from "./state/sort.ts"; -import { - advanceFocus, - countByGame, - nextToExpire, - outstanding, - resolveFocus, -} from "./state/lens.ts"; -import { clockFor, formatRemaining } from "../shared/time.ts"; -import { - dailiesId, - dailyDays as runDays, - dailySummary, - dayKey, - isDaily, - resolveDaily, -} from "../shared/daily.ts"; -import { isoWeekday } from "../shared/period.ts"; -import { tasksShown, type StripTask } from "./components/Dailies.tsx"; -import type { StripEditor, StripEvent, StripStanding } from "./components/EditPanel.tsx"; -import { goalStatus, readGoal, type GoalStatus } from "../shared/goal.ts"; -import { taskStatus } from "../shared/task.ts"; -import { - activeRoutines, - offered, - copyRoutineState, - mineFrom, - routinesFor, - routineToAsk, - type Routine, -} from "../shared/routines.ts"; -import { setFor, shownFor } from "../shared/perGame.ts"; -import { ENTRIES, NOTES, newestId, unseen } from "../shared/changelog.ts"; -import { ChangelogSheet } from "./components/ChangelogSheet.tsx"; -import { WhatsNew } from "./components/WhatsNew.tsx"; -import { strandedOccurrences } from "../shared/recurrence.ts"; -import { orderGames } from "./state/gameOrder.ts"; -import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx"; -import { metaOnTheme, useTheme } from "./state/theme.ts"; -import { - displayEventFor, - isCustomGameId, - recordFor, - type LaneId, -} from "../shared/custom.ts"; -import type { GameId } from "../shared/schema.ts"; -import { metaFor } from "../shared/games.ts"; - -/** - * How many deadlines the headline carries. - * - * One was the whole panel, and one is what a reader who has just finished it - * needs replacing. Three is what they asked for: enough to plan an evening - * around, few enough that the closest one still owns the page. - */ -const HEADLINE_DEADLINES = 3; - -/** - * How many rows a section shows before it offers the rest. - * - * Two games already run to twenty-one live events and every game added doubles - * down on that, which is the point at which a list stops being read at all. - * This truncates the *view* and nothing else: the order is untouched, the - * hidden rows are still counted in the header, still on the timeline, and one - * tap away here. - */ -const LIST_CAP = 6; - -/** - * Connection state. Offline is not an error here — the service worker serves - * the last feed it saw and countdowns run off the local clock — but it does - * change what the reader can trust, so it is surfaced rather than hidden. - */ -function useOnline(): boolean { - const [online, setOnline] = useState(() => - typeof navigator === "undefined" ? true : navigator.onLine, - ); - useEffect(() => { - const up = () => setOnline(true); - const down = () => setOnline(false); - window.addEventListener("online", up); - window.addEventListener("offline", down); - return () => { - window.removeEventListener("online", up); - window.removeEventListener("offline", down); - }; - }, []); - return online; -} - -/** Ticks once a second so countdowns stay honest without re-fetching. */ -function useNow(intervalMs = 1000): number { - const [now, setNow] = useState(() => Date.now()); - useEffect(() => { - const id = setInterval(() => setNow(Date.now()), intervalMs); - return () => clearInterval(id); - }, [intervalMs]); - return now; -} - -export function App() { - const [state, setState] = useState({ status: "loading" }); - const [openId, setOpenId] = useState(null); - /** Whether the changelog sheet is open. Per-visit, like the detail sheet. */ - const [changelogOpen, setChangelogOpen] = useState(false); - // The event most recently ignored, so it can be put back without hunting for - // a row that just disappeared. - const [lastIgnored, setLastIgnored] = useState<{ id: string; title: string } | null>(null); - const now = useNow(); - const online = useOnline(); - const { prefs, update, toggleGame, restorePrefs } = usePrefs(); - // Their answer from the first run, or their last tap on the tabs. Reading it - // from `prefs` is what stops a reload putting a timeline reader back on the - // list they did not choose. - const view = prefs.view; - const ignored = useMarkSet(KEYS.ignored); - const prog = useProgress(); - const daily = useDailyLog(); - const tasks = useTasks(); - const amounts = useAmounts(); - const mergeExtra = (incomingTasks: unknown, incomingAmounts: unknown) => { - tasks.merge(incomingTasks); - amounts.merge(incomingAmounts); - }; - const custom = useCustom(now); - // Colour only: which ground the page is drawn on, written to the document by - // the hook. Nothing else in the app asks what it is — the tokens in - // styles.css answer for every component — except the hues below. - const theme = useTheme(prefs.theme); - /** - * How every lane in this tree is named and coloured. - * - * App owns it because App is the only thing holding the reader's own games, - * and hands it down rather than letting components import a lookup that can - * only ever answer for the tracked ones. - * - * It is also where a hue meets the theme. A hue is data — ours in `games.ts`, - * theirs in their browser — and all of it was picked against the dark ground, - * so on paper the bright ones need darkening to stay readable. Doing it here - * means every lane label, chip, rail and bar in the tree gets the adjusted - * answer without a single component knowing a theme exists. - */ - const gameMeta = useMemo( - () => (id) => metaOnTheme(metaFor(id, custom.games), theme), - [custom.games, theme], - ); - // "Completed" is now one status among several; the rest of the UI still asks - // this question a lot, so keep a cheap shorthand. - const isDone = (id: string) => prog.progress[id]?.status === "done"; - - /** - * How far into an event the reader is, for ordering only. - * - * Ticking a day off a repeating event counts as "doing it" without them - * having to also set the status — the tick already said so, and asking twice - * is how a sort ends up lying about what you were in the middle of. - */ - const activityOf = (id: string): Activity => { - const status = prog.progress[id]?.status; - if (status === "done") return "done"; - if (status === "doing") return "doing"; - return daily.daysFor(id).length > 0 ? "doing" : "idle"; - }; - - /** The reader's check-in goal for an event, if they set a readable one. */ - const goalOf = (id: string) => readGoal(prog.progress[id]?.goal); - - /** - * Whether an event repeats, the reader's own answer included. Detection reads - * the source's wording; they can overrule it either way. A goal settles it - * outright: the reader has told us they come back to this one. - */ - const repeatsDaily = (row: RowEvent): boolean => - goalOf(row.event.id) !== undefined || - resolveDaily( - row.event, - prog.progress[row.event.id]?.daily, - prefs.detectDaily, - ); - - /** Where the reader is against an event's goal, or undefined without one. */ - const goalStatusOf = (row: RowEvent): GoalStatus | undefined => { - const goal = goalOf(row.event.id); - if (goal === undefined) return undefined; - return goalStatus({ - goal, - startsMs: row.clock.startsMs, - endsMs: row.clock.endsMs, - region: prefs.region, - game: row.event.game, - now, - logged: daily.daysFor(row.event.id), - amounts: amounts.amountsFor(row.event.id), - }); - }; - - /** - * Built-ins the reader turned on, less any they made their own — then theirs - * and the built-ins together, as the strip draws them. - */ - const copiedFrom = new Set( - tasks.tasks.flatMap((t) => (t.from === undefined ? [] : [t.from])), - ); - // Every built-in the reader could track, for the lists that offer them; the - // strip carries only those not stopped one by one. - const routinesHidden = new Set(prefs.routinesHidden ?? []); - const standardPulls = prefs.standardPulls ?? false; - const routines = activeRoutines(routineRule(prefs), copiedFrom, new Set(), standardPulls); - const stripTasks = [...tasks.tasks, ...routines.filter((r) => !routinesHidden.has(r.id))]; - - /** - * Copy a built-in into the reader's own tasks, ticks and numbers included. - * Additive: the built-in's own log stays under its id, so deleting the copy - * brings the original back as it was. A second press while a copy exists - * does nothing rather than making two. - */ - const makeItMine = (routine: Routine) => { - if (tasks.tasks.some((t) => t.from === routine.id)) return; - const id = tasks.addTask(mineFrom(routine)); - const copy = copyRoutineState(daily.logs, amounts.amounts, routine.id, id); - daily.merge(copy.daily); - amounts.merge(copy.amounts); - }; - - const taskStatusOf = (task: StripTask): GoalStatus => - taskStatus({ - task, - region: prefs.region, - now, - logged: daily.daysFor(task.id), - amounts: amounts.amountsFor(task.id), - }); - - /** Today's state for a repeating event, or undefined if it does not repeat. */ - const dailyBadge = (row: RowEvent): DailyBadge | undefined => { - if (!repeatsDaily(row)) return undefined; - const summary = dailySummary({ - startsMs: row.clock.startsMs, - endsMs: row.clock.endsMs, - region: prefs.region, - game: row.event.game, - now, - logged: daily.daysFor(row.event.id), - }); - return { doneToday: summary.doneToday, remaining: summary.remaining }; - }; - - const isIgnored = (id: string) => ignored.marks[id] !== undefined; - - /** An event as the strip's Edit panel needs it. */ - const stripEvent = (row: RowEvent): StripEvent => { - const run = runDays(row.clock.startsMs, row.clock.endsMs, prefs.region, row.event.game); - return { - id: row.event.id, - game: row.event.game, - title: row.event.title, - goal: goalOf(row.event.id), - hasStoredGoal: prog.progress[row.event.id]?.goal !== undefined, - detected: prefs.detectDaily && isDaily(row.event), - defaultWeekStartsOn: isoWeekday(dayKey(row.clock.startsMs, prefs.region, row.event.game)), - maxDays: run === null ? null : run.length, - }; - }; - - const toggleIgnored = (id: string, title: string) => { - const wasIgnored = isIgnored(id); - ignored.toggle(id); - setLastIgnored(wasIgnored ? null : { id, title }); - }; - - useEffect(() => { - const ac = new AbortController(); - fetchFeed(ac.signal) - .then((feed) => setState({ status: "ready", feed })) - .catch((err: unknown) => { - if (ac.signal.aborted) return; - setState({ - status: "error", - message: err instanceof Error ? err.message : "Could not load events.", - }); - }); - return () => ac.abort(); - }, []); - - // Merge may have folded another source's copy into a row since this reader - // last looked, and that copy's id may hold their ticks. The feed names every - // id it absorbed, so the marks follow with nobody editing a table - // (docs/DATA-MODEL.md § ID stability). - const feedEvents = state.status === "ready" ? state.feed.events : null; - const feedCarry = useMemo( - () => (feedEvents === null ? {} : aliasesFrom(feedEvents)), - [feedEvents], - ); - const carryProgressIds = prog.carry; - const carryDailyIds = daily.carry; - const carryIgnoredIds = ignored.carry; - useEffect(() => { - if (Object.keys(feedCarry).length === 0) return; - carryProgressIds(feedCarry); - carryDailyIds(feedCarry); - carryIgnoredIds(feedCarry); - }, [feedCarry, carryProgressIds, carryDailyIds, carryIgnoredIds]); - // An export written before a fusion still names the absorbed id, and the - // effect above has already run for this feed — so an import takes the same - // table on its way in. - const importProgressMerge = (c: ProgressMap) => prog.merge(carryProgress(c, feedCarry)); - const importDailyMerge = (c: DailyLogMap) => daily.merge(carryDaily(c, feedCarry)); - const importIgnoredMerge = (c: Marks) => ignored.merge(carryMarks(c, feedCarry)); - - const allRows = useMemo(() => { - if (state.status !== "ready") return []; - // The reader's own events are events. They sort, filter, focus, expire and - // tick exactly like scraped ones — what sets them apart is only that - // nothing is claimed about where their dates came from. - return [ - ...state.feed.events.filter((e) => e.status === "published"), - ...custom.rows, - ].map((event) => ({ event, clock: clockFor(event, prefs.region, now) })); - // `now` intentionally excluded: recomputing every clock each second is - // wasteful, and the countdown text re-renders from `now` anyway. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state, custom.rows, prefs.region, Math.floor(now / 60_000)]); - - // Feed lanes come from rows, the reader's from the games themselves — so a - // game they just created shows up in the filters before it holds anything. - // It is still not a scraped game with an empty feed: it has no source row, no - // freshness badge and no colophon credit. - const games = useMemo( - () => [...new Set([...allRows.map((r) => r.event.game), ...custom.lanes])], - [allRows, custom.lanes], - ); - - /** - * A lane the reader has never been offered starts switched off. - * - * Adding a source is our decision, not theirs, and a reader who plays two - * games did not ask for the other twelve. So a lane that is new to *them* - * is recorded and hidden, and the games chips in settings are where they - * take it up — the one place that lists every lane, on or off. - * - * The seeding branch is the whole reason this is safe: an existing reader - * has no `knownGames` at all, and treating that as "has been offered - * nothing" would switch off every game they already read. Absent means - * unrecorded, so the first pass records what is already on their screen and - * changes nothing else. - */ - useEffect(() => { - if (state.status !== "ready") return; - const patch = adoptNewLanes(games, prefs.knownGames, prefs.hiddenGames); - if (patch !== null) update(patch); - }, [state.status, games, prefs.knownGames, prefs.hiddenGames, update]); - - /** - * Every lane in the order the reader reads them in. - * - * `games` above stays the lane-*identity* list: `adoptNewLanes` diffs it to - * decide which games arrive switched off and `knownGames` is seeded from it, - * so reordering it at source would let a display preference reach the logic - * that hides a reader's games. Ordering is applied here instead, once, and - * handed to every surface that shows a game. - */ - const ordered = useMemo( - () => orderGames(games, prefs.gameOrder, (id) => gameMeta(id).name), - [games, prefs.gameOrder, gameMeta], - ); - - /** Games the reader plays, in their order. The focus bar rotates through these. */ - const enabled = useMemo( - () => ordered.filter((g) => !prefs.hiddenGames.includes(g)), - [ordered, prefs.hiddenGames], - ); - - // A focus on a game they have since switched off is ignored, not obeyed — - // otherwise the page is blank for a reason that lives in a panel at the - // bottom. The stored value is left alone so switching the game back on - // restores where they were. - const focus = resolveFocus(prefs.focusGame, enabled); - - /** - * The filters that decide whether a row is on screen at all. - * - * Extracted from `inScope` so the timeline's expanded occurrences pass - * through the same four questions. Restating them there would be a second - * copy that drifts, and each drift is a row the reader told us to hide - * appearing on the board. - */ - const inScopeOf = useCallback( - (rows: RowEvent[]) => - rows - .filter((r) => !prefs.hiddenGames.includes(r.event.game)) - .filter((r) => !r.clock.ended) - // Ignored events are gone from both views unless deliberately revealed - // — that is the whole point of ignoring one. - .filter((r) => prefs.showIgnored || !isIgnored(r.event.id)) - .filter((r) => prefs.showCompleted || !isDone(r.event.id)), - [prefs.hiddenGames, prefs.showCompleted, prefs.showIgnored, prog.progress, ignored.marks], - ); - - /** - * Everything the reader could be looking at, before focus narrows it. The - * focus chips count off this, so a chip can say what is waiting in a game - * that is not the one currently on screen. - */ - const inScope = useMemo(() => inScopeOf(allRows), [allRows, inScopeOf]); - - const visible = useMemo( - () => - inScope - .filter((r) => focus === null || r.event.game === focus) - // Sorting only ever groups: both modes fall back to soonest-ending - // inside a group, so choosing one never costs the deadline order. - .sort(compareRows(prefs.sort, activityOf)), - [inScope, focus, prefs.sort, prog.progress, daily.logs], - ); - - /** - * Fills the timeline's settled window with the rest of a rule's rhythm. - * - * Passed to `` as `expand` rather than inlined there — a hook - * cannot be called inside JSX. Filtered through `inScopeOf` and the focus - * chip so an expanded occurrence obeys exactly what a base row obeys: a - * hidden game, an ignored event, or a finished one stays off the board. - */ - const expandOccurrences = useCallback( - (min: number, max: number) => - inScopeOf( - custom - .occurrencesIn(min, max) - .map((event) => ({ event, clock: clockFor(event, prefs.region, now) })), - ).filter((r) => focus === null || r.event.game === focus), - // `now` is deliberately coarse here, as it is for `allRows`: - // re-expanding every rule each second would be wasted work. - // eslint-disable-next-line react-hooks/exhaustive-deps - [custom.occurrencesIn, inScopeOf, focus, prefs.region, Math.floor(now / 60_000)], - ); - - const live = visible.filter((r) => r.clock.live); - const upcoming = visible.filter((r) => r.clock.upcoming); - /** - * The unstarted events the checklist actually lists. - * - * `upcoming` stays the full count either way, because the page header states - * it — a section that is simply absent is indistinguishable from a quiet - * fortnight, and this app does not leave a reader to infer what it is not - * showing them. - */ - const listedUpcoming = prefs.showUpcoming ? upcoming : []; - - /** - * What the page is telling the reader to *do*, as opposed to what it is - * letting them look at. - * - * The headline and the dailies strip are both instructions, so both drop - * events the reader has finished or ignored — being pointed at a job you - * already did is the bug whether the pointer is a countdown or a checkbox. - * `showCompleted` deliberately does not reach this: that preference says keep - * them on screen, not keep nagging me about them. - */ - const todo = outstanding(live, isDone, isIgnored); - - /** - * The strip's Edit panel: the same `daily` and `goal` writes the detail sheet - * makes, offered where the reader is ticking. What can be added is what the - * strip would show if it repeated — running, not done, not ignored. - */ - const stripEditor: StripEditor = { - inStrip: todo.filter(repeatsDaily).map(stripEvent), - addable: todo.filter((r) => !repeatsDaily(r)).map(stripEvent), - onGoal: prog.setGoal, - onDaily: prog.setDaily, - onOpen: setOpenId, - shown: { - chores: prefs.showChores, - weekly: prefs.showWeeklyTasks, - monthly: prefs.showMonthlyTasks, - cycle: prefs.showCycleTasks, - }, - hasTasks: { - weekly: stripTasks.some((t) => t.period.unit === "week"), - monthly: stripTasks.some((t) => t.period.unit === "month"), - cycle: stripTasks.some((t) => t.period.unit === "cycle"), - }, - onShown: (patch) => - update({ - ...(patch.chores !== undefined ? { showChores: patch.chores } : {}), - ...(patch.weekly !== undefined ? { showWeeklyTasks: patch.weekly } : {}), - ...(patch.monthly !== undefined ? { showMonthlyTasks: patch.monthly } : {}), - ...(patch.cycle !== undefined ? { showCycleTasks: patch.cycle } : {}), - }), - standing: (focus === null ? enabled : [focus]).flatMap((game): StripStanding[] => { - if (isCustomGameId(game)) return []; - const rule = routineRule(prefs); - return [ - { - id: dailiesId(game as GameId), - game, - title: gameMeta(game).dailyTasks, - kind: "chore", - tracked: shownFor(choreRule(prefs), game), - }, - // A copy the reader made stands in for its built-in, so the original is - // not offered; the copy is theirs, in Your own. - ...routinesFor(game) - .filter((r) => !copiedFrom.has(r.id) && offered(r, standardPulls)) - .map((r): StripStanding => ({ - id: r.id, - game, - title: r.title, - kind: "routine", - tracked: shownFor(rule, game) && !routinesHidden.has(r.id), - })), - ]; - }), - onTrack: (item, track) => { - if (item.kind === "chore") { - const { on, off } = setFor(choreRule(prefs), item.game, track); - update({ choresOn: on, choresOff: off }); - return; - } - const routine = routinesFor(item.game).find((r) => r.id === item.id); - if (routine !== undefined) update(trackRoutinePatch(prefs, routine, track)); - }, - }; - const headline = nextToExpire(todo, HEADLINE_DEADLINES); - - // Counted across every game the reader plays, not just the focused one — a - // chip has to say what is waiting behind it to be worth tapping. - const scopedTodo = useMemo( - () => outstanding(inScope, isDone, isIgnored), - [inScope, prog.progress, ignored.marks], - ); - const perGame = useMemo(() => countByGame(scopedTodo), [scopedTodo]); - - /** - * The lists only ever build `allRows` from a rule's first two occurrences - * (`LIST_OCCURRENCES`), but the timeline draws every occurrence the board - * window admits — so a bar past the second names an id `allRows` cannot - * resolve. Resolved through the rule behind it rather than dropped, or every - * later bar would be a dead click with no way to reach per-occurrence - * completion. - */ - const openRow = (() => { - const hit = allRows.find((r) => r.event.id === openId) ?? null; - if (hit !== null || openId === null) return hit; - const event = displayEventFor(custom.events, openId, now); - if (event === null) return null; - return { event, clock: clockFor(event, prefs.region, now) }; - })(); - - /** One row, wired up. Both lists render the same thing from the same props. */ - const renderRow = (row: RowEvent) => ( - ignored.toggle(id)} - onOpen={setOpenId} - /> - ); - - if (state.status === "loading") { - return

        Loading events…

        ; - } - - if (state.status === "error") { - return ( - -
        -

        Events unavailable

        -

        - {state.message} -

        -
        -
        - ); - } - - // First run: ask which games before showing a calendar full of ones they - // don't play. Stored as hiddenGames (the inverse); what happens to a game - // added *later* is decided by `knownGames` above, not by this shape. - if (!prefs.onboarded) { - return ( - - - - update(firstRunPrefs(games, chosen, chosenView)) - } - /> - - - ); - } - - /** - * Working through games one at a time, which is how someone with four of them - * actually plays: clear one, move on. - * - * It goes at the top of the column it filters, and past `lg` the checklist's - * column is the rail: the bar rides with the deadlines and the dailies it - * narrows, pinned and still above every one of them, instead of spending the - * full width of a wide screen on a row of chips and pushing "next to expire" - * — the answer the reader came for — down the page to make room. On a phone, - * and on the timeline, which has no rail, that same rule puts it back at the - * top of the page. Rendered once per view, never twice on one page. - */ - const focusBar = ( - update({ focusGame })} - onAdvance={() => update({ focusGame: advanceFocus(focus, enabled) })} - /> - ); - - return ( - - -
        -
        -

        - EVENTCLOCK -

        -

        - {live.length} live · {upcoming.length} upcoming - {!online && ( - - - offline - - )} -

        -
        - -
        - {/* The id is a stored value (`prefs.view`) and the label is copy, so - they are allowed to drift: renaming the tab must not silently move - every reader to the other view. */} - {( - [ - ["soon", "Checklist"], - ["timeline", "Timeline"], - ] as const - ).map(([id, label]) => ( - - ))} -
        -
        - - {view === "soon" ? ( - /* - * Two columns once the screen has room for them, and the split is the - * one this codebase already draws everywhere else: what the page is - * *telling* the reader to do on the left, what it is *showing* them on - * the right. The deadlines and tonight's dailies are instructions, they - * are short, and they are what the reader came for — so on a wide - * screen they stop scrolling away and stay pinned beside the list, with - * the focus bar that narrows them at the top of the same column. - * - * Below `lg` this is one column in exactly the old order, because on a - * phone the same argument produces the same answer: put them first. - */ -
        - - -
        - {/* What changed since they last looked: above the lists, never in the - rail, which holds what the reader has to do. */} - update({ changelogSeen: newestId(ENTRIES) })} - onSeeAll={() => setChangelogOpen(true)} - /> - {live.length > 0 && ( -
        1 ? ( - update({ sort })} - /> - ) : undefined - } - > - -
        - )} - - {listedUpcoming.length > 0 && ( -
        1 ? ( - update({ sort })} - /> - ) : undefined - } - > - -
        - )} - - {/* Nothing listed is three different situations, and the reader can - only act on the one they are actually in. Held-back events come - first because that one has a switch behind it. */} - {live.length === 0 && listedUpcoming.length === 0 && ( -

        - {upcoming.length > 0 - ? `Nothing running right now. ${ - upcoming.length === 1 - ? "One event has" - : `${upcoming.length} events have` - } not started yet — switch on “Show events that haven't started”, under “What you see” in settings below.` - : focus !== null - ? `Nothing running in ${gameMeta(focus).name}. Try another game, or show all of them.` - : "Nothing to show. Every game is switched off, or you've finished everything and hidden completed events."} -

        - )} -
        -
        - ) : ( - <> - {focusBar} - - update({ timelineDayWidth })} - group={prefs.timelineGroup} - onGroup={(timelineGroup) => update({ timelineGroup })} - // The board holds these back itself rather than being handed a - // shorter list, so it can say how many are waiting when there is - // nothing else left to draw. The switch is in settings. - showUpcoming={prefs.showUpcoming} - splitUpcoming={prefs.timelineSplitUpcoming} - // Lanes stack in the reader's game order, so their main game is the - // top lane rather than whichever one held the first row. - gameOrder={ordered} - onOpen={setOpenId} - isDone={isDone} - expand={expandOccurrences} - /> - - )} - - update(trackRoutinePatch(prefs, routine, track)), - now, - // The index lists rules; the sheet opens rows. A repeating rule's - // own id is never a row — the lists hold its occurrences — so it is - // resolved to whichever occurrence is nearest before opening. - // The stored id is enough: `displayEventFor` resolves a rule to - // whichever occurrence the sheet can show, and to the rule itself - // when it has none. - onOpen: setOpenId, - }} - onExport={() => - exportProgress(prog.progress, daily.logs, ignored.marks, { - games: custom.games, - events: custom.events, - tasks: tasks.raw, - amounts: amounts.amounts, - }) - } - onImport={(file) => - void importProgress( - file, - importProgressMerge, - importDailyMerge, - importIgnoredMerge, - custom.merge, - mergeExtra, - ) - } - onExportAll={() => - exportAll(prog.progress, daily.logs, ignored.marks, prefs, { - games: custom.games, - events: custom.events, - tasks: tasks.raw, - amounts: amounts.amounts, - }) - } - onImportAll={(file) => - void importProgress( - file, - importProgressMerge, - importDailyMerge, - importIgnoredMerge, - custom.merge, - mergeExtra, - restorePrefs, - ) - } - /> - - {!online && ( -

        - You're offline. These are the events last downloaded - {" "} - {formatRemaining(now - Date.parse(state.feed.generatedAt))} ago, and - countdowns are still running. Anything rescheduled since then won't - show until you reconnect. -

        - )} - - {/* `hiddenGames` rather than `enabled`: the footer needs to know what the - reader turned *off*, and a lane absent from both is one the feed has - and this reader has never been offered — still not theirs to chase. */} - setChangelogOpen(true)} - onRefresh={async (): Promise => { - // The newest published feed, past the browser's copy. Swapped in only - // when it carries something new, so a check that found nothing leaves - // the page exactly as it was. Never reloads: the reader may be - // halfway through typing an event in. - const before = state.feed; - try { - const next = await fetchFeed(undefined, { fresh: true }); - const outcome = refreshOutcome(before, next); - if (outcome === "updated") setState({ status: "ready", feed: next }); - return outcome; - } catch (err) { - return refreshOutcome(before, err instanceof Error ? err : new Error(String(err))); - } - }} - /> - - {lastIgnored !== null && ( - { - ignored.toggle(lastIgnored.id); - setLastIgnored(null); - }} - onDismiss={() => setLastIgnored(null)} - /> - )} - - {changelogOpen && ( - setChangelogOpen(false)} /> - )} - - {openRow !== null && ( - toggleIgnored(id, openRow.event.title)} - onClose={() => setOpenId(null)} - own={(() => { - // The row may be one occurrence of a rule. Marks key off the - // occurrence; the record to edit is the rule behind it. - const record = recordFor(custom.events, openRow.event.id); - if (record === undefined) return undefined; - return { - record, - lanes: games, - games: custom.games, - onSave: (_id: string, draft: EventDraft) => - custom.editEvent(record.id, draft), - onDelete: () => custom.removeEvent(record.id), - strandedBy: () => - strandedOccurrences( - record, - now, - (id) => - prog.progress[id] !== undefined || - (daily.logs[id]?.days.length ?? 0) > 0, - ), - }; - })()} - /> - )} -
        -
        - ); -} - -/** - * Every screen goes through here, which is why the update notice lives here - * rather than beside the list: a reader who is being told "events unavailable" - * or is still picking their games needs the offer at least as much as one - * reading a calendar — a bundle too old for the feed it just downloaded - * (`fetchFeed`'s schemaVersion refusal) lands on exactly that error screen, and - * a reload is the fix. - */ -function Shell({ children }: { children: React.ReactNode }) { - const update = useAppUpdate(); - return ( -
        - {children} - {update.available && ( - - )} -
        - ); -} - -/** - * A titled block of the checklist, with room for one control on the right. - * - * There was a `hint` slot beside `action` — a line of prose for a section with - * no control — rendered as `action ?? hint`. Nothing could ever reach it: its - * only caller was "Running now", whose hint needed two live rows, and two live - * rows are two visible rows, which is exactly when the sort control appears and - * wins. What it would have said is on the page anyway, in the "Then" list of the - * headline panel, which names those deadlines and counts them down. - */ -function Section({ - title, - legend, - action, - children, -}: { - title: string; - legend?: boolean | undefined; - /** A control that belongs to this section, e.g. how it is ordered. */ - action?: React.ReactNode | undefined; - children: React.ReactNode; -}) { - return ( -
        -
        -

        {title}

        - {action} -
        - {legend === true && } - {children} -
        - ); -} - -/** - * A list of events, capped at a length someone will actually read. - * - * The reader who asked for this had two games switched on and twenty-one live - * events, and said the list stopped being usable — so the default view shows a - * handful and offers the rest. What it must never do is *reorder*: this slices - * the front off a list that is already in the order the reader chose, so the - * deadline guarantee holds for what is shown and what is hidden alike. - * - * Expanding is per-visit rather than a stored preference: it is an action taken - * while reading one list, not a statement about how they want the app to work. - */ -function EventList({ - rows, - render, -}: { - rows: RowEvent[]; - render: (row: RowEvent) => React.ReactNode; -}) { - const [showAll, setShowAll] = useState(false); - const shown = showAll ? rows : rows.slice(0, LIST_CAP); - const hidden = rows.length - shown.length; - - return ( - <> -
          {shown.map(render)}
        - {rows.length > LIST_CAP && ( - - )} - - ); -} - -/** - * Order the list by deadline, or by what the reader is partway through. - * - * Sits in the list's own header rather than down in settings: ordering is a - * thing you reach for while looking at the list, not a preference you go and - * configure. - */ -function SortControl({ - value, - onChange, -}: { - value: SortMode; - onChange: (mode: SortMode) => void; -}) { - return ( -
        - {SORT_MODES.map((mode) => { - const on = value === mode.id; - return ( - - ); - })} -
        - ); -} - -function exportProgress( - progress: Record, - daily: DailyLogMap, - ignored: Record, - own: Own, -) { - const data = buildExportData(progress, daily, ignored, own); - triggerDownload( - data, - `event-clock-progress-${new Date().toISOString().slice(0, 10)}.json`, - ); -} - -function exportAll( - progress: Record, - daily: DailyLogMap, - ignored: Record, - prefs: Prefs, - own: Own, -) { - const data = buildExportData(progress, daily, ignored, own, prefs); - triggerDownload( - data, - `event-clock-backup-${new Date().toISOString().slice(0, 10)}.json`, - ); -} - -/** Everything reader-authored that travels in an export beside progress. */ -type Own = Parameters[3]; - -async function importProgress( - file: File, - mergeProgress: (c: Record) => void, - mergeDaily: (c: DailyLogMap) => void, - mergeIgnored: (c: Record) => void, - mergeCustom: (games: unknown, events: unknown) => void, - mergeExtra: (tasks: unknown, amounts: unknown) => void, - restorePrefs?: ((prefs: unknown) => void) | undefined, -) { - try { - const text = await file.text(); - const data = parseImportData(JSON.parse(text)); - if (data === null) { - alert("That file isn't an Event Clock export."); - return; - } - if (data.progress !== null) mergeProgress(data.progress); - if (data.daily !== null) mergeDaily(data.daily); - if (data.ignored !== null) mergeIgnored(data.ignored); - mergeCustom(data.customGames, data.customEvents); - mergeExtra(data.tasks, data.amounts); - - if (restorePrefs !== undefined) { - if (data.prefs !== null) { - restorePrefs(data.prefs); - } else { - alert("Progress was imported, but this file did not include settings."); - } - } - } catch { - alert("That file couldn't be read. Export a fresh copy and try again."); - } -} - +/** The production entry stays stable while page coordination lives in ReaderApp. */ +export { ReaderApp as App } from "./ReaderApp.tsx"; diff --git a/src/client/ReaderApp.tsx b/src/client/ReaderApp.tsx new file mode 100644 index 0000000..445c610 --- /dev/null +++ b/src/client/ReaderApp.tsx @@ -0,0 +1,1423 @@ +import { EFFORT, type Effort } from "../shared/effort.ts"; +import { manyServers, serverSuffix } from "./state/reader/servers.ts"; +import { mintAccountId } from "./state/reader/ids.ts"; +import { trackedGames } from "./state/reader/updates.ts"; +import { freshness, type SourceHealth } from "../shared/feed.ts"; +import { formatInZone } from "./state/displayTime.ts"; +import { aliasesFrom, RETIRED_IDS } from "./state/retiredIds.ts"; +import { PinnedRoutine } from "./components/redesign/PinnedRoutine.tsx"; +import { DisplayTimeContext } from "./state/displayTime.ts"; +import type { CodesFeed } from "../shared/codes.ts"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { + fetchFeed, + fetchCodesFeed, + refreshOutcome, + type FeedState, + type RefreshOutcome, +} from "./api.ts"; +import { + initializeReader, + createReaderStore, + type ReaderStore, +} from "./state/reader/store.ts"; +import { ReaderContext, useReader } from "./state/reader/useReader.ts"; +import { finishPendingRestore } from "./state/reader/restore.ts"; +import { saveBackup } from "./state/reader/backups.ts"; +import { + pairKey, + type ReaderState, + type ArchiveRecord, +} from "./state/reader/schema.ts"; +import { actionMeta, recordOf } from "./state/reader/actions.ts"; +import { + readerEventRows, + type ReaderEventRow, +} from "./state/reader/eventRows.ts"; +import { routineRows } from "./state/reader/routines.ts"; +import { setupReader } from "./state/reader/onboarding.ts"; +import { resolveReaderClock, readerClockFor } from "../shared/readerClock.ts"; +import { + CustomGame, + CustomEvent, + type CustomGames, + type CustomEvents, + mintCustomGameId, + mintCustomEventId, + recordFor, +} from "../shared/custom.ts"; +import { metaFor } from "../shared/games.ts"; +import { readGoal } from "../shared/goal.ts"; +import { isDaily, resolveDaily } from "../shared/daily.ts"; +import { orderGames } from "./state/gameOrder.ts"; +import { advanceFocus } from "./state/lens.ts"; +import { restorePrefsValue } from "./state/usePrefs.ts"; +import { readableAmounts } from "./state/useAmounts.ts"; +import { validRecords, type EventDraft } from "./state/useCustom.ts"; +import { useTheme } from "./state/theme.ts"; +import { + appearanceTokens, + applyAppearance, + readableOnSurfaces, +} from "./state/appearance.ts"; +import { GameMetaProvider } from "./state/gameMeta.tsx"; +import { useAppUpdate } from "./state/useAppUpdate.ts"; +import { ENTRIES, NOTES, newestId, unseen } from "../shared/changelog.ts"; +import { + ChecklistEvent, + LedgerColumns, + UrgencyLegend, + NextDeadline, +} from "./components/redesign/ChecklistEvent.tsx"; +import { EventDetail } from "./components/EventDetail.tsx"; + +import { FocusPicker } from "./components/redesign/FocusPicker.tsx"; +import { YourOwn } from "./components/YourOwn.tsx"; +import { AboutPage } from "./components/redesign/AboutPage.tsx"; +import { parseRoute, routeHash } from "./state/route.ts"; + +import { UpdateNotice } from "./components/UpdateNotice.tsx"; +import { + AppNavigation, + mainViewPatch, + type AppPage, +} from "./components/redesign/AppNavigation.tsx"; +import { + SettingsLayout, + type SettingsPage, +} from "./components/redesign/SettingsLayout.tsx"; +import { ChangelogPage } from "./components/redesign/ChangelogPage.tsx"; +import { GameSettings } from "./components/redesign/GameSettings.tsx"; +import { RoutinesPage } from "./components/redesign/RoutinesPage.tsx"; +import { DailyPanel } from "./components/redesign/DailyPanel.tsx"; +import { RoutineGroup } from "./components/redesign/RoutineGroup.tsx"; +import { TimelinePage } from "./components/redesign/TimelinePage.tsx"; +import { focusBarShown } from "./components/redesign/FocusPicker.tsx"; +import { ArchiveDetail } from "./components/redesign/ArchiveDetail.tsx"; +import { ArchivePage } from "./components/redesign/ArchivePage.tsx"; +import { BackupPage } from "./components/redesign/BackupPage.tsx"; +import { AppearancePage } from "./components/redesign/AppearancePage.tsx"; +import { TimeSettings } from "./components/redesign/TimeSettings.tsx"; +import { UpdatesPanel } from "./components/redesign/UpdatesPanel.tsx"; +import { DataSourcesPage } from "./components/redesign/DataSourcesPage.tsx"; +import { FeedFooter } from "./components/redesign/FeedFooter.tsx"; +import { SetupWizard } from "./components/redesign/SetupWizard.tsx"; +import { ReturningTour } from "./components/redesign/ReturningTour.tsx"; + +/** The upcoming section's sub-line: "3 events in 2 games". */ +function upcomingSummary(rows: readonly { event: { game: string } }[]): string { + const games = new Set(rows.map((r) => r.event.game)).size; + return `${rows.length} ${rows.length === 1 ? "event" : "events"} in ${games} ${games === 1 ? "game" : "games"}`; +} + +export function ReaderApp() { + const [codes, setCodes] = useState(null); + const [feed, setFeed] = useState({ status: "loading" }); + const [store, setStore] = useState(null); + const [error, setError] = useState(""); + useEffect(() => { + const ac = new AbortController(); + void fetchCodesFeed(ac.signal) + .then(setCodes) + .catch(() => {}); + void fetchFeed(ac.signal) + .then((feed) => { + const recovery = finishPendingRestore(localStorage); + if (!recovery.ok) { + setError( + "Your interrupted restore could not be recovered. Free browser storage and reload; saved data has been retained.", + ); + return; + } + const initialized = initializeReader( + localStorage, + [ + ...new Set([ + ...feed.sources.map((s) => s.game), + ...feed.events.map((e) => e.game), + ]), + ], + new Date().toISOString(), + mintAccountId, + ); + if (!initialized.ok) { + setError( + "Your saved data could not be opened safely. It has been retained. Check browser storage access and reload.", + ); + return; + } + const created = createReaderStore(localStorage, initialized.state); + setStore(created); + setFeed({ status: "ready", feed }); + }) + .catch((e) => { + if (!ac.signal.aborted) + setError( + e instanceof Error ? e.message : "Events could not be loaded.", + ); + }); + return () => ac.abort(); + }, []); + useEffect(() => () => store?.dispose(), [store]); + if (error) + return ( +
        +

        Event Clock could not open

        +

        {error}

        + + +
        + ); + if (feed.status !== "ready" || store === null) + return ( +
        +

        Loading events…

        +
        + ); + return ( + + setFeed({ status: "ready", feed: next })} + /> + + ); +} +function ReaderContent({ + feed, + onFeed, + codes, + onCodes, +}: { + onCodes: (codes: CodesFeed) => void; + codes: CodesFeed | null; + feed: Extract["feed"]; + onFeed: (feed: Extract["feed"]) => void; +}) { + const { state, dispatch, saveError } = useReader(); + const prefs = restorePrefsValue(state.prefs); + const theme = useTheme(prefs.theme); + const tokens = useMemo( + () => appearanceTokens(state.prefs, theme), + [state.prefs, theme], + ); + const [now, setNow] = useState(Date.now()); + const [online, setOnline] = useState(navigator.onLine); + const [hashTick, setHashTick] = useState(0); + // A reload returns to the page in the address bar; with none, the default view. + const [startRoute] = useState(() => + typeof location === "undefined" ? null : parseRoute(location.hash), + ); + const [page, setPage] = useState(startRoute?.page ?? prefs.view); + const [settingsPage, setSettingsPage] = useState( + startRoute?.settings ?? "appearance", + ); + // A new tab starts at its top. Without this the window kept the last page's + // scroll, so leaving a scrolled Settings page opened the Checklist part-way + // down. The first render is skipped so a reload's restored scroll stands. + // Set by Replay the introduction, so the tour opens on its first step. + const [tourAutoStart, setTourAutoStart] = useState(false); + const firstPage = useRef(true); + useEffect(() => { + if (firstPage.current) { + firstPage.current = false; + return; + } + window.scrollTo(0, 0); + }, [page]); + const [gameTab, setGameTab] = useState<"setup" | "codes">("setup"); + const [codesGame, setCodesGame] = useState( + startRoute?.game, + ); + useEffect(() => { + if (!prefs.onboarded || typeof location === "undefined") return; + const hash = routeHash({ + page, + settings: settingsPage, + game: settingsPage === "games" ? codesGame : undefined, + }); + // Replaced, not pushed: the address follows the page without adding a + // history entry for every tab the reader clicks. + if (location.hash !== hash) history.replaceState(null, "", hash); + }, [page, settingsPage, codesGame, prefs.onboarded, hashTick]); + useEffect(() => { + const follow = () => { + const route = parseRoute(location.hash); + // An address naming no page keeps the current one, and says so. + if (!route) return setHashTick((n) => n + 1); + setPage(route.page); + if (route.settings) setSettingsPage(route.settings); + setCodesGame(route.game); + }; + window.addEventListener("hashchange", follow); + return () => window.removeEventListener("hashchange", follow); + }, []); + const [archivedOpen, setArchivedOpen] = useState(null); + const [open, setOpen] = useState(null); + const [transition, setTransition] = useState<{ + key: string; + accountId: string; + eventId: string; + title: string; + label: string; + phase: "hold" | "fade" | "hidden"; + } | null>(null); + const [backupError, setBackupError] = useState(""); + const appUpdate = useAppUpdate(); + const zone = + typeof state.prefs.timezone === "string" ? state.prefs.timezone : null; + const customGames = useMemo( + () => validRecords(state.customGames, CustomGame), + [state.customGames], + ); + const customEvents = useMemo( + () => validRecords(state.customEvents, CustomEvent), + [state.customEvents], + ); + const meta = useMemo( + () => (id: string) => { + const original = metaFor(id, customGames); + const override = recordOf(state.prefs.gameColors)[id]; + return { + ...original, + hue: readableOnSurfaces( + typeof override === "string" ? override : original.hue, + [tokens.ground, tokens.surface], + 4.5, + ), + }; + }, + [customGames, state.prefs.gameColors, tokens], + ); + useEffect(() => { + applyAppearance(state.prefs, theme); + }, [state.prefs, theme]); + useEffect(() => { + const timer = setInterval(() => setNow(Date.now()), 1000), + update = () => setOnline(navigator.onLine); + window.addEventListener("online", update); + window.addEventListener("offline", update); + return () => { + clearInterval(timer); + window.removeEventListener("online", update); + window.removeEventListener("offline", update); + }; + }, []); + useEffect(() => { + const table = { ...RETIRED_IDS, ...aliasesFrom(feed.events) }; + if ( + Object.values(state.data).some((d) => + [ + d.progress, + d.ignored, + ...Object.values(d.daily), + ...Object.values(d.amounts), + ].some((map) => + Object.keys(table).some((id) => Object.hasOwn(map, id)), + ), + ) || + [ + ...Object.values(state.archive), + ...Object.values(state.seenEvents), + ].some((r) => Object.hasOwn(table, r.event.id)) + ) + void dispatch({ type: "aliases", table, ...actionMeta() }); + }, [feed.events, state.data, state.archive, state.seenEvents, dispatch]); + const all = useMemo( + () => readerEventRows(state, feed.events, now), + [ + state.accounts, + state.customEvents, + state.seenEvents, + feed.events, + Math.floor(now / 60000), + ], + ); + useEffect(() => { + if (!prefs.onboarded) return; + const records = all + .filter( + (r) => + !r.clock.ended || + state.data[r.accountId]?.progress[r.event.id] || + state.data[r.accountId]?.ignored[r.event.id], + ) + .filter((r) => { + const previous = state.seenEvents[pairKey(r.accountId, r.event.id)]; + return ( + !previous || + JSON.stringify(previous.event) !== JSON.stringify(r.event) || + previous.clockRevision !== r.clockRevision + ); + }) + .map((r) => ({ + accountId: r.accountId, + event: r.event, + firstSeenAt: new Date(now).toISOString(), + lastSeenAt: new Date(now).toISOString(), + clockRevision: r.clockRevision, + })); + if (records.length) + void dispatch({ type: "seenEvents", records, ...actionMeta() }).then(() => + dispatch({ type: "reconcile", ...actionMeta() }), + ); + const entries = Object.values(state.accounts) + .filter((a) => state.eventSeen[a.id] === undefined) + .map((a) => ({ + id: a.id, + ids: feed.events.filter((e) => e.game === a.game).map((e) => e.id), + })); + if (entries.length) + void dispatch({ type: "seenBatch", entries, ...actionMeta() }); + }, [all, feed.events, state.eventSeen, prefs.onboarded, dispatch]); + useEffect(() => { + if (!prefs.onboarded) return; + const needs = Object.values(state.seenEvents).some((s) => { + const a = state.accounts[s.accountId]; + return ( + a && + !a.retiredAt && + !state.archive[pairKey(a.id, s.event.id)] && + (readerClockFor(s.event, a, now).ended || + recordOf(state.data[a.id]?.progress[s.event.id]).status === "done" || + state.data[a.id]?.ignored[s.event.id]) + ); + }); + if (needs) void dispatch({ type: "reconcile", ...actionMeta() }); + }, [ + prefs.onboarded, + state.seenEvents, + state.archive, + state.data, + Math.floor(now / 60000), + dispatch, + ]); + useEffect(() => { + if (!prefs.onboarded) return; + const timer = setTimeout(() => { + const r = saveBackup(localStorage, state, new Date().toISOString()); + setBackupError( + r.ok + ? "" + : "Automatic backup could not be saved. Export a copy in Settings → Backup.", + ); + }, 1200); + return () => clearTimeout(timer); + }, [state.revision, prefs.onboarded]); + useEffect(() => { + if (!transition) return; + const t = setTimeout( + () => setTransition((t) => (t ? { ...t, phase: "fade" } : null)), + 1500, + ); + const collapse = setTimeout( + () => setTransition((t) => (t ? { ...t, phase: "hidden" } : null)), + 1750, + ); + const undo = setTimeout(() => setTransition(null), 8000); + return () => { + clearTimeout(t); + clearTimeout(collapse); + clearTimeout(undo); + }; + }, [transition?.key]); + const changePage = (next: AppPage) => { + setPage(next); + const patch = mainViewPatch(next); + if (Object.keys(patch).length) + void dispatch({ type: "prefs", patch, ...actionMeta() }); + }; + /** Ask for the newest published feed and codes; shared by the footer and Data & sources. */ + const refreshFeed = async (): Promise => { + try { + const [next, nextCodes] = await Promise.all([ + fetchFeed(undefined, { fresh: true }), + fetchCodesFeed(undefined, { fresh: true }).catch(() => null), + ]); + if (nextCodes) onCodes(nextCodes); + const result = refreshOutcome(feed, next); + if (result === "updated") onFeed(next); + if (!nextCodes) return "partial"; + return result === "same" && + nextCodes && + JSON.stringify(codes) !== JSON.stringify(nextCodes) + ? "updated" + : result; + } catch { + return "failed"; + } + }; + const showSettings = (next: SettingsPage) => { + setSettingsPage(next); + setPage("settings"); + }; + const accounts = Object.values(state.accounts).filter( + (a) => + !a.retiredAt && + !prefs.hiddenGames.includes(a.game) && + (a.intent.events || Object.values(a.routines).some((r) => r.followed)), + ); + const ordered = orderGames( + [...new Set(accounts.map((a) => a.game))], + prefs.gameOrder, + (id) => meta(id).name, + ); + // A view without its focus bar shows every game: a focus chosen elsewhere + // must never filter a page that offers no way to see or clear it. + const focus = + focusBarShown(state.prefs, page) && ordered.includes(prefs.focusGame ?? "") + ? prefs.focusGame + : null; + const progress = (r: ReaderEventRow) => + recordOf(state.data[r.accountId]?.progress[r.event.id]); + const outstanding = all.filter( + (r) => + !r.clock.ended && + !state.archive[pairKey(r.accountId, r.event.id)] && + progress(r).status !== "done" && + !state.data[r.accountId]?.ignored[r.event.id], + ); + const scoped = outstanding.filter( + (r) => focus === null || r.event.game === focus, + ); + const counts = Object.fromEntries( + ordered.map((game) => [ + game, + outstanding.filter((r) => r.event.game === game).length, + ]), + ); + const pins = accounts + .filter((a) => focus === null || a.game === focus) + .flatMap((a) => routineRows(state, a.id, now)) + .filter((r) => r.placement.followed && r.placement.checklist); + const separate = state.prefs.dailyLayout !== "grouped"; + const visible = all.filter( + (r) => + (focus === null || r.event.game === focus) && + !r.clock.ended && + (!r.clock.upcoming || prefs.showUpcoming) && + (!state.archive[pairKey(r.accountId, r.event.id)] || + (transition?.key === pairKey(r.accountId, r.event.id) && + transition.phase !== "hidden")), + ); + if (prefs.sort === "doing") + visible.sort( + (a, b) => + Number(progress(b).status === "doing") - + Number(progress(a).status === "doing") || + (a.clock.endsMs ?? Infinity) - (b.clock.endsMs ?? Infinity), + ); + const focusBar = ( + + void dispatch({ type: "prefs", patch: { focusGame }, ...actionMeta() }) + } + onAdvance={() => + void dispatch({ + type: "prefs", + patch: { focusGame: advanceFocus(focus, ordered) }, + ...actionMeta(), + }) + } + /> + ); + const outcome = async (row: ReaderEventRow, kind: "done" | "ignored") => { + const r = await dispatch({ + type: "outcome", + accountId: row.accountId, + eventId: row.event.id, + outcome: kind, + ...actionMeta(), + }); + if (r.ok) { + setOpen(null); + setTransition({ + key: pairKey(row.accountId, row.event.id), + accountId: row.accountId, + eventId: row.event.id, + title: row.event.title, + label: kind === "done" ? "Completed" : "Ignored", + phase: "hold", + }); + } + }; + const renderRow = (row: ReaderEventRow) => { + const key = pairKey(row.accountId, row.event.id), + p = progress(row); + return ( +
      • + setOpen(row)} + /> + {transition?.key === key && transition.phase !== "hidden" && ( +

        + {transition.label} · Moved to Archive +

        + )} +
      • + ); + }; + const mainUpdates = ( + <> + { + setGameTab("codes"); + setCodesGame(game); + showSettings("games"); + }} + state={state} + feed={feed} + now={now} + onSources={() => showSettings("sources")} + onAction={dispatch} + /> + {unseen( + ENTRIES, + prefs.changelogSeen, + new Date(now).toISOString().slice(0, 10), + ).length > 0 && ( +
        + What's new in Event Clock + + +
        + )} + + ); + const ownProps = readerOwnProps( + state, + customGames, + customEvents, + dispatch, + setOpen, + all, + now, + ); + return ( + + +
        +
        + {prefs.onboarded ? ( + + ) : ( +

        + EventClock +

        + )} + {prefs.onboarded && ( + r.clock.live).length, + routines: accounts + .flatMap((a) => routineRows(state, a.id, now)) + .filter((r) => r.placement.followed && !r.status.met) + .length, + archive: Object.keys(state.archive).length, + }} + /> + )} +
        + {saveError && !saveError.ok && ( +

        + Your last change could not be saved ({saveError.reason}). Your + previous data remains available. Try again or export it in Backup. +

        + )} + {backupError && ( +

        + {backupError} +

        + )} + {!prefs.onboarded ? ( + { + const next = setupReader( + draft, + state, + new Date().toISOString(), + mintAccountId, + ); + for (const account of Object.values(next.accounts)) { + const result = await dispatch({ + type: "account", + account, + ...actionMeta(), + }); + if (!result.ok) return; + } + const result = await dispatch({ + type: "prefs", + patch: next.prefs, + ...actionMeta(), + }); + if (result.ok) setPage(draft.view); + }} + /> + ) : ( + <> + {state.prefs.redesignTourSeen !== "2026-10-redesign" && ( + { + if (stop !== "codes") { + if (stop === "settings") showSettings("appearance"); + else setPage(stop); + return; + } + // The Codes tab of a tracked game that has codes, else the + // Checklist, where new codes are announced. + const game = accounts + .map((a) => a.game) + .find((g) => codes?.codes.some((c) => c.game === g)); + if (!game) { + setPage("soon"); + return; + } + setGameTab("codes"); + setCodesGame(game); + showSettings("games"); + }} + onFinish={() => { + setTourAutoStart(false); + void dispatch({ + type: "prefs", + patch: { redesignTourSeen: "2026-10-redesign" }, + ...actionMeta(), + }); + setPage(prefs.view); + }} + /> + )} + {page === "soon" && ( +
        + +
        + {mainUpdates} +
        +

        Running now

        +
        +
        + {["standard", "compact"].map((d) => ( + + ))} +
        +
        + {(["ending", "doing"] as const).map((sort) => ( + + ))} +
        +
        + {/* One line under the title and the buttons: the count + on the left, live's legend on the right. */} +
        +

        + {visible.filter((r) => r.clock.live).length} events in{" "} + {new Set(visible.map((r) => r.event.game)).size} games + · sorted by what ends first + {!online ? " · offline" : ""} +

        + +
        +
        + {visible.length ? ( + <> + {prefs.density === "compact" && } +
          + {visible + .filter((r) => !r.clock.upcoming) + .map(renderRow)} +
        + {visible.some((r) => r.clock.upcoming) && ( +
        +
        +
        +

        Coming up

        +

        Not started yet

        +

        + {upcomingSummary( + visible.filter((r) => r.clock.upcoming), + )} +

        +
        +
        + {prefs.density === "compact" && ( + + )} +
          + {visible + .filter((r) => r.clock.upcoming) + .map(renderRow)} +
        +
        + )} + + ) : ( +

        + No events to show. Choose another game or change + tracking in Settings → Your games. +

        + )} +

        + Done or ignored events leave this list and wait in the{" "} + + . +

        +
        +
        + )} + {page === "timeline" && ( + <> + {mainUpdates} + + + )} + {page === "routines" && ( + + )} + {page === "archive" && ( + + )} + {page === "settings" && ( + { + setCodesGame(undefined); + setSettingsPage(p); + }} + onGame={(g) => { + setCodesGame(g); + setGameTab("setup"); + setSettingsPage("games"); + }} + > + {settingsPage === "appearance" || + (settingsPage === "games" && !codesGame) ? ( + { + setTourAutoStart(true); + void dispatch({ + type: "prefs", + patch: { redesignTourSeen: null }, + ...actionMeta(), + }); + }} + /> + ) : settingsPage === "games" && codesGame ? ( + + ) : settingsPage === "time" ? ( + + ) : settingsPage === "sources" ? ( + + trackedGames(state).has(s.game), + )} + now={now} + timezone={zone} + online={online} + onRefresh={refreshFeed} + showUpcoming={prefs.showUpcoming} + detectDaily={prefs.detectDaily} + standardPulls={state.prefs.standardPulls === true} + onPrefs={(patch) => + void dispatch({ type: "prefs", patch, ...actionMeta() }) + } + /> + ) : settingsPage === "backup" ? ( + + ) : settingsPage === "changelog" ? ( + + ) : settingsPage === "about" ? ( + + ) : settingsPage === "own" ? ( +
        +

        Your own events and games

        +

        + Only what you added yourself. Routines live on the + Routines tab. +

        + +
        + ) : null} +
        + )} + showSettings("about")} + onRefresh={refreshFeed} + /> + + )} + {transition && ( + + )} + {archivedOpen && ( + setArchivedOpen(null)} + /> + )} + {open && ( + { + setOpen(null); + showSettings("sources"); + }} + row={open} + state={state} + now={now} + timezone={zone} + onAction={dispatch} + onOutcome={(kind) => void outcome(open, kind)} + onClose={() => setOpen(null)} + ownProps={ownProps} + /> + )} + {appUpdate.available && ( + + )} +
        +
        +
        + ); +} +function readerOwnProps( + state: ReaderState, + games: CustomGames, + events: CustomEvents, + onAction: import("./components/redesign/GameSettings.tsx").ActionHandler, + onOpen: (row: ReaderEventRow) => void, + rows: ReaderEventRow[], + now: number, +): React.ComponentProps { + const lanes = [...new Set(Object.values(state.accounts).map((a) => a.game))]; + const saveEvent = (id: string, draft: EventDraft) => { + const at = new Date().toISOString(); + void onAction({ + type: "record", + collection: "customEvents", + id, + value: { + ...recordOf(state.customEvents[id]), + ...draft, + id, + startPrecision: draft.startHasTime ? "exact" : "day", + endPrecision: + draft.endsAt === null + ? "unknown" + : draft.endHasTime + ? "exact" + : "day", + summary: draft.summary?.trim() || null, + at: events[id]?.at ?? at, + updatedAt: at, + }, + ...actionMeta(), + }); + }; + return { + games, + events, + lanes, + now, + onOpen: (id) => { + const row = + rows.find((r) => r.event.id === id) || + rows.find((r) => r.event.id.startsWith(id + "#")); + if (row) onOpen(row); + }, + onAddGame: (name, hue) => { + const at = new Date().toISOString(), + id = mintCustomGameId(name, Object.keys(games)); + void onAction({ + type: "record", + collection: "customGames", + id, + value: { id, name, hue, at, updatedAt: at }, + ...actionMeta(), + }).then(async () => { + const { createAccount } = await import("./state/reader/accounts.ts"); + await onAction({ + type: "account", + account: createAccount( + id, + restorePrefsValue(state.prefs).region, + at, + mintAccountId(), + ), + ...actionMeta(), + }); + }); + }, + onEditGame: (id, name, hue) => + void onAction({ + type: "record", + collection: "customGames", + id, + value: { + ...recordOf(state.customGames[id]), + name, + hue, + updatedAt: new Date().toISOString(), + }, + ...actionMeta(), + }), + onRemoveGame: (id) => { + const blockedBy = Object.values(events).filter( + (e) => e.game === id, + ).length; + if (blockedBy) return { removed: false, blockedBy }; + void onAction({ + type: "record", + collection: "customGames", + id, + value: null, + ...actionMeta(), + }); + for (const a of Object.values(state.accounts).filter( + (a) => a.game === id, + )) + void onAction({ + type: "account", + account: { ...a, retiredAt: new Date().toISOString() }, + ...actionMeta(), + }); + return { removed: true, blockedBy: 0 }; + }, + onAddEvent: (draft) => saveEvent(mintCustomEventId(), draft), + }; +} +function ReaderDetail({ + row, + state, + now, + timezone, + onAction, + onOutcome, + onClose, + ownProps, + sources, + onSources, +}: { + sources: SourceHealth[]; + onSources: () => void; + row: ReaderEventRow; + state: ReaderState; + now: number; + timezone: string | null; + onAction: import("./components/redesign/GameSettings.tsx").ActionHandler; + onOutcome: (outcome: "done" | "ignored") => void; + onClose: () => void; + ownProps: React.ComponentProps; +}) { + const a = state.accounts[row.accountId]!; + const timing = resolveReaderClock(a, now); + const data = state.data[a.id]!; + const p = recordOf(data.progress[row.event.id]); + const raw = recordOf(data.daily[timing.revision]?.[row.event.id]); + const days = Array.isArray(raw.days) + ? raw.days.filter((d): d is string => typeof d === "string") + : []; + const goal = readGoal(p.goal); + const patch = (patch: Record) => + void onAction({ + type: "progress", + accountId: a.id, + itemId: row.event.id, + patch, + ...actionMeta(), + }); + const own = recordFor(ownProps.events, row.event.id); + const stale = freshness( + sources.filter((s) => s.game === row.event.game), + now, + ).stale; + return ( + +

        Event data may be out of date.

        + {stale[0]!.sources.map((s) => ( +

        + {s.sourceId} · + {s.lastConfirmedAt + ? `Last checked ${formatInZone(s.lastConfirmedAt, true, timezone)}` + : "No successful source check recorded"} + {s.contentChangedAt + ? ` · content changed ${formatInZone(s.contentChangedAt, true, timezone)}` + : ""} +

        + ))} + + + ) : undefined + } + row={{ ...row, clock: readerClockFor(row.event, a, now) }} + completed={p.status === "done"} + ignored={Boolean(data.ignored[row.event.id])} + status={ + p.status === "doing" || p.status === "done" ? p.status : undefined + } + effort={ + typeof p.effort === "string" && Object.hasOwn(EFFORT, p.effort) + ? (p.effort as Effort) + : undefined + } + note={typeof p.note === "string" ? p.note : ""} + region={a.region} + timing={timing} + timezone={timezone} + now={now} + daily={ + goal !== undefined || + resolveDaily( + row.event, + typeof p.daily === "boolean" ? p.daily : undefined, + state.prefs.detectDaily === true, + ) + } + detectedDaily={state.prefs.detectDaily === true && isDaily(row.event)} + dailyDays={days} + goal={goal} + hasStoredGoal={p.goal !== undefined} + amounts={readableAmounts( + data.amounts[timing.revision]?.[row.event.id] as + Record | undefined, + )} + onSetAmount={(id, period, value) => + void onAction({ + type: "amount", + accountId: a.id, + revisionId: timing.revision, + itemId: id, + period, + value, + ...actionMeta(), + }) + } + onDaily={(_id, daily, clearsGoal) => + patch({ + ...((clearsGoal ?? false) ? { goal: null } : {}), + daily: daily ?? null, + }) + } + onGoal={(_id, goal) => + patch({ goal: goal ?? null, ...(goal ? { daily: null } : {}) }) + } + onToggleDay={(id, day) => + void onAction({ + type: "day", + accountId: a.id, + revisionId: timing.revision, + itemId: id, + day, + checked: !days.includes(day), + ...actionMeta(), + }) + } + onIgnore={() => onOutcome("ignored")} + onStatus={(_id, status) => { + if (status === "done") onOutcome("done"); + else patch({ status: status ?? null }); + }} + onEffort={(_id, effort) => patch({ effort: effort ?? null })} + onNote={(_id, note) => patch({ note })} + onClose={onClose} + own={ + own + ? { + record: own, + lanes: ownProps.lanes, + games: ownProps.games, + onSave: (id, draft) => + void onAction({ + type: "record", + collection: "customEvents", + id, + value: { + ...recordOf(state.customEvents[id]), + ...draft, + startPrecision: draft.startHasTime ? "exact" : "day", + endPrecision: + draft.endsAt === null + ? "unknown" + : draft.endHasTime + ? "exact" + : "day", + summary: draft.summary?.trim() || null, + updatedAt: new Date().toISOString(), + }, + ...actionMeta(), + }), + onDelete: (id) => { + void onAction({ + type: "record", + collection: "customEvents", + id, + value: null, + ...actionMeta(), + }); + onClose(); + }, + strandedBy: () => + Object.keys(data.progress).filter((id) => + id.startsWith(own.id + "#"), + ).length, + } + : undefined + } + /> + ); +} diff --git a/src/client/api.ts b/src/client/api.ts index 427aac3..cd0ad79 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -1,3 +1,4 @@ +import { CodesFeedSchema, type CodesFeed } from "../shared/codes.ts"; import { EventFeed, SCHEMA_VERSION } from "../shared/feed.ts"; export type FeedState = @@ -50,7 +51,7 @@ export async function fetchFeed( return parsed.data; } -export type RefreshOutcome = "updated" | "same" | "failed"; +export type RefreshOutcome = "updated" | "same" | "failed" | "partial"; /** * What a "check for new data" brought back. @@ -67,3 +68,11 @@ export function refreshOutcome(before: EventFeed, after: EventFeed | Error): Ref JSON.stringify(before.sources) === JSON.stringify(after.sources); return same ? "same" : "updated"; } + +export async function fetchCodesFeed(signal?: AbortSignal, options: {fresh?: boolean} = {}): Promise { + const response = await fetch(new URL("data/codes.v1.json", document.baseURI), { signal: signal ?? null, ...(options.fresh ? {cache: "no-cache" as const} : {}) }); + if (!response.ok || (options.fresh && response.headers.get("x-served-from") === "sw-cache")) throw new Error("The codes feed is unavailable."); + const parsed = CodesFeedSchema.safeParse(await response.json()); + if (!parsed.success) throw new Error("The codes feed could not be read."); + return parsed.data; +} diff --git a/src/client/components/ChangelogSheet.tsx b/src/client/components/ChangelogSheet.tsx index 9baa3b3..95ba0c1 100644 --- a/src/client/components/ChangelogSheet.tsx +++ b/src/client/components/ChangelogSheet.tsx @@ -39,23 +39,20 @@ export function ChangelogSheet({ return () => window.removeEventListener("keydown", onKey); }, [onClose]); - const loose = entries.filter((e) => e.note === undefined); - const months = [...new Set(loose.map((e) => e.date.slice(0, 7)))]; - return (
        -
        + ); +} + +export function ChangelogContent({ notes, entries }: { notes: readonly Note[]; entries: readonly Entry[] }) { + const loose = entries.filter((e) => e.note === undefined); + const months = [...new Set(loose.map((e) => e.date.slice(0, 7)))]; + return (<>

        Changelog

        What changed, summarised from the project's history and design notes. Each links to the @@ -122,9 +119,7 @@ export function ChangelogSheet({ ))}

        )} - - - ); + ); } function EntryLine({ entry }: { entry: Entry }) { diff --git a/src/client/components/Colophon.tsx b/src/client/components/Colophon.tsx index ab67173..4fa38a5 100644 --- a/src/client/components/Colophon.tsx +++ b/src/client/components/Colophon.tsx @@ -131,7 +131,7 @@ const SITES: Record = { "endfield.wiki.gg": { name: "wiki.gg", url: "https://wiki.gg" }, }; -function siteFor(url: string): { name: string; url: string } { +export function siteFor(url: string): { name: string; url: string } { try { const host = new URL(url).host; return SITES[host] ?? { name: host, url: `https://${host}` }; @@ -570,6 +570,7 @@ export function Colophon({ } const OUTCOME: Record = { + partial: "Event data checked; redeem codes could not be checked.", updated: "Updated — newer data is on screen.", same: "Nothing newer published yet. Sources are read about twice a day.", failed: "Couldn't reach the server, so this is still the copy stored here.", diff --git a/src/client/components/Controls.tsx b/src/client/components/Controls.tsx index 9e88b5e..d4d935f 100644 --- a/src/client/components/Controls.tsx +++ b/src/client/components/Controls.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { createContext, useContext, useState } from "react"; import type { LaneId } from "../../shared/custom.ts"; import type { Region } from "../../shared/schema.ts"; import { REGION_LABEL } from "../../shared/time.ts"; @@ -11,6 +11,9 @@ import { isCustomGameId } from "../../shared/custom.ts"; import { exceptionLine, setFor, shownFor, type PerGame } from "../../shared/perGame.ts"; import { asOfLabel, offered, routinesFor } from "../../shared/routines.ts"; +const SettingsFilter = createContext(undefined); +const GROUP_PAGE: Record = { Games: "games", "Server region": "time", Appearance: "appearance", "What you see": "lists", "Your own games and events": "general", "Your progress": "backup" }; + const REGIONS: Array<{ id: Region; label: string }> = ( ["america", "europe", "asia"] as const ).map((id) => ({ id, label: REGION_LABEL[id] })); @@ -81,7 +84,9 @@ export function Controls({ onExportAll, onImportAll, own, + section, }: { + section?: string | undefined; games: LaneId[]; prefs: Prefs; onToggleGame: (g: LaneId) => void; @@ -121,7 +126,7 @@ export function Controls({ }; return ( -
        @@ -188,6 +193,10 @@ export function Controls({ {/* Genuinely only how the page looks, and instant: no reload, and nothing marked, typed or ticked is touched. */} +
        + + +
        -
        +
        ); } @@ -614,8 +623,10 @@ function Group({ state: string; children: React.ReactNode; }) { + const section = useContext(SettingsFilter); + if (section !== undefined && GROUP_PAGE[name] !== section) return null; return ( -
        +
        [p.type, p.value])); + return { date: `${parts.year}-${parts.month}-${parts.day}`, time: `${parts.hour}:${parts.minute}` }; + } const pad = (n: number) => String(n).padStart(2, "0"); return { date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`, @@ -475,7 +480,7 @@ export function eventStillNeeded(s: { * can be proved to complete into a saveable event without a click, and so the * still-needed line and Save's disabled state read the same answer. */ -export function eventFormState(v: EventFormValues, existingUntil: string | null) { +export function eventFormState(v: EventFormValues, existingUntil: string | null, timezone?: string | null) { // A preset's period is its window, so it has no end to state. The end the // reader may have typed under a different cadence is kept in state rather // than cleared — hiding a field and quietly discarding what is in it is how @@ -483,11 +488,11 @@ export function eventFormState(v: EventFormValues, existingUntil: string | null) const preset = v.cadence === "daily" || v.cadence === "weekly" || v.cadence === "monthly"; const datedWindow = v.cadence === "one-off" || v.cadence === "custom"; - const startsAt = v.startDate === "" ? null : readerInstant(v.startDate, v.startTime, "start"); + const startsAt = v.startDate === "" ? null : readerInstant(v.startDate, v.startTime, "start", timezone); const endsAt = !datedWindow || !v.endKnown || v.endDate === "" ? null - : readerInstant(v.endDate, v.endTime, "end"); + : readerInstant(v.endDate, v.endTime, "end", timezone); const endMissing = datedWindow && v.endKnown && v.endDate !== "" && endsAt === null; const backwards = startsAt !== null && endsAt !== null && endsAt <= startsAt; @@ -625,6 +630,7 @@ export function EventForm({ strandedBy?: ((draft: EventDraft) => number) | undefined; }) { const gameMeta = useGameMeta(); + const timezone = useContext(DisplayTimeContext); // The reader's own games first, and so the default too. Someone filling this // in by hand is usually doing it *because* the game isn't tracked; making @@ -635,8 +641,8 @@ export function EventForm({ ); const [v, setV] = useState(() => { - const start = fields(initial?.startsAt ?? null); - const end = fields(initial?.endsAt ?? null); + const start = fields(initial?.startsAt ?? null, timezone); + const end = fields(initial?.endsAt ?? null, timezone); // Round-tripped through the same fields-then-readerInstant path the live // form uses, rather than read straight off the record. The two disagree: a // day-precision end is stored as whatever instant it was written at, and @@ -647,11 +653,11 @@ export function EventForm({ const startTime = initial?.startPrecision === "exact" ? start.time : ""; const endTime = initial?.endPrecision === "exact" ? end.time : ""; const initialStartsAt = - start.date === "" ? null : (readerInstant(start.date, startTime, "start") ?? null); + start.date === "" ? null : (readerInstant(start.date, startTime, "start", timezone) ?? null); const initialEndsAt = initial?.endsAt == null || end.date === "" ? null - : (readerInstant(end.date, endTime, "end") ?? null); + : (readerInstant(end.date, endTime, "end", timezone) ?? null); const opening = openingControls( initialStartsAt, initialEndsAt, @@ -697,7 +703,7 @@ export function EventForm({ const set = (patch: Partial) => setV((prev) => ({ ...prev, ...patch })); const [chosen, setChosen] = useState(null); - const s = eventFormState(v, initial?.repeat?.until ?? null); + const s = eventFormState(v, initial?.repeat?.until ?? null, timezone); const { preset, datedWindow, endMs, measured, measuring, repeat, draft } = s; // Only a schedule change re-keys anything. Renaming does not — the token is @@ -735,6 +741,7 @@ export function EventForm({ onSave(draft); }} > +

        Dates and times use {timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone}.

        {initial === undefined && ( >( shown: { weekly: boolean; monthly: boolean; cycle: boolean }, ): T[] { const on = { week: shown.weekly, month: shown.monthly, cycle: shown.cycle } as const; - return tasks.filter((t) => on[t.period.unit]); + // A reader's own daily sits with the standing dailies, not these strips. + return tasks.filter((t) => t.period.unit !== "day" && on[t.period.unit]); } /** diff --git a/src/client/components/DailyChecklist.tsx b/src/client/components/DailyChecklist.tsx index e2177d7..73c6ff3 100644 --- a/src/client/components/DailyChecklist.tsx +++ b/src/client/components/DailyChecklist.tsx @@ -1,3 +1,4 @@ +import type { ResetTiming } from "../../shared/time.ts"; import { CATCH_UP_DAYS, catchUpDays, @@ -33,7 +34,9 @@ export function DailyChecklist({ logged, onToggleDay, goal, + timing, }: { + timing?: ResetTiming | undefined; startsMs: number; endsMs: number | null; region: Region; @@ -49,7 +52,7 @@ export function DailyChecklist({ */ goal?: GoalStatus | undefined; }) { - const summary = dailySummary({ startsMs, endsMs, region, game, now, logged }); + const summary = dailySummary({ startsMs, endsMs, region, game, now, logged, ...(timing === undefined ? {} : { timing }) }); const { days, today, doneToday, todayInWindow } = summary; const started = now >= startsMs; @@ -58,7 +61,7 @@ export function DailyChecklist({

        Daily checklist

        - resets in {formatRemaining(msUntilReset(now, region, game))} + resets in {formatRemaining(msUntilReset(now, region, game, timing))}

        @@ -113,7 +116,7 @@ export function DailyChecklist({ yesterday could never say so. Only the past, and only as far back as `catchUpDays` goes. */}
        - {catchUpDays(now, region, game, startsMs).map((day) => ( + {catchUpDays(now, region, game, startsMs, undefined, timing).map((day) => ( timezone === undefined ? defaultFormatAbsolute(instant, withTime) : formatInZone(instant, withTime, timezone); const gameMeta = useGameMeta(); const [editing, setEditing] = useState(false); const { event, clock } = row; // The run's claimable days caps an event-wide goal; null when the end is // unannounced, and then nothing is invented to cap it with. - const run = runDays(clock.startsMs, clock.endsMs, region, event.game); + const run = runDays(clock.startsMs, clock.endsMs, region, event.game, timing); const goalState = goal === undefined ? undefined @@ -118,6 +127,7 @@ export function EventDetail({ game: event.game, now, logged: dailyDays, + ...(timing === undefined ? {} : { timing }), amounts, }); const game = gameMeta(event.game); @@ -136,25 +146,26 @@ export function EventDetail({ }, [onClose]); return ( -
        +
        -
      • + ); } diff --git a/src/client/components/Fireworks.tsx b/src/client/components/Fireworks.tsx index 34fe57d..02ee7f8 100644 --- a/src/client/components/Fireworks.tsx +++ b/src/client/components/Fireworks.tsx @@ -32,25 +32,55 @@ const COLORS = [ "var(--color-ink)", ]; -export function Fireworks() { +/** + * The grand version, for finishing a whole section (every daily, or every + * routine in a game's group) rather than one item: more bursts across the + * section, wider and longer, in waves. Same fixed geometry rule as above. + */ +const GRAND_BURSTS: Array<{ x: number; y: number; delay: number }> = [ + { x: 12, y: 30, delay: 0 }, + { x: 50, y: 18, delay: 120 }, + { x: 86, y: 34, delay: 220 }, + { x: 30, y: 62, delay: 380 }, + { x: 70, y: 66, delay: 480 }, + { x: 20, y: 86, delay: 640 }, + { x: 80, y: 84, delay: 720 }, +]; +const GRAND_SPARKS = 14; + +export function Fireworks({ + grand = false, + hue, +}: { + /** The section-complete celebration rather than the single burst. */ + grand?: boolean; + /** A game's colour to mix into the sparks. */ + hue?: string | undefined; +} = {}) { + const bursts = grand ? GRAND_BURSTS : BURSTS, + sparks = grand ? GRAND_SPARKS : SPARKS, + colors = hue ? [hue, ...COLORS] : COLORS; return ( -
        - {BURSTS.map((burst, b) => ( +
        + {bursts.map((burst, b) => ( - {Array.from({ length: SPARKS }, (_, i) => ( + {Array.from({ length: sparks }, (_, i) => ( ))} diff --git a/src/client/components/Legend.tsx b/src/client/components/Legend.tsx index 9d617eb..4e9d70c 100644 --- a/src/client/components/Legend.tsx +++ b/src/client/components/Legend.tsx @@ -1,7 +1,7 @@ import type { Urgency } from "../../shared/time.ts"; import { URGENCY_COLOR } from "./Meter.tsx"; -const STEPS: Array<{ urgency: Urgency; label: string }> = [ +export const STEPS: Array<{ urgency: Urgency; label: string }> = [ { urgency: "calm", label: "a week+" }, { urgency: "near", label: "under a week" }, { urgency: "soon", label: "under 3 days" }, diff --git a/src/client/components/Meter.tsx b/src/client/components/Meter.tsx index ffd77c3..8f113cd 100644 --- a/src/client/components/Meter.tsx +++ b/src/client/components/Meter.tsx @@ -2,12 +2,14 @@ import type { Urgency } from "../../shared/time.ts"; const TICKS = 24; +// `--color-urgency-*` is set by the redesign's appearance (the live ramp, +// kept apart from its accent); the live site doesn't set it and falls back. export const URGENCY_COLOR: Record = { expired: "var(--color-faint)", - critical: "var(--color-critical)", - soon: "var(--color-soon)", - near: "var(--color-near)", - calm: "var(--color-calm)", + critical: "var(--color-urgency-critical, var(--color-critical))", + soon: "var(--color-urgency-soon, var(--color-soon))", + near: "var(--color-urgency-near, var(--color-near))", + calm: "var(--color-urgency-calm, var(--color-calm))", }; interface MeterProps { diff --git a/src/client/components/NextUp.tsx b/src/client/components/NextUp.tsx index 816d84e..4c5eba2 100644 --- a/src/client/components/NextUp.tsx +++ b/src/client/components/NextUp.tsx @@ -1,3 +1,4 @@ +import { REGION_LABEL } from "../../shared/time.ts"; import { useGameMeta } from "../state/gameMeta.tsx"; import { formatRemaining } from "../../shared/time.ts"; import type { RowEvent } from "./EventRow.tsx"; @@ -17,6 +18,7 @@ export function NextUp({ rows, focused, onOpen, + onOpenRow, }: { /** * The soonest-expiring events the reader has neither finished nor ignored, @@ -27,6 +29,7 @@ export function NextUp({ rows: RowEvent[]; /** Name of the game being focused on, when the page is narrowed to one. */ focused: string | null; + onOpenRow?: ((row: RowEvent) => void) | undefined; onOpen: (id: string) => void; }) { const gameMeta = useGameMeta(); @@ -69,14 +72,14 @@ export function NextUp({ inert and read as a heading rather than a way in. */} @@ -107,7 +110,7 @@ export function NextUp({

        Then

          {rest.map((row) => ( - + onOpenRow ? onOpenRow(row) : onOpen(id)} /> ))}
        diff --git a/src/client/components/TaskForm.tsx b/src/client/components/TaskForm.tsx index bc7037f..f3904f1 100644 --- a/src/client/components/TaskForm.tsx +++ b/src/client/components/TaskForm.tsx @@ -51,7 +51,7 @@ export function taskFieldsFrom(initial: TaskDraft | undefined, lanes: readonly L return { game: initial?.game ?? lanes[0] ?? "", title: initial?.title ?? "", - unit: p?.unit ?? "week", + unit: p === undefined || p.unit === "day" ? "week" : p.unit, weekday: p?.unit === "week" ? p.startsOn : 1, monthDay: String(p?.unit === "month" ? p.startsOn : 1), cycleDays: String(p?.unit === "cycle" ? p.days : 14), diff --git a/src/client/components/Timeline.tsx b/src/client/components/Timeline.tsx index b5ff4a5..1b12a61 100644 --- a/src/client/components/Timeline.tsx +++ b/src/client/components/Timeline.tsx @@ -1,4 +1,4 @@ -import { Fragment, useLayoutEffect, useRef } from "react"; +import { Fragment, useLayoutEffect, useRef, type ReactNode } from "react"; import { useGameMeta } from "../state/gameMeta.tsx"; import type { LaneId } from "../../shared/custom.ts"; import { DAY, endingSoonestFirst } from "../../shared/time.ts"; @@ -20,12 +20,6 @@ import { canStep, stepDayWidth, weekLabelStep } from "../state/zoom.ts"; */ const PIN = 8; -/** - * A sliver of time before now, so the "now" rule reads as a line in the view - * rather than merging with the border. - */ -const HALF_DAY_LEAD = 12 * 60 * 60 * 1000; - /** * How far back the view can be scrolled beyond the oldest running event, so * "when did this start?" is answerable without the window being unbounded. @@ -47,7 +41,7 @@ const PAST_LEAD = 7 * DAY; */ const PAST_LIMIT = 60 * DAY; -/** Where the now rule sits when the board opens: a little in from the edge. */ +/** Room left of the earliest start the board opens on, so its edge is seen. */ const OPEN_INSET = 28; /** @@ -116,8 +110,16 @@ export function Timeline({ splitUpcoming, gameOrder, onOpen, + onOpenRow, + isDoneRow, isDone, expand, + routines = [], + routinesInline = false, + label, + controls, + afterHeader, + servers = () => true, }: { rows: RowEvent[]; now: number; @@ -161,6 +163,8 @@ export function Timeline({ */ gameOrder?: readonly LaneId[]; onOpen: (id: string) => void; + onOpenRow?: (row: RowEvent) => void; + isDoneRow?: (row: RowEvent) => boolean; /** * Asked rather than derived from the progress store: an entry exists there * the moment a reader records an effort or a note, and dimming a bar for @@ -179,6 +183,21 @@ export function Timeline({ * terminate. */ expand?: ((minMs: number, maxMs: number) => RowEvent[]) | undefined; + /** + * Routine periods drawn in their own section under the event lanes, behind a + * divider. They never move the board's window: a period is clamped to it. + */ + routines?: readonly ClassicRoutine[]; + /** Routines at the end of each game's lane instead of their own section. */ + routinesInline?: boolean; + /** Short text at a bar's right end, such as time left or the end date. */ + label?: (row: RowEvent) => string | null; + /** More pill toggles for the board's header, after the stacking pills. */ + controls?: ReactNode; + /** Drawn between the header and the board, such as the focus bar. */ + afterHeader?: ReactNode; + /** Whether a game is on several servers, so its bars name the server. */ + servers?: (game: string) => boolean; }) { const gameMeta = useGameMeta(); const scroller = useRef(null); @@ -218,9 +237,23 @@ export function Timeline({ // still on screen — an event that began three days ago is context, not // history. Keyed on the rounded offset so it runs when the range changes, // not every second: re-scrolling on each tick would fight the reader. - const openAt = Math.round(Math.max(0, x(now - HALF_DAY_LEAD) - OPEN_INSET)); + // Opens far enough back that most running bars show their start, and so + // their names: back to the median running start, keeping now between a + // quarter and half of the way across. + const liveStarts = plotted + .filter((r) => r.clock.live) + .map((r) => x(r.clock.startsMs)) + .sort((a, b) => a - b); + const nowX = x(now); + const medianStart = liveStarts[Math.floor(liveStarts.length / 2)] ?? nowX; + const openFor = (viewport: number) => + Math.round(Math.max(0, openingLeft(nowX, medianStart, viewport))); + const openAt = `${Math.round(nowX)}:${Math.round(medianStart)}`; const jumpToNow = (behavior: ScrollBehavior) => - scroller.current?.scrollTo({ left: openAt, behavior }); + scroller.current?.scrollTo({ + left: openFor(scroller.current.clientWidth), + behavior, + }); /** * A moment in time to hold still through the next re-render, and where in the @@ -265,16 +298,28 @@ export function Timeline({ // Open at today rather than at the far past: it is what they came for. // Keyed on the rounded offset so it runs when the range changes, not every // second — re-scrolling on each tick would fight the reader's own scrolling. - el.scrollTo({ left: openAt, behavior: "instant" }); + el.scrollTo({ left: openFor(el.clientWidth), behavior: "instant" }); trackScroll(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [openAt]); - if (rows.length === 0) { + if (rows.length === 0 && routines.length === 0) { return ( -

        - Nothing to plot. Switch a game back on to see its schedule. -

        + <> + jumpToNow("smooth")} + /> + {afterHeader} +

        + Nothing to plot. Switch a game back on to see its schedule. +

        + ); } @@ -284,6 +329,49 @@ export function Timeline({ // under-counts what is actually drawn there. const marks = startMarkers(drawn, x); + const routineBar = (r: ClassicRoutine) => { + const game = gameMeta(r.game); + const left = Math.max(x(r.start), 0); + const width = Math.max(Math.min(x(r.end), chartWidth) - left, MIN_BAR); + return ( + + ); + }; + const months = monthBoundaries(min, max); const weeks = weekBoundaries(min, max); // Every Monday is right at the default scale and illegible at the widest zoom @@ -295,38 +383,18 @@ export function Timeline({ {/* The board's own header. The jump control lives out here rather than floating over the chart: pinned inside, it would sit on top of the calendar and cover the very dates it sends you back to. */} -
        - + jumpToNow("smooth")} + /> + {afterHeader} -
        -
        - zoom(-1)} - > - − - - zoom(1)} - > - + - -
        - - -
        -
        - - {plotted.length === 0 ? ( + {plotted.length === 0 && routines.length === 0 ? ( /* Not the same emptiness as no rows at all: everything the reader can see is still ahead of them, and the board is hiding it on purpose. Say which, or the control above reads as broken. */ @@ -344,9 +412,11 @@ export function Timeline({ /* * The pane scrolls, not the page — which is what lets the axis stay * put. Capped rather than fixed: three lanes take the height they need - * and nothing scrolls vertically at all. + * and nothing scrolls vertically at all. Capped at the window's height, + * so a full board fills the screen and the page footer stays below it + * until the reader scrolls past the board's end. */ - className="scroll-pane relative max-h-[72vh] overflow-auto overscroll-x-contain" + className="scroll-pane relative max-h-[100dvh] overflow-auto overscroll-x-contain" onScroll={trackScroll} >
        @@ -374,7 +444,18 @@ export function Timeline({ className="pointer-events-none absolute bottom-0 top-10 z-10 w-px bg-critical/80" style={{ left: x(now) }} > - + {/* Not `.eyebrow`: its faint ink left the word unreadable on + the chip. */} + now
        @@ -453,7 +534,8 @@ export function Timeline({ )}
        - {lane.rows.map(({ event, clock }, i) => { + {lane.rows.map((row, i) => { + const { event, clock } = row; const game = gameMeta(event.game); const unknownEnd = clock.endsMs === null; const notStarted = clock.upcoming; @@ -477,13 +559,13 @@ export function Timeline({ chartWidth, ); const width = Math.max(right - left, MIN_BAR); - const done = isDone(event.id); + const done = isDoneRow ? isDoneRow(row) : isDone(event.id); return ( - + {i === breakAt && }
        ); })} + {!routinesInline && routines.length > 0 && ( +
        +
        +

        + Routines +

        +
        + {[...new Set(routines.map((r) => r.game))].map((g) => ( +
        +

        + {gameMeta(g).short} +

        +
        + {routines.filter((r) => r.game === g).map(routineBar)} +
        +
        + ))} +
        + )} @@ -571,6 +692,98 @@ export function Timeline({ ); } +/** One routine period for the classic board's own section. */ +export interface ClassicRoutine { + key: string; + game: string; + title: string; + /** Short progress, such as "1/3". */ + detail: string; + start: number; + end: number; + done: boolean; + /** Time to reset or the reset date, when the reader asked for bar labels. */ + label?: string; + onOpen: () => void; +} + +/** + * The now chip's fill: one strong red in every theme, under white text (5:1), + * because the themed critical tone is tuned as text and washes out as a fill. + */ +const NOW_RED = "#d42a47"; + +/** One line of a bar's name; anything that wraps past it is hidden. */ +const NAME_LINE = "1.125rem"; + +/** Where the board opens: back to `startX`, with now 25–50% across. */ +export function openingLeft( + nowX: number, + startX: number, + viewport: number, +): number { + return Math.max( + nowX - viewport * 0.5, + Math.min(nowX - viewport * 0.25, startX - OPEN_INSET), + ); +} + +/** + * The board's header: stacking pills, any extra toggle pills, then the scale + * and today controls. Shared by both Timeline looks so their options match. + */ +export function BoardHeader({ + group, + onGroup, + controls, + canOut, + canIn, + onZoom, + onToday, +}: { + group: TimelineGroup; + onGroup: (group: TimelineGroup) => void; + controls?: ReactNode; + canOut: boolean; + canIn: boolean; + onZoom: (by: 1 | -1) => void; + onToday: () => void; +}) { + return ( +
        +
        + + {controls} +
        +
        +
        + onZoom(-1)} + > + − + + onZoom(1)} + > + + + +
        + +
        +
        + ); +} + /** * The span of time the board draws. * @@ -613,23 +826,51 @@ function StackControl({ onChange: (group: TimelineGroup) => void; }) { return ( -
        - {TIMELINE_GROUPS.map((mode) => { - const on = value === mode.id; + + ); +} + +/** + * An either/or choice drawn as two pills sharing one outline, the chosen side + * filled. Every option in the board's header uses it, so each reads as one + * question with two answers rather than a switch to be found and undone. + */ +export function PillChoice({ + label, + value, + options, + onChange, +}: { + label: string; + value: T; + options: readonly { id: T; label: string; hint?: string }[]; + onChange: (value: T) => void; +}) { + return ( +
        + {options.map((option) => { + const on = value === option.id; return ( ); })} diff --git a/src/client/components/redesign/AboutPage.tsx b/src/client/components/redesign/AboutPage.tsx new file mode 100644 index 0000000..fb11864 --- /dev/null +++ b/src/client/components/redesign/AboutPage.tsx @@ -0,0 +1,99 @@ +import { Fragment, type ReactNode } from "react"; +import type { SourceHealth } from "../../../shared/feed.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import { + AUTHOR, + BUG_URL, + FEATURE_URL, + IDEA_CREDITS, + REPO_URL, + RESET_CLOCK_CREDIT, + siteFor, +} from "../Colophon.tsx"; + +/** + * The Round 6 B “About Event Clock” pane: credits and the disclaimer only. + * Data freshness lives in Data & sources. + */ +export function AboutPage({ + sources, +}: { + /** Every source read, so every editor and game is credited, not just the reader's. */ + sources: SourceHealth[]; +}) { + const meta = useGameMeta(); + const sites = [ + ...new Map( + sources.map((s) => siteFor(s.url)).map((s) => [s.url, s]), + ).values(), + ].sort((a, b) => a.name.localeCompare(b.name)); + const games = [...new Set(sources.map((s) => s.game))]; + const studios = [ + ...new Set(sources.map((s) => meta(s.game).studio).filter(Boolean)), + ]; + const link = (href: string, label: ReactNode) => ( + + {label} + + ); + const dotted = (items: ReactNode[]) => + items.map((item, i) => ( + + {i > 0 && " · "} + {item} + + )); + return ( +
        +

        About Event Clock

        +
        +

        + Live and upcoming gacha events, sorted by what expires next. An + unofficial, fan-made calendar with progress saved in your browser. +

        +

        Made by

        +

        + {link(AUTHOR.site, AUTHOR.name)} ·{" "} + {link(AUTHOR.github, "StereotypicalCat")} +

        +

        + {link(REPO_URL, "Source code")} + {link(BUG_URL, "Report a problem")} + {link(FEATURE_URL, "Request a feature")} + {link(AUTHOR.kofi, "Support the project")} +

        +

        With thanks to the source editors

        +

        + The source communities compile and maintain the schedules; Event Clock + rearranges their work. {dotted(sites.map((s) => link(s.url, s.name)))} + . +

        +

        + {link(RESET_CLOCK_CREDIT.url, RESET_CLOCK_CREDIT.name)} provides the + reference for per-server reset clocks. +

        +

        The studios and games

        +

        + {studios.join(" · ")}, who make the games worth keeping track of:{" "} + {dotted( + games.map((g) => ( + + {meta(g).name} + + )), + )} + . All game names, event names and trademarks belong to their + respective owners. +

        +

        Ideas and acknowledgements

        +

        {dotted(IDEA_CREDITS.map((c) => link(c.url, c.handle)))}.

        +

        Unofficial and unaffiliated

        +

        + Not endorsed by or connected to any game publisher or source named + here. Dates can be wrong or go out of date: check the original source + before the final hours. +

        +
        +
        + ); +} diff --git a/src/client/components/redesign/AppNavigation.tsx b/src/client/components/redesign/AppNavigation.tsx new file mode 100644 index 0000000..cc6f398 --- /dev/null +++ b/src/client/components/redesign/AppNavigation.tsx @@ -0,0 +1,43 @@ +import type { Prefs } from "../../state/usePrefs.ts"; +export type AppPage = "soon" | "timeline" | "routines" | "archive" | "settings"; +export const APP_PAGES: readonly [AppPage, string][] = [ + ["soon", "Checklist"], + ["timeline", "Timeline"], + ["routines", "Routines"], + ["archive", "Archive"], + ["settings", "Settings"], +]; +export function mainViewPatch(page: AppPage): Partial { + return page === "soon" || page === "timeline" ? { view: page } : {}; +} +export function AppNavigation({ + page, + onPage, + counts = {}, +}: { + counts?: Partial>; + page: AppPage; + onPage: (page: AppPage) => void; +}) { + return ( + + ); +} diff --git a/src/client/components/redesign/AppearancePage.tsx b/src/client/components/redesign/AppearancePage.tsx new file mode 100644 index 0000000..e352652 --- /dev/null +++ b/src/client/components/redesign/AppearancePage.tsx @@ -0,0 +1,375 @@ +import type { CSSProperties } from "react"; +import { recordOf, actionMeta } from "../../state/reader/actions.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import { orderGames } from "../../state/gameOrder.ts"; +import type { ReaderState } from "../../state/reader/schema.ts"; +import type { ActionHandler } from "./GameSettings.tsx"; +import { ChecklistDisplay } from "./RoutinesPage.tsx"; +import { timelineLook } from "./TimelinePage.tsx"; +import { ViewCards } from "./ViewCards.tsx"; +import { focusBarShown } from "./FocusPicker.tsx"; +import { URGENCY_STEPS, urgencyChoice } from "../../state/appearance.ts"; + +/** The live legend's words for each step. */ +const URGENCY_LABEL = { + calm: "A week or more", + near: "Under a week", + soon: "Under 3 days", + critical: "Under a day", +} as const; +export function AppearancePage({ + state, + onAction, + onReplayTour, +}: { + state: ReaderState; + onAction: ActionHandler; + /** Starts the returning tour at its first step. */ + onReplayTour?: () => void; +}) { + const meta = useGameMeta(); + const p = state.prefs, + patch = (patch: Record) => + void onAction({ type: "prefs", patch, ...actionMeta() }); + const hidden = Array.isArray(p.hiddenGames) + ? (p.hiddenGames as string[]) + : []; + const saved = Array.isArray(p.gameOrder) + ? (p.gameOrder as string[]) + : undefined; + const byName = (g: string) => meta(g).name; + const accounts = Object.values(state.accounts).filter((a) => !a.retiredAt); + // Only the games being tracked: not hidden, with events or a followed routine. + const tracked = orderGames( + [ + ...new Set( + accounts + .filter( + (a) => + !hidden.includes(a.game) && + (a.intent.events || + Object.values(a.routines).some((r) => r.followed)), + ) + .map((a) => a.game), + ), + ], + saved, + byName, + ); + const untracked = orderGames( + [...new Set(accounts.map((a) => a.game))].filter( + (g) => !tracked.includes(g), + ), + saved, + byName, + ); + const move = (index: number, by: -1 | 1) => { + const next = tracked.slice(); + const [g] = next.splice(index, 1); + next.splice(index + by, 0, g!); + patch({ gameOrder: [...next, ...untracked] }); + }; + return ( +
        +

        Appearance

        +
        + {(["structured", "relaxed"] as const).map((id) => ( + + ))} +
        +
        +

        Theme

        +

        + Choose a light or dark workspace, or follow your device. +

        +
        + {["dark", "light", "system"].map((theme) => ( + + ))} +
        +
        +
        +

        Colour scheme

        +
        + {["slate", "warm", "plum", "custom"].map((s) => ( + + ))} +
        +
        + {p.scheme === "custom" && ( +
        + {["dark", "light"].map((theme) => ( +
        + + {theme === "dark" ? "Dark colours" : "Light colours"} + + {["ground", "surface", "text", "accent"].map((k) => ( + + ))} +
        + ))} +

        + Your chosen colours are saved. Displayed text and surfaces adjust to + keep them readable. +

        +
        + )} +
        +

        Urgency colours

        +

        + Countdowns and time bars change colour as an event's end gets closer. + Pick each step for dark and light. They work with any scheme, and are + only adjusted if needed to stay readable. +

        +
        + {(["dark", "light"] as const).map((theme) => { + const chosen = urgencyChoice(p, theme), + own = recordOf(recordOf(p.urgencyPalette)[theme]); + return ( +
        + + {theme === "dark" ? "Dark theme" : "Light theme"} + + {URGENCY_STEPS.map((k) => ( + + ))} + {Object.keys(own).length > 0 && ( + + )} +
        + ); + })} +
        +
        +
        +

        Default view

        +

        + What opens when you come back. What's new shows in both, so you won't + miss it either way. +

        + patch({ view })} + /> +
        + + + Your games, events and progress stay in this browser. + +
        +
        + {tracked.length > 1 && ( +
        +

        Game order

        +

        + The games you track, in the order focus, routines and the timeline + list them. Move the ones you play most to the top. +

        +
          + {tracked.map((g, i) => ( +
        1. +
        2. + ))} +
        + {saved && ( + + )} +
        + )} +
        +

        Timeline design

        +

        + The new look puts a name column beside each bar. The classic look + draws filled bars with the title inside, grouped under each game. +

        +
        + {(["new", "classic"] as const).map((look) => ( + + ))} +
        +
        +
        +

        Focus bar

        +

        + The game pills that narrow a view to one game. A view without them + always shows every game. +

        + {(["checklist", "timeline"] as const).map((view) => ( + + ))} +
        +
        +

        Checklist

        +
        + + +
        +

        + Focusing a game opens its routines automatically. Pins on the Routines + tab decide which routines appear on your Checklist. +

        +
        +
        + ); +} diff --git a/src/client/components/redesign/ArchiveDetail.tsx b/src/client/components/redesign/ArchiveDetail.tsx new file mode 100644 index 0000000..c10053d --- /dev/null +++ b/src/client/components/redesign/ArchiveDetail.tsx @@ -0,0 +1,143 @@ +import { useEffect, useRef } from "react"; +import { serverSuffix } from "../../state/reader/servers.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import { formatInZone } from "../../state/displayTime.ts"; +import { actionMeta, recordOf } from "../../state/reader/actions.ts"; +import type { ArchiveRecord, ReaderState } from "../../state/reader/schema.ts"; +import type { ActionHandler } from "./GameSettings.tsx"; + +/** An archive is a saved snapshot, including its clock and progress, not a live editor. */ +export function ArchiveDetail({ + record, + state, + onAction, + onClose, +}: { + record: ArchiveRecord; + state: ReaderState; + onAction: ActionHandler; + onClose: () => void; +}) { + const dialog = useRef(null); + const meta = useGameMeta()(record.event.game); + const account = state.accounts[record.accountId]; + const clock = account?.clocks.find((c) => c.id === record.clockRevision); + const progress = recordOf(record.progress); + const zone = + typeof state.prefs.timezone === "string" ? state.prefs.timezone : null; + const outcome = { + done: "Completed", + ignored: "Ignored", + unfinished: "Ended unfinished", + }[record.outcome]; + useEffect(() => { + dialog.current?.showModal(); + }, []); + return ( + +
        +

        + {meta.name} + {serverSuffix( + state, + record.event.game, + clock?.region ?? account?.region, + true, + )} +

        + +
        +

        {record.event.title}

        + {record.event.summary &&

        {record.event.summary}

        } +
        +
        +
        Outcome
        +
        + {outcome} · {formatInZone(record.outcomeAt, true, zone)} +
        +
        +
        +
        End recorded
        +
        + {record.resolvedEnd === null + ? "Unannounced" + : formatInZone( + new Date(record.resolvedEnd).toISOString(), + record.event.endPrecision === "exact", + zone, + )} +
        +
        +
        +
        Saved progress
        +
        + {record.days.length} check-ins + {typeof progress.effort === "string" ? ` · ${progress.effort}` : ""} +
        +
        +
        + {typeof progress.note === "string" && progress.note && ( +
        +

        Your note

        +

        {progress.note}

        +
        + )} + {record.days.length > 0 && ( +
        + Recorded check-ins +

        {record.days.join(" · ")}

        +
        + )} + {Object.keys(record.amounts).length > 0 && ( +
        + Recorded amounts +
        + {Object.entries(record.amounts).map(([period, raw]) => ( +
        +
        {period}
        +
        {String(recordOf(raw).value ?? raw)}
        +
        + ))} +
        +
        + )} + {record.event.sourceUrl && ( +

        + + Original source ↗ + +

        + )} +

        + Saved when this event entered your archive. Restoring keeps its + progress; an event that has already ended remains in history. +

        + +
        + ); +} diff --git a/src/client/components/redesign/ArchivePage.tsx b/src/client/components/redesign/ArchivePage.tsx new file mode 100644 index 0000000..e2f26ec --- /dev/null +++ b/src/client/components/redesign/ArchivePage.tsx @@ -0,0 +1,275 @@ +import { useState, type CSSProperties } from "react"; +import { serverSuffix } from "../../state/reader/servers.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import { formatInZone } from "../../state/displayTime.ts"; +import { actionMeta } from "../../state/reader/actions.ts"; +import type { ReaderState, ArchiveRecord } from "../../state/reader/schema.ts"; +import { pairKey } from "../../state/reader/schema.ts"; +import { ArchiveStory } from "./ArchiveStory.tsx"; +import type { ActionHandler } from "./GameSettings.tsx"; +import { DAY } from "../../../shared/time.ts"; + +export function ArchivePage({ + state, + onAction, + now, + onOpen, +}: { + state: ReaderState; + onAction: ActionHandler; + now: number; + onOpen: (record: ArchiveRecord) => void; +}) { + const [game, setGame] = useState(null), + [outcome, setOutcome] = useState("all"), + [history, setHistory] = useState(false), + meta = useGameMeta(); + const records = Object.values(state.archive).sort((a, b) => + b.outcomeAt.localeCompare(a.outcomeAt), + ); + const games = [...new Set(records.map((r) => r.event.game))], + zone = + typeof state.prefs.timezone === "string" ? state.prefs.timezone : null; + const shown = records.filter( + (r) => + (!game || r.event.game === game) && + (outcome === "all" || outcome === r.outcome), + ); + const start = + Math.floor( + Math.min(now, ...shown.map((r) => Date.parse(r.event.startsAt))) / DAY, + ) * DAY; + const end = Math.max( + start + DAY, + now, + ...shown.map((r) => r.resolvedEnd ?? Date.parse(r.outcomeAt)), + ); + const days = Math.ceil((end - start) / DAY), + trackWidth = Math.max(800, days * 16); + const summary = (items: ArchiveRecord[]) => + `${items.filter((r) => r.outcome === "done").length} of ${items.filter((r) => r.outcome !== "ignored").length} done · ${items.filter((r) => r.outcome === "unfinished").length} unfinished`; + const outcomeBar = (items: ArchiveRecord[]) => ( + + ); + const restore = (r: ArchiveRecord) => + void onAction({ + type: "restore", + accountId: r.accountId, + eventId: r.event.id, + ...actionMeta(), + }); + return ( +
        +
        +
        + + +
        +

        Recorded history · kept in this browser

        +
        + +
        +

        Archive

        +

        + Finished, ignored or ended. Kept in your save and off the Checklist + and Timeline. Open one to see it as it was. +

        +
        + {["all", "done", "unfinished", "ignored"].map((o) => ( + + ))} +
        +
        +
        + +
        + {!shown.length && ( +

        + {records.length + ? "No events match these filters." + : "No archived events yet. Completed and ignored events will appear here."} +

        + )} + {history && shown.length > 0 ? ( +
        +
        +
        + Recorded events +
        + {Array.from({ length: Math.ceil(days / 7) }, (_, i) => ( + + {formatInZone(start + i * 7 * DAY, false, zone).replace( + / \d{4}$/, + "", + )} + + ))} +
        +
        + {shown.map((r) => ( +
        + +
        + +
        +
        + ))} +
        +
        + ) : ( + games + .filter((g) => !game || game === g) + .map((g) => { + const own = shown.filter((r) => r.event.game === g); + return ( + own.length > 0 && ( +
        +

        {meta(g).name}

        +
        +
        +

        Saved events

        + {summary(own)} + {outcomeBar(own)} +
        + {own.map((r) => ( +
        + + +
        + ))} +
        +
        + ) + ); + }) + )} +

        + History from before this redesign may be incomplete. Unknown end + dates stay unknown; earlier progress is preserved. +

        +
        +
        +
        + ); +} diff --git a/src/client/components/redesign/ArchiveStory.tsx b/src/client/components/redesign/ArchiveStory.tsx new file mode 100644 index 0000000..dccbbd0 --- /dev/null +++ b/src/client/components/redesign/ArchiveStory.tsx @@ -0,0 +1,97 @@ +import { archiveStats } from "../../state/reader/archiveStats.ts"; +import type { ArchiveRecord } from "../../state/reader/schema.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import type { CSSProperties } from "react"; +export function ArchiveStory({ + records, + now, + onGame, + game, +}: { + records: ArchiveRecord[]; + now: number; + onGame?: (game: string | null) => void; + game?: string | null; +}) { + const meta = useGameMeta(); + const s = archiveStats(records, now); + return ( +
        +

        Your season, in a few numbers

        +

        + Your recorded history ·{" "} + {records.filter((r) => r.outcome === "done").length} completed,{" "} + {records.filter((r) => r.outcome === "ignored").length} set aside. A + record of your saved events. +

        +
        +
        + + {s.monthly.rate === null ? "—" : s.monthly.rate + "%"} + + completed this month + + {s.overall.rate === null + ? "No eligible history yet" + : s.overall.rate + "% overall"} + +
        +
        + {s.currentRun} + completion run + Best: {s.bestRun} +
        +
        + {s.checkIns} + event check-ins + Summed per event and server +
        +
        + {s.unfinished} + ended unfinished + Ignored events excluded from completion rates +
        +
        + {Object.entries(s.games).map(([id, g]) => { + const total = g.done + g.ignored + g.unfinished; + return ( + + ); + })} +

        + Recorded archive history only. Ignored events are excluded from + completion rates. Select a game to filter the records below. +

        + {s.shortfalls.map((r) => ( +

        + {r.title}: {r.needed} check-ins short of your recorded goal. +

        + ))} +
        + ); +} diff --git a/src/client/components/redesign/BackupPage.tsx b/src/client/components/redesign/BackupPage.tsx new file mode 100644 index 0000000..63a6ec5 --- /dev/null +++ b/src/client/components/redesign/BackupPage.tsx @@ -0,0 +1,390 @@ +import { withReaderLock } from "../../state/reader/lock.ts"; +import { mergeReaderData } from "../../state/reader/import.ts"; +import { useState } from "react"; +import { + backupTier, + deleteManualBackup, + MANUAL_BACKUP_LIMIT, + readBackups, + readManualBackups, + saveManualBackup, + type BackupSnapshot, +} from "../../state/reader/backups.ts"; +import { prepareRestore, undoRestore } from "../../state/reader/restore.ts"; +import { + buildReaderExport, + parseReaderExport, +} from "../../state/reader/export.ts"; +import { readLegacy } from "../../state/reader/store.ts"; +import { triggerDownload } from "../../state/export.ts"; +import { formatInZone } from "../../state/displayTime.ts"; +import type { ReaderState } from "../../state/reader/schema.ts"; +export function BackupPage({ state }: { state: ReaderState }) { + const [backups] = useState(() => readBackups(localStorage)); + const [manual, setManual] = useState(() => readManualBackups(localStorage)); + const [confirming, setConfirming] = useState(null); + const [selected, setSelected] = useState(null); + const [message, setMessage] = useState(""); + const zone = + typeof state.prefs.timezone === "string" ? state.prefs.timezone : null; + const restore = async (payload: unknown) => { + const result = await withReaderLock(() => + prepareRestore(localStorage, payload, new Date().toISOString()), + ).catch(() => ({ ok: false as const, reason: "unavailable" })); + if (result.ok) location.reload(); + else + setMessage( + `Restore could not be completed (${result.reason}). Your current data is still available; export a copy before trying again.`, + ); + }; + const importProgress = async (file: File) => { + try { + const parsed = parseReaderExport(JSON.parse(await file.text())); + if (!parsed) { + setMessage("This is not a supported Event Clock export."); + return; + } + const merged = mergeReaderData(state, parsed); + setSelected({ + id: "progress-import", + at: new Date().toISOString(), + payload: buildReaderExport( + merged, + readLegacy(localStorage), + new Date().toISOString(), + ), + }); + setMessage( + "Progress will be merged. Your appearance and other local preferences will be kept.", + ); + } catch { + setMessage("This file could not be read as an Event Clock export."); + } + }; + const preview = selected ? parseReaderExport(selected.payload) : null; + const now = Date.now(); + const summary = (payload: Record) => { + const r = parseReaderExport(payload); + if (!r) return "Unreadable backup"; + // The games that snapshot was tracking: an account exists for every game. + const hidden = Array.isArray(r.prefs.hiddenGames) + ? (r.prefs.hiddenGames as unknown[]) + : []; + const accounts = Object.values(r.accounts).filter( + (a) => + !a.retiredAt && + !hidden.includes(a.game) && + (a.intent.events || Object.values(a.routines).some((p) => p.followed)), + ); + const games = new Set(accounts.map((a) => a.game)).size; + const routines = accounts.reduce( + (n, a) => n + Object.values(a.routines).filter((p) => p.followed).length, + 0, + ); + const archived = Object.keys(r.archive).length; + return `${games} game${games === 1 ? "" : "s"} · ${routines} routine${routines === 1 ? "" : "s"} · ${archived} archived event${archived === 1 ? "" : "s"}`; + }; + const tiers = [ + { id: "daily", title: "Daily", span: "Last 7 days" }, + { + id: "weekly", + title: "Weekly", + span: "Older snapshots, folded into 4 weeks", + }, + { + id: "monthly", + title: "Monthly", + span: "Older snapshots, folded into 6 months", + }, + ] as const; + const fileButton = (label: string, onFile: (file: File) => void) => ( + + ); + const importAll = async (file: File) => { + try { + const payload: unknown = JSON.parse(await file.text()); + const reader = parseReaderExport(payload); + if (!reader) { + setMessage("This file is not a supported Event Clock backup."); + return; + } + if ((payload as { scope?: unknown }).scope === "progress") { + await importProgress(file); + return; + } + setSelected({ + id: "import", + at: new Date().toISOString(), + payload: payload as Record, + }); + } catch { + setMessage("This file could not be read as JSON."); + } + }; + return ( +
        +

        Backup

        +

        + Automatic snapshots keep a recent history of your data in this browser. + The latest snapshot in each period is kept as older days fold into + weeks, then months. +

        +
        +
        +
        7 days
        +
        Daily snapshots
        +
        +
        +
        4 weeks
        +
        Weekly snapshots
        +
        +
        +
        6 months
        +
        Monthly snapshots
        +
        +
        +

        + Backups are created while you use the app. Days you don't open it have + no new snapshot. These copies live in this browser, and clearing its + storage removes them; export a file to keep a copy elsewhere. +

        +
        + + + {fileButton("Import all data", (file) => void importAll(file))} +
        +

        + Progress only, keeping this browser's settings: + + + +

        +

        + {message} +

        + {selected && preview && ( +
        +

        Backup from {formatInZone(selected.at, true, zone)}

        +

        {summary(selected.payload)}

        +

        + This replaces your current data. A recovery copy is saved first. +

        +
        + + +
        +
        + )} +
        +
        +

        Your backups

        + + {manual.length} of {MANUAL_BACKUP_LIMIT} · Kept until you delete + them + +
        + {manual.length === 0 ? ( +

        + Back up now saves a copy here. Automatic history never replaces it. +

        + ) : ( + manual.map((b) => ( +
        +
        + + {summary(b.payload)} +
        + {confirming === b.id ? ( +
        + Delete this backup? + + +
        + ) : ( +
        + + +
        + )} +
        + )) + )} +
        +

        Automatic history

        + {backups.length === 0 ? ( +

        + No saved history yet. A backup is created when you use the app. +

        + ) : ( + tiers.map((tier) => { + const rows = backups.filter((b) => backupTier(b.at, now) === tier.id); + if (!rows.length) return null; + return ( +
        +
        +

        {tier.title}

        + + {rows.length} snapshot{rows.length === 1 ? "" : "s"} ·{" "} + {tier.span} + +
        + {rows.map((b) => ( +
        +
        + + {summary(b.payload)} +
        + +
        + ))} +
        + ); + }) + )} +
        + ); +} diff --git a/src/client/components/redesign/ChangelogPage.tsx b/src/client/components/redesign/ChangelogPage.tsx new file mode 100644 index 0000000..e7e1ad5 --- /dev/null +++ b/src/client/components/redesign/ChangelogPage.tsx @@ -0,0 +1,119 @@ +import { + commitUrl, + compareUrl, + KIND_LABEL, + specUrl, + type Entry, + type Note, +} from "../../../shared/changelog.ts"; +import { asOfLabel } from "../../../shared/routines.ts"; +import { REPO_URL } from "../Colophon.tsx"; + +const KIND_ORDER: readonly Entry["kind"][] = ["feature", "games", "fix"]; + +/** + * The Round 6 B changelog page (`.changelog-page .rel`): a dated, typed entry + * per release note, its summary, and the commits it covers as a list, with the + * commit range and design notes beneath. Loose commits follow by month. + */ +export function ChangelogPage({ + notes, + entries, +}: { + notes: readonly Note[]; + entries: readonly Entry[]; +}) { + const loose = entries.filter((e) => e.note === undefined); + const months = [...new Set(loose.map((e) => e.date.slice(0, 7)))]; + return ( +
        +

        Changelog

        +

        + Changes and release notes for Event Clock, summarised from the project's + history and design notes. Each links to the commits it covers. +

        + {notes.map((note) => { + const covered = entries.filter((e) => e.note === note.id); + const kinds = KIND_ORDER.filter((k) => + covered.some((e) => e.kind === k), + ); + return ( + + ); + })} + {months.length > 0 && ( +
        +

        Other changes

        + {months.map((month) => ( +
        +
        + {asOfLabel(`${month}-01`).replace(/^1 /, "")} +
        +
          + {loose + .filter((e) => e.date.startsWith(month)) + .map((entry) => ( + + ))} +
        +
        + ))} +
        + )} +
        + ); +} + +function EntryItem({ entry }: { entry: Entry }) { + return ( +
      • + {entry.title} + + {KIND_LABEL[entry.kind]} + + {entry.ref} + + +
      • + ); +} diff --git a/src/client/components/redesign/ChecklistEvent.tsx b/src/client/components/redesign/ChecklistEvent.tsx new file mode 100644 index 0000000..a80261a --- /dev/null +++ b/src/client/components/redesign/ChecklistEvent.tsx @@ -0,0 +1,247 @@ +import { useContext, type CSSProperties } from "react"; +import type { ReaderEventRow } from "../../state/reader/eventRows.ts"; +import { useGameMeta } from "../../state/gameMeta.tsx"; +import { DisplayTimeContext, formatInZone } from "../../state/displayTime.ts"; +import { formatRemaining } from "../../../shared/time.ts"; +import { Meter, URGENCY_COLOR } from "../Meter.tsx"; +import { STEPS } from "../Legend.tsx"; +import { isCustomEventId } from "../../../shared/custom.ts"; + +/** Compact's column headings: the Round 6 B ledger (`table.led`) as a grid over list rows. */ +/** The compact ledger's column labels; events not started yet count down to their start. */ +export function LedgerColumns({ upcoming = false }: { upcoming?: boolean }) { + return ( + + ); +} +/** + * Live's legend, said once under "Running now": what the bars and their + * colours mean. Colours come from the same `URGENCY_COLOR`, so they follow + * the reader's urgency colours. + */ +export function UrgencyLegend() { + return ( +
        +

        + + bars = time left +

        +

        + colour = ends in + {STEPS.map((step) => ( + +