docs: cover the refresh pipeline, sanitisation and dailies

The status sections claimed the feed was generated from fixtures and the
scheduler unbuilt, which stopped being true. Also documents the
sanitisation stage and the two new key spaces — `dailies:<game>` and
game-day keys — beside the existing warning about event IDs, since they
carry the same "no server-side recovery" property.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 21:19:08 +02:00
co-authored by Claude Opus 5
parent 8e42afdd7a
commit 02863ed008
4 changed files with 243 additions and 42 deletions
+41 -4
View File
@@ -197,8 +197,9 @@ Namespaced, versioned, and small. Nothing here ever goes to the server.
```ts
"gacha-tracker:v1:progress" // { [eventId]: { status?, effort?, note?, at } }
"gacha-tracker:v1:daily" // { [id]: { days: ["2026-08-15", ...], at } }
"gacha-tracker:v1:ignored" // { [eventId]: { at } } — "stop showing me this"
"gacha-tracker:v1:prefs" // { region, hiddenGames[], showCompleted, showIgnored,
"gacha-tracker:v1:prefs" // { region, hiddenGames[], sort, showCompleted, showIgnored,
// regionConfirmed, onboarded }
"gacha-tracker:v1:completions" // SUPERSEDED — read once to migrate, never written
```
@@ -225,6 +226,36 @@ about it would be fabricating the reader's own input.
Ignores stay in a separate store because they mean something different: a done event is dimmed and
still counted, an ignored one disappears from both views.
### Daily checklists
Some events are not one job with a deadline but twenty small jobs on twenty separate deadlines, and
a missed day is gone whatever you do afterwards. `daily` records which game-days the reader ticked
off, keyed by:
- an **event ID**, for a repeating event in the feed (`src/shared/daily.ts` § `isDaily`), or
- **`dailies:<game>`**, the standing per-game chore — commissions, sanity, daily training. No source
publishes these, so they are a fixed client-side list, never feed data. The two-segment shape
cannot collide with an event ID, which is always `game:slug:date`.
Day keys are `YYYY-MM-DD` in **game-day space, not UTC**: gacha servers roll the day at 04:00 local
server time, so a player finishing at 02:00 is still on the previous day's dailies, and the key is
computed against the reader's chosen region (`RESET_HOUR_LOCAL`, `dayKey`). Keys sort
lexicographically, which is what "how many days are left" and streak counting rely on.
Three rules this store keeps, for the same reason the rest of the client does — nothing else holds
a copy:
- **A tick is never removed except by the reader.** Ticks that fall outside the window the feed now
claims still count; a source quietly moving a date must not erase a fortnight's streak.
- **An unannounced end yields no checklist**, not a checklist of guessed length. `dailyDays` returns
null when `endsAt` is null, and the UI says how many days are ticked instead of how many are left.
- **Past days stay editable.** People log in and tick up later, and a checklist that cannot be
corrected stops being trusted after the first mistake.
Dailiness is derived from the published event — `type: "login"`, or wording like "daily",
"check-in", "7-day" in the title or summary — and never from a game's habits or an event's length.
It adds no schema field, so nothing about the feed contract or the event ID changes.
### Migration from `completions`
`completions` used membership to mean "done", which cannot express "started". `progress` replaces it
@@ -254,13 +285,19 @@ old key.
"genshin:windblume-festival:2026-03-14": { "status": "done", "at": "..." },
"hsr:garden-of-plenty:2026-08-14": { "status": "doing", "effort": "grind", "at": "..." }
},
"daily": {
"endfield:bedazzling-dawnstar:2026-08-12": { "days": ["2026-08-13", "2026-08-14"], "at": "..." },
"dailies:genshin": { "days": ["2026-08-14", "2026-08-15"], "at": "..." }
},
"ignored": { "zzz:some-event-i-skip:2026-08-19": { "at": "..." } },
"prefs": { "region": "europe", "hiddenGames": [], "onboarded": true }
"prefs": { "region": "europe", "hiddenGames": [], "sort": "ending", "onboarded": true }
}
```
Import **merges** both sets: a mark present in either the file or the current device survives, and
import never removes one. Losing a user's marks to a bad import is unrecoverable, so the merge is
Import **merges** every set: a mark present in either the file or the current device survives, and
import never removes one. `daily` merges as a **union of days per ID** — every day either side
recorded is a day the reader actually played. An export written before daily checklists existed
simply has no `daily` key, which is not an error. Losing a user's marks to a bad import is unrecoverable, so the merge is
deliberately one-directional. A file whose `format` is unrecognised is refused outright rather than
partly applied.