Route source-classified activities into an independent Miliastra lane and
show dated battle passes once under Cycles. Genshin's About tab can disable
TCG tracking without deleting progress or history.
Activate routing and presentation together so no intermediate version moves
saved progress while still rendering its old owner. Preserve identities,
clock revisions and deletion timestamps across late writes and backups.
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]>
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]>
Both fortnightly. Beyond the Rails is ticked once a cycle and rolls on
the Asia server's reset for every reader: on Europe the owner read it
ending Wednesday 21:00 UTC, Asia's Thursday 05:00. The Fund counts fons
against the guide's base cap of 1,000,000; the note says a reader's own
cap is higher and how to set it.
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]>
PRD F13 said the form asked how often after Kind and had no word on
examples; F17 and DESIGN.md had no example row. Adds the spec and plan,
and ticks the TODO item.
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]>
A reload can be served the browser's cached feed for minutes. The button
beside the data's age asks the server past that copy, swaps in a newer
feed without reloading, and says honestly what came back. Newer is
judged on events and sources, never the build stamp. The age now says
"just now" rather than "ended ago" when a fresh feed is a moment ahead
of the page's clock.
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]>
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]>
readableHue returned every dark hue untouched on the claim that all of
them already cleared 4.5:1 there. Five did not, and Fate's navy was
1.8:1, so its name under the headline deadline was nearly invisible.
Those five are now lifted in lightness only, keeping hue and saturation;
the fifteen that pass are unchanged. The owner approved relaxing the
"dark theme does not move" rule to this, and AGENTS.md and PRD F15 say so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The footer's staleness notice is an instruction, and the only remedy it
offers is "go and check that game's source page". That is not something a
reader can act on for a game they turned off and cannot see a single row
of — and with nineteen lanes, naming eleven they do not play buries the one
they do. A new lane arrives switched off (F8), so an untouched install was
being warned about most of the catalogue.
Scoped in two places, not one: the named list, and the count the
summarising branch measures against. Without the second, that branch would
never fire for a reader with most of the calendar off and they would get a
list where a sentence was the readable answer.
The notice now also states whose games it counted, in the words NextUp
already uses for the same set. Narrowing what the footer measured while
still printing "nothing has refreshed" would turn a claim about four games
into one about eighteen, in the one paragraph on the page whose job is
being trusted about age.
The headline age and the credits stay whole: the first is a fact about the
feed and feeds the bug form, the second is attribution owed to every source
we read regardless of what is on screen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review found the new settings row opening nothing for a rule whose `until`
precedes its `startsAt`. That parses — nothing ties the two together — and
yields no occurrence at all, so nextOccurrences and the anchor fallback both
come back empty, nearestOccurrence returns null, and openRow cannot resolve
a bare rule id. It was the undeletable record this index exists to rescue,
now with a button that lies about it.
So resolution moves out of App into displayEventFor, with the rule itself as
the floor: a stored id always names something the reader can edit and
delete, whatever the rule does or does not generate. Exported because
nothing here can click and no test renders App, which is exactly how a dead
button shipped — the chain is now covered without a DOM.
Two of the caption tests could not fail, proven by mutation rather than
read: deleting the whole "starts" branch and removing the null-end guard
both left the suite green, because `not.toContain("ended")` was never
watching the branch at risk. They assert what the caption says now.
And a repeating series that has stopped said only how often it repeats — in
the one place whose job is explaining why an event is on no other surface. A
healthy cadence explains nothing; it says when it stopped.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An event of theirs that had ended was on no surface at all. Every list and
the board drop a row once its end has passed, and settings only counted the
events a game held rather than naming them — so a one-off became unreachable
the day it finished: impossible to edit, and impossible to delete out of a
store nothing else can see. Repeating events escaped only because their
occurrences roll forward.
So settings names them now, under the game they were filed against, each row
opening the same detail sheet a row on the front page does. Nothing about
how they are managed changes; what was missing was the way back to them.
Two things the index has to get right or it leaves the same hole it closes.
The lists hold occurrences, never rules, so a repeating rule's own id opens
nothing — nearestOccurrence bridges that, and answers for a finished series
too by falling back to the first occurrence, since a rule whose `until` has
passed would otherwise be exactly as stuck. And an event filed under a game
we track has no row of theirs to nest under; the form allows that, so those
get their own heading rather than trailing the list and reading as though
they belonged to whichever game came last.
Each row says why it is not on the front page — ended and when, or its
cadence — because a list of bare titles leaves you guessing which of two
entries is the dead one.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Review found the headline feature unreachable. `valid` still demanded an end
date unconditionally, while every sibling rule had been gated on
`datedWindow`. A fresh form starts with endKnown true and endDate empty, so
picking weekly hid both the field and the checkbox and left Save disabled
with nothing on screen a reader could do about it. Editing an existing event
was fine, which is why no test caught it: every static render drives the form
through `initial`, and cadenceOf never returns a preset for an event that has
an end.
So the three states that needed a click to reach are now pure functions that
do not. endStated pins exactly that case. repeatOf takes the cadence and
answers for all five, which also makes `until` surviving the preset path
provable — mutating it away previously left the suite green.
Two more from the same review. The unknown-end note keyed off the cadence, so
custom with the repeat set to never promised a countdown that would never
arrive; it keys off the rule now, which is what actually decides. And a delay
whose start and end disagree about having a time of day can never land a
whole unit from the anchor — unstatable, not merely unstated — so it says so
rather than disabling Save in silence.
"State it myself" was also a one-way door: nothing ever cleared it, so the
measured reading could not be recovered without abandoning the form.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The repeat controls sat after the dates and applied to whatever was there,
which meant a weekly chore still had to answer an end date it has no honest
answer to. Asking first inverts that: the cadence decides which dates are
even questions.
A preset carries no window. Weekly means the week is the window, so there is
no end to type and no ignorance to admit — five fields instead of eight, and
it stores exactly what the model already renders as back-to-back
occurrences. A dated recurring event is therefore a custom, which is where
the forever/delay control now lives; a one-off shows no repeat machinery at
all.
cadenceOf derives which of the five a saved event opens in, so a rule made
before this control existed opens in whichever answer describes it. Nothing
about the schema moved.
Switching cadence hides the end date rather than clearing it: hiding a field
and quietly discarding what is in it is how a form loses somebody's work
when they change their mind back.
The duplicate note was found by rendering the form and looking at it. Both
of the older notes explain the end-date field, so a preset — which has no
such field — was showing two sentences that said nearly the same thing.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The PRD said a reader's rule could optionally stop on a date; the until
control was deliberately descoped from the form during planning, and the
PRD was written as though it shipped. Corrected the PRD to describe what
actually ships, and noted that the field still exists in the schema —
reachable today only by importing a file that carries one.
That gap had a second-order bug behind it: because the edit form rebuilt
`repeat` from scratch on every save, hard-coding `until: null`, a rule that
already carried a non-null `until` had it silently reset to eternal on any
edit at all, including a pure title fix. Extracted the `repeat`-building
logic into `repeatFrom`, which now carries `initial?.repeat?.until` forward,
and is exported so the behaviour is provable without a submit nothing in
this suite's renderToStaticMarkup tests can click.
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]>
They were one group called "Reading", grouped on "both are how do I read
this?" — which summarised as "Europe · Dark", two unrelated answers joined
by a dot, under a name for neither of them, in a panel whose whole premise
is that a closed line answers its own group.
The region is not a reading preference. Region-scoped ends, a date printed
without a time, and every daily streak are all cut on that server's clock,
so it is the one control in this panel that can make a countdown wrong —
and it was filed behind a word for the theme. Split, each line answers one
question, and the region group says what it governs when opened.
The eyebrow above each pill row goes with it, since the group summary now
asks the question the eyebrow was repeating; aria-label keeps the
accessible name, which is what stops six unlabelled buttons in a row from
being all a screen reader hears. The summary assertions are looked up by
group name rather than by index, so splitting a group no longer moves every
assertion below it onto a neighbour.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Looked at on a 1440px screen it was the one block on the page not using
the column it sits in. The rows were held to max-w-3xl inside an 1150px
shell, but the section's own rule spans the shell — so every hairline
inside the panel stopped a third of the column short of the one above it,
between a two-column checklist and a three-column footer that both go
full width. The measure was there to keep a name and its state reading as
one line, but that pairing is what "running now" already does with a
title and its countdown directly above, so full width is the house
pattern rather than a stretch. Prose inside a group keeps its measure.
The game list was the worse half: eighteen rows of four controls at
max-w-md is a thousand pixels of ribbon down the left with two thirds of
the screen empty beside it. It takes two columns past lg once there are
nine or more games, flowing down then across so the numbers still read in
a straight line and an arrow still swaps a row with its neighbour. Below
nine, and on a phone, it is unchanged.
The last group also loses its bottom rule, which was a second hairline a
few pixels above the footer's own.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The panel was one open block of everything in two columns. That reads fine at
four games and stops working at eighteen: the game list alone is eighteen rows
of four controls each, and it sat above the checkbox somebody had scrolled down
here to tick. Nothing was findable and everything was in the way.
Collapsing it into groups is only half an answer, though — a group showing
nothing but its name turns "what is my region set to?" into a click, which
trades one kind of friction for another. So each summary carries its group's
current state: "Europe · Dark", "17 of 18 on · A–Z", "plus finished, not
started". Closed, the panel is a five-line report of how the app is configured;
opening one is for changing an answer rather than reading it. The states are
derived from `prefs` at render, never stored, so they cannot drift from the
controls they describe.
Native `<details>`, for the reason the reorder arrows are ordinary buttons:
keyboard and screen reader reach it with no second implementation. `summary` is
not an `a`, `button` or `[tabindex]`, so it needed its own focus-visible rule —
the shared one does not reach it.
Two things fell out along the way. The region and appearance rows had no
accessible name at all, so a screen reader read six unlabelled buttons in a row;
they and the board's split pills are one `PillGroup` now, with the `role="group"`
the board's own controls already carry. And the empty states that point at
"Show events that haven't started" now name the group holding it, which is what
makes shipping the groups closed safe.
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]>
The picker took `available` in feed order — whichever game happened to hold
the first event row — which means something everywhere else in the app and
nothing on this screen, where the reader is scanning for the two or three
names they already know.
Sorted on the name printed on the button rather than the LaneId, since the id
is not what a reader sees, and through localeCompare rather than `<`, because
a code-point sort files hololive Dreams after every capitalised game instead
of between Genshin and Honkai.
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]>
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 the scale only by how far out it goes — three months on a
screen — which left the close end reading as decoration rather than the
answer to two events ending a day apart. The suite count moves with the
test the previous commit added.
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]>
F4a called it "a bar at the top of the page", which the previous two
commits made false above `lg` — the working agreement's two-column bullet
and the PRD's desktop split both described a rail the bar was not in.
"Above everything it affects" is the rule that survived the move; where
that lands now depends on whether the layout has a rail.
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]>
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]>
"Ending soon" described the sort, not the job — and it sat one line above a
sort control whose own option reads "Ending soonest", so the tab and the
ordering looked like the same control said twice. Checklist is what a reader
with four games is doing with it.
The stored id stays "soon". That value is in `prefs.view` on real devices, and
a label is copy: renaming one must never move a reader to the other view.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>