mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later day to mark it sick, tap again to clear; past days inert. Writes the sickdays wire format (state sick/cleared + actor, one HLC per write) to sickdays-sync/devices via crdt_sync syncLog. - Mutual exclusion in both pickers: each syncs the other pool on open and refuses a date it holds, naming the pool; an unreadable pool refuses all. - Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the ring with reason "sick_day" (no markFired, no ring_started); StickCheck verdict sick_day, streak skips sick days; stakes card says so. - Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request code 1006) runs a headless engine (poolSyncMain) that pulls both pools from RTDB and rewrites both native caches, so a PC mark arrives with the app closed. The ring decision stays a cache read. - JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision, PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check. pylint hook skipped: verify.py imports sickdays, not in the hook env until the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
84 lines
4.5 KiB
Markdown
84 lines
4.5 KiB
Markdown
# Free days — the phone picker for the shared pool
|
||
|
||
The pool itself is `~/src/utils/freedays` (Python): one global set of days on
|
||
which every gate app stands down, 35 per calendar year, declared for today or
|
||
later, never backdated, refunded only while still in the future. Read its
|
||
README for the rules. This file covers the phone half, which lives in this
|
||
repo because the alarm is the one gate where standing down is a physical
|
||
event — the phone stays silent — rather than an absent dialog.
|
||
|
||
## Where things are
|
||
|
||
| | |
|
||
|---|---|
|
||
| Picker | `phone_app/lib/screens/free_days_screen.dart` (+ `free_days_body.dart`, the shared `day_calendar.dart`), the calendar icon on the home screen |
|
||
| Rules, Dart port | `phone_app/lib/services/free_day_pool.dart` — same record shape as the Python package, byte for byte |
|
||
| Sync | `phone_app/lib/services/free_day_sync.dart` — Firebase only, `freedays-sync/devices/<uuid>/free_days.json` |
|
||
| Ring path | `android/.../FreeDays.kt` + the check at the top of `AlarmReceiver.onReceive` |
|
||
|
||
## The calendar
|
||
|
||
A Monday-first month grid (adapted from punchme's, whose own free days are a
|
||
separate concept and are not read here). One tap per future date: an empty day
|
||
is booked with no reason prompt, a booked day is released and refunded. Today
|
||
and the past are read-only — releasing a day that has started still spends
|
||
it. Tue/Wed/Thu (`workdays`, mirroring `freedays.WORKDAYS`) are shaded; past
|
||
free days stay visible, faded. Months run from January of this year to
|
||
December of next. The "Booked ahead" list below keeps reasons from PC marks
|
||
and its own Release buttons.
|
||
|
||
## Why there is a native cache
|
||
|
||
The alarm fires from Kotlin with no Flutter engine running, so at 08:00 the
|
||
Dart pool is unreadable. Dart hands the current free dates to
|
||
`FreeDays.store` (channel method `setFreeDays`) after **every** mark, release
|
||
and sync; `AlarmReceiver` reads that set before starting `AlarmService`. On a
|
||
free day it logs `free day: not ringing; re-armed for tomorrow` and returns —
|
||
nothing rings, nothing is shown, and the next morning is normal.
|
||
|
||
**Fail closed.** An unreadable or missing cache is "not a free day", matching
|
||
`freedays.is_free_day()` on the PC: a corrupt pool restores the normal rules,
|
||
it never silences the alarm. Verified on the `wake_alarm_api36` AVD
|
||
(2026-09-11, `adb root` + `am broadcast --include-stopped-packages -n
|
||
…/.AlarmReceiver`): today in the set → skip line, no service; a different
|
||
date → service start; a garbage XML file → service start.
|
||
|
||
## What reaches the phone when
|
||
|
||
- **Marked on the phone:** immediate. The cache is rewritten before the sync
|
||
even runs, so an offline phone still stands down on a day it marked itself.
|
||
- **Marked on the PC (`freedays mark …`):** on the phone's next sync: app
|
||
launch, either day picker opening, or the **pre-alarm sync** (21:00 and
|
||
A − 30 min, app closed; `DOCS-sick-days.md`). That sync needs network at
|
||
its slot; if it fails the cache keeps its last sync and the phone rings.
|
||
|
||
A date is never both free and sick: this picker syncs the sick-day pool on
|
||
open and refuses a sick date (`… is a sick day — clear it in Sick days
|
||
first.`), and refuses every booking if that pool cannot be read.
|
||
|
||
## Cross-language check
|
||
|
||
The Dart port is a second implementation of the rules, so it is verified
|
||
against the Python one rather than against itself:
|
||
`scripts/free_days_interop/check.sh` (a pre-commit hook on both inputs)
|
||
writes one pool through `FreeDayPool` — mark, mark today, mark+release a
|
||
future day, mark+release after arrival — replays the same edits through
|
||
`freedays.mark`/`release` into a scratch pool, and fails unless the free set,
|
||
spent set, `remaining` and `is_free_day` verdicts agree. Dart's own numbers
|
||
are never the reference: comparing them to what Python reads from Dart's
|
||
file cannot see a drift in what Dart *writes*. Proved on 2026-09-11 by
|
||
dropping the sticky `consumed` from `release` — the gate reported
|
||
`remaining: dart=33 python=32`. Needs the freedays venv
|
||
(`~/src/utils/freedays/install.sh`).
|
||
|
||
## Do NOT
|
||
|
||
- Don't give this namespace a GitHub mirror. The alarm record keeps its
|
||
mirror because un-migrated installs depend on it; this pool never had one.
|
||
- Don't tombstone a date to un-mark it. crdt tombstones are monotonic; a
|
||
deleted date could never be marked again. Flip `state` to `cleared`.
|
||
- Don't write `consumed` back to `false`. It is the sticky flag that stops
|
||
two devices either side of midnight from refunding a day already used.
|
||
- Don't add a warning, a notification or a ring-time prompt. The pool is
|
||
consulted silently; the count is shown only when the picker is opened.
|