mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +01:00
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>
88 lines
4.7 KiB
Markdown
88 lines
4.7 KiB
Markdown
# Migration: GitHub Gist → shared `crdt_sync`
|
|
|
|
`phone_app`'s alarm sync was moved off a bespoke GitHub Gist onto the shared
|
|
[`crdt_sync`](https://github.com/kuhyx/utils/tree/main/crdt_sync_dart) library
|
|
(pinned tag `crdt_sync_dart-v0.3.0`), matching how the other companion apps
|
|
sync.
|
|
|
|
## What changed
|
|
|
|
- **phone_app (Dart):** `gist_service.dart` is gone. `AlarmSyncService` stores
|
|
the alarm locally in a `crdt_sync` `LogStore` (one HLC-stamped `Record`) and
|
|
pushes it to `wake-alarm-sync/devices/phone/alarm.json` in `kuhyx/syncs` via
|
|
`crdt_sync`'s `GitHubClient`. The device-flow OAuth scope moved `gist` →
|
|
`repo` (the Contents API needs private-repo access).
|
|
- **shutdown-wrapper.sh (PC, Bash):** now reads 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]`). Config
|
|
moved from `~/.config/wake_alarm/gist_sync.json` `{token, gist_id}` to
|
|
`~/.config/wake_alarm/sync.json` `{token}` (repo-scoped).
|
|
|
|
## Setup after pulling this
|
|
|
|
1. On the phone: Settings → **Connect GitHub** (now requests `repo` scope; the
|
|
old gist-scoped token won't reach the Contents API).
|
|
2. On the PC: `~/.config/wake_alarm/sync.json` → `{ "token": "<repo-scoped>" }`,
|
|
then re-run `install.sh` so `/usr/local/bin/shutdown` picks up the new
|
|
reader.
|
|
|
|
## Surprises / non-obvious decisions
|
|
|
|
- **The consumer is a Python-less Bash reader** (`shutdown-wrapper.sh` runs at
|
|
shutdown, when a Python runtime may not be available), so it can't use the
|
|
`crdt_sync` library — it parses the log with `jq`. What makes that tractable:
|
|
`Hlc.toStr()` is **lexicographically sortable == chronological**, so
|
|
"latest wins" across device dirs is a plain string max, no date math.
|
|
- **The phone is push-only** (sole writer); there is no pull/merge. Its local
|
|
`LogStore` is always the source of truth.
|
|
- Any parse miss still falls back to `WAKE_AFTER_HOURS`, so a bad read can
|
|
never brick the morning wake.
|
|
|
|
# Migration: hibernate-scheduled → per-minute trigger timer
|
|
|
|
The PC is no longer shut down at night, which broke the whole scheduling
|
|
model: the wake time used to be decided once, at hibernate, and baked into the
|
|
RTC. With no shutdown there is no resume, so nothing fired the alarm at all —
|
|
and `wake-alarm.service` was `WantedBy=graphical-session.target`, so on an
|
|
always-on machine it fired at *login* time instead (`_should_run_alarm()` has
|
|
no time gate).
|
|
|
|
## What changed
|
|
|
|
- **New `wake-alarm-trigger.timer`** ticks every minute and runs
|
|
`wake_alarm._trigger`, which fires `morning-routine.service` when the
|
|
current local time is inside
|
|
`[phone alarm, phone alarm + TRIGGER_CATCHUP_MINUTES)`. This is now the
|
|
primary path; the phone can move the alarm at any hour and the PC follows.
|
|
- **New `wake_alarm/_sync.py`** — a Python reader for the same alarm file
|
|
`shutdown-wrapper.sh` parses with `jq`, plus an on-disk cache
|
|
(`~/.config/wake_alarm/alarm_cache.json`).
|
|
- **`wake-alarm.service` lost its `[Install]` section** and is no longer
|
|
enabled; `install.sh` disables it to clear stale symlinks.
|
|
- **Token resolution gained a fallback** to the sibling apps'
|
|
`~/.config/{diet_guard,screen_locker}/sync_token`, in both readers.
|
|
- `shutdown-wrapper.sh` and `sleep-hook.sh` keep working unchanged, now as the
|
|
PC-is-powered-down fallback rather than the main mechanism.
|
|
|
|
## Surprises / non-obvious decisions
|
|
|
|
- **A timer, not a resident scheduler.** Each tick is a fresh process, so
|
|
there is no daemon to die silently and no wall-clock jump to reason about
|
|
across suspend/resume. `Persistent=true` plus the catch-up window is what
|
|
makes a suspended PC still fire on resume.
|
|
- **Per-minute ticks, but a 5-minute cache.** Ticking every 5 minutes would
|
|
also quantise *firing* to 5 minutes (07:03 → 07:05). Ticking every minute
|
|
and only hitting the network when the cache is stale gives exact-minute
|
|
firing at ~288 GitHub reads/day.
|
|
- **Degraded reads log at INFO.** At one tick a minute, a WARNING on a
|
|
persistent condition is 1440 journal lines a day. The single WARNING that
|
|
survives is `_trigger`'s "firing now on a degraded source".
|
|
- **The sync had been dead since the crdt_sync migration.** `wake-alarm-sync/`
|
|
did not exist in `kuhyx/syncs` at all — the phone had never successfully
|
|
pushed, and `~/.config/wake_alarm/sync.json` was never created, so the
|
|
hibernate path had been silently using the `+8h` fallback. The phone app
|
|
needs one successful "Set Alarm & Sync" to create that path.
|
|
- **Still open:** alarms edited directly in the Android Clock app do not reach
|
|
the sync. Closing that needs an `ACTION_NEXT_ALARM_CLOCK_CHANGED` receiver
|
|
in the phone app — planned, not yet built.
|