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
+61
View File
@@ -0,0 +1,61 @@
import { expect, test } from "bun:test";
import { mintAccountId } from "../src/client/state/reader/ids.ts";
import { actionMeta } from "../src/client/state/reader/actions.ts";
import { initializeReader } from "../src/client/state/reader/store.ts";
import { saveBackup, readBackups } from "../src/client/state/reader/backups.ts";
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
import { READER_KEYS } from "../src/client/state/reader/schema.ts";
import { MemoryStorage } from "./helpers/reader.ts";
const at = "2026-10-16T12:00:00.000Z";
function withoutRandomUUID(run: () => void) {
const before = Object.getOwnPropertyDescriptor(crypto, "randomUUID");
Object.defineProperty(crypto, "randomUUID", {
configurable: true,
value: undefined,
});
try {
run();
} finally {
if (before) Object.defineProperty(crypto, "randomUUID", before);
else Reflect.deleteProperty(crypto, "randomUUID");
}
}
test("HTTP first-visit migration succeeds without randomUUID and retains legacy bytes", () => {
withoutRandomUUID(() => {
const storage = new MemoryStorage();
const raw = JSON.stringify({
"genshin:example:2026-10-01": { note: "Keep my progress", at },
});
storage.setItem("gacha-tracker:v1:progress", raw);
const result = initializeReader(storage, ["genshin"], at, mintAccountId);
expect(result.ok).toBe(true);
expect(storage.getItem("gacha-tracker:v1:progress")).toBe(raw);
expect(storage.getItem(READER_KEYS.reader)).not.toBeNull();
if (result.ok)
expect(
Object.values(result.state.data)[0]!.progress[
"genshin:example:2026-10-01"
],
).toHaveProperty("note", "Keep my progress");
});
});
test("HTTP edits receive distinct operation IDs without randomUUID", () => {
withoutRandomUUID(() => {
const first = actionMeta(),
second = actionMeta();
expect(first.opId).toMatch(/^[a-zA-Z0-9-]+$/);
expect(first.opId).not.toBe(second.opId);
});
});
test("HTTP local backups save normally without randomUUID", () => {
withoutRandomUUID(() => {
const storage = new MemoryStorage();
const state = migrateLegacy({}, ["genshin"], at, () => "account:one");
expect(saveBackup(storage, state, at)).toEqual({ ok: true });
expect(readBackups(storage)).toHaveLength(1);
});
});