mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-11 19:14:03 +01:00
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
47 lines
2.2 KiB
Markdown
47 lines
2.2 KiB
Markdown
# 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.
|
|
|