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]>
The data layer for PRD F13, with no UI and no behaviour change yet.
src/shared/custom.ts defines the two key spaces, their schemas, and the
projection into what the views read. Two properties are the point of it:
- A reader's event id is random, not derived from their title. They can type a
scraped event's exact name and date, which under ${game}:${slug}:${date} is a
byte-identical key — one completion mark and one streak silently shared by two
events. Randomness also means renaming their own event never moves its id.
- A reader's event carries no sourceUrl, so a hand-entered date can never be
attributed to a source, and claims no region split, because they entered one
instant and inventing three would fabricate two of them.
The rest is widening what was GameId-shaped into a lane that may be one of
theirs: clockFor takes the boundary fields structurally so their events run on
the identical countdown rather than a second one, day keys fall back to the
regional default for a lane with no server map, and gameMeta becomes a context
resolver so metaFor stays pure and total — a lane can outlive its game when an
import carries an event whose game did not come with it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
Adds the game and its Game8 events page, the second of the games named in
the release thread. `parseAdjacentFullRange` reads the shape this page
uses: two full dates divided by an `<hr>` rather than a dash, so a
tag-stripping reader sees only whitespace between them. It is anchored at
both ends and needs a year on each half, because without that "August 12,
2026 Day 3 rewards" reads "Day 3" as an end.
Three of the four live rows publish. "Take Your Heart" ends 30 days after
each player makes an account, so it has no calendar date and no honest
place on a calendar. "Login Campaigns" names two candidate ends
("July 16/30, 2026") — it keeps its real start and takes no end, and the
leftover is not shown as a summary either, since a date the parser
refused to trust must not reappear dressed as information.
The page lists each live event twice: once in a bare Event|Duration table
and again under its own heading with Start Date / End Date rows and a
paragraph of prose. The second copy corroborates the dates, so deduping
now fills a missing summary from the copy it drops. Dates are still taken
wholesale from the better-dated copy and never blended.
Verified by re-extracting the page independently of the parser: 4 rows,
3 events, the fourth correctly skipped.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Adds the game and its Game8 events page. One `SOURCES` entry against the
existing game8 parser, plus one date shape that page needs:
`parseLabelledStartEnd` reads a duration cell holding `Start: <date>` and
`End: <date>` split by a `<br>`, which a tag-stripping reader flattens
into one run of text.
Four of the seven events say `End: Permanent`. That is the source telling
us there is no deadline, so they publish with `endsAt: null` and
`endPrecision: "unknown"` rather than a plausible date. The labelled cell
is also structure end to end, so it never becomes a summary — stripping
the leading half would leave "End: Permanent" standing where a
description belongs.
Verified by re-extracting the page with a throwaway script independent of
the parser: 7 rows in, 7 events out, every date matching.
Note the source itself is stale — its "Current Events" table still lists
January-May 2025 and "Upcoming Events" says there are none. The adapter is
right; the page looks abandoned, and Nikki coverage worth relying on needs
a second source at a higher priority.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
A login campaign is not one job with a deadline. It is twenty small jobs
on twenty separate deadlines, and a day you miss is gone whatever you do
afterwards — which a single "done" tick cannot express.
Repeating events now get a checklist: today's tick, a strip of every day
in the run showing what you got and what you missed, the streak, and how
many chances are left. Past days stay editable, because people tick up
later and a checklist you cannot correct stops being trusted after the
first mistake. Alongside it sits today's dailies — commissions, sanity,
daily training — one tick per game, keyed `dailies:<game>`, since no
source publishes those and they are the only thing on the page that
expires tonight rather than next patch.
Days roll at 04:00 server time per region, not midnight: finishing at
02:00 is still yesterday, and a naive UTC date would tick the wrong box
for four hours every night.
Dailiness is read off what the source published — a login event type, or
"daily"/"check-in"/"7-day" wording — never from a game's habits or an
event's length. That adds no schema field, so the feed contract and every
event ID are untouched. An unannounced end yields a tick count, not a
checklist of invented length, and a tick is never removed except by the
reader, including ticks outside the window the feed now claims: a source
quietly moving a date must not erase a fortnight's streak that exists
nowhere else.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
It still thanked only Game8 and hardcoded the studio list, so wiki.gg went
uncredited and unmentioned in the disclaimer after being added as a source.
Sources and studios are now read from the feed and the game registry, so
adding either credits the right people automatically. A hardcoded credit goes
stale the moment someone adds a source, and out of date is the one thing a
credit must not be.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Time is this product's subject, so the vocabulary lives in one module:
remaining, progress through a window, and urgency. Urgency is derived from
absolute time left, deliberately independent of proportion — a 90-day event
with three hours left is as urgent as a 3-day one.
An unannounced end is never urgent and has no progress: "we don't know" and
"loads of time" are different facts, and conflating them is what this
product exists to avoid.
Games carry a hue for identity only. Urgency is a separate axis, so one
glance answers both "whose event is this?" and "how long have I got?".
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>