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
+58
View File
@@ -0,0 +1,58 @@
import { expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { runRefresh } from "../scripts/refresh-sources.ts";
import { SnapshotStore } from "../src/ingest/snapshots.ts";
import { CODE_SOURCES } from "../src/ingest/codes/registry.ts";
import { sampleEvent } from "./helpers/reader.ts";
test("event and code sources share host spacing and snapshot gates", async () => {
const root = await mkdtemp("/tmp/codes-refresh-");
try {
const html = await Bun.file(
"fixtures/genshin/game8-codes-2026-10-16.html",
).text();
const slept: number[] = [];
const code = CODE_SOURCES[0]!;
const summary = await runRefresh({
adapters: [
{ ...code, id: "event-test", parse: () => [sampleEvent] },
code,
],
store: new SnapshotStore(root),
robots: {
allows: async () => ({ allowed: true, reason: "test permission" }),
},
fetchImpl: async () => new Response(html, { status: 200 }),
userAgent: "event-clock-test",
now: () => new Date("2026-10-16T12:00:00Z"),
sleep: async (n) => {
slept.push(n);
},
dryRun: false,
only: null,
force: false,
timeoutMs: 20000,
log: () => {},
rebuildFeed: null,
});
expect(summary.changed).toBe(2);
expect(slept).toEqual([2000]);
expect(summary.outcomes[1]?.eventCount).toBe(10);
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("the CLI recognises the code source during a request-free dry run", () => {
const p = Bun.spawnSync(
[
"bun",
"run",
"scripts/refresh-sources.ts",
"--dry-run",
"--only",
"genshin-game8-codes",
],
{ stdout: "pipe", stderr: "pipe" },
);
expect(p.exitCode).toBe(0);
expect(new TextDecoder().decode(p.stdout)).toContain("genshin-game8-codes");
});