fix(deadline): a date-only job is late only after its day #208

Merged
nalum merged 1 commit from fix/date-only-lateness into main 2026-08-26 18:33:41 +00:00
Owner

Closes #186.

Both clients asked deadline < now and stopped there. A date-only deadline
is stored as local midnight, so a job due today was drawn late from 00:00 —
flagged for the whole day it was actually due.

The server never agreed: reminder.go compares dates, ringing JobDue on
the due date and JobOverdue only from the day after. This closes a live
client/server disagreement rather than adjusting a preference. A timed
deadline keeps its exact semantics — late at 09:45 means late at 09:45.

conformance/deadline.json pinned the old behaviour in a case whose name
asserted it, so the case is renamed and flipped, and four more pin the
boundary in both directions and the unchanged timed cases.

Late is derived on the device, so it needs something to make a card flip at
09:45 without a refresh — now was computed per render and nothing ticked.
Both surfaces now share one heartbeat: on web a module-level singleton
behind useSyncExternalStore, aimed at the next minute boundary rather
than a drifting interval, waking on visibilitychange and focus; on
Android a lifecycle-aware rememberNow that does not tick in the
background and re-reads the clock on resume.

Determinism lives in the value, not the timer: the snapshot is the
truncated minute, so a frozen clock returns an identical instant however
often the timer fires. Every golden suite was run — web screens, live
pages, and Roborazzi — and no golden moved, because every seeded
deadline is timed and no fixture sits in the window the rule changes.

🤖 Generated with Claude Code

Closes #186. Both clients asked `deadline < now` and stopped there. A date-only deadline is stored as local midnight, so a job due today was drawn late from 00:00 — flagged for the whole day it was actually due. The server never agreed: `reminder.go` compares dates, ringing JobDue on the due date and JobOverdue only from the day after. This closes a live client/server disagreement rather than adjusting a preference. A timed deadline keeps its exact semantics — late at 09:45 means late at 09:45. `conformance/deadline.json` pinned the old behaviour in a case whose name asserted it, so the case is renamed and flipped, and four more pin the boundary in both directions and the unchanged timed cases. Late is derived on the device, so it needs something to make a card flip at 09:45 without a refresh — `now` was computed per render and nothing ticked. Both surfaces now share one heartbeat: on web a module-level singleton behind `useSyncExternalStore`, aimed at the next minute boundary rather than a drifting interval, waking on `visibilitychange` and `focus`; on Android a lifecycle-aware `rememberNow` that does not tick in the background and re-reads the clock on resume. Determinism lives in the value, not the timer: the snapshot is the truncated minute, so a frozen clock returns an identical instant however often the timer fires. Every golden suite was run — web screens, live pages, and Roborazzi — and **no golden moved**, because every seeded deadline is timed and no fixture sits in the window the rule changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(deadline): a date-only job is late only after its day
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m38s
check / report (pull_request) Successful in 4s
android / build (pull_request) Successful in 6m34s
check / web (pull_request) Successful in 4m14s
android / report (pull_request) Successful in 4s
bf1925c317
Both clients asked `deadline < now` and stopped there. A deadline with
no clock time is written as midnight, so a job due today was drawn with
the red Overdue badge from 00:00 — for the whole day it was actually
due. A date-only deadline names a day, not that day's first instant; it
is late only once the day has ended, local to the reader. A timed
deadline is untouched and still turns late on its own minute: two
granularities, both deliberate.

The server has always agreed. The deadline reminder compares dates, so
it rings JobDue on the due date and JobOverdue only from the day after —
the phone said late while the notification said due. That disagreement
is what closes here; nothing on the server moves.

Late is derived on the device from a clock, and derived state needs a
heartbeat, so both surfaces gain one shared minute ticker rather than a
timer per card: useNow() on the web, rememberNow() on Android. Each
catches up on wake (visibilitychange/focus; repeatOnLifecycle at
STARTED) so a laptop back from three hours' sleep does not serve a stale
minute. Determinism is kept in the value, not the timer — the snapshot
is the minute itself, so a frozen clock never re-renders however often
the timer fires, and the cards take `now` as a parameter so the render
and screenshot suites hand them a fixed instant.

conformance/deadline.json flips the case that pinned the old verdict and
gains the boundary in both directions, so the two surfaces are held to
the same line rather than to one value.

Test report

Suite Tests Result Skipped
Unit 1410 ✅ pass 1
Integration 113 ✅ pass —

Coverage: 26.9%

Updated by the check workflow · commit 133570be03

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1410 | ✅ pass | 1 | | Integration | 113 | ✅ pass | — | **Coverage:** 26.9% <sub>Updated by the check workflow · commit 133570be03c05c947eafb2ed9962fa3feed9173e</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 48 ✅ pass 0

Coverage: 2.9% of lines

Updated by the android workflow · commit 133570be03

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 48 | ✅ pass | 0 | **Coverage:** 2.9% of lines <sub>Updated by the android workflow · commit 133570be03c05c947eafb2ed9962fa3feed9173e</sub>
nalum force-pushed fix/date-only-lateness from bf1925c317
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m38s
check / report (pull_request) Successful in 4s
android / build (pull_request) Successful in 6m34s
check / web (pull_request) Successful in 4m14s
android / report (pull_request) Successful in 4s
to 133570be03
Some checks failed
check / commits (pull_request) Successful in 13s
check / go (pull_request) Successful in 2m44s
android / build (pull_request) Successful in 7m14s
check / web (pull_request) Successful in 4m29s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
android / build (push) Has been cancelled
android / report (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
check / commits (push) Has been cancelled
tag / tag (push) Has been cancelled
2026-08-26 07:03:59 +00:00
Compare
nalum changed target branch from feat/tokens-warning-role to main 2026-08-26 18:33:38 +00:00
nalum merged commit 133570be03 into main 2026-08-26 18:33:41 +00:00
nalum deleted branch fix/date-only-lateness 2026-08-26 18:33:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!208
No description provided.