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:
co-authored by
Claude Opus 5.5
parent
ddd9673f86
commit
50c6e91b00
@@ -0,0 +1,120 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
|
||||
import { ReaderStateSchema } from "../src/client/state/reader/schema.ts";
|
||||
const at = "2026-10-16T12:00:00.000Z";
|
||||
const raw = {
|
||||
"gacha-tracker:v1:prefs": JSON.stringify({
|
||||
region: "europe",
|
||||
view: "timeline",
|
||||
theme: "light",
|
||||
hiddenGames: ["hsr"],
|
||||
gameOrder: ["genshin", "hsr"],
|
||||
showRoutines: true,
|
||||
routinesHidden: ["routine:genshin:weekly-bosses"],
|
||||
}),
|
||||
"gacha-tracker:v1:progress": JSON.stringify({
|
||||
"genshin:test:2026-10-01": { status: "done", at, goal: { future: true } },
|
||||
"foreign-id": { note: "keep", at },
|
||||
}),
|
||||
"gacha-tracker:v1:daily": JSON.stringify({
|
||||
"dailies:genshin": { days: ["2026-10-15"], at },
|
||||
}),
|
||||
"gacha-tracker:v1:tasks": JSON.stringify({
|
||||
"mytask:abcdef": {
|
||||
id: "mytask:abcdef",
|
||||
game: "genshin",
|
||||
title: "Mine",
|
||||
period: { unit: "week", startsOn: 1 },
|
||||
measure: { kind: "days", required: 1 },
|
||||
at,
|
||||
updatedAt: at,
|
||||
future: 42,
|
||||
},
|
||||
}),
|
||||
"gacha-tracker:v1:amounts": JSON.stringify({
|
||||
"mytask:abcdef": { "2026-10-12": { value: 7, at } },
|
||||
}),
|
||||
};
|
||||
test("migration preserves view, region, game choices and exact history keys", () => {
|
||||
let n = 0;
|
||||
const state = migrateLegacy(
|
||||
raw,
|
||||
["genshin", "hsr"],
|
||||
at,
|
||||
() => `account:test${++n}`,
|
||||
);
|
||||
expect(ReaderStateSchema.safeParse(state).success).toBe(true);
|
||||
expect(state.prefs.view).toBe("timeline");
|
||||
expect(state.prefs.hiddenGames).toEqual(["hsr"]);
|
||||
const account = Object.values(state.accounts).find(
|
||||
(a) => a.game === "genshin",
|
||||
)!;
|
||||
expect(account.region).toBe("europe");
|
||||
const data = state.data[account.id]!;
|
||||
expect(data.progress["genshin:test:2026-10-01"]).toEqual({
|
||||
status: "done",
|
||||
at,
|
||||
goal: { future: true },
|
||||
});
|
||||
expect(data.daily[account.clocks[0]!.id]?.["dailies:genshin"]).toEqual({
|
||||
days: ["2026-10-15"],
|
||||
at,
|
||||
});
|
||||
expect(data.amounts[account.clocks[0]!.id]?.["mytask:abcdef"]).toEqual({
|
||||
"2026-10-12": { value: 7, at },
|
||||
});
|
||||
expect(account.routines["routine:genshin:weekly-bosses"]?.followed).toBe(
|
||||
false,
|
||||
);
|
||||
expect(state.tasks["mytask:abcdef"]).toHaveProperty("future", 42);
|
||||
expect(raw["gacha-tracker:v1:progress"]).toContain("foreign-id");
|
||||
});
|
||||
test("completion-only installs and games absent from feed retain their state", () => {
|
||||
const state = migrateLegacy(
|
||||
{
|
||||
"gacha-tracker:v1:completions": JSON.stringify({
|
||||
"zzz:old:2026-01-01": { at },
|
||||
}),
|
||||
},
|
||||
[],
|
||||
at,
|
||||
() => "account:old",
|
||||
);
|
||||
const account = Object.values(state.accounts)[0]!;
|
||||
expect(account.game).toBe("zzz");
|
||||
expect(state.data[account.id]?.progress["zzz:old:2026-01-01"]).toEqual({
|
||||
status: "done",
|
||||
at,
|
||||
});
|
||||
});
|
||||
test("malformed blobs cannot hide valid state in other legacy keys", () => {
|
||||
const state = migrateLegacy(
|
||||
{ ...raw, "gacha-tracker:v1:tasks": "broken" },
|
||||
["genshin"],
|
||||
at,
|
||||
() => "account:one",
|
||||
);
|
||||
expect(state.tasks).toEqual({});
|
||||
expect(state.data["account:one"]?.progress).toHaveProperty(
|
||||
"genshin:test:2026-10-01",
|
||||
);
|
||||
});
|
||||
test("new catalogue games remain switched off for a returning reader", () => {
|
||||
let n = 0;
|
||||
const s = migrateLegacy(
|
||||
{
|
||||
"gacha-tracker:v1:prefs": JSON.stringify({
|
||||
knownGames: ["genshin"],
|
||||
hiddenGames: [],
|
||||
onboarded: true,
|
||||
}),
|
||||
},
|
||||
["genshin", "hsr"],
|
||||
at,
|
||||
() => `account:new${++n}`,
|
||||
);
|
||||
expect(s.prefs.hiddenGames).toEqual(["hsr"]);
|
||||
expect(
|
||||
Object.values(s.accounts).find((a) => a.game === "hsr")?.intent.events,
|
||||
).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user