feat(custom): let readers add their own games and events

The interface for PRD F13. A game (name and lane colour) and events against it,
or against a tracked game when a source missed one, managed from the settings
panel and from the event's own detail sheet.

Two things the forms are careful about:

- "I don't know when it ends" is an offered answer, not a blank field. A
  mandatory end date would push the reader into inventing one, which is the
  failure the parsers are forbidden from committing — it would just be the
  reader committing it instead. An unknown end behaves as it does for a scraped
  event: no countdown, no checklist.
- A hand-entered date is never dressed up as a source's. The row carries a
  "yours" chip, the sheet says so under the title, and the Source link is absent
  rather than dead.

Deleting a game is refused while it still holds events, and says how many.
Deleting an event leaves the marks and ticks attached to it alone.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-17 18:24:09 +02:00
co-authored by Claude Opus 5
parent d046758671
commit c87ea4c602
8 changed files with 786 additions and 5 deletions
+26 -1
View File
@@ -76,15 +76,17 @@ re-verify a sample against the live page afterward.
```
src/shared/ schema.ts (the contract), time.ts, daily.ts, effort.ts, games.ts, feed.ts
custom.ts — reader-authored games and events, and their key spaces
src/ingest/ html.ts, dates.ts (nine formats), merge.ts, sanitize.ts, robots.ts, snapshots.ts
parsers/ game8.ts, wikigg.ts, akwiki.ts — keyed by SITE, not game
adapters/ index.ts — SOURCES registry binding url+game+parser, and the sanitize seam
src/client/ React app, service worker, manifest
state/ progress, daily log, ignores, prefs, sort — all localStorage
useCustom.ts — the reader's own games and events (PRD F13)
lens.ts — who sees which rows (focus, outstanding, next-to-expire); pure
scripts/ build-feed.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
serve.ts static server + /api/health
test/ 366 tests
test/ 396 tests
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
snapshots/ current page per source, rewritten by refresh — see its README
```
@@ -229,6 +231,29 @@ event's length. It adds **no schema field**, so the feed contract is untouched.
recording agreement would freeze today's guess and stop a better parser from ever reaching that
event. Neither control ever deletes a mark or a logged day, so both are reversible.
## Events the reader entered themselves
No adapter list covers a ten-game player, so a reader can define a game and type in events
(PRD F13, `src/shared/custom.ts`, `src/client/state/useCustom.ts`). They join the same lists,
timeline, sort, filters, progress, ignore and daily stores as scraped events. Four rules:
- **Their ids live in their own spaces**: `mygame:<slug>` and `myevent:<random>`. Never
`${game}:${slug}:${date}` — a reader can type a scraped event's exact title and date, and that
collision would silently share one completion mark and one streak between two events. Random also
means renaming their own event never moves its id. `dailies`, `mygame` and `myevent` are reserved
first segments and **none may ever become a `GameId`**; a test pins this.
- **Nothing they type enters the ingest pipeline.** `sanitize.ts` and `merge.ts` are for pages we do
not control. Their events are not fetched, parsed, merged, scored or quarantined.
- **A hand-entered date is never attributed to a source.** No `sourceUrl`, no source link, and the
row and detail sheet both say it is theirs. `"I don't know when it ends"` is an offered answer, for
the same reason the parsers are forbidden from guessing one.
- **They are in the export.** These exist in one browser and nowhere else, so an export without them
is a lossy backup. Import merges by id and never removes.
A lane may now be a game the reader invented, so `gameMeta` is a context resolver (`metaFor`, pure
and total) rather than a direct lookup — a lane can outlive its game when an import carries an event
whose game did not come with it.
## Conventions
- **Zod schemas are the single source of truth for types.** Derive with `z.infer<>`; never