mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 18:07:22 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Has been cancelled
Tests / web-coverage (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Tests / flutter-coverage (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWVSoczW2NHQd887FUm
190 lines
6.5 KiB
Python
190 lines
6.5 KiB
Python
"""Read-only status snapshot: today, this week, shutdown projection, budgets.
|
|
|
|
Every function in this module only reads files already on disk — no ADB, no
|
|
sudo, no network, no writes. ``_status.py`` stays untouched (it's an
|
|
existing, tested, deliberately *mutating* CLI used by a human running
|
|
``--status`` who already expects that); this module is the new, genuinely
|
|
side-effect-free layer that both the i3blocks summary line and the Tkinter
|
|
status view are built on.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, date, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
from screen_locker import _manual_workout
|
|
from screen_locker._compliance_state import explain_lock_decision
|
|
from screen_locker._status_projection import (
|
|
_shutdown_projection,
|
|
format_summary_line,
|
|
)
|
|
from screen_locker._status_types import (
|
|
DayStatus,
|
|
ManualWorkoutBudgetStatus,
|
|
ShutdownProjection,
|
|
ShutdownProjectionDay,
|
|
StatusSnapshot,
|
|
WeeklySummary,
|
|
)
|
|
|
|
__all__ = [
|
|
"DayStatus",
|
|
"ManualWorkoutBudgetStatus",
|
|
"ShutdownProjection",
|
|
"ShutdownProjectionDay",
|
|
"StatusSnapshot",
|
|
"WeeklySummary",
|
|
"format_summary_line",
|
|
"gather_status",
|
|
]
|
|
from screen_locker._constants import (
|
|
EARLY_BIRD_PENDING_FILE,
|
|
EXTRA_BENEFITS_FILE,
|
|
MANUAL_WORKOUT_BUDGET_PER_7_DAYS,
|
|
MANUAL_WORKOUT_BUDGET_PER_30_DAYS,
|
|
SHUTDOWN_CONFIG_FILE,
|
|
)
|
|
from screen_locker._extra_benefits import (
|
|
current_streak,
|
|
has_extended_early_bird,
|
|
week_rewards,
|
|
)
|
|
from screen_locker._log_io import load_workout_log
|
|
from screen_locker._morning_session import has_workout_skip_today
|
|
from screen_locker._night_lockdown import tonights_lockdown
|
|
from screen_locker._sick import workout_standdown_dates
|
|
from screen_locker._weekly_check import (
|
|
COUNTED_WORKOUT_TYPES,
|
|
WEEKLY_WORKOUT_MINIMUM,
|
|
count_day_credits,
|
|
is_relaxed_day,
|
|
)
|
|
|
|
_DEFAULT_LOG_FILE = Path(__file__).resolve().parent / "log.json"
|
|
|
|
|
|
def _day_status(
|
|
day_date: date, entries: list[dict], sick_days: frozenset[date]
|
|
) -> DayStatus:
|
|
"""Build a :class:`DayStatus` for one calendar day from its entry list.
|
|
|
|
``day_count`` comes from
|
|
:func:`~screen_locker._weekly_check.count_day_credits`, the same rule
|
|
enforcement uses, so the week total sums to
|
|
:func:`~screen_locker._weekly_check.count_weekly_workouts` by construction.
|
|
It used to re-derive the rule as ``verified + (1 if has_manual else 0)``,
|
|
which disagreed on any day with two manual workouts -- and since this value
|
|
feeds the status view's ``weekly_minimum_met`` explanation, the window could
|
|
explain a different decision than the locker actually made.
|
|
"""
|
|
iso = day_date.isoformat()
|
|
label = day_date.strftime("%a %b %d")
|
|
types = tuple(
|
|
str(e.get("workout_data", {}).get("type", ""))
|
|
for e in entries
|
|
if isinstance(e, dict)
|
|
)
|
|
sources = [
|
|
str(e.get("workout_data", {}).get("source", ""))
|
|
for e in entries
|
|
if isinstance(e, dict)
|
|
]
|
|
return DayStatus(
|
|
date=iso,
|
|
label=label,
|
|
entry_types=tuple(t for t in types if t),
|
|
source=" · ".join(s for s in sources if s),
|
|
counted=any(t in COUNTED_WORKOUT_TYPES for t in types),
|
|
day_count=count_day_credits(iso, [e for e in entries if isinstance(e, dict)]),
|
|
is_sick_day=day_date in sick_days,
|
|
)
|
|
|
|
|
|
def _week_days(log_file: Path, *, today_local: datetime) -> tuple[DayStatus, ...]:
|
|
"""Return this ISO week's days from Monday through today."""
|
|
log_data = load_workout_log(log_file)
|
|
sick_days = workout_standdown_dates()
|
|
today_date = today_local.date()
|
|
monday = today_date - timedelta(days=today_date.weekday())
|
|
days: list[DayStatus] = []
|
|
day = monday
|
|
while day <= today_date:
|
|
days.append(_day_status(day, log_data.get(day.isoformat(), []), sick_days))
|
|
day += timedelta(days=1)
|
|
return tuple(days)
|
|
|
|
|
|
def _weekly_summary(days: tuple[DayStatus, ...]) -> WeeklySummary:
|
|
"""Summarize a week's workout count against the weekly minimum."""
|
|
counted = sum(d.day_count for d in days)
|
|
return WeeklySummary(
|
|
days=days,
|
|
counted_count=counted,
|
|
minimum=WEEKLY_WORKOUT_MINIMUM,
|
|
remaining=max(0, WEEKLY_WORKOUT_MINIMUM - counted),
|
|
extra=max(0, counted - WEEKLY_WORKOUT_MINIMUM),
|
|
)
|
|
|
|
|
|
def _manual_workout_budget_status(
|
|
log_file: Path, *, today: str
|
|
) -> ManualWorkoutBudgetStatus:
|
|
"""Summarize rolling manual-workout budget usage."""
|
|
return ManualWorkoutBudgetStatus(
|
|
used_7d=_manual_workout.count_in_window(log_file, 7, today=today),
|
|
budget_7d=MANUAL_WORKOUT_BUDGET_PER_7_DAYS,
|
|
used_30d=_manual_workout.count_in_window(log_file, 30, today=today),
|
|
budget_30d=MANUAL_WORKOUT_BUDGET_PER_30_DAYS,
|
|
exhausted=_manual_workout.is_budget_exhausted(log_file, today=today),
|
|
)
|
|
|
|
|
|
def gather_status(
|
|
*,
|
|
log_file: Path = _DEFAULT_LOG_FILE,
|
|
extra_benefits_file: Path = EXTRA_BENEFITS_FILE,
|
|
shutdown_config_file: Path = SHUTDOWN_CONFIG_FILE,
|
|
early_bird_pending_file: Path = EARLY_BIRD_PENDING_FILE,
|
|
now: datetime | None = None,
|
|
) -> StatusSnapshot:
|
|
"""Gather a full :class:`StatusSnapshot` from on-disk state only.
|
|
|
|
Never touches ADB, sudo, or the network — safe to call on every i3blocks
|
|
tick and every status-window open/refresh.
|
|
"""
|
|
instant = now if now is not None else datetime.now(tz=UTC)
|
|
today_local = instant.astimezone()
|
|
today_str = today_local.date().isoformat()
|
|
|
|
days = _week_days(log_file, today_local=today_local)
|
|
week = _weekly_summary(days)
|
|
early_bird_extended = has_extended_early_bird(
|
|
extra_benefits_file, today=today_local
|
|
)
|
|
|
|
lock_explanation = explain_lock_decision(
|
|
log_file=log_file,
|
|
early_bird_pending_file=early_bird_pending_file,
|
|
weekly_minimum_met=week.counted_count >= WEEKLY_WORKOUT_MINIMUM,
|
|
relaxed_day=is_relaxed_day(today=today_local),
|
|
wake_skip=has_workout_skip_today(),
|
|
evening_lockdown=tonights_lockdown(instant) is not None,
|
|
now=instant,
|
|
)
|
|
|
|
return StatusSnapshot(
|
|
today=days[-1],
|
|
week=week,
|
|
streak=current_streak(extra_benefits_file),
|
|
early_bird_extended=early_bird_extended,
|
|
week_rewards=week_rewards(extra_benefits_file, today=today_local),
|
|
shutdown=_shutdown_projection(
|
|
shutdown_config_file=shutdown_config_file,
|
|
today_local=today_local,
|
|
),
|
|
lock_explanation=lock_explanation,
|
|
manual_workout_budget=_manual_workout_budget_status(log_file, today=today_str),
|
|
generated_at=instant.isoformat(),
|
|
)
|