wake-alarm/wake_alarm
Krzysztof kuhy Rudnicki 95be57b575 Make the alarm fully automatic and un-weakenable (08:00, earlier-only)
The alarm was opt-in: it only armed when the user tapped "Set Alarm & Sync to
PC", so it could be defeated by simply not tapping it, or by cancelling it the
night before. It is now automatic and there is no lever a half-asleep user can
pull that buys them sleep.

- AlarmPolicy bakes the schedule in at 08:00 daily. The only edit the UI
  offers is moving the NEXT alarm earlier; setEarlier() refuses anything at or
  after the baseline, and refuses to relax an active override. Earlier-only is
  the whole design: every available change costs the user sleep.
- The ratchet resets after each fire, so an early start is one-shot. A one-way
  ratchet with no reset is a trap, not a defence — the first 05:00 would
  otherwise be permanent with no way back short of reinstalling.
- load() re-validates the stored value and ignores anything at or after the
  baseline. SharedPreferences is not a trust boundary.
- No cancel. The Kotlin handler is deleted, not just the Dart wrapper, so the
  capability genuinely does not exist rather than being one invokeMethod away.
- Arming happens in main()'s init seam and via AlarmWatchdog, a daily
  self-rescheduling check that re-arms if nothing is scheduled — closing the
  gap where a force-stop or a failed re-arm goes unnoticed until the morning
  it does not ring. BootReceiver re-establishes it, since setInexactRepeating
  does not survive a reboot.
- Publishing to the PC is automatic too; it used to need the same tap.

PC half, so the two agree with the network down:
- FALLBACK_ALARM_HOUR 13 -> 8, matching the phone's baseline. At 13:00 a
  Firebase outage silently moved the alarm five hours LATER — the one
  direction the design refuses to allow.
- _trigger._clamp_to_baseline() refuses any resolved alarm later than the
  baseline, whatever its source. Without it the PC would faithfully reproduce
  a stale or tampered remote value and the forbidden direction stays reachable
  through sync.

Tests: 465 Python at 100% branch coverage, 79 Dart. Both ratchet directions
are covered, per the standing rule about monotonic counters. test_sync.py and
test_trigger.py were split (payload, token, CLI, clamp) to stay under the
250-line gate; the split lost indirect coverage of extract_hlc/newest_alarm,
which now have direct tests instead.

`python -m wake_alarm._sync --show` and `_trigger --dry-run` both re-run and
still print real output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
2026-08-27 23:03:00 +02:00
..
tests Make the alarm fully automatic and un-weakenable (08:00, earlier-only) 2026-08-27 23:03:00 +02:00
__init__.py Add tests and fix pre-commit issues across all projects 2026-04-12 20:45:24 +02:00
_alarm_display.py feat: split oversized modules for 500-line limit, fix kasa coverage gap 2026-06-14 07:19:37 +02:00
_alarm_flash.py Phase 4: wire sink reactivation into the alarm (finishes item 6) 2026-08-27 20:14:00 +02:00
_alarm_payload.py Discover alarm device directories instead of reading a fixed phone path 2026-08-09 17:06:43 +02:00
_alarm_session.py Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
_alarm_state.py Phase 5: the power button no longer ends the alarm (item 2) 2026-08-27 20:26:37 +02:00
_alarm_timers.py Phase 4: wire sink reactivation into the alarm (finishes item 6) 2026-08-27 20:14:00 +02:00
_alarm_view.py Size the alarm's type in pixels, and gate every screen on fitting 768px 2026-08-03 16:04:27 +02:00
_alarm.py Phase 5: the power button no longer ends the alarm (item 2) 2026-08-27 20:26:37 +02:00
_audio.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_beep_loop.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_brightness.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_challenges.py Fire alarm daily, harden dismissal challenge; ignore wake_state.json 2026-07-10 23:14:40 +02:00
_constants.py Make the alarm fully automatic and un-weakenable (08:00, earlier-only) 2026-08-27 23:03:00 +02:00
_correlation.py Phase 5: the power button no longer ends the alarm (item 2) 2026-08-27 20:26:37 +02:00
_countdown.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_curtains.py Phase 7: curtains open on the alarm, no-op until hardware exists (item 5) 2026-08-27 21:00:11 +02:00
_dismiss_view.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_escalation.py Phase 2b: add the 5-minute audio failsafe, and retune escalation to fit it 2026-08-26 23:17:41 +02:00
_escape_flow.py Size the alarm's type in pixels, and gate every screen on fitting 768px 2026-08-03 16:04:27 +02:00
_gating.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_inhibit.py Phase 6: vmbox becomes the gate for the system scripts (item 10) 2026-08-27 20:49:16 +02:00
_lock_setup.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_log_dedupe.py Phase 1: structured JSONL logging with cross-process dedupe 2026-08-26 23:00:08 +02:00
_log_sink.py Phase 1: structured JSONL logging with cross-process dedupe 2026-08-26 23:00:08 +02:00
_logging_setup.py Phase 1: structured JSONL logging with cross-process dedupe 2026-08-26 23:00:08 +02:00
_reactivate.py Phase 4: wire sink reactivation into the alarm (finishes item 6) 2026-08-27 20:14:00 +02:00
_rounds.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_screen_flash.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_sinks.py Replace sleep-and-hope audio waits with per-cycle presence checks 2026-07-11 18:51:03 +02:00
_smart_plug.py Extract wake_alarm from testsAndMisc as a standalone repo 2026-06-22 12:31:40 +02:00
_startup.py Phase 7: curtains open on the alarm, no-op until hardware exists (item 5) 2026-08-27 21:00:11 +02:00
_state.py Extract wake_alarm from testsAndMisc as a standalone repo 2026-06-22 12:31:40 +02:00
_sync.py Discover alarm device directories instead of reading a fixed phone path 2026-08-09 17:06:43 +02:00
_tone.py Phase 3: remove the max-fan ramp, split _alarm.py, wire the new modules 2026-08-26 23:46:10 +02:00
_trigger.py Make the alarm fully automatic and un-weakenable (08:00, earlier-only) 2026-08-27 23:03:00 +02:00