wake-alarm/DOCS-architecture.md
Krzysztof kuhy Rudnicki b3e8c237af Phase 3b: finish item 14 -- delete the fan script, clean up installed state
Completes the fan removal started in 9e0cd12, which took the Python out but
left the shell side behind.

- Delete wake-alarm-fans.sh.
- install.sh step 7 now REMOVES the retired script instead of installing it.
  An upgrade has to actively clean up: a machine that ran the old installer
  keeps a passwordless-root sudoers entry for a script that no longer exists,
  which is worse than the fan ramp ever was.

Verified on this machine rather than assumed -- both stale artifacts really
were present:

    /usr/local/bin/wake-alarm-fans.sh                        (existed)
    kuhy ALL=(root) NOPASSWD: /usr/local/bin/wake-alarm-fans.sh   (granted)

After the cleanup: script gone, /run state gone, the fan NOPASSWD line gone,
`visudo -c` parses, `sudo -n true` still works, and the rtcwake NOPASSWD entry
the alarm actually needs is untouched. The sed was rehearsed against a fake
sudoers file first, since getting it wrong locks the user out of sudo.

- AGENTS.md: drop the stale "ramps fans to 100%" description, and point the
  module map at the new DOCS-architecture.md (moved there to keep AGENTS.md
  under the 250-line cap).
- DOCS-architecture.md: where each behaviour now lives after the split, the
  HDMI-silence root cause, and the two timing invariants that must not
  collide -- both of which silently gut the alarm if broken.

404 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
2026-08-27 20:04:01 +02:00

2.2 KiB

wake_alarm architecture

Reference for the module layout after the 2026-08-26 split, and the two failure modes that motivated it.

Where behaviour lives

_alarm.py owns the window and the dismiss flow and nothing else. When looking for behaviour, start here:

Question Module
Should the alarm run at all today? _gating (no time gate — that is _trigger)
What should it sound like right now? _escalation (pure), driven by _beep_loop
Why did it go quiet? _escalation — the failsafe at FAILSAFE_SILENCE_SECONDS
Why is there no sound on HDMI? _sinks + _reactivate (see below)
Challenge presentation / rounds _rounds, _countdown, _dismiss_view
Recurring timers _alarm_timers (a mixin; they reschedule via root.after)
Screen flash _screen_flash (~1.3 Hz, deliberately under the 3 Hz threshold)
Startup hardware _startup (display, brightness, plug + pre-roll)
Structured logs _logging_setup, _log_sink, _log_dedupe, _correlation

The audio failure this fixed

_activate_all_outputs() ran once, at startup, and _parse_profile_line drops any profile reporting available: no. At wake time the monitor has not finished its HDMI handshake, so the profile is skipped — and while a card's profile is off its sink does not exist, so _play_on_all_sinks's per-cycle re-enumeration can never find it. _reactivate re-runs activation on a decaying schedule and logs the enumerated sink list each pass.

A powered-off speaker is not detectable from software — the analog sink is present and IDLE either way. That is a hardware problem; see DOCS-speaker-recommendation.md.

Timing constants that must not collide

FAILSAFE_SILENCE_SECONDS (300) silences audio only; the lock, challenge and workout gate persist. Two invariants are locked in by tests in test_escalation.py, because breaking either silently guts the alarm:

  • PHASE_MEDIUM_END * 60 < FAILSAFE_SILENCE_SECONDS — otherwise the escalation never leaves its softest rung before going quiet.
  • ESCAPE_OFFER_AFTER_SECONDS (900) > FAILSAFE_SILENCE_SECONDS — otherwise the escape hatch appears exactly as the alarm stops being annoying.