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) <[email protected]>
Defaults to never, so the form a reader already knows is unchanged until
they reach for this.
The unknown-end note had to change with it. "It'll show with no countdown
and no daily checklist" is true of an unbounded event and false once an
interval bounds it — and leaving it there would talk a reader out of the
simplest way to record a weekly reset. With a rule set it says each one
runs until the next one opens.
Refusal reuses comesRoundEarly rather than restating it, so the form cannot
drift from the schema it has to agree with.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Every other surface here is handed `now` — the headline, the board, the dailies
strip, the detail sheet — for the reason the parsers are: a function that reads
the clock cannot be rendered against a fixed instant, so nothing about it can be
asserted. The list row was the one exception, calling the wall clock twice for
its window caption and its "starts in".
Two things follow from fixing it. Those numbers were counted from a different
instant than the `clock` sitting beside them in the same row, which is a
disagreement nobody would ever notice and nobody could ever prove. And the
countdown is now testable: rendering one row at two instants gives the two
answers the injected clock implies, which is what the new test pins.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The link sits last in the existing row beside @StereotypicalCat and Source
code, in that row's own muted grey, with the same hover and the same small
mark. No accent colour, no button, no sentence asking for anything, and the
footer still contains no appeal anywhere — a test asserts that. This page earns
its keep by being trusted about dates, and a tip jar competing with the
disclaimer two paragraphs above spends that trust to make an ask. As one more
link it costs nothing and is there for a reader already looking.
The row also moves above "Additional ideas and design from". That row is who
built this and where to find them; the credit below it is other people. Reading
order now follows that split, instead of routing a reader past five strangers'
handles to reach the author's own links.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The footer only spoke up past the two-day threshold, and then only as a
count of stale sources. A page silent about its own age reads as current,
so it now says when the data last refreshed on every load — absolute date
plus relative age, next to where the warning appears. This is the half of
PRD F7 that was never built.
Where the number comes from is the whole point. `freshness()` in
src/shared/feed.ts takes the newest lastSuccessAt and never generatedAt:
the feed is rebuilt on every deploy whether or not anything was refetched,
so a build stamp would report a calendar as minutes old while its events
came from a fixture captured months ago. A game is also only as fresh as
its oldest source, or Endfield's live wiki would vouch for its stalled
Game8 page. That matters concretely here — eight sources cannot be fetched
from CI at all, so this notice is what stands between a reader and a
confidently stale calendar.
The stale warning now names the lagging games and how far behind each is,
because a count is not something a reader can act on while a name tells
them which source page to check. Two exceptions found by rendering it
against the real feed: past four games it summarises the remainder, and
when every game is behind — what a refresh that stopped running looks like
— it collapses to one sentence rather than ten names repeating one age.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Someone filling this form in by hand is usually doing it because the game isn't
tracked, so making them scroll past nine that are gets the common case
backwards. The default selection follows the top of the list rather than staying
on whatever the feed happened to return first.
The sort groups and does not reshuffle: tracked games keep their feed order
behind the reader's own.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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) <[email protected]>