mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 21:07:14 +01:00
- 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>
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""``--declare-rest-day``: plan a rest day from the command line.
|
|
|
|
python -m screen_locker.screen_lock --declare-rest-day 2026-10-12
|
|
python -m screen_locker.screen_lock --declare-rest-day tomorrow
|
|
python -m screen_locker.screen_lock --declare-rest-day --list
|
|
|
|
The clock is checked against NTP first and an unconfirmable clock is a
|
|
refusal, not a pass: unlike the lock's own check (which fails open when NTP
|
|
is unreachable), a wound-back clock with the network off is exactly how
|
|
"today" would be passed off as tomorrow.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from datetime import date, timedelta
|
|
import logging
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._constants import MAX_CLOCK_SKEW_SECONDS
|
|
from screen_locker._day import today_str
|
|
from screen_locker._rest_day import declare, rest_days
|
|
from screen_locker._time_check import _query_ntp_offset
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _say(message: str) -> None:
|
|
sys.stdout.write(f"{message}\n")
|
|
|
|
|
|
def _parse_day(raw: str) -> date:
|
|
today = date.fromisoformat(today_str())
|
|
if raw == "tomorrow":
|
|
return today + timedelta(days=1)
|
|
return date.fromisoformat(raw)
|
|
|
|
|
|
def _clock_confirmed() -> str | None:
|
|
"""``None`` if NTP confirms the clock, else why the declaration is refused."""
|
|
offset = _query_ntp_offset()
|
|
if offset is None:
|
|
return "NTP is unreachable, so the clock cannot be confirmed; try online"
|
|
if abs(offset) > MAX_CLOCK_SKEW_SECONDS:
|
|
return f"the clock is off by {abs(offset):.0f}s from NTP"
|
|
return None
|
|
|
|
|
|
def run_declare_rest_day(argv: Sequence[str]) -> int:
|
|
"""Declare (or list) rest days. Returns a process exit code."""
|
|
parser = argparse.ArgumentParser(
|
|
prog="screen-locker --declare-rest-day",
|
|
description="Declare a FUTURE rest day (max 2 per ISO week).",
|
|
)
|
|
parser.add_argument("day", nargs="?", help="YYYY-MM-DD or 'tomorrow'")
|
|
parser.add_argument("--list", action="store_true", help="list rest days")
|
|
args = parser.parse_args(list(argv))
|
|
if args.list or args.day is None:
|
|
days = sorted(rest_days())
|
|
_say("\n".join(d.isoformat() for d in days) or "no rest days declared")
|
|
return 0
|
|
try:
|
|
day = _parse_day(args.day)
|
|
except ValueError as exc:
|
|
_logger.warning("Unparsable rest day %r: %s", args.day, exc)
|
|
_say(f"error: {exc}")
|
|
return 2
|
|
refusal = _clock_confirmed()
|
|
if refusal is not None:
|
|
_logger.warning("Rest day refused: %s", refusal)
|
|
_say(f"refused: {refusal}")
|
|
return 1
|
|
result = declare(day)
|
|
if not result.ok:
|
|
_logger.warning("Rest day refused: %s", result.reason)
|
|
_say(result.reason if result.ok else f"refused: {result.reason}")
|
|
return 0 if result.ok else 1
|