Write down the two invariants this session cost

The sequential bootstrap in HomeScreen.initState and the published-age check
are both the kind of thing a later change reverts as tidy-up: one looks like
three independent futures that could run in parallel, the other like a
duplicate of the cache-age check. Both took a three-week silent outage to
find, so they are recorded with the failure rather than just the rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcdngP688mW6rJye3LRkSN
This commit is contained in:
Krzysztof Rudnicki 2026-08-30 16:38:04 +02:00
parent 6be5420fcf
commit 3410b1bc45

View File

@ -45,6 +45,21 @@ passing sent the user back to the start.
deleted, not merely unrouted: the ring screen is reached by walking in from
another room.
**`HomeScreen.initState` must stay strictly sequential.** It runs
`_applyPolicy()` → `_loadSyncState()` → `_syncToPc()` through one
`_bootstrapSync()`, because the last reads what the first two write and
`AlarmPublisher.publish` takes the service as a *parameter* — captured by
value, so a later assignment can never reach it. Running them concurrently
(as they once were) meant the startup publish always passed `null`, and since
that was the only publish site, **nothing published for three weeks** while
the PC honoured a record from 2026-08-09. `_pickEarlier` must publish too, or
the one schedule edit the app offers never reaches the PC.
Relatedly: `publish()` reports a missing *backend* and a missing *store*
differently on purpose. One string for both is what made the above look like a
configuration the user had never done, while Firebase was connected the whole
time.
`RingScreen` takes an injectable `clock`. `Stopwatch` reads the wall clock,
which `tester.pump()` does not advance, so without that seam no widget test
can reach the two-minute floor or the five-minute bypass.
@ -112,6 +127,16 @@ systemd-sleep hook: a suspended or just-booted machine still fires.
- Resolution chain in `get_alarm()`: fresh cache (< 5 min) → live fetch → stale
cache → `FALLBACK_ALARM_HOUR`. The cache bounds remote reads to ~288/day
despite the per-minute tick.
- **A record older than `PUBLISHED_ALARM_MAX_AGE_SECONDS` (14 days) is skipped
at every step, including the cache.** The check is on when the *phone
stamped it*, never on when we fetched — checking the fetch is trivially
defeated, because the cache is rewritten from the same stale record every few
minutes, so `fetched_at` is always fresh however old the record behind it is.
That is why the winning HLC's instant is carried through `_alarm_payload` →
`_sync_transport` → the cache's `published_at`. Expiring is safe because
`FALLBACK_ALARM_HOUR` *is* the phone's baseline. An **undated** record is
trusted, not expired: the GitHub mirror has no per-field HLC, so treating
"cannot tell" as "too old" would break that backend entirely.
- **Degraded reads log at INFO, not WARNING.** `get_alarm()` runs every minute,
so a persistent condition logged loudly is 1440 identical journal lines a day.
`_trigger` emits the single WARNING that matters: it is firing *now* on a
@ -205,5 +230,8 @@ The dev venv lacks `gatelock`, so anything importing `_state` (including
- Don't raise the log level of `_sync`'s degraded-read paths.
- Don't let the bypass skip the walk, and don't add a challenge kind that
hides its own prompt.
- Don't make `HomeScreen.initState` concurrent again, and don't collapse
`publish()`'s two failure messages back into one.
- Don't date a record by when the PC fetched it.
- Don't delete `/etc/sudoers.d/wake-alarm`.
- Don't commit `wake_state.json`.