screen-locker/screen_locker/_day.py
Krzysztof kuhy Rudnicki 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

33 lines
1.5 KiB
Python

"""The one definition of "what day is it" for every day-keyed file.
``log.json``, ``early_bird_pending.json`` and ``wake_state.json`` are all
keyed by ``YYYY-MM-DD``.
Until 2026-09-13 each writer and reader derived that key on its own, and
most of them used ``datetime.now(tz=UTC)`` while the lock window, the phone
app, RunnerUp verification and the status window used LOCAL time. In CEST
that is a two-hour window after local midnight in which a workout logged
"today" was filed under yesterday — the lock check then looked for today,
found nothing, and locked the PC for a workout that was on disk.
The day is the **local calendar day**: it is what the user sees on the
clock when they log, what the lock screen names when it says "no workout
for today", and what the phone already writes. Every module goes through
:func:`today_str` so the two can never disagree again; tests freeze the
clock by patching ``screen_locker._day.datetime``.
"""
from __future__ import annotations
from datetime import UTC, datetime
def today_str(now: datetime | None = None) -> str:
"""Return the local calendar day of *now* (default: this instant) as ``YYYY-MM-DD``.
*now* may be in any timezone; it is converted to the machine's local zone
first, which is the only conversion that turns ``22:02Z`` on the 12th
into the 13th the user actually logged on.
"""
instant = now if now is not None else datetime.now(tz=UTC)
return instant.astimezone().strftime("%Y-%m-%d")