mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 16:23:02 +02:00
- Refactor RunnerUp verification: extract RunnerUpDbMixin (_runnerup_db.py), split _scan_and_fill_week_runnerup into a helper _try_fill_runnerup_for_date to keep cyclomatic complexity ≤10 - Generalise TCX lookup to any date in the ISO week (was today-only); all gap days Mon→today auto-filled on every startup and 08:30 timer firing - Add _adjust_shutdown_time_by(): +1h per extra workout beyond the 4-workout minimum, capped at midnight (hour=24) - Add _shutdown_base.py: daily reset of shutdown config to a stored base so the bonus doesn't silently accumulate across days - Add _extra_benefits.py: streak tracking, skip credits (earn (n-4) credits for 5+ workout weeks), early-bird extension to 09:00 for eligible weeks - Add --status mode (_status.py): non-locking CLI view showing per-day breakdown (✓/✗), RunnerUp auto-scan, bonus status, shutdown time, streak, skip credits, and early-bird status - Hook carrot into _check_non_verify_exits: bonus applied whenever auto-fill pushes weekly count above the minimum - Pass all pre-commit hooks (ruff, mypy, pylint, bandit, shellcheck, codespell, max-file-length); 508 tests at 100% branch coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017auyHmf2ZwQcDAwXaSo7KX
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
"""Daily shutdown-time base reset for the screen locker.
|
|
|
|
On each new calendar day the shutdown config is reset to base hours (21:00
|
|
by default) so that the day's workout bonuses always layer on top of a known
|
|
floor rather than accumulating indefinitely across days.
|
|
|
|
The sick-day state file is cleared on reset so the sick-restore path cannot
|
|
overwrite the fresh base when it runs later in the same startup.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import logging
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_BASE_HOUR = 21
|
|
|
|
|
|
def get_base_hours(state_file: Path) -> tuple[int, int]:
|
|
"""Return ``(base_mon_wed_hour, base_thu_sun_hour)`` from *state_file*.
|
|
|
|
Falls back to ``(21, 21)`` if the file is missing or corrupt.
|
|
"""
|
|
if not state_file.exists():
|
|
return (_DEFAULT_BASE_HOUR, _DEFAULT_BASE_HOUR)
|
|
try:
|
|
with state_file.open() as f:
|
|
state: dict[str, Any] = json.load(f)
|
|
return (
|
|
int(state.get("base_mon_wed_hour", _DEFAULT_BASE_HOUR)),
|
|
int(state.get("base_thu_sun_hour", _DEFAULT_BASE_HOUR)),
|
|
)
|
|
except (OSError, json.JSONDecodeError, ValueError):
|
|
return (_DEFAULT_BASE_HOUR, _DEFAULT_BASE_HOUR)
|
|
|
|
|
|
def reset_to_base_if_new_day(
|
|
state_file: Path,
|
|
mixin: object,
|
|
sick_day_state_file: Path | None = None,
|
|
) -> bool:
|
|
"""Reset the shutdown config to base hours if a new calendar day has begun.
|
|
|
|
Writes base hours via *mixin*._write_shutdown_config (with restore=True so
|
|
the script allows moving the time earlier), updates ``last_reset_date`` in
|
|
*state_file*, and removes *sick_day_state_file* if it exists so the
|
|
sick-restore path does not fight with the fresh base on the same startup.
|
|
|
|
Returns True if a reset was performed, False if today was already reset.
|
|
"""
|
|
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
|
|
|
if state_file.exists():
|
|
try:
|
|
with state_file.open() as f:
|
|
state: dict[str, Any] = json.load(f)
|
|
if state.get("last_reset_date") == today:
|
|
return False
|
|
except (OSError, json.JSONDecodeError):
|
|
pass
|
|
|
|
base_mw, base_ts = get_base_hours(state_file)
|
|
|
|
# Preserve the morning-end hour from the live config.
|
|
config = mixin._read_shutdown_config()
|
|
morning_end = config[2] if config else 5
|
|
|
|
ok: bool = mixin._write_shutdown_config(base_mw, base_ts, morning_end, restore=True)
|
|
if not ok:
|
|
_logger.warning("Daily base reset: failed to write shutdown config.")
|
|
return False
|
|
|
|
# Clear stale sick-day state so it does not override the base reset.
|
|
if sick_day_state_file is not None and sick_day_state_file.exists():
|
|
try:
|
|
sick_day_state_file.unlink()
|
|
_logger.info("Daily base reset: cleared stale sick-day state.")
|
|
except OSError as exc:
|
|
_logger.warning(
|
|
"Daily base reset: could not remove sick-day state: %s", exc
|
|
)
|
|
|
|
new_state: dict[str, Any] = {
|
|
"base_mon_wed_hour": base_mw,
|
|
"base_thu_sun_hour": base_ts,
|
|
"last_reset_date": today,
|
|
}
|
|
try:
|
|
with state_file.open("w") as f:
|
|
json.dump(new_state, f, indent=2)
|
|
except OSError as exc:
|
|
_logger.warning("Daily base reset: failed to write state file: %s", exc)
|
|
|
|
_logger.info("Daily base reset: Mon-Wed=%d, Thu-Sun=%d.", base_mw, base_ts)
|
|
return True
|