wake-alarm/DOCS-morning-session-pc.md
2026-11-07 08:12:01 +01:00

146 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The morning session on the PC — the carrot, and what it costs to skip
Companion to `DOCS-morning-session.md` (the phone side). This is how the
phone's morning turns into "no workout lock, no LeetCode lock" on the PC, and
why an aborted morning turns into neither.
## Why
On 2026-09-19 the alarm rang, the full dismiss ran, the session reached the
bathroom tag — and the phone was powered off. Nothing on the PC noticed: the
phone was publishing every finished session and nothing read it, the
workout-lock "carrot" was a hardcoded 05:00–08:30 window in screen-locker
with no link to getting up, and leetcode-guard locked at login regardless. So
finishing the morning was worth at most 30 minutes, and aborting it cost
nothing. Powering the phone off cannot be prevented (device-owner
provisioning, and a 10 s hardware off still wins); it can be *noticed*.
## The rule
The goal is *waking up*, not reaching a clock. So the PC is open from the
moment it is usable (the night lockdown lifts at 05:00) until the alarm has
rung and been dismissed; the dismiss plus the morning session is the "are
you still up" check, and passing it keeps the PC open until 11:00
(2026-09-20 — the first version only opened at 08:30 and locked someone who
got up at 07:00, the opposite of the point):
| the phone says | `exempt_until` | meaning |
|---|---|---|
| nothing started today (`none`) | **alarm + 15 min** (`SESSION_NONE_GRACE`; alarm = the phone's, clamped to ≤ 08:00 like the trigger) | up early is fine; the phone rings at the alarm, and a dismiss that has not produced a session by then means "not up" |
| started, not finished (`in_progress`) | `min(started_at + 90 min, 09:30)` | provisional — the legs take ~40 min; relative to the start because an alarm earlier than 08:00 is legal and a fixed 09:30 would hand an aborted 06:15 session three hours |
| `completed`, finished inside that same cutoff | **11:00** | the carrot |
| `completed` *after* the cutoff | none | a phone switched back on at noon completes the legs then; that un-locks nothing |
| `failed` | none | today's ordinary ladder |
| alarm unknown (unconfigured PC) | none | fail closed |
Fail closed: a sync outage, a refresher that is not running, a bad signature
— all read as "no carrot". Nothing here can make a lock *earlier* than today.
## The plumbing
**wake-alarm is the only Firebase reader.** `wake-alarm-session.timer`
(`*-*-* 05..06:00/10:00`, `07..10:00/2:00` and `11:00:00`, `Persistent=true`,
~133 reads/day — from 05:00 because that is when the login-armed locks
decide) runs `python -m wake_alarm._session --refresh`:
1. `_sync_transport.fetch_firebase_logs()` — every device's `alarm.json`.
2. `_session_payload.newest_session()` — the finished entries of
`morning_sessions` plus `current_session` when it is live, ranked by
`(date, finished-before-live, HLC, device_id)`. **By content, never by
comparing the two fields' stamps**: a phone that lost the network at
finish and never cleared `current_session` still resolves to the finished
morning. The phone's `active` becomes `in_progress`.
3. `_session.to_entry()` + `write_entry()` — an HMAC-signed
`~/.local/state/wake_alarm/morning_session.json` that **always describes
today** (PC-local date): today's session with `exempt_until` signed in, or
a `none` entry (exempt until alarm + 15 min) when the phone has nothing
for today. The entry is also what lets a consumer tell "fresh, no session"
from "the refresher has not run yet" by the date alone.
Dates are the phone's local day throughout. `wake_alarm._state._today_str`
is UTC and must not be used here (screen-locker compares against its local
`today_str`; a UTC date would miss by a day every evening).
**Consumers apply one predicate**: signature verifies ∧ `date == local today`
∧ `exempt_until` present ∧ `now < exempt_until`. Neither re-derives the
cutoffs, neither imports `crdt_sync`, neither talks to Firebase:
- screen-locker `screen_locker/_morning_session.py` → **the early-bird
window *is* this carrot** (`EarlyBirdMixin._is_early_bird_time`, since
2026-09-20; the 05:00–08:30 clock and the extended-to-09:00 reward are
gone). The `wake_alarm_skip` rung defers and banks the early-bird marker;
when the carrot ends, the next run tries a phone/RunnerUp workout before
locking (`early-bird-workout-check.timer` at 09:30:30 and 11:00:30). The
decision record carries `exempt_until` and `outcome`.
- leetcode-guard `leetcode_guard/_morning_session.py` → `_cli._run_lock`
returns `EXIT_OK` with `deferred: …` before `build_client()`. A run
deferral, never ledger state (`decide()` stays pure; a deleted file cannot
mint an unlocked day). `leetcode-guard.timer` gained 09:30 and 11:00 slots
so a deferred run is re-decided at both boundaries. `--check` prints a
`morning` line.
**A PC booted mid-morning** has a missing or yesterday's file until the
timer's `Persistent=true` catch-up lands, and that needs the network. Only
the enforce path, only inside 05:00–11:00, and only when the file is not
today's, both consumers retry for ≤ 180 s (5 s steps) and then proceed as
normal with a WARNING. Status paths never wait; tests pin the retry to 0.
The refresher itself retries an unreadable backend every 5 s for 150 s
(`REFRESH_RETRY_SECONDS`) before exiting 1 with a WARNING: a PC booted at
07:48 had no DNS for its first seconds, and the old single try left the next
chance at the 07:50 tick (2026-11-07).
**`next_step`** (signed with the rest): while the session is `in_progress`,
the label of the first leg not in the phone's `passed` map, else `null`.
Names and labels live in `_session_legs.py` and are pinned to the phone's
`SessionLeg` by `wake_alarm/tests/fixtures/session_legs.json`, asserted by a
Python and a Dart test.
## Known limits (v1, deliberate)
- A lock already on screen is not lifted by a later publish: a session
that completes at 09:45 after the 09:30 lock stays locked. The publish
would have to arrive before the boundary. (The 05:00 login-arm no longer
hits this: the day starts exempt.)
- A session finishing inside ~2 min before 09:30 may be seen as
`in_progress` at the 09:30 tick and lock; bounded by the timer cadence.
## The workday stick (2026-09-23)
The carrot above only pulls on a day the PC is actually being used to enjoy
it — a weekend. On a workday the user leaves for the office regardless, so
losing/keeping the PC's lock was worth nothing, and powering the phone off
at ring time (which stops the alarm dead — nothing left to sync, no session
ever recorded) cost nothing either.
On **Tuesday/Wednesday/Thursday** — `wake_alarm._constants.STICK_DAYS`, the
same weekdays screen-locker already calls "relaxed"
(`_weekly_check.is_relaxed_day`) and leetcode-guard already prices as cheap
(`Pricing.doubled_days`) — a genuine miss (`failed`, or `none` past its own
`SESSION_NONE_GRACE`; a *late* `completed` is not a miss, the walk still
happened) costs tomorrow's leniency in both apps instead of just resetting
to normal. `wake_alarm._workday_penalty.maybe_apply()` runs at the end of
every `_session.refresh()` and, when a stick-day miss is detected and today
is not a free day, signs and writes
`~/.local/state/wake_alarm/workday_penalty.json` (`{"penalty_date":
"<tomorrow>", "issued_date": "<today>"}`) — deliberately a separate file
from `morning_session.json`, since that one always describes *today* and is
overwritten at the date rollover, so it cannot carry a marker forward.
screen-locker and leetcode-guard each read it with the same HMAC + date
predicate they already use for `morning_session.json`.
A phone-side stick (locking down distracting apps on the phone itself,
signalled to `phone-focus-mode`'s `focus-owner`) is the other half of this
and lives in `phone_app/`, not here — see its own commit/doc for the exact
signal shape.
## Commands
```sh
python -m wake_alarm._session --refresh # fetch, select, rewrite the file
python -m wake_alarm._session --show # the verified file + a verdict line
python -m wake_alarm._energy # {"date", "level"} or exit 1
systemctl --user list-timers | grep wake-alarm-session
journalctl --user -u wake-alarm-session.service -n 20
leetcode-guard --check # the "morning" line
```