feat: the redesigned Event Clock

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-18 01:55:16 +02:00
co-authored by Claude Opus 5.5
parent ddd9673f86
commit 50c6e91b00
284 changed files with 45100 additions and 1628 deletions
+49
View File
@@ -0,0 +1,49 @@
import { expect, test } from "bun:test";
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
import {
buildReaderExport,
parseReaderExport,
} from "../src/client/state/reader/export.ts";
const at = "2026-10-16T12:00:00.000Z";
test("full reader export round-trips unknown fields without including backup history", () => {
const reader = {
...migrateLegacy({}, ["genshin"], at, () => "account:one"),
future: { keep: true },
};
const value = buildReaderExport(
reader,
{
"gacha-tracker:v1:tasks": '{"unreadable":true}',
"gacha-tracker:v2:backups": "do-not-recurse",
},
at,
);
expect(parseReaderExport(value)).toEqual(reader);
expect(JSON.stringify(value)).not.toContain("do-not-recurse");
});
test("unknown versions and invalid known fields cannot be restored", () => {
const value = buildReaderExport(
migrateLegacy({}, [], at, () => "account:one"),
{},
at,
);
expect(parseReaderExport({ ...value, version: 999 })).toBeNull();
expect(
parseReaderExport({ ...value, reader: { version: 2, accounts: [] } }),
).toBeNull();
});
test("existing version-one exports remain convertible", () => {
const reader = parseReaderExport({
format: "gacha-tracker-export",
version: 1,
exportedAt: at,
prefs: { region: "asia" },
progress: { "genshin:a:2026-01-01": { status: "done", at } },
daily: {},
ignored: {},
customGames: {},
customEvents: {},
});
expect(reader?.version).toBe(2);
expect(Object.values(reader!.accounts)[0]?.region).toBe("asia");
});