Commit Graph

167 Commits

Author SHA1 Message Date
1a305c0627 Sick days: per-locker keep (sickdays 0.2.0) for the workout lock
The workout lock, its explanation, the status view and the workday penalty's
penalised day now ask _sick.stands_down_workout: a sick day that keeps the
workout locker behaves as a normal day. The penalty's ring day is judged by
the alarm switch. Shutdown (20:00 floor, grace floor, base, target, preview
scripts) keeps is_sick: the floor belongs to the day, not to a locker. The
one-tap button still keeps nothing. Test pool fixture and preview harness now
redirect every sickdays resolve_paths binding, so keep edits never reach the
real pool. Pins bumped to sickdays-v0.2.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
2026-10-16 15:18:49 +02:00
7d8daca111 sick days: one shared sickdays pool, 20:00 shutdown floor, one-tap lock button
A sick day is now a date in the shared sickdays pool (utils sickdays 0.1.0)
and nothing else. Removed: the 7/30/90-day budget, the escalating lockout
countdown, the justification form, the commitment prompt and its double
debt, the workout-debt counter, the -60 min shutdown cut and its
sick_day_state.json, the post-sick-day --verify-workout mode, the sick
auto-upgrade, scripts/revoke_sick_day.py and their tests.

- Lock screen: "I'm sick" on the retry, no-phone and both Tue-Thu screens;
  one tap -> sickdays.add_sick_day(today) -> sick floor -> best-effort
  background sickdays.sync_quietly -> unlock. Refusals (free day,
  unreadable pool) are shown and the lock stays.
- Shutdown: the reset resolves with earned_time.resolve(..., sick=True)
  (max(20:00, ladder), capped 23:00) and saves the sick lift with the grace
  lift, so later credits absorb it instead of stacking on 20:00. A day
  marked after the reset is lifted by the new _sick_floor live pass (every
  5-min tick, --sync-only, --apply-bonuses, and the tap itself).
- Workout lock and grace floor stand down on a sick day; workouts that day
  credit normally through the sync paths.
- wake-alarm workday_penalty.json is void when its issued_date or
  penalty_date is a sick day.
- Status window, i3 line, CLI --status, MCP flags and web reasons read
  sickdays.
- scripts/migrate_sick_history.py imports sick_history.json's dates via
  sickdays.import_legacy, verifies and removes the file; idempotent, run by
  install.sh.
- Pins: earned-time 0.11.0, freedays 0.4.0, sickdays 0.1.0 (tags pending);
  pre-commit 4.7.0, ruff 0.17.1 (freshness gate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
2026-10-16 13:30:39 +02:00
96ad163ee6 earned_time 0.9.0: tutor earner renamed automation -> tutor
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Pins bumped at all four sites; tests follow TUTOR / tutor_bonus_units.
pylint 4.1.3 (freshness gate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
2026-10-12 16:38:25 +02:00
5eb18b92ca deps: earned_time 0.8.0 (tutor credit per active minute); tutor rung tests for minute rows and legacy blocks
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:32:43 +02:00
384ffcda7d tests: earned_time 0.7.0 tutor policy (confirmed, 15 min per block); hook envs on 0.7.0
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:15:35 +02:00
b37b7559d8 workout poke moves to tcp/8774: 8773 is book-guard's desktop wrapper, whose window the poke server had been answering since 2026-10-09 (phone app rebuilt, selftest default, contract doc)
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrBEzjPRdx5oyp8NtTLW5F
2026-10-10 09:38:21 +02:00
fc5453495c drop the weekly shutdown bonus entirely; every bonus stops at the shutdown ceiling
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:59:50 +02:00
87c50825f9 drop the banked extra-workout shutdown bonus (no new hours from 2026-W42; streak + early-bird kept)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
A 5+ workout week no longer banks (count - 4)h, and the every-4-week streak
milestone +1h is gone. The streak counter and the 09:00 early-bird extension
stay. The applying path is untouched, so 2026-W41's already-banked 2h keeps
applying through Sunday 2026-10-11; old weekly_shutdown_bonus_hours keys load
and are preserved as-is. The next-week status preview no longer promises
bonus hours.

Claude-Session: https://claude.ai/code/session_01WfVGhY8XcsQsC7qz9UU8xb

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:32:40 +02:00
2140ca5180 deps: earned_time 0.6.1 (tutor cutover; new-gate rungs spare the floor)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:24:48 +02:00
3a2ad0245c deps: ruff 0.17.0 (CPY001 ignored: no copyright policy)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 22:05:02 +02:00
215f03ecfc earned_time 0.6.0: maturity-aware penalty start (first_credits); grace floor covers every gate earner
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 21:55:21 +02:00
bbc14ec04c earned_time 0.6.0: per-day registry consumer, sleep-ladder date-proof tests, pin v0.6.0
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 21:36:53 +02:00
52fd8217f5 test: pin credit time in TestRealLedgers so the grace floor can't win after 19:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
2026-10-09 19:24:34 +02:00
965b9c4ebb Sync: Firebase only — drop the GitHub transport (kuhyx/syncs stays a frozen archive)
The PC push aborted before it ever tried Firebase whenever the GitHub PAT
was missing ("Workouts NOT synced: no sync token"), so once the PAT was lost
this machine published nothing although Firebase was configured and live.
The phone has not written the GitHub repo since 2026-08-21.

- sync_client() / sync_client_or_reason(): firebase_client_for("screen_locker")
  only, proven with can_access_remote(), healed once from a sibling app's
  session, else a warning naming what will not sync (push AND pull) plus a
  recorded degraded source. No config -> warning, no fallback.
- _manual_push and scripts/tombstone_sync_record.py take their client from
  sync_client_or_reason(); the push result carries the reason.
- Deleted: read_sync_token, remote_client, the mirror client, _push_outcome
  (the half-landed-push case no longer exists), SYNC_TOKEN_FILE and
  SYNC_REPO_OWNER/NAME, the sync_token test fixture. Status backend is
  "firebase" or "none".
- crdt-sync pinned to v0.9.2 in pyproject, requirements.txt and
  install_systemd.sh (the installer still said v0.9.0).

kuhyx/syncs/screen-locker-sync is left untouched as an archive; no code
reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
2026-10-09 19:18:56 +02:00
a826cfa166 workout-poke: instant phone credit over LAN (signed POST :8773) and Firebase stream
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
2026-10-09 19:05:51 +02:00
aedfdbc7f5 Credit: one reward per workout slot, desktop notification, --help, runnerup-watch ignores re-pulls
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
2026-10-09 19:05:08 +02:00
373a075606 Shutdown: walks as workout, rest days, grace floor, workout ledger (earned-time 0.4.0)
- A day's RunnerUp walks (_Walking.tcx) count once their moving time sums to 40 min
- Rest days: declared ahead, 2 per ISO week, HMAC-signed; pay the first workout unit
- Grace floor: shutdown >= first task + 60 min (capped); skipped on a sick day
- Workout ledger: one signed credit row per log credit slot and rest day
- Day-aware ladder amounts from earned_time 0.4.0; preview_shutdown dry-run script

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrjUTo1yhgAsUUasdw4bpG

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 10:43:45 +02:00
f77e601893 runnerup-watch: credit a RunnerUp run the moment its TCX upload finishes; lock the workout-log write
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 20:36:38 +02:00
d1817675c4 earner-bonus: add the new modules, unit and tests missed by 7851d59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 20:44:01 +02:00
7851d592fd shutdown bonus: apply the moment a gate writes its ledger (earner-bonus.path)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 20:42:13 +02:00
ec7f4805f7 pin earned-time-v0.3.0 (registers the automation earner)
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Has been cancelled
Tests / web-coverage (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Tests / flutter-coverage (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 15:20:10 +02:00
2dd719e6a4 earned-time-v0.2.0 (anki); shutdown projection prices each day on its own date
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 14:18:42 +02:00
275a0862fc shutdown: minutes end to end (*_MINUTES schedule keys, HH:MM helper args, to_hours removed)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:48:24 +02:00
ed5fa133d0 shutdown: derive base and bonuses from the shared earned_time registry
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
The base hour, the workout/LeetCode/reading hours and the reading cut date
were duplicated here and in steam-backlog-enforcer. They now come from
earned_time (kuhyx/utils, earned-time-v0.1.0): the reset sums the registry,
the live pass iterates its flat earners (stamps unchanged:
<name>_bonus_date), and _leetcode_bonus/_reading_bonus are replaced by the
shared HMAC ledger reader. A gate registered later needs no code here.

The schedule still stores whole hours; _earned.to_hours raises on a
remainder rather than flooring it. Verified identical to the old code on
today's real ledgers and log (reset target, stamps, live passes, base hours).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aktgaq93yt51c6D3X5Fzsm
2026-10-04 18:55:51 +02:00
0b0cda6782 Morning-session reader moves to gatelock 0.9.0
_morning_session.py is now a thin binding of screen-locker's path and retry
budget over gatelock.morning_session, shared with leetcode-guard and
book-guard; the verdict tests live there once. Same behaviour, same string.
Also bumps mypy 2.4.0 / pylint 4.1.1 / ruff 0.16.10 (newest stable gate) and
the stale gatelock pins in install_systemd.sh and the pylint hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbkN4ZB4aUPJPzR2prRan
2026-10-02 14:05:40 +02:00
deb89b3e03 Relaxed days from freedays.WORKDAYS; delete the dead rtcwake mixin and its stale ALARM_DAYS
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 20:24:01 +02:00
0921146044 Tests can no longer write the live desktop Firebase credential
`flutter test` runs on a Linux host, so credentialStore()'s Platform.isLinux
branch wrote $HOME/.config/screen_locker/firebase_auth.json -- the refresh
token the screen-locker daemon signs in with. firebase_backend_google_test's
"stores the account under the email Firebase reports" case overwrote it with
the fixture token 'refresh' on every local run; the daemon only survived by
self-healing from a cached token.

Dart:
- lib/services/desktop_config_dir.dart: one resolver, desktopConfigRoot(),
  for every desktop path (the credential file's read/write/delete and the
  ~/.config/crdt-sync account read). @visibleForTesting override; under
  FLUTTER_TEST it throws StateError if the root is, or is inside, the real
  $HOME/.config -- so a test that bypasses the override fails loudly instead
  of writing. HOME unset now yields null instead of a cwd-relative .config.
- The crdt-sync read resolves the root outside its catch-all, so the guard
  cannot be swallowed as "not connected".
- test/flutter_test_config.dart gives every test its own temp config root.
- The Google sign-in test now asserts the token landed in that temp root.
- path_provider is unbound under flutter test (MissingPluginException), so
  the getApplicationSupportDirectory callers cannot reach live dirs.

Python (tests only): isolate_sync_token now patches every import-time
binding of SYNC_TOKEN_FILE (_constants, _manual_push, _sync_status were
missed), and test_home_isolation scans all runtime modules for any Path
still bound inside the real ~/.config/screen_locker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFwA9nymhUdTrVF65kigTV
2026-09-27 08:35:18 +02:00
6a07dc56aa Reading hour from book-guard: +1h shutdown; base 20->19 from 2026-10-01
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter-coverage (push) Waiting to run
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 19:02:24 +02:00
584177ec2e Lose Tue/Wed/Thu leniency when wake-alarm signals a missed prior-day ring
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter-coverage (push) Waiting to run
Tests / web-coverage (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 21:23:31 +02:00
9ab7c09c79 The early-bird window is the wake-alarm carrot, not a clock
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
05:00-08:30 (09:00 with the extended reward) locked nobody but did hold the
workout lock off on a clock that knew nothing about whether the user was
up, next to the morning-session rung that did. EarlyBirdMixin._is_early_bird_time
is now that rung's verdict: open before the alarm, during the session, and
until 11:00 once it completed in time -- exactly what wake-alarm signs. The
wake_alarm_skip rung banks the pending marker so the carrot's end still tries
a phone/RunnerUp workout before locking; early-bird-workout-check.timer moves
to 09:30:30 and 11:00:30, the two possible ends. The consumer's boot-retry
window starts at 05:00 with the refresher.

Also: the sick screen lists four past justifications, one line each -- six
full-length ones pushed it 7 px past 1366x768, which the screens-fit gate
only sees with a real history on disk. Takes @types/node 26.6.2, eslint 10.11.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTdfXjbGYYrY7Quxcfz6mg
2026-09-20 08:39:44 +02:00
53bbdda419 Read the morning-session carrot from wake-alarm's signed file
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
The wake_alarm_skip rung has been dead since the repos split: it read
../wake_alarm/wake_state.json (no longer a path) for a skip_workout field
nothing ever wrote, and answered "no skip" for a year without an error.
It now reads ~/.local/state/wake_alarm/morning_session.json, written
HMAC-signed by wake-alarm every two minutes through the morning, and the
whole decision is: signature ok, dated today, now < exempt_until. A
completed-in-time morning defers the lock to 11:00; a live one until
min(start + 90 min, 09:30); nothing else changes the ladder. The enforce
path waits up to 30 s for a freshly booted PC's refresher; status paths
never wait. Takes Flutter 3.47.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
2026-09-19 16:41:17 +02:00
81284f160b Pin the clock in the sync-degradation tests; CI ran them inside the early-bird window
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 10:07:11 +02:00
c0099dff94 Refuse to build a lock screen over a workout already logged today
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:58:13 +02:00
9b84af7537 Count the manual budget per workout, keep a synced twin's id, and let a logged workout outrank an expired early bird
Three faults that together locked kuhy out on 2026-09-18 with a workout
on disk and "Manual-workout budget exhausted" on the screen.

1. The football logged at 00:02 on 2026-09-13 was rekeyed to its local
   day (keeping workout_id manual:2026-09-12T14:30), published, and pulled
   straight back by the 09:40 tick as a new record: _already_ingested only
   matched sync_record_id (which a locally logged entry never carries) and
   the chokepoint re-derived the id from the corrected date, so the same
   workout landed twice. Ingest now files a synced manual under the wire
   id it arrived with and recognises an entry whose own workout_id is that
   record.

2. count_in_window counted raw manual_workout entries, so the twin took a
   second budget slot for a week (2/2) while the weekly count, which goes
   through credit_key, saw one. The budget now shares credit_key -- the
   rule the docstring says every counter must share -- and distinct
   workouts on one day still cost a slot each.

3. _check_today_state_exits returned False (full lock) from the expired
   early-bird branch before ever consulting has_logged_today, so a manual
   walk entered on the lock screen at 09:32 held for exactly one tick: the
   09:35 run locked again ("also=workout_logged_today"), and kept doing so
   every 5 minutes while the day's marker lasted, which is how a sick day
   got spent over a logged workout. The explain chain already treated the
   expired marker as non-terminal; the live ladder now falls through the
   same way.

_manual_sync.py went over the 250-line cap and is split: draft
reconstruction lives in _manual_sync_draft.py, re-exported from the
original path so patch targets are unchanged.

Replayed on the real log: count_day_credits("2026-09-13") == 1 while the
old count_in_window said 2; after the fix the status API reports
used_7d=1, exhausted=False. test_manual_rekey_dedup.py fails 3/4 on HEAD.
1334 tests, 100% coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:44:28 +02:00
a22ec380ca Credit RunnerUp runs by the day rule, from the WebDAV drop dir, and never twice
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:14:17 +02:00
02a93f1322 Shutdown +1h on a day with a LeetCode solve; base moves 21:00 -> 20:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:06:26 +02:00
86b63108c7 Tombstone sync records durably: a ledger the PC re-pushes every tick
manual:2026-09-13T14:30 -- the synced copy of the football that this
morning's day-key rekey let back in under a second id -- made every
workout-sync tick log "Manual-workout budget exhausted -- not ingested".
Tombstoning it once did not stick: sync_log pulls only the OTHER devices'
logs and _manual_push re-derives the PC's log from log.json each tick, so
the next push overwrote the tombstone ("tombstoned", then "LIVE" five
seconds later). Deletion is monotonic only for records still in the merge.

- New _sync_tombstones: {id: {deleted_hlc, reason, tombstoned_at}} in
  screen_locker/sync_tombstones.json (gitignored with its siblings). The
  HLC is minted once and stored so re-pushing does not churn the store.
  The ledger path is resolved at call time, not as a default argument, so
  the test suite's redirect actually applies -- verified by hashing the
  real ledger before and after the run.
- _manual_push.records_from_workout_log merges the ledger over the live
  derivation, so every push carries the tombstones.
- scripts/tombstone_sync_record.py <id> [--apply --reason ...]: dry run
  reports live/tombstoned; --apply writes the ledger, runs one tick and
  reports what the store holds afterwards. Rerunnable.
- tests/_isolated_state.py redirects SYNC_TOMBSTONES_FILE (both bindings).

Verified: --apply -> "tombstoned"; a second ordinary tick -> "already
tombstoned" with the same HLC; workout-sync.service now logs "Sync record
manual:2026-09-13T14:30 is tombstoned -- skipping it" and the budget line
for that id is gone. log.json untouched (the local entry stays; it shares
the original's credit per duplicate_credits.json).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7vgacLhb4WJjxuVj2QLEP
2026-09-13 20:20:06 +02:00
82f3462896 Replay today's earned shutdown credit in the daily base reset
The i3bar showed 21:00 on 2026-09-13 with a counted workout on disk. The
football was logged at 00:02 and credited against the previous day's
config (already at the 23 ceiling, so it bought nothing); the 09:15 base
reset then wrote 21/21 and only re-layered the weekly bank, never asking
what today's log had already earned. Live credits are read-add-write, so
every workout credited between local midnight and the first locker tick
of the day lost its hours to the reset that followed.

- _shutdown_base.reset_to_base_if_new_day takes the log and writes
  min(23, base + hours today's entries earned) in one write, using the
  same credit rule as the weekly count. Idempotent: still once per day.
- _workout_credit names FIRST_WORKOUT_BONUS_HOURS=2 /
  EXTRA_WORKOUT_BONUS_HOURS=1 and derives earned_shutdown_bonus_hours()
  from them; _shutdown and the +1h path use the constants instead of
  literals so the replay and the live paths cannot drift.
- _weekly_check.credit_key: a synced copy whose sync_record_id names an
  entry already filed on the same day shares that entry's slot. This
  morning's rekey (967e3cd) let the sync pull the football back in as
  manual:2026-09-13T14:30, which counted as a second credit (weekly 4/5
  instead of 3/5) and would have made the replay ask for 24.
- annotate_duplicate_credits.py passes the day's id index so the ledger
  records the manual duplicate the same way it records phone/PC pairs.

Verified through the real path: cleared last_reset_date, started
workout-locker.service -> "--restore 23 23 5", "Daily base reset:
Mon-Wed=23, Thu-Sun=23 (base 21/21 + 2h already earned today)",
shutdown_countdown.sh prints "23:00"; a second start changed nothing.
leetcode-guard grants no shutdown time anywhere (grepped screen-locker,
digital-wellbeing, i3 config) — the 23:00 comes from the workout alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7vgacLhb4WJjxuVj2QLEP
2026-09-13 20:00:05 +02:00
967e3cd322 Key every workout day on the LOCAL calendar day, not UTC
A manual workout logged at 00:02 CEST on 2026-09-13 was filed under
2026-09-12: every day-key derivation used datetime.now(tz=UTC), and at
that instant UTC was still yesterday. The 09:18 lock check looked for
2026-09-13, found nothing, and locked the PC — for a workout that was on
disk. The budget counter (a trailing window) counted it fine, which is
why the status showed 1/2 used while the locker still locked. Any log
between local midnight and 02:00 CEST hit this; it had happened before.

- New screen_locker/_day.py: today_str() is the one definition of "what
  day is it" — the local calendar day the user sees on the clock, which
  is also what the phone app, RunnerUp verification, the status window
  and the freedays pool already used. All 14 UTC sites (log writer, lock
  predicates, explain chain, early-bird, wake state, shutdown/sick state,
  sick budget, manual CLI, phone verification) now go through it, writers
  and readers together so no new midnight window opens between them.
- write_signed_entry dedups by workout_id across ALL days: a workout id is
  globally unique, and an entry moved to its correct day must not let its
  synced twin (still stamped with the old day) re-ingest as a duplicate.
- scripts/rekey_log_entry.py: move a wrongly-filed entry to its real day
  (the HMAC never covered the day key, so it still verifies).
- test_local_day_key.py freezes the clock at the exact failing instant.

Replayed the real timeline (log at 00:02:35, evaluate at 09:18:53, weekly
3/5) against HEAD in a throwaway worktree vs this tree:
  HEAD  -> logged under 2026-09-12, LOCK
  fixed -> logged under 2026-09-13, no lock (already_logged)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcU5dCcUNHMAMzUNChdrrJ
2026-09-13 09:50:06 +02:00
9b39196671 Take gatelock 0.8.1: nested LockConfig
The grab axes are grab=GrabPolicy(kind=..., disable_vt=...,
preempt_weaker_holder=...) and every colour read goes through
config.palette; nothing about the lock's behaviour changed. Also carries
the pylint hook on 4.0.8 with the source-only scope config; the gate
stays at 8.0 until the remaining findings are cleared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
2026-09-12 19:52:38 +02:00
e98884b200 Re-point paths for the ~ reorganisation (home-tidy)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 18:46:28 +02:00
0fb2da9c86 Say what is_scheduled_skip_today actually reads now
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
The MCP field kept its name when its source moved to the shared free-day
pool, which is fine for a persisted log value but misleading for a field
a Claude session reads and reasons about: nothing said it was no longer
this app's scheduled_skips.json, and a true there now means every gate
app in the fleet is standing down, not just this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:06:33 +02:00
d6e8c0973f Replace scheduled skips with the shared free-day pool
Fifth and last app onto freedays (~/utils/freedays). screen-locker is the
one that already had this idea: scheduled_skips.json was a bare list of
dates, checked first in the lock chain, that only this app could see.
That is exactly what the shared pool generalises, so the chain now asks
freedays and a day taken here also stands down diet-guard, wake-alarm,
leetcode-guard and home-guard.

Sick days are deliberately untouched. Their rolling windows (1/7d, 3/30d,
10/90d), the 120-character justification and the escalating countdown all
exist to make a sick day *hard* to take. Free days are the opposite --
declared ahead, no reason, consecutive runs allowed. Merging the two
policies would have had to ruin one of them, so only the storage of the
frictionless half moved.

This fixes a real bug in passing. is_scheduled_skip_today compared
against a UTC day while add_scheduled_skip.py wrote a local one, so a
skip added near midnight could name a date the chain never matched.
freedays owns "what is today" for both sides now, in local time, and the
disagreement cannot recur.

scripts/migrate_scheduled_skips.py moves the existing dates across; it is
idempotent and has a --dry-run. Already run here: the 8 real skips
(2026-05-19/20/21, 05-29, 05-30, 08-17, 08-22, 08-29) are in the pool and
count against 2026, leaving 27 of 35. The original file is gitignored and
was left on disk untouched.

add_scheduled_skip.py becomes a thin wrapper over freedays rather than
being deleted, because arm.sh calls it and the name is in muscle memory.
Left as-is it would have kept writing to a file nothing reads.

conftest.py went over the 250-line cap, so the two fixtures that keep the
suite off real user state moved to _real_state_fixtures.py. They belong
together and neither subsumes the other: _isolate_home covers paths
resolved from Path.home() at call time, while the freedays redirect
covers paths bound into module constants at import time, which
redirecting home afterwards cannot reach.

requirements.txt also takes pytest-randomly 5.0.0, ruff 0.16.6 and
types-requests 2.33.0.20260906, and the pylint hooks' gatelock/crdt-sync
pins move off v0.7.1/v0.9.0 -- pip refuses two direct URLs for one
distribution, so those stale pins would have made the hook environments
unresolvable rather than merely old.

1256 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:42:41 +02:00
1d5965b9a7 Close the four gaps that let a decided lock never reach the screen
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 15:24:45 +02:00
8f911585c1 Serve the lock decision, its history and its health at 127.0.0.1:8770
The only status surface was a GTK tray icon, commented out of ~/.config/i3
on 2026-08-21. So when the weekly count went wrong, nothing on the machine
said so for two weeks -- the same silence that let a deleted timer job stop
enforcement for thirteen days in August.

Three read-only endpoints, and a React 19 UI over them:

  /api/status     the decision, the week, and whether today has a workout
  /api/decisions  the recorded trail, newest first
  /api/health     timers armed, disarm marker, log and trail freshness

The Why view is the one that matters: it renders the run of
"weekly_minimum_met 5/5" ticks that hid this week's bug, without anyone
having to reach for journalctl.

/api/status also carries gaming.workout_today, which is the fact
steam-backlog-enforcer will cut the daily budget on. Deliberately just the
fact -- the enforcer owns the hour values, so the two repos cannot come to
disagree about what a workout day is. It is derived from count_day_credits,
the same rule the locker enforces with.

_compliance_state_word moves from status_view (tkinter) into
_status_projection, and the systemd arming check moves from
scripts/check_enforcement_armed.py into screen_locker/_armed_state.py, so
the server can answer "is enforcement armed" without a second copy of the
logic that would be free to disagree with the gate.

web/ mirrors steam-backlog-enforcer/web but tightens it: that config has no
"strict" in any tsconfig, extends only tseslint recommended (no type-aware
rules at all), and gates neither lint nor typecheck in pre-commit. Here all
three run, on strictTypeChecked + stylisticTypeChecked, with
noUncheckedIndexedAccess and exactOptionalPropertyTypes. 44 tests, 100% of
statements, branches, functions and lines.

typescript stays pinned at 6.0.3: 7.0.2 is out, but every published
typescript-eslint -- stable, canary and rc-v8 -- declares
typescript ">=4.8.4 <6.1.0". The fleet allowlist already carries that entry.

The 250-line cap now covers ts/tsx. It was python/shell/dart only, which
would have made web/ exempt from the gate the rest of the repo lives under
-- the same omission that exempted the whole Flutter app until 2026-08-15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
2026-08-29 22:50:19 +02:00
dd8a39ade5 Record which log entries are two recordings of one workout
The counting fix made the numbers right; the history still read as though
2026-08-24 held two gym sessions. This annotates the pairs instead of
deleting one of each, for two independent reasons.

Deleting is self-undoing: every re-ingest path dedups against log.json's
own contents rather than a remote ledger, so _session_sync re-pulls a
removed pc_workout_verified from the CRDT store on the next 15-minute tick
and re-applies its shutdown credit on the way in. And mutating signed
history is against the position revoke_sick_day.py already states -- the
correction belongs beside the record, not on top of it.

Scanning the real log found six shared credits, not the three spotted by
hand: 2026-07-17, 07-27, 08-10, 08-21, 08-24, 08-28. This has been
happening since July.

check_ledger_agrees refuses to write a ledger whose arithmetic disagrees
with count_day_credits, so the ledger cannot come to describe a rule the
locker does not use. The file is gitignored with the other state JSONs --
it names workout dates.

The guard's test needs a fixture restoring sys.exit. conftest's autouse
_block_real_tk_and_exit patches "screen_locker.screen_lock.sys.exit", and
that attribute path resolves to the global sys module, so sys.exit is
stubbed process-wide for every test in the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
2026-08-29 22:31:32 +02:00
8ac7af158d Count one StrongLifts session once, not once per ingestion path
The locker did not fire on 2026-08-29, or for the two weeks before it.
Every 30 minutes it logged:

  DECISION lock=no reason=weekly_minimum_met weekly=5/5

The 5 was wrong. A StrongLifts session is ingested twice -- the phone sync
writes phone_verified, the PC session sync writes pc_workout_verified, both
from the same WorkoutSession payload -- and workout_id is "<type>:<date>",
so the two copies get different IDs and neither dedups the other away.
count_weekly_workouts then counted each entry individually.

Real totals against what was counted:

  week of 2026-08-17    5 counted    4 real
  week of 2026-08-24    5 counted    3 real

Both weeks were waived. Nothing reported a problem, because 5/5 is exactly
what a compliant week looks like.

Add credit_key/count_day_credits to _weekly_check as the single rule for
"does this entry earn a credit, and is it the same credit as another": the
two StrongLifts ingestion paths share one slot per day, everything else
stays per-entry so two runs or two manual workouts in a day still count
twice. Exported, so counters share the rule instead of re-deriving it --
re-derivation is what caused this.

Route _status_data._day_status through it too. It had re-derived the rule
as "verified + (1 if has_manual else 0)", which disagreed with
count_weekly_workouts on any day with two manual workouts; since that value
feeds the status view's weekly_minimum_met explanation, the window could
explain a decision the locker never made.

Two existing fixtures reached their counts using two same-day
phone_verified entries -- the double-count itself. They now use a gym
session plus a run, which is two real workouts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
2026-08-29 22:23:17 +02:00
ec0946f64e Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.

What the stale target was hiding: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That last one makes the source
3.14-only at the syntax level, which is what `requires-python = ">=3.14"`
already promised; kuhy chose it deliberately over holding ruff a release back
(2026-08-29).

The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.

ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:42:47 +02:00
5d0c4d0e3a Cap --restore at 23:00 so it is a reward, not an off switch
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
--restore exists so the workout bonus can push shutdown later, and it had no
upper bound at all: `sudo adjust_shutdown_schedule.sh --restore 24 24 0` from
any shell undid the entire ratchet, with nothing written down. It is now
clamped to a ceiling and every restore is recorded.

Clamped rather than refused, so the reward path keeps working: the bonus asks
for up to 24 and simply lands on 23. Ceiling chosen by kuhy as today's earned
state -- base is 21, live was 23.

Two corrections to the previous commit, both found by reading the callers:

- It tightened validation from 0-24 to 0-23 on the grounds that the error
  message said 0-23. That was backwards. 24 is a real, documented value:
  _adjust_shutdown_time_by caps at 24 to mean midnight, and
  day-specific-shutdown-check.sh catches it through the morning-window
  condition. The message was what was wrong, so the message is what changed.
- The audit line does NOT go in /etc/shutdown-schedule-overrides.conf, which
  was the original plan. That file is parsed by day-specific-shutdown-check.sh
  as start|end|created|reason, and a line covering the current time makes it
  exit 0 and SKIP the shutdown. An audit trail written there would have
  suppressed the very curfew it documented. It goes to
  /var/log/shutdown-restore.log, and a test asserts the conf stays empty.

The log write says so on stderr when it fails, per this repo's no-silent-
failures rule; it is not fatal, because refusing an earned reward over an
unwritable log file would be the worse failure.

29 shell cases and 1146 python tests pass. Verified in a vmbox sandbox as root
against a real /etc and a real chattr: 24 clamps to 23 and is logged, the
overrides conf is untouched, and 25 is still refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5jMviyk15MKw1PqW1pzKr
2026-08-28 19:59:43 +02:00
2bc31cf587 Ratchet MORNING_END_HOUR, in the direction it actually runs
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
adjust_shutdown_schedule.sh compared only $1 and $2, so the third value passed
through unchecked in both directions: the morning shutdown window could be
shortened at will while the two evening hours were guarded.

The fix is not the obvious one. day-specific-shutdown-check.sh shuts down while
the clock is BELOW morning_end_minutes, so a LARGER MORNING_END_HOUR means a
longer shutdown window -- stricter. Copying the evening hours' `-gt` comparison
would have ratcheted it backwards and blessed exactly the loosening it was
meant to stop. The guard is `-lt`, and the inversion is now stated in the
header rather than left to be rediscovered.

Two smaller fixes in the same path: validation accepted 24 while its own error
message said 0-23, and the config is now unset before being sourced so an
unreadable file falls back to the documented loosest defaults instead of
inheriting whatever those names last held.

--restore is deliberately unchanged. It looks like a bypass, and it is one, but
it is load-bearing: shutdown_base.json says 21 while the live config is 23,
because the workout reward pushes the schedule later THROUGH --restore.
Bounding it at the base -- as was originally proposed -- would have capped the
reward at 21 and broken a daily flow. Bounding it needs a policy ceiling, which
is a decision, not a refactor.

The script is now sourceable, with the config path overridable, so the ratchet
has tests at all: 20 cases covering both directions, the source-clobber trap,
and the missing/unreadable config fallbacks. A ratchet with no test is how the
$3 gap survived for months.

Verified in a vmbox sandbox as root against a real /etc and a real chattr (the
immutable flag genuinely applied, which a tmpfs cannot do): a stricter write
lands, a looser evening hour is refused, a shortened morning window is refused
with the reason, and --restore still loosens. Not exercised against the host's
own /etc/shutdown-schedule.conf, deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5jMviyk15MKw1PqW1pzKr
2026-08-28 19:28:06 +02:00