The shared pool (~/utils/freedays) was CLI-only: the gates on the PC stood down, but the phone kept ringing at 08:00 and there was nowhere to mark a day from bed. This is the one picker for every app. FreeDayPool is a Dart port of the Python rules with the identical record shape (id = ISO date; state/consumed/actor/reason on one tick; un-mark is a state flip, never a tombstone), verified by a real cross-language round trip: a log written here reads back through freedays.is_free_day and remaining() with the same verdicts and count, and the PC's live pool reads here as the same 27 of 35 that `freedays status` prints. The alarm fires from Kotlin with no engine running, so the pool is mirrored into a native SharedPreferences set after every mark, release and sync (channel method setFreeDays), and AlarmReceiver consults it before starting AlarmService. Fail closed: a missing or corrupt cache rings. All three branches exercised on the API 36 AVD by broadcasting the receiver. Firebase only for this namespace -- no GitHub mirror; the alarm record keeps its own because un-migrated installs depend on it. Known gap, documented rather than papered over: a day marked on the PC reaches the phone at the next app launch or picker open, not overnight. The ring path takes no background dependency by design, so mark on the phone, or open the app once after marking on the PC. Verified on the phone (release build, 23181JEGR08034): picker shows the budget, marks 2026-09-30 with a reason, pushes it to freedays-sync/devices/<uuid>, releases it with a full refund back to 35. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pBgaXSJKyY3DarYpDatDF |
||
|---|---|---|
| .github/workflows | ||
| bottles | ||
| docs/explanations | ||
| phone_app | ||
| prompts | ||
| scripts | ||
| wake_alarm | ||
| .fvmrc | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .versioncode-offset | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| DOCS-free-days.md | ||
| DOCS-phone-alarm.md | ||
| DOCS-vmbox.md | ||
| install.sh | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
| TODO-file-length-250.md | ||
| TODO-hardening-continue.md | ||
| uninstall.sh | ||
| wake-alarm-trigger.service | ||
| wake-alarm-trigger.timer | ||
wake_alarm
The phone is the alarm. phone_app/ arms and rings it, and getting past it
means walking between two NFC tags and answering three questions. This repo's
Python half no longer wakes anyone: all it does is read the alarm time the
phone published and start the morning routine (the workout lock) at that time.
The dismiss sequence
Strict, ordered, and latched — every stage required, and a stage once passed never reverts:
tag 1 -> 25 steps + 2 min -> tag 2 -> 3 challenges
The two tags must be distinct and live in different rooms; the same id in both slots is refused at registration. Answers are cumulative, so a wrong one costs the attempt and nothing else.
One fallback, deliberately narrow. After 5 minutes of ringing — or immediately if NFC is off or fewer than two tags are registered — the two tag stages are skipped and the challenge count rises to 6. The walk is never skipped. Without a hatch of some kind, a dead tag at 06:00 leaves an alarm that cannot be switched off at all.
Details, and the things that are a hard crash or a silent no-ring:
DOCS-phone-alarm.md.
The PC half
A per-minute systemd user timer (wake-alarm-trigger.timer) reads the alarm
the phone published through the shared crdt_sync transport, and at that time
starts morning-routine.service, which runs the workout lock. It records a
once-per-day latch so the rest of the catch-up window does not restart it.
That is all it does. There is no alarm window, no audio, no smart plug, no
rtcwake, and no shutdown wrapper.
Install
bash install.sh # trigger timer
bash uninstall.sh # removes it, and any retired waking machinery
install.sh installs the package into system Python's user site-packages (the
units run /usr/bin/python directly, not a venv — see CLAUDE.md) and enables
the timer. It also clears the retired pieces an older install left behind,
notably /usr/local/bin/shutdown, which is root-owned and would otherwise keep
turning shutdown now into a hibernate forever.
Reading the phone's alarm uses the shared ~/.config/crdt-sync/firebase.json.
The GitHub mirror (fallback only) needs a repo-scoped token at
~/.config/wake_alarm/sync.json as {"token": "..."}.
Usage
python -m wake_alarm._sync --show # what alarm time the PC resolves
python -m wake_alarm._trigger --dry-run # what the next tick would do
journalctl --user -fu wake-alarm-trigger.service # watch it live
Development
python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/pre-commit install && .venv/bin/pre-commit install --hook-type pre-push
.venv/bin/python -m pytest wake_alarm/tests/ --cov=wake_alarm --cov-branch --cov-fail-under=100
cd phone_app && flutter test && flutter analyze
The dev venv lacks gatelock, so anything importing _state (including
_trigger) needs /usr/bin/python3, not .venv/bin/python.
See CLAUDE.md for the sync resolution chain, the cross-repo coupling to
morning_routine, and production deployment gotchas.