The day-precision note ("The source gave a date but no time of day, so this
counts down to that day's server reset...") rendered for any event with
endPrecision "day" and a stated end, with no check on where the date came
from. For a reader's own event that is false three times over: there is no
source (sourceUrl is null, sourceId is "you"), nobody gave a date — for a
repeating occurrence with no stated end the app derived it from the interval
— and it does not count down to a server reset, since boundaryMs only applies
that shift when extractionMethod === "parser". Gated the note on the same
condition so the copy and the countdown agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every other paragraph in that stack carries an mt-*; this one did not, so
"every 2 weeks" sat flush against the bottom of the window table with no
gap. Static-markup tests assert on content, not spacing, so nothing caught
it — the Task 10 review did, by reading the surrounding rhythm.
Reading cadenceLabel once into a local rather than calling it at both the
guard and the render also drops the non-null assertion the second call
needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Occurrence ids carry their own start day, so moving the anchor or the
interval re-keys every occurrence and the marks under the old ids stop
being reachable. Nothing is rewritten — removeEvent makes the same trade,
and useMarkSet never removes because nothing else holds a copy — but the
reader is told the count first, the way removeGame reports blockedBy
instead of cascading.
Informs, never blocks. Renaming still costs nothing: the token is random
precisely so fixing a typo never moves an id, and movesOccurrences is what
keeps the warning off a rename and off a bare change of `until`.
cadenceLabel sits beside the form's own vocabulary so the sheet cannot
describe a rule differently from the control that set it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dark tokens re-struck under `[data-theme="light"]`. Nothing sets that
attribute yet, so the app still renders exactly as it did.
Two things could not be tokens. Game hues are data — ours and the
reader's — and every one was picked against a near-black ground, so
`readableHue` darkens them along the same hue until they read on paper,
and returns dark untouched by construction. And the attribute has to be
on the root before React can mount, or a reader who chose light gets a
dark flash on every load, so the shell reads the preference itself.
That leaves the ground colour written in three files that cannot import
each other, which is what the test pins together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The detail sheet formatted the stored ISO, so a day-precision end rendered
as 00:00Z — the previous day for every reader west of UTC, and now a
different day from the timer sitting beside it. Both dates come off the
clock instead, which resolves them the same way.
The note under them said the end was "accurate to the day only" and left
it there. It can say where the count actually lands, and should: a reader
deciding whether to log in tonight wants the reset, not a shrug. It still
says this is a reading rather than a time the source printed.
REGION_LABEL moves next to the region table it names, which is also how
the note gets "Europe" rather than the raw id.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interface for PRD F13. A game (name and lane colour) and events against it,
or against a tracked game when a source missed one, managed from the settings
panel and from the event's own detail sheet.
Two things the forms are careful about:
- "I don't know when it ends" is an offered answer, not a blank field. A
mandatory end date would push the reader into inventing one, which is the
failure the parsers are forbidden from committing — it would just be the
reader committing it instead. An unknown end behaves as it does for a scraped
event: no countdown, no checklist.
- A hand-entered date is never dressed up as a source's. The row carries a
"yours" chip, the sheet says so under the title, and the Source link is absent
rather than dead.
Deleting a game is refused while it still holds events, and says how many.
Deleting an event leaves the marks and ticks attached to it alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
The button advanced one step round the untouched → doing → done cycle,
so pressing "Mark done" on a fresh event produced "doing it" and needed
a second press. Worse, the press after "done" cleared the status
outright rather than undoing anything.
Three states need three targets — that is the control directly above it.
This one is the commit, so it sets done and back. cycleStatus had no
other caller and is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
The done checkbox now cycles untouched → doing → done, in the order people
actually move through. Effort, status and a note are set from the detail
sheet.
Rows carry chips for what the reader said, and a "tight" or "running out of
time" flag when the remaining time no longer covers the effort they recorded.
The detail sheet shows the arithmetic behind that rather than just asserting
it, and calls it a rough guide.
Imports from before this change are mapped forward rather than dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ignoring is not completing. "Done" keeps an event visible and counted;
"not interested" removes it from both views, which is the entire point.
Completions and ignores are the same shape and want the same guarantees, so
they now share one mark-set implementation — union merge on import, never a
removal, since nothing else holds a copy. Both ride in the export file.
Ignored events stay recoverable: the count and a reveal toggle appear in
settings once there is something to reveal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opens on the single event closest to expiring, at a size nothing else
competes with — the reader arrives with one question. Below it, live events
ordered by what ends soonest, then a quiet timeline view with one lane per
game.
Countdowns use tabular figures so ticking never reflows the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>