mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 18:07:22 +01:00
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
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""Read the morning-session carrot the wake-alarm service signed for today.
|
|
|
|
The phone's morning (weigh-in, shower, dress, desk) is published to the PC
|
|
and distilled by wake-alarm into ``MORNING_SESSION_FILE``. This module never
|
|
re-derives the policy: the file carries a signed ``exempt_until``, and the
|
|
only decision here is *HMAC ok, dated today, now < exempt_until*. Anything
|
|
else -- missing file, stale date, failed session, bad signature -- is "no
|
|
skip", i.e. today's ordinary ladder.
|
|
|
|
The one wrinkle is a PC booted mid-morning: the file is missing or yesterday's
|
|
until wake-alarm's ``Persistent=true`` timer catches up, which needs the
|
|
network. The enforce path waits for that, bounded; status paths never do.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
import json
|
|
import logging
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gatelock.log_integrity import verify_entry_hmac
|
|
|
|
from screen_locker._constants import MORNING_SESSION_FILE
|
|
from screen_locker._day import today_str
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# Only inside this local window is a non-today file worth waiting for: the
|
|
# refresher runs 05:00-11:00, so outside it "not today's" is simply the truth.
|
|
MORNING_WINDOW: tuple[tuple[int, int], tuple[int, int]] = ((5, 0), (11, 0))
|
|
MORNING_RETRY_SECONDS: float = 30.0
|
|
MORNING_POLL_SECONDS: float = 5.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MorningSkip:
|
|
"""A skip the morning session earned: what the phone said, and until when."""
|
|
|
|
outcome: str
|
|
exempt_until: datetime
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.outcome} session, no lock until {self.exempt_until:%H:%M}"
|
|
|
|
|
|
def _read_verified() -> dict[str, object] | None:
|
|
"""The file's entry when it exists, parses and verifies; else None.
|
|
|
|
Missing is debug (it is the normal state outside the morning); anything
|
|
else is a warning, because it means the refresher or the key is broken.
|
|
"""
|
|
if not MORNING_SESSION_FILE.exists():
|
|
_logger.debug("No morning session file at %s", MORNING_SESSION_FILE)
|
|
return None
|
|
try:
|
|
entry = json.loads(MORNING_SESSION_FILE.read_text())
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
_logger.warning("Cannot read %s: %s", MORNING_SESSION_FILE, exc)
|
|
return None
|
|
if not isinstance(entry, dict) or not verify_entry_hmac(entry):
|
|
_logger.warning("Morning session file is not a signed object")
|
|
return None
|
|
return entry
|
|
|
|
|
|
def _load_today(now: datetime) -> dict[str, object] | None:
|
|
"""Today's verified entry, or None."""
|
|
entry = _read_verified()
|
|
if entry is None or entry.get("date") != today_str(now):
|
|
return None
|
|
return entry
|
|
|
|
|
|
def _skip_from(entry: dict[str, object], now: datetime) -> MorningSkip | None:
|
|
"""The skip an entry grants at ``now``, if its window is still open."""
|
|
until = entry.get("exempt_until")
|
|
if not isinstance(until, str):
|
|
return None
|
|
try:
|
|
exempt_until = datetime.fromisoformat(until)
|
|
except ValueError:
|
|
_logger.warning("Morning session exempt_until unreadable: %r", until)
|
|
return None
|
|
if now >= exempt_until:
|
|
return None
|
|
return MorningSkip(outcome=str(entry.get("outcome")), exempt_until=exempt_until)
|
|
|
|
|
|
def _in_window(now: datetime) -> bool:
|
|
(start_h, start_m), (end_h, end_m) = MORNING_WINDOW
|
|
minutes = now.hour * 60 + now.minute
|
|
return start_h * 60 + start_m <= minutes < end_h * 60 + end_m
|
|
|
|
|
|
def morning_skip_today(
|
|
now: datetime | None = None,
|
|
*,
|
|
wait: bool,
|
|
sleep: Callable[[float], None] = time.sleep,
|
|
) -> MorningSkip | None:
|
|
"""The skip the morning session earned, or None.
|
|
|
|
With ``wait`` (the enforce path only), a file that is not today's during
|
|
the morning window is retried for up to ``MORNING_RETRY_SECONDS`` so a
|
|
freshly booted PC gives wake-alarm's catch-up run a chance to land.
|
|
"""
|
|
now = (now or datetime.now(tz=UTC)).astimezone()
|
|
entry = _load_today(now)
|
|
waited = 0.0
|
|
while entry is None and wait and _in_window(now) and waited < MORNING_RETRY_SECONDS:
|
|
sleep(MORNING_POLL_SECONDS)
|
|
waited += MORNING_POLL_SECONDS
|
|
now = datetime.now(tz=UTC).astimezone()
|
|
entry = _load_today(now)
|
|
if entry is None:
|
|
if waited:
|
|
_logger.warning(
|
|
"No morning session for today after %.0fs; "
|
|
"is wake-alarm-session.timer running?",
|
|
waited,
|
|
)
|
|
return None
|
|
return _skip_from(entry, now)
|
|
|
|
|
|
def has_workout_skip_today() -> bool:
|
|
"""Whether the morning session earned a skip right now. Never waits."""
|
|
return morning_skip_today(wait=False) is not None
|