A code redeemed on its last server sweeps in the game's colour and greys
in place for 0.8 s, then lands in Redeemed or expired. A completed event
greys and shines while its Moved to Archive notice holds, now 0.9 s
rather than 1.5 s, which read as unresponsive; an ignored one only greys.
Settings → Appearance gains Motion & celebrations as three What to track
cards: Reduce motion (stops every animation and the fireworks, adding to
the device's own setting), Fireworks, and Shine. Reduce motion locks the
other two off without forgetting them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The redesign only celebrated a finished section, so ticking one daily or
completing an event drew nothing. Now a daily ticked, a routine met, an
amount typed to its target and an event marked completed each fire the
small burst; the item that finishes every daily or a game's routines gets
only the grand celebration. Settings → Appearance → Celebrations
(prefs.celebrations, on unless false) turns every firework off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Follow-up to the 2026-10-18 review of the v1→v2 migration. Start-up
(finishing a pending restore, then migrating or reconciling) now runs under
the lock every save takes, so a second tab's save cannot be overwritten;
where no lock exists it opens unlocked, as before. The automatic backups get
a size cap like Back up now's: past it the oldest are dropped, never the
newest, leaving the main save room to grow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tour now floats over the page it describes and must be finished
before the app can be used: new readers straight after setup, returning
readers on their next visit, and anyone who saw the earlier tour. The
page behind stays visible and scrollable but inert.
"Pick a layout" (on the Checklist) and the Timeline step switch Relaxed /
Structured and New / Classic in place; the selected look is kept.
Returning readers start on Relaxed. A replay from Settings stays
optional and closable.
redesignTourSeen now records new / returning / replay / 2026-10-tour-2;
any other value means not yet taken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.
Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.
Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game-Time-Master lists NTE Europe on UTC+1, but the game disagrees: on
2026-10-16 two in-game timers ran out at 03:00 UTC — one on 26 Oct, after the
clocks change — and Realm of Greed says "resets every Monday at 5:00". Only a
fixed UTC+2 fits all three.
Re-labels the hour 03:00-04:00 UTC of every European reader's NTE day.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The design spec (entries with the sentence each rests on, what was looked for
and not shipped, and the facts gathered for N-week cycles and dated pass
windows), the plan, PRD F12 and F17, DATA-MODEL's prefs, and TODO.md's open
in-game checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When merge fuses two sources' copies, the loser's id leaves the calendar, and it may already hold a reader's ticks from a day it was published alone. Until now that needed a hand-edited RETIRED_IDS entry per fusion.
The survivor now lists what it absorbed in an optional mergedIds field. When a feed arrives the client turns those into carry-forward pairs, skipping any id the feed still publishes, and an import takes the same table. Optional, so an older cached feed still validates and an older bundle drops it unread. RETIRED_IDS stays for parser changes, whose old ids no feed will name again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game8 heads Genshin's pass "Battle Pass - Moontrace"; Fandom lists "Moontrace" and files the category in a column. The label minted a second id for one event, so every battle pass since the Fandom source landed has been on the calendar twice, and a reader reported it.
Both sources now mint the same id and merge fuses them by id. The two Game8 ids that moved, Frostfarer and Moontrace (the only ones git history shows ever shipped), go in RETIRED_IDS so readers' marks follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An event id is a localStorage key, so a fix that changes one orphans every tick, note and ignore filed under it. retiredIds.ts holds an explicit table of old id -> new id, and the progress, daily and ignored stores move entries across on load and on import.
It moves rather than copies: a copy left under the old id would be carried forward again on every load and bring back a mark the reader had cleared. When both ids hold an entry, each store's own import merge decides, except that progress keeps any field only the older row held. The table starts empty; the change that moves ids adds its entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Giving Genshin's "To Temper Thyself and Journey Far" its five-of-seven goal
meant finding it in the list, opening its sheet, saying it repeats and only
then setting the goal. The strip now has an Edit mode beside Catch up that
lists its events by game with their goals, opens the sheet's goal control in
place, and adds any running event a game at a time. It calls the same
setDaily/setGoal the sheet does, so it can store nothing the sheet would not.
Weekly and monthly tasks are not due tonight, so they move to a row under the
dailies, and each period can be switched off (showWeeklyTasks,
showMonthlyTasks; default on, so nothing disappears on upgrade).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new reader should see what's coming before deciding they don't want
it. It's set at first-run setup rather than in the defaults, so an
existing install whose saved prefs predate the field keeps its answer
instead of having it switched on underneath it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The changelog's own entry names the commit before this one, since a
commit cannot contain its own hash. AGENTS.md now asks every
reader-visible change for an entry, and every multi-commit feature for a
note, so the record keeps up. PRD F18 and the storage and design records
describe what shipped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Make it mine's copying was inline in App and untested; it is now a pure
function with a test, so a change to either store can't make the copy
silently arrive empty. The question's buttons now say what they answer
when heard out of context. The prefs block and the spec now match what
shipped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review found that import and edit rebuilt a task from this build's parse.
That stripped any field it didn't know, and import dropped a newer
version's task entirely. Both now keep the record as stored and validate
only to display it.
An event whose goal is points can't be ticked, so it no longer counts as
waiting today: it counts with its week. The month-day field reset
itself to 1 when emptied; it now keeps a draft.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The product, storage and design records now cover F17. They say why the
task and amount stores, unlike the custom-event store, never filter on
load, which is the rule a future store would most likely copy wrongly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review found three ordinary actions that deleted a goal this build could
not read: "It repeats daily", pressing an already-pressed Off, and "not
daily" without saying so. Only "not daily" clears a goal now, and its
warning keys off what is stored, not what parses.
The count field was bound straight to the goal, so emptying it snapped
the old number back and typing appended to it. It now keeps a draft.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The product, storage and design records now say what a goal is, why it
never marks an event done, and why an unreadable goal is kept rather than
stripped. That last rule is the one a future cleanup would most likely
undo by mistake.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rewrites the reset table for every corrected game, records the rule that a
game's offset must make its day key name the source's calendar day, and
names Game-Time-Master as the evidence for server clocks a source does not
state (transcribed, never vendored: it is GPL-3.0).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A game's servers need not agree on the reset hour any more than on the
offset. Reverse: 1999 and Nikke spell their 05:00 out for all three
regions, so no reader's day key moves.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Infinity Nikki's wiki prints "There are no Events in this category"
between versions, and the refresh runner has honoured that since
2026-09-03 — it stores the empty parse as the source's real answer
rather than letting a quiet lane reach the broken tier. The feed build
never asked, so the same page arrived at CI as parsedCount 0,
indistinguishable from a parser that has stopped reading a redesigned
page, and brokenSources failed every build while every refresh stayed
green.
The runner's verdict cannot travel on its own: only a parser has seen
the page, and by the time brokenSources runs there is nothing left but
the feed. So the fact rides on SourceHealth, defaulted so an older feed
the service worker cached keeps validating and reads as the strict
answer.
Both ends now ask it the same way — of an empty parse only, from the
page's own words only — because a redesign yields zero rows too, and
excusing that is the silently emptied calendar the gate exists for.
The rule sits in a module rather than in build-feed.ts, which writes
public/ and so runs a build if a test imports it. That is how the two
ends drifted apart with nothing to catch it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review pointed out that `dailyGroups` was well covered and the wiring to it
was not: passing the wrong pref from App, binding the checkbox to a
neighbour, or hardcoding `true` in Dailies each shipped with the suite
green. Three of those are now caught by rendering the strip and the settings
panel rather than the function behind them — verified by performing the
mutations, not by reading the code.
The fourth, App handing Dailies the wrong pref, is still uncovered: no test
renders App, and closing it needs a feed and a click library this project
does not have.
Also pins the default. Flipping that one line silently empties Today's
dailies for every existing reader, which is the whole argument of the
comment above it, and nothing was watching — `defaults` is exported so the
value and the upgrade path can both be asserted.
And DATA-MODEL's enumeration of the prefs blob had gone stale; AGENTS.md
names it as the doc to update when a stored key moves.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Three sentences were false once a rule could bound an occurrence. DATA-MODEL
gains the derived occurrence key and what a reschedule costs; F13 gains the
rule and the reason an occurrence need not state its end.
SOURCES' arustats note stays, with the answer beside it: the reason
scheduleBosses is unread was a design gap, that gap is closed, and reading
it is now a parser change. Nothing on the ingest side has moved — this note
is what stops the next person re-deriving why.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
SOURCES.md § 13 declined this game's only known source partly because every
boundary was a week bucket, and we have now built a different source with that
same objection standing. Leaving that contradiction unwritten is how a later
pass rediscovers the trade the expensive way, so § 14 records the decision
itself: the conduct evidence, which three of § 13's four objections arustats
answers, which one it does not, and that any source stating a date per event
retires it. § 13 is marked superseded rather than rewritten — its verdict on
marisaimpact.com still stands and was not re-tested.
AGENTS.md gets the same warning where a parser author will hit it, because the
estimate is not visible from the code: the numbers look like every other
day-precision date in the project.
Also corrects the counts this game made false — nine parsers, twenty sources,
nineteen games. The "stopped working at eighteen" passages are left alone: they
date the settings-panel redesign, and bumping them would falsify the history
rather than update a count.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The progress store merged like the mark stores do, keeping whichever copy had
the earlier timestamp. That is right for a mark — `at` is when the reader made
it, membership is the whole fact, and the oldest stamp is the truest one — and
wrong here, because this record *is* the data. A status, an effort, a note and
whether an event repeats all live in it, and `at` says when one of those last
changed.
So the earlier copy winning discarded every edit made after it, in both of the
directions an import actually happens in: restoring a backup taken before an
evening's work undid the evening, and importing an old file onto a device with
newer progress rolled the device back. Neither is recoverable — there is no
account and no server holding a second copy.
The later record wins now. Nothing is removed in either direction, and taking
the maximum of two timestamps is as order-independent and idempotent as the rule
it replaces. `mergeProgress` came out of the hook to be tested, which is also
how the ignored store's opposite rule is now written down rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The rules a future change could undo without noticing, written where whoever
touches that area next will read them.
Three of them are traps rather than descriptions. An absent `gameOrder` means
the reader has never placed a game, not an empty order — reading it the other
way hands every existing install a blank list, which is the `knownGames`
mistake again. `games` is left in feed order deliberately, because the code
that hides a reader's games diffs it. And the catch-up window bounds display
and never storage: a fortnight's streak exists on one device and nowhere else,
so nothing prunes the log against a window.
The spec that produced the work goes in alongside, including the helper it
specified and the implementation dropped, so the next reader sees why rather
than wondering.
Also corrects the test count in AGENTS.md, which said 772 and was already
stale before this.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
F1 described a board-only setting and said in as many words that the
checklist "has listed them all along", which is now the opposite of what
happens. F2 did not mention the section at all. Both say it once, in the
place a reader of either would look.
The prefs key space records the rename and why the old value is carried
rather than dropped — the next person to move a field will want the
precedent, and the reasoning is the half a diff cannot show.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
F1 described bars that carry their own not-started marking and stopped
there. It now has the label naming where the running ones end, and the
choice between keeping that block and dropping it for one deadline order
— including why the second is a re-sort rather than a hidden heading,
which is the part a future change would otherwise get wrong.
The heading itself shipped a commit earlier without this; same feature,
so it is written up once here rather than split across two entries.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
F1 described a control in the board's header that is now a row in
settings, and the reasoning is worth keeping rather than the location
alone: the header holds the controls that reshape what is on the board,
settings holds the ones that decide what is on it. That is the line, and
it is what would otherwise be re-argued the next time a filter needs a
home.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
F1 said the timeline plots what the reader can see, which is now half
true, and the prefs key space did not list the field the board writes.
Both record the argument the code only implies: the window is drawn from
what is plotted, so on-by-default is not a fuller board, it is a smaller
one for everything that is actually running.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The Game8 page stopped being updated on 31 August 2025 and mentions 2026 zero
times, and it had been publishing five year-old events with endsAt: null — which
the app renders as live-with-unknown-end indefinitely. That is worse than an
empty lane, and nothing in the pipeline catches it, because a stale page is not
a broken one: it fetches, it parses, it passes every test.
The replacement is the game's Fandom wiki, permitted by the same standard
robots.txt that cleared Nikke, maintained to within two days, and shaped as
Current / Upcoming / Permanent / Past tables of Event | Duration | Description
| Type.
Its durations state a wall clock on both sides and no zone anywhere for that
column, so parseZonelessClockRange reads the clock and discards it, publishing
the printed date at day precision. That invents nothing and treats these cells
exactly as every Game8 date already is. Converting instead would mean picking an
offset, and the offset moves the day: July 16 20:00 read as UTC-7 is July 17,
and the start's day is half of every event ID this game will ever have.
The Game8 entry is removed rather than kept as a second opinion, since a source
whose every row is wrong is not corroboration. Its fixture stays: it is the only
page here carrying the labelled "Start: ... End: Permanent" shape, and the test
now drives it through the parser directly.
Titles come from a link's title attribute, which never passes through text(), so
they are entity-decoded by hand — otherwise "Alison's Travel Shop" becomes a
slug, and a slug is a localStorage key. The ingest sanitiser caught exactly that
during the build.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Its robots.txt was read in a browser and is the standard Fandom file: no
Disallow: / for *, /api.php?action= explicitly allowed, and only the namespaces
we never want refused. The AI crawlers it blocks by name are not us. That was
the one thing missing — the API itself has always answered our own User-Agent
with a 200, and it is only robots.txt that a datacentre address cannot read.
The zone lives in the column header, Start(UTC+9) / End(UTC+9), and no date in
any cell carries an offset. So the header is the safety property: a table that
stops naming its zone is refused rather than read as UTC, which is the Blue
Archive hazard arriving one column to the left.
Two shapes worth knowing. Every title is an image read from <a title>, and the
newest row is the one whose logo has not been uploaded yet — it renders as a
red link reading "File:Persona on Frontline logo.png", so a reader that only
understood the link title would silently drop today's live event. And story
events state a clock only on the end, so the start keeps the day the page
printed rather than being shifted nine hours into the previous one; the start's
day is half an event ID. That is the FGO rule applied to the opposite gap.
resetOffsets and resetHourLocal ship with the game because the page evidences
both: events end 04:59:59 and the banner replacing them starts 05:00:00, one
second later, on a stated UTC+9. Adding that later would re-label day keys
readers had already logged ticks under.
Seconds now survive the date helpers, since rounding 04:59:59 to the minute
would make an event overlap the banner that succeeds it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
uma.moe stays declined — its API sits behind a Cloudflare Turnstile proof
header, and an adapter would mean defeating a deliberate access control. This
is the surface that is simply open, and it needed the parser change in the
commit before this one to be readable at all.
The stable URL matters: Game8 also publishes monthly "August 2026 Release
Schedule" pages whose id changes every month, which a static registry cannot
follow. "List of All Banners" does not move.
Four current banners, both columns of the side-by-side table, and none of the
Previous Banners below them. The tenth game8 source, so it carries the same CI
blindness as the other nine and only a manual refresh moves it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
No parser work at all — a GameId, a games entry, a SOURCES line, a fixture and
a test. The page lists six current events and the parser publishes four; the
two it skips print "Start Date: -", and no start means no event ID, so that is
the skip-never-guess rule working rather than a silent drop.
The cost is worth stating plainly: this is the ninth game8 source, and game8's
edge answers the Actions runner with a 202 and a bot-management body, so the
lane is built from a checked-in fixture in CI from day one and only a manual
`bun run refresh` moves it. freshness() discloses that in the footer, which is
what it is for.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This wiki publishes the same schedule twice and the fuller surface is the worse
one. /wiki/Banner_List has 55 clean rows with full wall clocks and states no
timezone anywhere on the page; the front page's Current Banners module emits
the same instants as real <time datetime="...-07:00"> elements. The two agree
exactly, which is strong evidence the list is UTC and is still only evidence —
so we read the surface that says what it means and pay four live banners
instead of a full history for it. If an editor ever states the zone on
Banner_List, that page becomes the better source immediately.
Two ways this could have failed silently. The template writes its BEM
underscores as __, so a selector written against the class name a
browser shows matches nothing at all; and banner names are red links to
?action=edit&redlink=1, which Miraheze's robots.txt disallows, so a href
carrying a query is refused and the page URL stands in.
No resetOffsets, and here the source did state an offset: -07:00, which is US
Pacific and therefore shifts twice a year. That is the Fate/Grand Order gap
arriving through a page that looks like it answered the question.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The best date material this project has after wiki.gg: every row states an
exact instant on both boundaries and names the zone, so nothing is converted
and nothing is assumed. `parseIsoClockRangeUtc` requires that `(UTC)` rather
than defaulting to it — a row that ever loses it should drop out instead of
landing hours off a boundary the reader is watching in-game.
The hazard here is the Server column. CN, EN and JP rows share one table and
the Chinese schedule runs about a year ahead, which is the akwiki CN-column
problem verbatim: publish EN, skip the rest. Betas are fenced off separately,
being dated exactly like everything else and playable by nobody, and the page
is an archive back to 2023, so currency is decided against ctx.now as in
bawiki. That leaves one live event today — thin, and true.
No resetOffsets: the EN boundaries land on three different clocks, which is a
patch window rather than a reset hour.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
PRD gets F15 and the fork that asked for it, DATA-MODEL the prefs field
and the fact that a script outside the bundle now reads that key, and
ARCHITECTURE the pre-paint script — the one piece of this that is neither
CSS nor React.
The AGENTS rule is the part worth reading twice: a component that names a
colour instead of a token now looks right in one theme and wrong in the
other, and nothing will tell you which.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
F1 described "one lane per game" as a fact about the timeline, which is
now false half the time, and the prefs key space did not list the pref
that decides it. Both say why the merged board sorts with
endingSoonestFirst rather than a bare end date, since that is the part a
later change is most likely to "simplify".
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A source that prints "August 19, 2026" and no time is stored at 00:00Z,
which is a placeholder for "somewhere in this day" rather than a claim
about when the day opens. The countdown read it literally, which turns it
into exactly that claim — and the UTC day is nobody's. Wuthering Waves
events dated the 19th were still running three hours after we had retired
them, because a European player's day opens at 04:00 on a UTC+1 server;
Asia was four hours out the other way and America nine. Sixty-two of the
eighty-six published events were affected, across ten of thirteen games,
so the reader was reading a wrong number far more often than a right one.
A day-precision boundary now resolves to the reset that opens that
game-day on the reader's server — the same grid daily.ts keys every tick
by, and the only clock we hold for a game. That is a reading of the date
the source printed, not a time invented for it: nothing stored moves, no
event ID moves, and the parsers still refuse to guess.
Two boundaries are never re-anchored. A regionEnds value is already the
instant a source stated per server, so anchoring it would throw a fact
away. And an event the reader typed in was resolved by readerInstant in
their own timezone, which is a stated time too — theirs.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The adapter shipped with no docs at all, which left the one thing worth
writing down unwritten: this wiki publishes two servers' schedules on
two cross-linked pages, and the Japanese one parses just as cleanly as
ours. That now has a row in the assessed-sources table, a section under
Fandom, and a line in the ingestion pitfalls table alongside the akwiki
CN column and the bawiki JP tab — three sources bitten the same way is
a pattern, not a coincidence.
Also records what resetOffsets cannot express, since fgo is the first
game to need it: a server clock that shifts with daylight saving.
DATA-MODEL's GameId listing was two games behind the schema, missing
holodori as well as fgo.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
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]>
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]>
`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]>
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]>