screen-locker/screen_locker
Krzysztof kuhy Rudnicki 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
..
tests Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
__init__.py Fix test discovery and coverage for python_pkg modules 2025-12-02 22:33:34 +01:00
_adb_transport.py Split _shutdown, _phone_verification and _status_data 2026-08-15 20:11:28 +02:00
_armed_state.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_auto_upgrade.py Say every reason a lock decision had, not just the first one 2026-08-23 07:02:41 +02:00
_cli.py Log manual workouts headlessly, with the form's rules intact 2026-08-27 20:50:52 +02:00
_compliance_predicates.py Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
_compliance_state.py Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
_compliance_trace.py Detect a Firebase credential the server rejects, not just one that fails to build 2026-08-27 21:32:39 +02:00
_constants.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_credential_recovery.py Detect a Firebase credential the server rejects, not just one that fails to build 2026-08-27 21:32:39 +02:00
_decision_log.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_decision_reasons.py Say every reason a lock decision had, not just the first one 2026-08-23 07:02:41 +02:00
_decision_trail.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_degraded_sources.py Detect a Firebase credential the server rejects, not just one that fails to build 2026-08-27 21:32:39 +02:00
_device.py Push under a persisted per-install uuid, not a fixed role id 2026-08-09 17:31:15 +02:00
_early_bird.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_extra_benefits.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_heat_skip.py Stop the relaxed-day popup from re-firing every 30 minutes after Skip 2026-08-18 21:55:59 +02:00
_http_workout_fetch.py Split _shutdown, _phone_verification and _status_data 2026-08-15 20:11:28 +02:00
_instance.py Queue behind stronger lockers, single-instance guard, fix budget-crash in fit check 2026-08-21 13:04:54 +02:00
_log_io.py Stop the relaxed-day popup from re-firing every 30 minutes after Skip 2026-08-18 21:55:59 +02:00
_log_mixin.py Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
_manual_cli.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_manual_push.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_manual_sync.py Stop the relaxed-day popup from re-firing every 30 minutes after Skip 2026-08-18 21:55:59 +02:00
_manual_workout_dialog.py Split _manual_workout_dialog.py and _manual_workout.py 2026-08-15 20:16:33 +02:00
_manual_workout_sport_fields.py Split _manual_workout_dialog.py and _manual_workout.py 2026-08-15 20:16:33 +02:00
_manual_workout_types.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_manual_workout_widgets.py Split _manual_workout_dialog.py and _manual_workout.py 2026-08-15 20:16:33 +02:00
_manual_workout.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_mcp.py Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
_phone_verification.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_push_outcome.py Detect a Firebase credential the server rejects, not just one that fails to build 2026-08-27 21:32:39 +02:00
_queue_state.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_queue_wait.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_runnerup_backfill.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_runnerup_db.py Retry RunnerUp verification every 15 min, and stop hiding why it fails 2026-08-12 19:17:29 +02:00
_runnerup_tcx.py Split _runnerup_verification, _ui_widgets and _sick_dialog 2026-08-15 20:37:15 +02:00
_runnerup_verification.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_session_sync.py Credit StrongLifts sessions from the headless sync, as pc_workout_verified 2026-08-22 11:26:20 +02:00
_shutdown_base.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_shutdown_sick_state.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_shutdown.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_sick_budget.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_sick_commitment.py Split _runnerup_verification, _ui_widgets and _sick_dialog 2026-08-15 20:37:15 +02:00
_sick_dialog.py Split _runnerup_verification, _ui_widgets and _sick_dialog 2026-08-15 20:37:15 +02:00
_sick_tracker.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_source_health.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_startup_checks.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_status_data.py Replace scheduled skips with the shared free-day pool 2026-09-07 15:42:41 +02:00
_status_projection.py Serve the lock decision, its history and its health at 127.0.0.1:8770 2026-08-29 22:50:19 +02:00
_status_sections.py Split _compliance_state.py and status_view.py 2026-08-15 20:01:12 +02:00
_status_types.py Split _shutdown, _phone_verification and _status_data 2026-08-15 20:11:28 +02:00
_status_view_verify.py Make Check Phone week-aware; add RunnerUp GPS tolerance; fix CLI weekly count 2026-07-18 06:55:55 +02:00
_status.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_surface_group.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_sync_client.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_sync_mixin.py Credit StrongLifts sessions from the headless sync, as pc_workout_verified 2026-08-22 11:26:20 +02:00
_sync_records.py Apply sync tombstones across the device union, not per device log 2026-08-21 13:38:52 +02:00
_sync_retry.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_sync_status.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_temperature_status_mixin.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_temperature.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_time_check.py Support multiple workouts per day; fix the dead sync; ban silent failures 2026-07-17 09:22:31 +02:00
_ui_flows_lockout.py Split _ui_flows.py into sick-day and lockout flow mixins 2026-08-15 19:56:16 +02:00
_ui_flows_relaxed.py Stop the relaxed-day popup from re-firing every 30 minutes after Skip 2026-08-18 21:55:59 +02:00
_ui_flows_sick.py Split _ui_flows.py into sick-day and lockout flow mixins 2026-08-15 19:56:16 +02:00
_ui_flows.py Tell "could not check" apart from "no workout", and stop double-granting bonuses 2026-08-24 13:07:20 +02:00
_ui_form_fields.py Split _runnerup_verification, _ui_widgets and _sick_dialog 2026-08-15 20:37:15 +02:00
_ui_widgets.py Split _runnerup_verification, _ui_widgets and _sick_dialog 2026-08-15 20:37:15 +02:00
_unlock_view.py Stop the lock screen scrolling itself, and make every screen fit 768px 2026-08-03 15:57:48 +02:00
_wake_alarm.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_wake_state.py Grade against 3.14 too, not just run on it 2026-08-29 11:42:47 +02:00
_web_decisions.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_web_payload.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_web_server.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_weekly_check.py Count one StrongLifts session once, not once per ingestion path 2026-08-29 22:23:17 +02:00
_window_setup.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
_workout_app.py Put the demo's escape hatch inside the app, the only place it can be received 2026-08-22 12:41:07 +02:00
_workout_credit.py Credit StrongLifts sessions from the headless sync, as pc_workout_verified 2026-08-22 11:26:20 +02:00
_workout_handoff.py Make the demo escapable, and stop claiming the blocking was deliberate 2026-08-22 12:33:07 +02:00
_workout_ready.py Supervise the workout app as the lock surface, on a deadline that holds 2026-08-22 11:52:02 +02:00
_workout_session.py Put the demo's escape hatch inside the app, the only place it can be received 2026-08-22 12:41:07 +02:00
_workout_sync.py Credit StrongLifts sessions from the headless sync, as pc_workout_verified 2026-08-22 11:26:20 +02:00
adjust_shutdown_schedule.sh Cap --restore at 23:00 so it is a reward, not an off switch 2026-08-28 19:59:43 +02:00
screen_lock.py Close the four gaps that let a decided lock never reach the screen 2026-08-30 15:24:45 +02:00
status_view.py Serve the lock decision, its history and its health at 127.0.0.1:8770 2026-08-29 22:50:19 +02:00