mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:07:16 +01:00
Adds StatusSectionsMixin._section_streak, called from StatusWindow.render: the stored consecutive 5+-workout-week streak and the reward message the week transition produced, display only. A streak of 0 is stated plainly, never padded; a "Streak reset" message is warning-coloured, not green. get_status already returned the streak (it is asdict(gather_status())); StatusSnapshot gains week_rewards. The week-transition strings used to go to _logger.info only. process_week_transition now also keeps them under a week_rewards key in the existing extra_benefits_state.json (no new file), read back for the current ISO week by week_rewards(). Their "early-bird extended to 09:00" clause is dropped: early_bird_extended is read only by display code, no lock or window decision uses it (early bird is wake-alarm's signed exemption since 2026-09-20), so it is not shown in the window either. Per kuhy: no daily-streak state, no shutdown time for streaks, no freezes or grace days; no enforcement logic touched. Red first: new section/state/snapshot tests failed, then passed. On a scratch copy of the real state the window shows "7-week streak" and "6 workouts in 2026-W43! 7-week streak", no early-bird line. Removes prompts/TODO-dopamine-ux-04-screen-locker.md (done). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
215 lines
7.9 KiB
Python
215 lines
7.9 KiB
Python
"""The individual rendered sections of the status window.
|
|
|
|
Split out of :mod:`screen_locker.status_view` to keep every file under the
|
|
250-line cap. Composed into ``StatusWindow`` there, alongside the other view
|
|
mixins, so the class's public surface is unchanged.
|
|
|
|
Every method here is display-only: it reads the snapshot it is handed and
|
|
paints widgets. Nothing in this module writes to disk or reaches the network.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._earned import hhmm
|
|
from screen_locker._sync_status import format_sync_line, gather_sync_status
|
|
|
|
if TYPE_CHECKING:
|
|
import tkinter as tk
|
|
|
|
from screen_locker._compliance_state import LockExplanation
|
|
from screen_locker._status_data import (
|
|
DayStatus,
|
|
ManualWorkoutBudgetStatus,
|
|
ShutdownProjection,
|
|
WeeklySummary,
|
|
)
|
|
|
|
|
|
class StatusSectionsMixin:
|
|
"""Renders each section of the status window from a snapshot."""
|
|
|
|
def _section_today(self, parent: tk.Widget, day: DayStatus) -> None:
|
|
"""Render today's outcome."""
|
|
del parent
|
|
mark = "✓" if day.counted else ("😷" if day.is_sick_day else "—")
|
|
entry_str = ", ".join(day.entry_types) or (
|
|
"sick day" if day.is_sick_day else "no entry yet"
|
|
)
|
|
self._text(
|
|
f"{mark} Today ({day.label}): {entry_str}",
|
|
role="body",
|
|
color=self._colors.palette.success
|
|
if day.counted
|
|
else self._colors.palette.warning,
|
|
)
|
|
if day.source:
|
|
# Secondary provenance note on an already-shown entry -- a
|
|
# deliberately caption-sized exception to the 16px floor.
|
|
self._text(
|
|
day.source, role="caption", color=self._colors.palette.muted, pad="xs"
|
|
)
|
|
|
|
def _section_week(self, parent: tk.Widget, week: WeeklySummary) -> None:
|
|
"""Render this ISO week's per-day breakdown and totals."""
|
|
del parent
|
|
self._label(
|
|
f"This Week: {week.counted_count}/{week.minimum}", role="body", pad="sm"
|
|
)
|
|
for day in week.days:
|
|
mark = "✓" if day.counted else ("😷" if day.is_sick_day else "·")
|
|
entry_str = ", ".join(day.entry_types) or (
|
|
"sick day" if day.is_sick_day else "no entry"
|
|
)
|
|
self._text(
|
|
f"{mark} {day.label}: {entry_str}",
|
|
role="body",
|
|
color=self._colors.palette.muted,
|
|
pad="xs",
|
|
)
|
|
if week.remaining > 0:
|
|
self._text(
|
|
f"Need {week.remaining} more this week.",
|
|
role="body",
|
|
color=self._colors.palette.warning,
|
|
)
|
|
elif week.extra > 0:
|
|
self._text(
|
|
f"{week.extra} above the weekly minimum!",
|
|
role="body",
|
|
color=self._colors.palette.success,
|
|
)
|
|
|
|
def _section_streak(
|
|
self, parent: tk.Widget, streak: int, week_rewards: tuple[str, ...] = ()
|
|
) -> None:
|
|
"""Render the consecutive 5+-workout-week streak and this week's reward.
|
|
|
|
Display only, and exactly the stored values (``extra_benefits_state``):
|
|
a streak of 0 is said plainly, never padded or rounded up. Nothing here
|
|
grants time or relaxes a lock. The early-bird extension is not shown:
|
|
it no longer changes anything (wake-alarm owns early bird).
|
|
"""
|
|
del parent
|
|
self._label("Workout streak", role="body", pad="sm")
|
|
if streak > 0:
|
|
self._text(
|
|
f"🔥 {streak}-week streak (5+ workouts a week)",
|
|
role="body",
|
|
color=self._colors.palette.success,
|
|
)
|
|
else:
|
|
self._text(
|
|
"No streak yet: 5+ workouts in a week starts one.",
|
|
role="body",
|
|
color=self._colors.palette.muted,
|
|
)
|
|
for message in week_rewards:
|
|
# A reset is news, not a celebration: it must not be painted green.
|
|
lost = message.startswith("Streak reset")
|
|
self._text(
|
|
message,
|
|
role="body",
|
|
color=self._colors.palette.warning
|
|
if lost
|
|
else self._colors.palette.success,
|
|
)
|
|
|
|
def _section_lock_explanation(
|
|
self, parent: tk.Widget, expl: LockExplanation
|
|
) -> None:
|
|
"""Render why the lock did/didn't fire today, plus its evaluation trace."""
|
|
del parent
|
|
self._label("Why the lock did/didn't fire", role="body", pad="sm")
|
|
self._text(
|
|
expl.reason,
|
|
role="body",
|
|
color=self._colors.palette.danger
|
|
if expl.fired
|
|
else self._colors.palette.success,
|
|
)
|
|
if expl.auto_upgrade.would_attempt:
|
|
self._text(
|
|
f"Pending auto-upgrade: {expl.auto_upgrade.reason}",
|
|
role="label",
|
|
color=self._colors.palette.warning,
|
|
)
|
|
|
|
def _section_manual_workout_budget(
|
|
self, parent: tk.Widget, manual: ManualWorkoutBudgetStatus
|
|
) -> None:
|
|
"""Render rolling manual-workout budget usage."""
|
|
del parent
|
|
self._label("Manual Workout Budget", role="body", pad="sm")
|
|
self._text(
|
|
f"{manual.used_7d}/{manual.budget_7d} week · "
|
|
f"{manual.used_30d}/{manual.budget_30d} month",
|
|
role="body",
|
|
color=self._colors.palette.danger
|
|
if manual.exhausted
|
|
else self._colors.palette.muted,
|
|
)
|
|
|
|
def _section_sync_backend(self, parent: tk.Widget) -> None:
|
|
"""Render which backend this machine syncs through.
|
|
|
|
Read from local state only (see :mod:`screen_locker._sync_status`), so
|
|
opening or refreshing the window never makes a network call. Coloured
|
|
by ``healthy`` rather than by backend: "configured but never pushed"
|
|
looks fine everywhere else and produces no data, which is exactly the
|
|
state worth surfacing here.
|
|
"""
|
|
del parent
|
|
status = gather_sync_status()
|
|
self._label("Sync backend", role="body", pad="sm")
|
|
self._text(
|
|
format_sync_line(status),
|
|
role="body",
|
|
color=self._colors.palette.muted
|
|
if status.healthy
|
|
else self._colors.palette.danger,
|
|
)
|
|
|
|
def _section_shutdown(
|
|
self, parent: tk.Widget, shutdown: ShutdownProjection
|
|
) -> None:
|
|
"""Render tonight's live config, rest-of-week, and next-week preview."""
|
|
del parent
|
|
self._label("Shutdown Time", role="body", pad="sm")
|
|
if shutdown.tonight is not None:
|
|
mon_wed, thu_sun, _morning = shutdown.tonight
|
|
self._text(
|
|
f"Live config — Mon-Wed {hhmm(mon_wed)}, Thu-Sun {hhmm(thu_sun)}",
|
|
role="body",
|
|
)
|
|
else:
|
|
self._text(
|
|
"Live shutdown config unavailable.",
|
|
role="body",
|
|
color=self._colors.palette.warning,
|
|
)
|
|
# Rest-of-week/next-week/explanation are speculative annotations, not
|
|
# the section's primary content -- a deliberate caption-sized
|
|
# exception to the 16px floor, all sharing one muted tone instead of
|
|
# three unrelated ad hoc grays.
|
|
rest_line = ", ".join(
|
|
f"{d.label} {hhmm(d.minutes)}" for d in shutdown.rest_of_week
|
|
)
|
|
self._text(
|
|
f"Rest of week: {rest_line}",
|
|
role="caption",
|
|
color=self._colors.palette.muted,
|
|
)
|
|
next_line = ", ".join(
|
|
f"{d.label} {hhmm(d.minutes)}" for d in shutdown.next_week_preview
|
|
)
|
|
self._text(
|
|
f"Next week (speculative): {next_line}",
|
|
role="caption",
|
|
color=self._colors.palette.muted,
|
|
)
|
|
self._text(
|
|
shutdown.explanation, role="caption", color=self._colors.palette.muted
|
|
)
|