Commit Graph
19 Commits
Author SHA1 Message Date
Lucas WintherandClaude Opus 5 ba39ad2bce feat(timeline): let the reader set the scale
One density cannot answer both questions the board is asked. A patch cycle is
six weeks, a login campaign runs for months, and 13px a day is a compromise
between "what am I in the middle of this week?" and "how do the next three
months line up?" that serves neither well.

A pair of controls steps through a ladder of day widths and the choice is
remembered, on the same argument as the view tabs: a reader who has said how
they want to read this should not say it again on the next load.

Two things it had to get right. Zooming holds the middle of the view still —
rescaling around the left edge of a three-month board throws away whatever
they had scrolled to, and re-opening at today would undo the scrolling that
got them there. And the dated ticks thin out as the scale shrinks, because a
week is 42px at the widest setting and the dates would sit on top of each
other; the gridlines stay weekly, since they carry the rhythm rather than the
reading.

The scale is stored as the measurement, not a step number, and read through
`snapDayWidth`. An export written against a different ladder then opens on
something close to what its reader chose, and a corrupt value opens on the
default rather than a board one pixel wide.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 04:13:31 +02:00
Lucas WintherandClaude Opus 5 d5dbe72e17 feat(prefs): a game we add arrives switched off
Adding a source is our decision, not the reader's. Eleven lanes became
fourteen this week, and for someone who plays two that is not a feature
arriving — it is their calendar filling with events they will never open,
which is the thing the first-run picker exists to prevent.

`knownGames` records every lane a reader has been offered; a lane missing from
it is new to them, so it is recorded and hidden on sight. The games chips in
settings list every lane, on or off, which is where they take one up.

The case that had to be right is the reader who installed before any of this
existed: they have no `knownGames` at all, and reading that as "has been
offered nothing" would switch off every game they already read. Absent means
unrecorded — the first pass records what is already on their screen and
changes nothing else. Lanes they invented are recorded but never hidden;
typing a game in is asking for it.

The decision is a pure function so this is provable rather than watched for.
One real cost, stated in the PRD rather than hidden: a reader whose game
finally arrives is not told, which makes the colophon roadmap matter more.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 04:05:23 +02:00
Lucas WintherandClaude Opus 5 7ab801f409 feat(holodori): track hololive Dreams from its own wiki
New holodoriwiki parser plus the holodori-holodoriwiki-events source,
giving the game its two live and upcoming events at exact precision.

holodori.wiki is Miraheze, so this is the same call bawiki makes and for
the same reason: robots.txt disallows /w/ and /*?action=, which closes the
API route and leaves the rendered /wiki/Events page as the surface * is
allowed. It serves our own User-Agent a 200, sets no Content-Signal and no
Crawl-delay for us, and the wiki is CC BY-SA 4.0.

What makes this source unusually good is that it states its timezone on
every cell — 08/17/2026 8:00PM (JST) — so it is the only wiki source here
publishing exact precision on both boundaries without a per-region timer.
parseSlashClockZone requires that zone rather than defaulting to UTC: a row
that ever loses it drops out instead of silently landing nine hours off,
and an abbreviation absent from the table is refused rather than guessed,
which is why the table holds only JST and not the plausible-looking
candidates that are not fixed offsets. The 12-hour clock gets its own
handling and its own tests, because 12PM and 12AM are the two readings a
naive parser gets wrong while still producing a valid-looking instant.

Inclusion is fenced by the Current Events <h2> and bounded by the next one.
That matters more than it looks: Past Events sits directly below with
identical columns, so a reader taking every wikitable would put the back
catalogue on the calendar with nothing to mark it. Rows are still checked
against ctx.now on top of the heading, because Current is maintained by
hand and goes stale before anyone moves a row down.

Two rows are deliberately missing. Beginner Mission runs Game Launch →
Unknown, and no start means no event ID — a permanent tutorial chore is not
what a calendar of deadlines is for. An Unknown end is kept as endsAt null,
and unlike bawiki this parser does not drop a started-but-undated row: the
heading has already said the event is running, which is the fact bawiki
lacks when reading an archive.

Every event title is still a red link — the wiki has no article for any of
them yet — so each points at ?action=edit&redlink=1, a create-page form on
a path this robots.txt disallows. Refusing a href with a query keeps those
out and falls back to the events page; the articles get linked when they
exist, with no change here.

holodori gets resetOffsets of UTC+9 across all three regions, evidenced
rather than assumed the way Arknights' is: the game launched worldwide
simultaneously on one service, every boundary is stated in JST, and
Training Support Missions ends at 3:59AM JST, one minute before a 04:00
local reset. Only the offset is overridden; the hour is the default.
Setting it now is free, because no reader has a day key for a game the app
has never shipped — the same override a year from now would re-label ticks
already logged.

Both boundaries and the exclusions are asserted against an independent
extraction off the fixture, computed through a timezone library rather than
by the parser's own arithmetic.

Several counts in AGENTS.md and PRD.md were already stale before this — the
Fate/Grand Order source did not update them — and are corrected to what the
tree now holds rather than to what it held yesterday.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 03:39:49 +02:00
Lucas WintherandClaude Opus 5 7804a08863 feat(prefs): ask which view to open on, and remember the answer
`view` was component state, so a reader who preferred the timeline was put
back on the list by every reload — and which view opened at all had been
decided for them twice over: the PRD said calendar, the app shipped the list.
Neither was the reader's answer.

So the first run asks, and the tabs write to `prefs` from then on. The
question ships pre-answered with the list, because a reader cannot choose
between two layouts they have not seen and that is the one that answers "what
expires next" in a look — and each option is drawn rather than described, for
the same reason. The screen says where to change it afterwards, since the tabs
are small text in a corner.

Adds `view` to the prefs key space. Additive and defaulted, so an existing
reader's stored prefs open exactly where they did before.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-18 03:28:03 +02:00
Lucas WintherandClaude Opus 5 d843c7cae7 feat(ba): track Blue Archive from its own wiki
New bawiki parser plus the ba-bawiki-events source, giving the game its
two live and upcoming events at day precision.

It reads the rendered /wiki/Events page, which is the opposite call to the
Fandom source next door and for the same kind of reason: bluearchive.wiki
is Miraheze, whose robots.txt disallows /w/ and /*?action=, so there the
API is the closed route and the page is the surface * is allowed. It
serves our own User-Agent a 200, sets no Content-Signal and no Crawl-delay
for us. The Fandom wiki was declined earlier as a JP archive yielding
nothing live; this is the live source that assessment pointed at.

Three page facts shape the parser, each a way to publish a confidently
wrong date. The schedule is a JP/Global tabber and the Japanese version
runs four to nine months ahead, so only Global is published — the akwiki
hazard. The Global tab's nav button carries the id
tabber-Global_version-label and precedes both panels, so slicing from the
first matching id reads the Japanese schedule while believing it read
ours; the first version of this parser did exactly that and published a
JP-only event. And three tabs on the page are named Global, the schedule
plus Mini-Event and Joint Firing Drill, so the schedule is found by its
Name (EN) header rather than by position, with canParse asserting the same
lookup — a renamed tab or column fails the run instead of emptying the
lane.

The page states no time of day and no timezone anywhere, which is why the
dates are day precision and why ba gets no resetOffsets. It also settles
the five other schedule tables, which do carry a wall clock but name no
zone and mostly do not say which server: reading those as UTC would invent
the fact that matters most, and rounding to a day would not save it,
because a 04:00 local boundary falls either side of UTC midnight depending
on the offset assumed and the start's day is part of the event ID. They
are left unparsed deliberately.

The two-event count is asserted against an independent extraction off the
fixture, and both starts corroborate against the per-event infoboxes
elsewhere on the page — GL 2026-08-04 and GL 2026-09-01, where the JP
reruns those tabs would have given are 2026-04-01 and 2026-05-06.

Three comment counts were already stale before this and are corrected to
what the tree now holds rather than to what it held yesterday.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 23:24:11 +02:00
Lucas WintherandClaude Opus 5 1a65a1f06c feat(games): track Reverse: 1999, which resets at 05:00
Adds the r1999 GameId and its metadata, plus GameMeta.resetHourLocal for
the fact that makes it unlike every other game here: it rolls its day at
05:00 server time rather than 04:00, on a single global UTC-5 server, so
its day-key boundary is 10:00 UTC.

resetOffsets could not carry that. Landing 10:00 UTC through the offset
would mean claiming a UTC-6 server, and serverOffsetUtc answers more
questions than this one. The new field overrides RESET_HOUR_LOCAL per game
and is absent everywhere else, so no existing reader's logged day moved —
a test pins that for the games that already had readers.

Both facts are read off the source, not assumed: all 154 rows of the
wiki's event list state (UTC-5) and run 05:00 to 04:59, an event ending
one minute before the reset the next one begins on.

No adapter yet, so the game has no events until one lands.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 22:45:40 +02:00
Lucas WintherandClaude Opus 5 4029885833 docs: bring reader-authored games and events into scope
The release thread's only feature request from the reader with the most games,
asked twice and asked for nothing else. No adapter roadmap reaches a ten-game
juggler, so this is the part of the product that serves readers we will never
scrape for.

Records the decision in the PRD as F13 rather than letting the code drift from a
spec that still lists user-submitted events as out of scope, and specifies the
key spaces in DATA-MODEL before any of it is built — a reader's event must never
be minted as ${game}:${slug}:${date}, because they can type a title identical to
a scraped one and the collision would silently share one completion mark, note
and streak between two events.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 18:10:41 +02:00
Lucas WintherandClaude Opus 5 1dd58d3f52 feat(arknights): track Arknights from its own wiki
The most-named game in the release thread, and the cheapest one to add: the
GameId, hue and dailyTasks have been sitting in games.ts since launch with no
source behind them.

arknights.wiki.gg is a better source than Game8 for this game, but it is not the
mp-event shape wikigg.ts reads, so it gets its own parser. Two properties of the
page drive the code:

- Every row lists CN and Global, about five months apart. Only Global is
  published; a row without one yields no event rather than borrowing the CN
  date. A CN date on a Global calendar is a confidently wrong date.
- Only the next boundary carries a machine-readable timer — the end while an
  event runs, the start while it is upcoming — so precision differs per side and
  flips when the event goes live. An exact instant is accepted only when it
  falls on the same UTC day as the date beside it, because startsAt.slice(0, 10)
  is part of the event ID: a start that moved a day would orphan every
  completion mark on the morning the event began.

Also sets resetOffsets for all three regions to UTC-7. Not a blanket per-game
offset — Arknights genuinely runs one Global server for every region we model,
and the page evidences the clock: every ending event ends at 10:59:59Z, which is
03:59:59 at UTC-7, one second before a 04:00 reset.

Six events, counted independently against the page before and after parsing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 18:05:01 +02:00
Lucas WintherandClaude Opus 5 8c2ec19c22 docs: record the two new sources and the shapes they added
Nine sources across eight games now, two more Game8 page shapes, two more
date formats, and the h4 rule that neither game's page can be read
without.

Also notes what the two new games do *not* have: no `resetOffsets` entry,
so both take the regional default. Neither source states a server map, and
`resetOffsets` feeds `dayKey`, which is a localStorage key — so that is
recorded as an assumption to confirm rather than left to look like a
verified fact.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-17 17:21:19 +02:00
Lucas WintherandClaude Opus 5 3434ac02cf feat(daily): ship daily detection off, and say it's experimental
Dailiness is guessed from the source's wording, which is a weak signal
and wrong in both directions. Guessing on a reader's behalf and hoping
they notice is the wrong default for a checklist they may build a streak
on, so `detectDaily` now starts false and the control carries an
Experimental tag saying what it does and doesn't know.

The default applies to new readers only — a stored `prefs` keeps its
value, because switching it off under someone who has been ticking
auto-detected checklists would pull those chips out of the strip with no
explanation. Nothing is deleted either way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 20:57:26 +02:00
Lucas WintherandClaude Opus 5 d70720e2af feat(filter): focus one game at a time
Switching games on and off says which games you play, and is set once.
It's the wrong tool for what a player of four games does while reading —
clear one game, move to the next — which cost two taps per game and left
the settings panel no longer describing what they play.

Focus is a lens over that filter, not a second filter: a bar at the top,
above everything it narrows, with a "next game" control that steps
through and ends by returning to all. It never touches hiddenGames, and
a focus on a game since switched off is ignored rather than obeyed, so
it can't strand you on a blank page whose cause is elsewhere. Each chip
carries that game's outstanding count, so a game with nothing waiting
says so before you visit it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 20:42:42 +02:00
Lucas WintherandClaude Opus 5 e6e4f7085a fix(daily): reset Endfield's European day on the server it's on
Endfield has two server groups, not three: Europe is served off the
Americas machine on a fixed UTC-5, so a European player's day rolls at
09:00 UTC. We were resetting it six hours early, at 03:00, which ticked
the wrong box every morning between those two instants.

Adds GameMeta.resetOffsets, a sparse per-region override, and threads an
optional `game` through every day-key function. Per region rather than
per game on purpose: a blanket offset would drag Asia — which does have
its own Endfield server — onto the Americas clock, moving day keys for
readers who never had the bug. A regression test pins Asia's output as
identical to before.

Day keys are localStorage keys, so this re-labels ticks logged between
03:00 and 09:00 UTC by European Endfield players, one day backward. No
tick is deleted and past days stay editable, but a streak can read as
broken for a day. That is the cost of correcting a wrong reset; leaving
it wrong is worse.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-16 20:37:47 +02:00
Lucas WintherandClaude Opus 5 41f0330e02 feat: let the reader switch off daily detection
Guessing from a source's wording is right most of the time, and someone
who finds it wrong often enough should be able to stop it rather than
dismiss the same false positive every patch.

Off, only events the reader marked themselves get a checklist. It
silences the guess rather than deleting anything: overrides, ticks and
streaks all survive, so switching it back on restores exactly what was
there. With it off, marking an event stores an explicit yes, since there
is no longer a detection for an override to agree with.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-15 22:26:16 +02:00
Lucas WintherandClaude Opus 5 7f384eb9db feat: let the reader mark an event as repeating daily
Dailiness was read off the source's wording alone, which is wrong in both
directions: a grind that resets every day but whose page never prints
"daily" got no checklist, and a banner whose blurb mentions "daily login
rewards" got one nobody could dismiss.

The reader's answer now wins. The control sits exactly where the
checklist goes — the one place the answer visibly matters — so marking an
event and ticking today off are the same gesture in the same place.

An override is stored only when it disagrees with detection. Recording
agreement would freeze today's guess into the reader's own data, so a
later parser improvement could never reach that event.

Marked events also join today's dailies at the top of the page, beside
the per-game chores: at 23:50 a login campaign and a commission run are
the same job, and ticking one should not mean opening a sheet to find its
checklist.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-15 22:14:37 +02:00
Lucas WintherandClaude Opus 5 02863ed008 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]>
2026-08-15 21:19:08 +02:00
Lucas WintherandClaude Opus 5 2b9338a8b0 docs: cover progress, effort and the completions migration
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-15 01:49:53 +02:00
Lucas WintherandClaude Opus 5 692b5a83d5 docs: bring every markdown file up to date with the code
The docs had drifted in ways that would mislead: DATA-MODEL documented a
localStorage shape the code stopped using (completedAt, no ignored store),
INGESTION claimed three Game8 templates when five are known, ARCHITECTURE
still listed the whole client and time.ts as unbuilt, and the review-quarantine
skill described a pipeline that does not exist yet without saying so.

Adds the parser roster and the six date formats as tables, documents the
subpath/base-href and offline behaviour, and records the new product surface
(first-run picker, ignore, offline, credit) as PRD features.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-15 01:27:56 +02:00
Lucas WintherandClaude Opus 5 3ea7286f56 docs: drop the LLM extraction layer, document multi-source ingestion
Event data is parsed deterministically; there is no model call, API key or
per-run cost anywhere in the pipeline. A source that cannot be parsed
deterministically gets no adapter rather than an inference fallback.

Documents the parser/adapter/merge split, records that Game8 uses three
page templates, and adds Neverness to Everness.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-15 00:11:08 +02:00
Lucas Winther 5db608563f Initial agent auxilliary files. 2026-08-14 23:45:35 +02:00