From 9ab7c09c792e05ed4896372c9080af7010a82826 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 20 Sep 2026 08:39:44 +0200 Subject: [PATCH] The early-bird window is the wake-alarm carrot, not a clock 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) Claude-Session: https://claude.ai/code/session_01XTdfXjbGYYrY7Quxcfz6mg --- AGENTS.md | 11 +- early-bird-workout-check.timer | 17 +- screen_locker/_auto_upgrade.py | 34 ++-- screen_locker/_compliance_predicates.py | 18 --- screen_locker/_compliance_state.py | 22 +-- screen_locker/_constants.py | 7 + screen_locker/_early_bird.py | 47 +++--- screen_locker/_morning_session.py | 4 +- screen_locker/_sick_dialog.py | 5 +- screen_locker/_sick_tracker.py | 5 +- screen_locker/_status_data.py | 1 - screen_locker/tests/_isolated_state.py | 1 - .../tests/test_compliance_state_part2.py | 43 +---- .../tests/test_compliance_state_part4.py | 71 ++++----- screen_locker/tests/test_early_bird.py | 150 +++--------------- screen_locker/tests/test_early_bird_part2.py | 2 +- screen_locker/tests/test_local_day_key.py | 1 - .../tests/test_sick_tracker_part2.py | 12 ++ screen_locker/tests/test_sync_degradation.py | 2 - screen_locker/tests/test_systemd_units.py | 10 +- screen_locker/tests/test_wake_skip.py | 101 +++++------- .../tests/test_weekly_logic_part2.py | 6 +- web/package-lock.json | 35 +--- web/package.json | 4 +- 24 files changed, 207 insertions(+), 402 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b666670..0ae1cf8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,16 @@ own `submitted_at`, flat +1h/day, and *fail closed* -- an unreadable ledger or key earns nothing. leetcode-guard itself stays read-only; it never writes the config. -## The morning session is the carrot +## The morning session is the carrot — and the early-bird window is it + +`EarlyBirdMixin._is_early_bird_time` no longer reads a clock: it is open +exactly while wake-alarm's signed file grants an exemption (before the +alarm, during the session, until 11:00 once it completed in time). The +05:00–08:30 window and the extended-to-09:00 reward locked someone who got +up at 07:00 and were a second mechanism next to the one that knows whether +the user is up (2026-09-20). The pending marker is banked by the +`wake_alarm_skip` rung; `early-bird-workout-check.timer` (09:30:30, 11:00:30) +re-checks just after the carrot's two possible ends. The `wake_alarm_skip` rung reads wake-alarm's HMAC-signed `~/.local/state/wake_alarm/morning_session.json` (`_morning_session.py`), not diff --git a/early-bird-workout-check.timer b/early-bird-workout-check.timer index 0350f09..22e30f6 100644 --- a/early-bird-workout-check.timer +++ b/early-bird-workout-check.timer @@ -10,14 +10,17 @@ Description=Re-check workout after early bird grace period expires (08:30 normal # After=graphical-session.target, which is where that ordering belongs. [Timer] -# Fires at 08:30 for the normal early-bird window (5:00–8:30). -# Also fires at 09:05 to catch the extended window (5:00–9:00) earned by 5+ workout weeks. -# Both runs are idempotent: the service handles "already logged" and "window still active". -OnCalendar=*-*-* 08:30:00 -OnCalendar=*-*-* 09:05:00 +# The early-bird window is the wake-alarm morning-session carrot since +# 2026-09-20 (screen_locker/_early_bird.py): it ends at the signed +# exempt_until -- 11:00 after a completed morning, min(start+90 min, 09:30) +# after an aborted one, alarm+15 min when no session started. These runs land +# just after the two boundaries the 5-minute tick could miss by up to five +# minutes; every run is idempotent ("already logged", "carrot still in force"). +OnCalendar=*-*-* 09:30:30 +OnCalendar=*-*-* 11:00:30 Unit=workout-locker.service -# Persistent=true: a machine asleep or off at 08:30 would otherwise skip the -# re-check entirely, leaving the early-bird marker banked and the day +# Persistent=true: a machine asleep or off at a boundary would otherwise skip +# the re-check entirely, leaving the early-bird marker banked and the day # unenforced. Both runs are idempotent (see above), so a catch-up run is safe. Persistent=true AccuracySec=1s diff --git a/screen_locker/_auto_upgrade.py b/screen_locker/_auto_upgrade.py index 6571814..f2fadcd 100644 --- a/screen_locker/_auto_upgrade.py +++ b/screen_locker/_auto_upgrade.py @@ -21,7 +21,7 @@ import sys from screen_locker import _sick_tracker from screen_locker._decision_log import LockDecision, record_decision from screen_locker._decision_reasons import reasons_extra -from screen_locker._morning_session import has_workout_skip_today, morning_skip_today +from screen_locker._morning_session import has_workout_skip_today from screen_locker._weekly_check import has_weekly_minimum, is_relaxed_day _logger = logging.getLogger(__name__) @@ -109,15 +109,13 @@ class AutoUpgradeMixin(_ReasonsMixin): # (_compliance_state, where the expired marker is non-terminal) said # "lock skipped". Fall through to the same ladder as any other morning. if pending and window_open: - # The ONLY thing that closes this window is - # early-bird-workout-check.timer. When that timer was disabled by an - # ordering cycle (2026-08-04) this branch silently deferred the lock - # every single day, forever. + # The window is the wake-alarm carrot (see EarlyBirdMixin): it + # closes when the signed exempt_until passes, and the 5-minute + # tick plus early-bird-workout-check.timer then re-decide. _skip( "early_bird_window_active", - "Early bird window still active — deferring to the 08:30/09:05 " - "re-check timer.", - recheck_by="early-bird-workout-check.timer", + f"Morning-session carrot still in force: {self._morning_skip}.", + recheck_by="workout-locker.timer", **self._other_conditions("early_bird_window_active"), ) elif self._is_sick_day_today(): @@ -138,22 +136,18 @@ class AutoUpgradeMixin(_ReasonsMixin): "Workout already logged today.", **self._other_conditions("workout_logged_today"), ) - elif (skip := morning_skip_today(wait=True)) is not None: + elif window_open: + # Bank the marker so the run that sees the carrot end tries a + # phone/RunnerUp workout before falling through to a lock. + self._save_early_bird_pending() + skip = self._morning_skip _skip( "wake_alarm_skip", - f"Morning session earned it: {skip}.", - exempt_until=skip.exempt_until.isoformat(), - outcome=skip.outcome, + f"Morning session earned it: {skip or 'carrot in force'}.", + exempt_until=skip.exempt_until.isoformat() if skip else None, + outcome=skip.outcome if skip else None, **self._other_conditions("wake_alarm_skip"), ) - elif window_open: - self._save_early_bird_pending() - _skip( - "early_bird_banked", - "Early bird time — banked the pending marker, will re-check " - "at 08:30/09:05.", - recheck_by="early-bird-workout-check.timer", - ) else: return False return True diff --git a/screen_locker/_compliance_predicates.py b/screen_locker/_compliance_predicates.py index 2592c85..dc57840 100644 --- a/screen_locker/_compliance_predicates.py +++ b/screen_locker/_compliance_predicates.py @@ -21,11 +21,6 @@ from typing import TYPE_CHECKING import freedays from gatelock.log_integrity import compute_entry_hmac, verify_entry_hmac -from screen_locker._constants import ( - EARLY_BIRD_END_HOUR, - EARLY_BIRD_END_MINUTE, - EARLY_BIRD_START_HOUR, -) from screen_locker._day import today_str from screen_locker._log_io import load_workout_log from screen_locker._sick_tracker import is_sick_day as _is_sick_day @@ -141,16 +136,3 @@ def is_sick_day_today(history: SickHistory, *, today: str | None = None) -> bool the status layer have one predicate module to import from. """ return _is_sick_day(history, today=today) - - -def _early_bird_window_open(*, extended: bool, local_minutes: int) -> bool: - """Deliberate, independent reimplementation of ``_is_early_bird_time``. - - Not shared with ``EarlyBirdMixin._is_early_bird_time`` because too many - existing tests are pinned to its ``_get_local_time_minutes`` patch point. - Kept here as a small, separate, pure re-implementation for the status - layer only — see the module docstring in ``_early_bird.py``. - """ - start = EARLY_BIRD_START_HOUR * 60 - end = 9 * 60 if extended else EARLY_BIRD_END_HOUR * 60 + EARLY_BIRD_END_MINUTE - return start <= local_minutes < end diff --git a/screen_locker/_compliance_state.py b/screen_locker/_compliance_state.py index 42349f3..ac46a28 100644 --- a/screen_locker/_compliance_state.py +++ b/screen_locker/_compliance_state.py @@ -22,7 +22,6 @@ from datetime import UTC, datetime from typing import TYPE_CHECKING from screen_locker._compliance_predicates import ( - _early_bird_window_open, has_logged_today, is_early_bird_pending, is_relaxed_day_skipped_today, @@ -66,7 +65,6 @@ def explain_lock_decision( log_file: Path, early_bird_pending_file: Path, sick_history: SickHistory, - extended_early_bird: bool, weekly_minimum_met: bool, relaxed_day: bool, wake_skip: bool = False, @@ -82,16 +80,13 @@ def explain_lock_decision( instant = now if now is not None else datetime.now(tz=UTC) today_str = local_day(instant) local_dt = instant.astimezone() - local_minutes = local_dt.hour * 60 + local_dt.minute scheduled = is_scheduled_skip_today(local_dt.date()) pending = is_early_bird_pending(early_bird_pending_file, today=today_str) - window_open = _early_bird_window_open( - extended=extended_early_bird, local_minutes=local_minutes - ) + # The early-bird window IS the wake-alarm carrot (EarlyBirdMixin). + window_open = wake_skip expired = pending and not window_open early_bird_open_and_pending = pending and window_open - window_end_label = "09:00" if extended_early_bird else "08:30" sick_today = is_sick_day_today(sick_history, today=today_str) logged = has_logged_today(log_file, today=today_str) relaxed_day_already_skipped = relaxed_day and is_relaxed_day_skipped_today( @@ -192,19 +187,6 @@ def explain_lock_decision( if result is not None: return result - result = _check( - "early_bird_time_fresh", - fired=window_open, - reason_true=( - f"Currently inside the early-bird window (05:00-{window_end_label})." - ), - reason_false="Not currently inside the early-bird window.", - terminal_reason="Inside the early-bird window — lock skipped, pending marker " - f"would be saved for the {window_end_label} re-check.", - ) - if result is not None: - return result - result = _check( "relaxed_day_already_skipped", fired=relaxed_day_already_skipped, diff --git a/screen_locker/_constants.py b/screen_locker/_constants.py index 51a8358..ca2c056 100644 --- a/screen_locker/_constants.py +++ b/screen_locker/_constants.py @@ -31,6 +31,13 @@ SICK_LOCKOUT_MULTIPLIER_PER_RECENT = 2 SICK_JUSTIFICATION_MIN_CHARS = 120 # How many past sick justifications to show on the dialog (read-only). SICK_HISTORY_REVIEW_COUNT = 10 +# The sick screen lists past justifications one per line, at most this many +# and each cut to this many characters: six full-length ones pushed the +# screen 7 px past 1366x768, which the screens-fit gate only sees with a real +# history on disk. The review *count* above is for the budget logic, not the +# screen. +SICK_SCREEN_RECENT_LINES = 4 +SICK_JUSTIFICATION_PREVIEW_CHARS = 36 # Forced read-only delay before SUBMIT enables when a commitment was made. SICK_COMMITMENT_FORCED_READ_SECONDS = 5 # Breaking a commitment counts as this many sick budget days. diff --git a/screen_locker/_early_bird.py b/screen_locker/_early_bird.py index 5c5e1bc..3ab5bf2 100644 --- a/screen_locker/_early_bird.py +++ b/screen_locker/_early_bird.py @@ -4,27 +4,22 @@ The early-bird "still waiting to see if a real workout shows up" flag is a same-day pending marker, not a workout — it is intentionally kept out of log.json (which is reserved for real outcomes) and instead lives in its own self-expiring, HMAC-signed state file, mirroring the pattern used by -``wake_alarm._state`` in the companion wake-alarm service. +``wake_alarm._state`` in the companion wake-alarm service. It is written when +the wake-alarm carrot defers the lock, so that when the carrot ends the next +run tries a phone/RunnerUp workout before locking. """ from __future__ import annotations -from datetime import UTC, datetime import json import logging from gatelock.log_integrity import compute_entry_hmac from screen_locker._compliance_state import is_early_bird_pending -from screen_locker._constants import ( - EARLY_BIRD_END_HOUR, - EARLY_BIRD_END_MINUTE, - EARLY_BIRD_PENDING_FILE, - EARLY_BIRD_START_HOUR, - EXTRA_BENEFITS_FILE, -) +from screen_locker._constants import EARLY_BIRD_PENDING_FILE from screen_locker._day import today_str -from screen_locker._extra_benefits import has_extended_early_bird +from screen_locker._morning_session import MorningSkip, morning_skip_today _logger = logging.getLogger(__name__) @@ -35,27 +30,27 @@ def _today_str() -> str: class EarlyBirdMixin: - """Mixin providing early-bird time window checks and pending-state helpers.""" + """Mixin providing the early-bird window check and pending-state helpers. - def _get_local_time_minutes(self) -> int: - """Return current local time as minutes from midnight.""" - now = datetime.now(tz=UTC).astimezone() - return now.hour * 60 + now.minute + Since 2026-09-20 the window is not a wall clock but the wake-alarm + morning-session carrot: open exactly while wake-alarm's signed + ``morning_session.json`` says the morning is being earned (before the + alarm, during the session, and until 11:00 once it completed in time). + The old 05:00-08:30 clock locked someone who got up at 07:00 -- the + opposite of the goal -- and it was a second mechanism next to the one + that actually knows whether the user is up. + """ + + _morning_skip: MorningSkip | None = None def _is_early_bird_time(self) -> bool: - """Return True if current local time is in the early bird window. + """Whether the wake-alarm carrot is in force right now. - Normally the window closes at 08:30. When the current ISO week has an - extended early-bird reward (earned by 5+ workouts the prior week) the - window extends to 09:00. + Waits (bounded) for a freshly booted PC's refresher; the verdict is + kept on the instance so the skip detail can quote its ``exempt_until``. """ - minutes = self._get_local_time_minutes() - start = EARLY_BIRD_START_HOUR * 60 - if has_extended_early_bird(EXTRA_BENEFITS_FILE): - end = 9 * 60 # 09:00 - else: - end = EARLY_BIRD_END_HOUR * 60 + EARLY_BIRD_END_MINUTE - return start <= minutes < end + self._morning_skip = morning_skip_today(wait=True) + return self._morning_skip is not None def _is_early_bird_pending(self) -> bool: """Check if today has an unresolved early-bird pending marker.""" diff --git a/screen_locker/_morning_session.py b/screen_locker/_morning_session.py index e234ace..30acab2 100644 --- a/screen_locker/_morning_session.py +++ b/screen_locker/_morning_session.py @@ -32,8 +32,8 @@ if TYPE_CHECKING: _logger = logging.getLogger(__name__) # Only inside this local window is a non-today file worth waiting for: the -# refresher runs 07:00-11:00, so outside it "not today's" is simply the truth. -MORNING_WINDOW: tuple[tuple[int, int], tuple[int, int]] = ((7, 0), (11, 0)) +# 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 diff --git a/screen_locker/_sick_dialog.py b/screen_locker/_sick_dialog.py index a7c56db..588ea0d 100644 --- a/screen_locker/_sick_dialog.py +++ b/screen_locker/_sick_dialog.py @@ -12,6 +12,7 @@ from screen_locker import _sick_tracker from screen_locker._constants import ( SICK_COMMITMENT_FORCED_READ_SECONDS, SICK_JUSTIFICATION_MIN_CHARS, + SICK_SCREEN_RECENT_LINES, ) from screen_locker._sick_commitment import SickCommitmentMixin from screen_locker._surface_group import FrameGroup, TextGroup @@ -40,7 +41,9 @@ class SickDialogMixin(SickCommitmentMixin): _sick_tracker.budget_summary(history), color=self._colors.palette.warning ) - recent = _sick_tracker.format_recent_justifications(history) + recent = _sick_tracker.format_recent_justifications( + history, n=SICK_SCREEN_RECENT_LINES + ) if recent: self._text( "Recent sick days:", diff --git a/screen_locker/_sick_tracker.py b/screen_locker/_sick_tracker.py index 3a17633..f904771 100644 --- a/screen_locker/_sick_tracker.py +++ b/screen_locker/_sick_tracker.py @@ -19,6 +19,7 @@ from screen_locker._constants import ( SICK_HISTORY_FILE, SICK_HISTORY_REVIEW_COUNT, SICK_JUSTIFICATION_MIN_CHARS, + SICK_JUSTIFICATION_PREVIEW_CHARS, ) from screen_locker._sick_budget import ( SickHistory, @@ -235,7 +236,9 @@ def format_recent_justifications( lines: list[str] = [] for entry in entries: date_str = entry.get("date", "?") - symptom = entry.get("symptom", "?") + symptom = str(entry.get("symptom", "?")) + if len(symptom) > SICK_JUSTIFICATION_PREVIEW_CHARS: + symptom = symptom[: SICK_JUSTIFICATION_PREVIEW_CHARS - 1] + "…" severity = entry.get("severity", "?") lines.append(f"{date_str} sev {severity}/10 — {symptom}") return "\n".join(lines) diff --git a/screen_locker/_status_data.py b/screen_locker/_status_data.py index b043834..d903231 100644 --- a/screen_locker/_status_data.py +++ b/screen_locker/_status_data.py @@ -194,7 +194,6 @@ def gather_status( log_file=log_file, early_bird_pending_file=early_bird_pending_file, sick_history=sick_history, - extended_early_bird=early_bird_extended, weekly_minimum_met=week.counted_count >= WEEKLY_WORKOUT_MINIMUM, relaxed_day=is_relaxed_day(today=today_local), wake_skip=has_workout_skip_today(), diff --git a/screen_locker/tests/_isolated_state.py b/screen_locker/tests/_isolated_state.py index ec8bae8..ade4eae 100644 --- a/screen_locker/tests/_isolated_state.py +++ b/screen_locker/tests/_isolated_state.py @@ -28,7 +28,6 @@ ISOLATED_STATE: tuple[tuple[str, tuple[str, ...]], ...] = ( "_constants.EXTRA_BENEFITS_FILE", "_startup_checks.EXTRA_BENEFITS_FILE", "_sync_mixin.EXTRA_BENEFITS_FILE", - "_early_bird.EXTRA_BENEFITS_FILE", "_status.EXTRA_BENEFITS_FILE", ), ), diff --git a/screen_locker/tests/test_compliance_state_part2.py b/screen_locker/tests/test_compliance_state_part2.py index 4e02b75..390bdc1 100644 --- a/screen_locker/tests/test_compliance_state_part2.py +++ b/screen_locker/tests/test_compliance_state_part2.py @@ -38,7 +38,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, ) @@ -58,9 +57,9 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, + wake_skip=True, now=now, ) assert result.fired is False @@ -80,7 +79,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, now=now, @@ -98,7 +96,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(sick_days=[_today()]), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -115,7 +112,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -128,7 +124,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, wake_skip=True, @@ -137,47 +132,25 @@ class TestExplainLockDecision: assert result.fired is False assert result.stage == "wake_alarm_skip" - def test_fresh_early_bird_time_skips(self, tmp_path: Path) -> None: + def test_carrot_in_force_skips_as_wake_alarm(self, tmp_path: Path) -> None: + """Before the alarm, during the session, until 11:00 -- one stage, one reason.""" files = self._files(tmp_path) - now = datetime.now(tz=UTC).astimezone().replace(hour=6, minute=0) result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, - now=now, + wake_skip=True, ) assert result.fired is False - assert result.stage == "early_bird_time_fresh" - assert "08:30" in result.reason - assert "09:00" not in result.reason - - def test_fresh_early_bird_time_names_09_00_when_extended( - self, tmp_path: Path - ) -> None: - """The re-check time isn't ambiguous — it's whichever one actually applies.""" - files = self._files(tmp_path) - now = datetime.now(tz=UTC).astimezone().replace(hour=6, minute=0) - result = explain_lock_decision( - **files, - sick_history=SickHistory(), - extended_early_bird=True, - weekly_minimum_met=False, - relaxed_day=False, - now=now, - ) - assert result.fired is False - assert result.stage == "early_bird_time_fresh" - assert "09:00" in result.reason - assert "08:30" not in result.reason + assert result.stage == "wake_alarm_skip" + assert "early-bird" not in result.reason def test_relaxed_day_skips(self, tmp_path: Path) -> None: files = self._files(tmp_path) result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=True, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -201,7 +174,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=True, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -214,7 +186,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=True, relaxed_day=False, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -227,7 +198,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, now=datetime.now(tz=UTC).astimezone().replace(hour=12, minute=0), @@ -243,7 +213,6 @@ class TestExplainLockDecision: result = explain_lock_decision( **files, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, ) diff --git a/screen_locker/tests/test_compliance_state_part4.py b/screen_locker/tests/test_compliance_state_part4.py index 83a9860..e58f510 100644 --- a/screen_locker/tests/test_compliance_state_part4.py +++ b/screen_locker/tests/test_compliance_state_part4.py @@ -2,52 +2,39 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from screen_locker import _compliance_state +from screen_locker._sick_tracker import SickHistory + +if TYPE_CHECKING: + from pathlib import Path -class TestEarlyBirdWindowOpen: - """The early-bird window's start/end boundaries, including the extension.""" +class TestEarlyBirdWindowIsTheCarrot: + """The window is whatever wake-alarm's signed file says -- no wall clock.""" - """Direct tests for the module-private, deliberately independent reimplementation.""" + def test_no_clock_predicate_remains(self) -> None: + assert not hasattr(_compliance_state, "_early_bird_window_open") - def test_before_window(self) -> None: - """Before the start hour the window is shut.""" - assert ( - _compliance_state._early_bird_window_open(extended=False, local_minutes=299) - is False + def test_window_open_mirrors_wake_skip(self, tmp_path: Path) -> None: + files = { + "log_file": tmp_path / "log.json", + "early_bird_pending_file": tmp_path / "eb.json", + } + open_ = _compliance_state.explain_lock_decision( + **files, + sick_history=SickHistory(), + weekly_minimum_met=False, + relaxed_day=False, + wake_skip=True, ) - - def test_at_start(self) -> None: - """The start minute itself is inside the window.""" - assert ( - _compliance_state._early_bird_window_open(extended=False, local_minutes=300) - is True - ) - - def test_before_end(self) -> None: - """A minute before the end is still inside the window.""" - assert ( - _compliance_state._early_bird_window_open(extended=False, local_minutes=509) - is True - ) - - def test_at_end_exclusive(self) -> None: - """The end minute is exclusive: the window is already shut.""" - assert ( - _compliance_state._early_bird_window_open(extended=False, local_minutes=510) - is False - ) - - def test_extended_before_end(self) -> None: - """With the extension earned, the window runs later.""" - assert ( - _compliance_state._early_bird_window_open(extended=True, local_minutes=539) - is True - ) - - def test_extended_at_end_exclusive(self) -> None: - """The extended end is exclusive too.""" - assert ( - _compliance_state._early_bird_window_open(extended=True, local_minutes=540) - is False + shut = _compliance_state.explain_lock_decision( + **files, + sick_history=SickHistory(), + weekly_minimum_met=False, + relaxed_day=False, + wake_skip=False, ) + assert open_.stage == "wake_alarm_skip" + assert shut.stage == "would_lock" diff --git a/screen_locker/tests/test_early_bird.py b/screen_locker/tests/test_early_bird.py index c373666..f0f45b2 100644 --- a/screen_locker/tests/test_early_bird.py +++ b/screen_locker/tests/test_early_bird.py @@ -1,147 +1,47 @@ -"""Tests for early bird carrot feature in screen locker.""" +"""The early-bird window is the wake-alarm morning-session carrot.""" from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch -from screen_locker.tests.conftest import ( - create_locker, -) +from screen_locker._morning_session import MorningSkip +from screen_locker.tests.conftest import create_locker if TYPE_CHECKING: from pathlib import Path - from screen_locker.screen_lock import ScreenLocker - - -class TestGetLocalTimeMinutes: - """Tests for _get_local_time_minutes helper.""" - - def test_returns_int_within_day_range( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Returns an integer between 0 and 1439 (minutes in a day).""" - locker = create_locker(mock_tk, tmp_path) - result = locker._get_local_time_minutes() - assert isinstance(result, int) - assert 0 <= result < 24 * 60 +_SKIP = MorningSkip(outcome="completed", exempt_until=datetime(2099, 1, 1).astimezone()) class TestIsEarlyBirdTime: - """Tests for _is_early_bird_time based on local clock.""" + """``_is_early_bird_time`` is the carrot verdict, kept for the skip detail.""" - def _locker( - self, - mock_tk: MagicMock, - tmp_path: Path, - minutes: int, - ) -> ScreenLocker: + def test_open_while_the_carrot_is_in_force( + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path + ) -> None: locker = create_locker(mock_tk, tmp_path) - object.__setattr__( - locker, - "_get_local_time_minutes", - MagicMock(return_value=minutes), - ) - return locker - - def test_within_window( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """6:00 AM (360 min) is within the early bird window.""" - locker = self._locker(mock_tk, tmp_path, 360) - assert locker._is_early_bird_time() is True - - def test_at_start_of_window( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """5:00 AM (300 min) is the inclusive start of the window.""" - locker = self._locker(mock_tk, tmp_path, 300) - assert locker._is_early_bird_time() is True - - def test_just_before_start( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """4:59 AM (299 min) is before the window.""" - locker = self._locker(mock_tk, tmp_path, 299) - assert locker._is_early_bird_time() is False - - def test_just_before_end( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """8:29 AM (509 min) is still within the window.""" - locker = self._locker(mock_tk, tmp_path, 509) - assert locker._is_early_bird_time() is True - - def test_at_end_of_window( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """8:30 AM (510 min) is the exclusive end — not in window.""" - locker = self._locker(mock_tk, tmp_path, 510) - assert locker._is_early_bird_time() is False - - def test_after_window( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """9:00 AM (540 min) is past the window.""" - locker = self._locker(mock_tk, tmp_path, 540) - assert locker._is_early_bird_time() is False - - def test_extended_window_ends_at_9am( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """When has_extended_early_bird is True, window closes at 09:00 (540 min).""" - locker = self._locker(mock_tk, tmp_path, 539) # 08:59 — still inside with patch( - "screen_locker._early_bird.has_extended_early_bird", - return_value=True, - ): + "screen_locker._early_bird.morning_skip_today", return_value=_SKIP + ) as read: assert locker._is_early_bird_time() is True + read.assert_called_once_with(wait=True) + assert locker._morning_skip == _SKIP - def test_extended_window_closed_at_9am( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + def test_shut_without_a_carrot( + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """Extended window excludes exactly 09:00 (540 min).""" - locker = self._locker(mock_tk, tmp_path, 540) # 09:00 — exclusive end - with patch( - "screen_locker._early_bird.has_extended_early_bird", - return_value=True, - ): + locker = create_locker(mock_tk, tmp_path) + with patch("screen_locker._early_bird.morning_skip_today", return_value=None): assert locker._is_early_bird_time() is False + assert locker._morning_skip is None - def test_midnight( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + def test_no_wall_clock_is_consulted( + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """Midnight (0 min) is outside the window.""" - locker = self._locker(mock_tk, tmp_path, 0) - assert locker._is_early_bird_time() is False + """The 05:00-08:30 clock is gone: 07:00 with a carrot is open, 06:00 without is shut.""" + locker = create_locker(mock_tk, tmp_path) + assert not hasattr(locker, "_get_local_time_minutes") + with patch("screen_locker._early_bird.morning_skip_today", return_value=None): + assert locker._is_early_bird_time() is False diff --git a/screen_locker/tests/test_early_bird_part2.py b/screen_locker/tests/test_early_bird_part2.py index 466ffe2..e773d51 100644 --- a/screen_locker/tests/test_early_bird_part2.py +++ b/screen_locker/tests/test_early_bird_part2.py @@ -125,7 +125,7 @@ class TestInitEarlyBirdFlow: patch.object(ScreenLocker, "_start_phone_check"), patch.object(ScreenLocker, "_start_verify_workout_check"), patch( - "screen_locker._auto_upgrade.morning_skip_today", + "screen_locker._early_bird.morning_skip_today", return_value=None, ), pytest.raises(SystemExit), diff --git a/screen_locker/tests/test_local_day_key.py b/screen_locker/tests/test_local_day_key.py index 0768da1..18e826d 100644 --- a/screen_locker/tests/test_local_day_key.py +++ b/screen_locker/tests/test_local_day_key.py @@ -101,7 +101,6 @@ class TestAfterMidnightLog: log_file=log_file, early_bird_pending_file=tmp_path / "early_bird_pending.json", sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=True, relaxed_day=False, now=JUST_AFTER_MIDNIGHT_UTC, diff --git a/screen_locker/tests/test_sick_tracker_part2.py b/screen_locker/tests/test_sick_tracker_part2.py index 62f533f..286f73c 100644 --- a/screen_locker/tests/test_sick_tracker_part2.py +++ b/screen_locker/tests/test_sick_tracker_part2.py @@ -11,6 +11,7 @@ from screen_locker import _sick_tracker from screen_locker._constants import ( SICK_HISTORY_REVIEW_COUNT, SICK_JUSTIFICATION_MIN_CHARS, + SICK_JUSTIFICATION_PREVIEW_CHARS, ) from screen_locker._sick_tracker import ( JustificationDraft, @@ -198,3 +199,14 @@ class TestRecentJustifications: history = SickHistory(justifications=[{}]) out = format_recent_justifications(history) assert "?" in out + + def test_format_keeps_a_long_symptom_to_one_line(self) -> None: + """A 48-char symptom wrapped and pushed the sick screen past 768 px.""" + history = SickHistory( + justifications=[ + {"date": "2026-05-01", "symptom": "x" * 48, "severity": 7}, + ], + ) + out = format_recent_justifications(history) + assert out.endswith("…") + assert len(out.split("— ")[1]) == SICK_JUSTIFICATION_PREVIEW_CHARS diff --git a/screen_locker/tests/test_sync_degradation.py b/screen_locker/tests/test_sync_degradation.py index f358305..04e69a3 100644 --- a/screen_locker/tests/test_sync_degradation.py +++ b/screen_locker/tests/test_sync_degradation.py @@ -62,7 +62,6 @@ class TestUnreadableSourceIsNotNoWorkout: **self._files(tmp_path), now=self._NOON, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, degraded_sources=(_sync_client.DegradedSource("firebase", "HTTP 400"),), @@ -77,7 +76,6 @@ class TestUnreadableSourceIsNotNoWorkout: **self._files(tmp_path), now=self._NOON, sick_history=SickHistory(), - extended_early_bird=False, weekly_minimum_met=False, relaxed_day=False, ) diff --git a/screen_locker/tests/test_systemd_units.py b/screen_locker/tests/test_systemd_units.py index d146b40..7194478 100644 --- a/screen_locker/tests/test_systemd_units.py +++ b/screen_locker/tests/test_systemd_units.py @@ -122,16 +122,16 @@ class TestEnforcementTimersAreCycleFree: class TestEarlyBirdTimerWindows: - """The early-bird timer must keep closing both grace windows.""" + """The early-bird timer must re-check just after both carrot boundaries.""" - def test_covers_normal_and_extended_windows(self) -> None: - """08:30 closes the normal window; 09:05 closes the extended one.""" + def test_covers_both_carrot_boundaries(self) -> None: + """09:30 ends an aborted morning's grace; 11:00 ends a completed one's.""" text = (REPO_ROOT / "early-bird-workout-check.timer").read_text( encoding="utf-8" ) calendars = _directive_values(text, "OnCalendar") - assert "*-*-* 08:30:00" in calendars - assert "*-*-* 09:05:00" in calendars + assert "*-*-* 09:30:30" in calendars + assert "*-*-* 11:00:30" in calendars class TestPeriodicLockerTimer: diff --git a/screen_locker/tests/test_wake_skip.py b/screen_locker/tests/test_wake_skip.py index 6766637..56463ba 100644 --- a/screen_locker/tests/test_wake_skip.py +++ b/screen_locker/tests/test_wake_skip.py @@ -1,87 +1,70 @@ -"""Tests for wake alarm skip integration in screen_lock.py.""" +"""The wake-alarm carrot in the lock's startup ladder (``_check_today_state_exits``).""" from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING +from pathlib import Path from unittest.mock import MagicMock, patch from screen_locker._morning_session import MorningSkip +from screen_locker.screen_lock import ScreenLocker from screen_locker.tests.conftest import create_locker _SKIP = MorningSkip(outcome="completed", exempt_until=datetime(2099, 1, 1).astimezone()) -if TYPE_CHECKING: - from pathlib import Path + +def _locker( + tmp_path: Path, *, carrot: MorningSkip | None, has_logged: bool = False +) -> None: + """Build a locker whose early-bird window is the given carrot verdict.""" + with ( + patch.object(Path, "resolve", return_value=tmp_path / "screen_locker"), + patch.object(ScreenLocker, "has_logged_today", return_value=has_logged), + patch.object(ScreenLocker, "_is_sick_day_today", return_value=False), + patch.object(ScreenLocker, "_is_early_bird_pending", return_value=False), + patch("screen_locker._early_bird.morning_skip_today", return_value=carrot), + patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False), + patch.object(ScreenLocker, "_start_phone_check"), + patch.object(ScreenLocker, "_start_relaxed_day_flow"), + patch.object(ScreenLocker, "_start_verify_workout_check"), + patch.object(ScreenLocker, "_scan_and_fill_week_runnerup", return_value=0), + ): + ScreenLocker(demo_mode=True) class TestWakeSkipIntegration: - """Tests for workout skip via wake alarm in screen locker init.""" - - def test_exits_when_wake_skip_active( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + def test_exits_and_banks_the_marker_while_the_carrot_is_in_force( + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """Screen locker exits if wake alarm granted workout skip today.""" - with patch( - "screen_locker._auto_upgrade.morning_skip_today", - return_value=_SKIP, + with ( + patch.object(ScreenLocker, "_save_early_bird_pending") as save, + patch("screen_locker._auto_upgrade.record_decision") as record, ): - create_locker(mock_tk, tmp_path, has_logged=False) - + _locker(tmp_path, carrot=_SKIP) mock_sys_exit.assert_called_once_with(0) + save.assert_called_once() + decision = record.call_args.args[0] + assert decision.reason == "wake_alarm_skip" + assert decision.extra["exempt_until"] == _SKIP.exempt_until.isoformat() + assert "no lock until" in decision.detail - def test_does_not_exit_when_no_wake_skip( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + def test_does_not_exit_without_a_carrot( + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """Screen locker proceeds normally if no wake skip active.""" - with patch( - "screen_locker._auto_upgrade.morning_skip_today", - return_value=None, - ): - locker = create_locker(mock_tk, tmp_path, has_logged=False) - + _locker(tmp_path, carrot=None) mock_sys_exit.assert_not_called() - assert locker is not None def test_logged_today_takes_precedence( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """has_logged_today exits before wake skip is even checked.""" - with patch( - "screen_locker._auto_upgrade.morning_skip_today", - return_value=_SKIP, - ): - create_locker(mock_tk, tmp_path, has_logged=True) - - # Exits because has_logged_today, not because of wake skip + with patch("screen_locker._auto_upgrade.record_decision") as record: + _locker(tmp_path, carrot=_SKIP, has_logged=True) mock_sys_exit.assert_called_once_with(0) + assert record.call_args.args[0].reason == "workout_logged_today" def test_verify_only_mode_ignores_wake_skip( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, + self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path ) -> None: - """verify_only mode checks sick day log, not wake skip.""" - with patch( - "screen_locker._auto_upgrade.morning_skip_today", - return_value=_SKIP, - ): - create_locker( - mock_tk, - tmp_path, - verify_only=True, - is_sick_day_log=True, - ) - - # In verify_only mode, exits don't happen from wake skip path + with patch("screen_locker._early_bird.morning_skip_today", return_value=_SKIP): + create_locker(mock_tk, tmp_path, verify_only=True, is_sick_day_log=True) mock_sys_exit.assert_not_called() diff --git a/screen_locker/tests/test_weekly_logic_part2.py b/screen_locker/tests/test_weekly_logic_part2.py index 07a0a97..5320b19 100644 --- a/screen_locker/tests/test_weekly_logic_part2.py +++ b/screen_locker/tests/test_weekly_logic_part2.py @@ -151,7 +151,7 @@ class TestCheckTodayStateExits: patch.object(locker, "_is_sick_day_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False), patch( - "screen_locker._auto_upgrade.morning_skip_today", + "screen_locker._early_bird.morning_skip_today", return_value=_SKIP, ), ): @@ -171,7 +171,7 @@ class TestCheckTodayStateExits: patch.object(locker, "_is_sick_day_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False), patch( - "screen_locker._auto_upgrade.morning_skip_today", + "screen_locker._early_bird.morning_skip_today", return_value=None, ), patch.object(locker, "_is_early_bird_time", return_value=True), @@ -193,7 +193,7 @@ class TestCheckTodayStateExits: patch.object(locker, "_is_sick_day_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False), patch( - "screen_locker._auto_upgrade.morning_skip_today", + "screen_locker._early_bird.morning_skip_today", return_value=None, ), patch.object(locker, "_is_early_bird_time", return_value=False), diff --git a/web/package-lock.json b/web/package-lock.json index 69678ce..d791f86 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,12 +17,12 @@ "@testing-library/jest-dom": "7.0.1", "@testing-library/react": "16.3.3", "@testing-library/user-event": "14.6.7", - "@types/node": "26.6.1", + "@types/node": "26.6.2", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "6.1.1", "@vitest/coverage-v8": "5.0.1", - "eslint": "10.10.0", + "eslint": "10.11.0", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.7", "globals": "17.12.0", @@ -124,7 +124,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -479,7 +478,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -528,7 +526,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1097,7 +1094,6 @@ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1237,12 +1233,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz", - "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~8.9.0" } @@ -1253,7 +1248,6 @@ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1264,7 +1258,6 @@ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.3.0" } @@ -1314,7 +1307,6 @@ "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", @@ -1549,7 +1541,6 @@ "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/istanbul-lib-coverage": "^1.0.0", @@ -1640,7 +1631,6 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1803,7 +1793,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", @@ -2052,12 +2041,11 @@ } }, "node_modules/eslint": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", - "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", "dev": true, "license": "MIT", - "peer": true, "workspaces": [ "packages/*" ], @@ -2613,7 +2601,6 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -3233,7 +3220,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3243,7 +3229,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.28.0" }, @@ -3537,7 +3522,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3634,7 +3618,6 @@ "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", @@ -3713,7 +3696,6 @@ "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.1", @@ -3925,7 +3907,6 @@ "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index 8538ec4..4c4adbf 100644 --- a/web/package.json +++ b/web/package.json @@ -23,12 +23,12 @@ "@testing-library/jest-dom": "7.0.1", "@testing-library/react": "16.3.3", "@testing-library/user-event": "14.6.7", - "@types/node": "26.6.1", + "@types/node": "26.6.2", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "6.1.1", "@vitest/coverage-v8": "5.0.1", - "eslint": "10.10.0", + "eslint": "10.11.0", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.7", "globals": "17.12.0",