The data model, PRD F17, README and DESIGN described one global chores switch and a checkbox per game for built-ins. Adds the spec and plan, and ticks the TODO item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.3 KiB
Per-game switches for chores and built-in tasks — design
Date: 2026-10-14. Status: approved (repository owner, 2026-10-14). TODO.md item 2.
Problem
What you see governs two things the app supplies on a game's behalf, and governs them two different ways:
- Daily chores — one global switch,
prefs.showChores(default on). No per-game control: a reader who plays Genshin daily and HSR casually cannot drop only HSR's chore. - Built-in weekly tasks — per-game opt-in only,
prefs.routineGames(absent = none), drawn as one checkbox per game that has any. No global answer, and the checkbox list grows with every game and every built-in we add.
Wanted: one pattern for both — a global switch, and per-game exceptions reachable without a checkbox per game in the group.
Decisions
-
One rule: a global answer and two exception lists. For each game, on-list → shown; off-list → hidden; otherwise the global answer. Pure, in
src/shared/perGame.ts:type PerGame = { all: boolean; on?: readonly string[] | undefined; off?: readonly string[] | undefined }; shownFor(rule: PerGame, game: string): boolean setFor(rule: PerGame, game: string, value: boolean): { on: string[]; off: string[] }setForremoves the game from both lists, then adds it to the one matchingvalueonly ifvaluediffers fromall— an exception is a difference, not a restatement. A game in both lists (only reachable by hand-edited storage) resolves to the on-list. -
Storage — nothing migrated, every current answer keeps its meaning.
Pref Meaning Absent showChores(exists)global chores answer true, as todaychoresOn?,choresOff?(new)games that differ none showRoutines?(new)global built-ins answer unanswered → treated as off (opt-in kept) routineGames?(exists)built-ins on for these games — now the on-exception list none routinesOff?(new)built-ins off for these games none routinesAsked?(exists)games the strip has asked about none A reader with
routineGames: ["zzz"]and nothing else sees exactly what they saw before: global off, ZZZ on. An older tab still running the cached shell readsroutineGamesand shows the same games. Rejected: one override map per feature with a load-time migration (an older tab would lose the reader's built-ins); one exception list whose meaning inverts when the global flips (flipping one switch would silently redefine every exception). -
Exceptions survive a global flip. Changing the global switch rewrites nothing else; a game the reader singled out stays singled out. Discards nothing, like every switch in this group.
-
The strip's question still answers the same setting.
routineToAskasks only whileshowRoutinesis absent (the reader has never answered globally), about a game on screen that is effectively off and not yet inroutinesAsked. Yes writesroutineGamesviasetFor(which, with the global off, adds the game) and recordsroutinesAsked; No thanks recordsroutinesAskedonly. Once the reader answers the global switch, the strip stops asking — they have stated an answer for every game at once. A chip flip in settings also records the game inroutinesAsked, as the per-game checkbox did. -
UI — a disclosure under each global switch (owner's choice of the three placements offered). In What you see:
Show each game's own daily choreskeeps its checkbox. Under it, a faint line states the exceptions and a Choose games text button opens a wrap of game chips (closed by default, per-visit state like show all N).Show built-in weekly tasksis a new global checkbox, offered only when some game on the list has built-ins, replacing the per-game checkboxes. Same line and disclosure; its hint keeps the source-and-date disclosure per game in each chip'stitle.- The line reads off the rule: global on →
Every game/All but Genshin Impact/All but 3 games; global off →No game/Only Zenless Zone Zero/Only 3 games. Derived at render, never stored. - Chips are
<button aria-pressed>in the game-focus chip style (hue when on, faint when off), in the reader's game order, wrapping at phone width. Chore chips list the reader's enabled tracked games (a hidden game draws no chore anyway; a reader's ownmygame:lanes have none). Built-in chips list every game on the list that has built-ins, as the checkboxes did. - The strip's Edit panel keeps its single
Daily choresswitch, bound to the global answer.
-
Consumers read the rule, not the raw pref.
dailyGroups'showChoresparameter acceptsboolean | ((lane) => boolean); App passes(lane) => shownFor(choreRule(prefs), lane).Dailiestakes the same.activeRoutines(rule, copiedFrom)androutineToAsk(visible, rule, asked, answered)take thePerGamerule.Controls' weekly/monthly switch availability uses the effective rule.usePrefs.tsexportschoreRule(prefs)androutineRule(prefs)so nothing assembles a rule from fields by hand.
Out of scope
Monthly built-ins (none exist); per-game control of the reader's own tasks (they are already per game by construction); changing the Edit panel.
Testing
perGame: the truth table ofshownFor;setForwrites only differences and clears the other list; both-lists resolves on.- Carry-over: prefs with only
showChores/routineGamesgive the same chores and built-ins as before. restorePrefsValue: new arrays sanitised likeroutineGames,showRoutineskept only if boolean, absent stays absent.routineToAsk: no question onceshowRoutinesis set; skips effectively-on games.dailyGroups: a per-lane predicate drops one game's chore and keeps the other's.Controls: the exception line for each case; no per-game checkboxes; the built-in switch readsshowRoutines; chips render only when opened (static render: closed).- Browser: the group opened, both disclosures expanded, at 390px and desktop width, both themes.
Docs
docs/DATA-MODEL.md prefs keys, docs/PRD.md F17 built-ins paragraph, README.md chores
sentence, docs/DESIGN.md component entry for the disclosure, TODO.md ticked, changelog entry.