The PC is no longer shut down at night, which broke the scheduling model
outright. The wake time used to be decided once, at hibernate, and baked
into the RTC by shutdown-wrapper.sh. With no shutdown there is no resume,
so sleep-hook.sh never ran and nothing fired the alarm at all.
Worse, wake-alarm.service was WantedBy=graphical-session.target and
enabled by install.sh, while _should_run_alarm() gates on day / dismissed
/ workout-logged but has no *time* gate. On an always-on machine that
target is reached on every login, so the alarm fired at login time on any
alarm day — which is every day.
Replace both with a systemd user timer ticking once a minute:
- _sync.py reads the alarm the phone pushed to kuhyx/syncs, mirroring
shutdown-wrapper.sh's jq parse so the two readers cannot drift. Caches
each read, then degrades fresh cache -> fetch -> stale cache ->
FALLBACK_ALARM_HOUR, so a network blip can never cost a wake-up.
stdlib-only: the units run /usr/bin/python, so a dependency here is a
production install-path hazard.
- _trigger.py fires morning-routine.service (never wake-alarm.service —
that skips the workout lock) when local time is inside
[alarm, alarm + TRIGGER_CATCHUP_MINUTES). The catch-up window covers a
suspended or just-booted machine; starting an already-active unit is a
no-op, so an alarm on screen needs no extra dedupe state.
- wake-alarm.service loses its [Install]; install.sh disables it.
A timer rather than a resident scheduler: each tick is a fresh process,
so there is no daemon to die silently and no wall-clock jump to handle
across suspend/resume. Ticks are per-minute for exact-minute firing, but
only hit the network when the cache is older than 5 minutes — ~288
GitHub reads/day, and phone changes land within ~5 minutes.
Degraded reads log at INFO, not WARNING: at one tick a minute a
persistent condition would be 1440 identical journal lines a day. The one
WARNING kept is _trigger firing on a non-phone alarm time.
Token resolution gains a fallback to the sibling apps' repo-scoped
sync_token, in both readers, so one PAT covers every app on kuhyx/syncs.
Note this surfaced that wake-alarm-sync/ does not exist in kuhyx/syncs at
all: the phone has never completed a push since the crdt_sync migration,
so the hibernate path had been silently using the +8h fallback. Fixing
that is a phone-side action (reconnect GitHub with repo scope, then one
"Set Alarm & Sync"). Alarms edited directly in the Clock app still do not
reach the sync — that needs an ACTION_NEXT_ALARM_CLOCK_CHANGED receiver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
phone_app: the alarm now syncs through the shared crdt_sync transport
(v0.2.0) into the fixed private repo kuhyx/syncs, replacing the bespoke
Gist. AlarmSyncService is the sole writer: it holds the alarm locally in a
crdt_sync LogStore (one HLC-stamped Record) and pushes it to
wake-alarm-sync/devices/phone/alarm.json. gist_service.dart is deleted;
SyncSettings drops the Gist id and keeps only a token; the device-flow scope
moves from `gist` to `repo` (the Contents API needs private-repo access).
shutdown-wrapper.sh: the PC reader now fetches that alarm from the Contents
API (Accept: application/vnd.github.raw, so no base64) and parses the
crdt_sync Record Log with jq (.alarm.fields.alarm_hour[0]). Single-file read
of the sole "phone" device; unchanged fallback to WAKE_AFTER_HOURS on any
miss so a parse failure can never brick the wake. Config moves from
gist_sync.json {token, gist_id} to sync.json {token}.
Verified: flutter analyze clean + tests pass; shellcheck clean and the full
get_synced_alarm_epoch verified against the real Log format; on-device the
app builds, installs with adb install -r (no data loss), launches and
renders. Live round-trip needs a one-time repo-scoped re-auth on the phone
and ~/.config/wake_alarm/sync.json on the PC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
- _constants.py: ALARM_DAYS now every day (was Mon/Fri/Sat/Sun);
DISMISS_CODE_LENGTH 8->10 and DISMISS_ROUNDS_REQUIRED 2->4 so
dismissing forces staying awake longer.
- _challenges.py / _alarm.py: expanded dismissal challenge logic, with
matching test updates.
- shutdown-wrapper.sh: updated hibernate/rtcwake wrapper.
- .gitignore: restore the missing rule for wake_alarm/wake_state.json
(HMAC dismissal runtime state — must never be tracked, per CLAUDE.md).
Tests: 191 passed, 100% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
Rewrites python_pkg.wake_alarm imports to wake_alarm, vendors the
shared configure_logging helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
untracks wake_state.json (runtime HMAC state, now gitignored). Scaffolds
standalone lint/test config copied from the already-corrected diet_guard
scaffold (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage), plus
the wave.Wave_write generated-members fix this package's _audio.py needs.