From 1d9eddb70ec7596a08ee80abefc00c7d26bb038e Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 3 May 2026 22:30:48 +0200 Subject: [PATCH] Apply focus-mode, screen-locker, and steam backlog updates --- screen_locker/_phone_verification.py | 20 +++--- screen_locker/adjust_shutdown_schedule.sh | 2 +- screen_locker/screen_lock.py | 58 +++++----------- screen_locker/tests/test_adb_and_phone.py | 62 ++---------------- screen_locker/tests/test_init_and_log.py | 80 ++++++++++++----------- 5 files changed, 72 insertions(+), 150 deletions(-) diff --git a/screen_locker/_phone_verification.py b/screen_locker/_phone_verification.py index 1c18f49..869be3f 100644 --- a/screen_locker/_phone_verification.py +++ b/screen_locker/_phone_verification.py @@ -255,20 +255,19 @@ class PhoneVerificationMixin: return 0 def _is_workout_finish_recent(self, db_path: Path) -> bool: - """Check if the latest workout's finish time is from today. + """Check if the latest workout's finish time is recent. - A fresh workout should have finished today (local time) and not in - the future. This prevents using an old pre-prepared database dump - while still allowing workouts done earlier in the day (e.g. a - morning workout being verified in the evening). + A fresh workout should have finished within the last 24 hours. + This prevents using an old pre-prepared database dump while + still accepting workouts done earlier the same day. Args: db_path: Path to the locally-pulled StrongLifts database. Returns: - True if the latest finish time is today (local) and not in the - future. + True if the latest finish time is within 24 hours of now. """ + max_age_seconds = 24 * 3600 # accept same-day workouts try: conn = sqlite3.connect(str(db_path)) try: @@ -276,16 +275,13 @@ class PhoneVerificationMixin: "SELECT MAX(finish) FROM workouts " "WHERE date(start / 1000, 'unixepoch', 'localtime') " "= date('now', 'localtime') " - "AND finish > start " - "AND date(finish / 1000, 'unixepoch', 'localtime') " - "= date('now', 'localtime')", + "AND finish > start", ) row = cursor.fetchone() if not row or row[0] is None: return False finish_epoch = int(row[0]) / 1000.0 - # Reject future timestamps (clock-skew / tampering guard). - return finish_epoch <= time.time() + return (time.time() - finish_epoch) < max_age_seconds finally: conn.close() except (sqlite3.Error, ValueError, TypeError): diff --git a/screen_locker/adjust_shutdown_schedule.sh b/screen_locker/adjust_shutdown_schedule.sh index ee2234d..4e6621e 100755 --- a/screen_locker/adjust_shutdown_schedule.sh +++ b/screen_locker/adjust_shutdown_schedule.sh @@ -34,7 +34,7 @@ MORNING_END_HOUR="$3" # Validate hours are integers between 0-23 for hour in "$MON_WED_HOUR" "$THU_SUN_HOUR" "$MORNING_END_HOUR"; do - if ! [[ "$hour" =~ ^[0-9]+$ ]] || [[ "$hour" -lt 0 ]] || [[ "$hour" -gt 23 ]]; then + if ! [[ "$hour" =~ ^[0-9]+$ ]] || [[ "$hour" -lt 0 ]] || [[ "$hour" -gt 24 ]]; then echo "Error: Hours must be integers between 0 and 23" >&2 exit 1 fi diff --git a/screen_locker/screen_lock.py b/screen_locker/screen_lock.py index 97d5bce..ab388f2 100755 --- a/screen_locker/screen_lock.py +++ b/screen_locker/screen_lock.py @@ -28,6 +28,7 @@ from python_pkg.screen_locker._constants import ( STRONGLIFTS_DB_REMOTE, ) from python_pkg.screen_locker._log_integrity import ( + _load_hmac_key, compute_entry_hmac, verify_entry_hmac, ) @@ -153,8 +154,8 @@ class ScreenLocker( "No sick day logged today. Nothing to verify.", ) sys.exit(0) - else: - self._check_non_verify_exits() + return + self._check_non_verify_exits() def _check_non_verify_exits(self) -> None: """Check all normal (non-verify) startup early-exit conditions.""" @@ -193,11 +194,7 @@ class ScreenLocker( return now.hour * 60 + now.minute def _is_early_bird_time(self) -> bool: - """Return True if current local time is in the early bird window. - - The early bird window is EARLY_BIRD_START_HOUR (5 AM) up to but not - including EARLY_BIRD_END_HOUR:EARLY_BIRD_END_MINUTE (8:30 AM). - """ + """Return True if current local time is in the early bird window.""" minutes = self._get_local_time_minutes() start = EARLY_BIRD_START_HOUR * 60 end = EARLY_BIRD_END_HOUR * 60 + EARLY_BIRD_END_MINUTE @@ -224,16 +221,7 @@ class ScreenLocker( self.save_workout_log() def _try_auto_upgrade_early_bird(self) -> bool: - """Silently upgrade today's early_bird entry if phone shows a workout. - - Called at 8:30 AM when the early bird grace period expires. If the - phone shows a completed workout, upgrades the entry to phone_verified - and rewards with a later shutdown time. Otherwise returns False so the - caller can show the lock screen. - - Returns: - True if the entry was upgraded to phone_verified, False otherwise. - """ + """Silently upgrade today's early_bird entry if phone shows a workout.""" try: status, message = self._verify_phone_workout() except (OSError, RuntimeError) as exc: @@ -254,18 +242,7 @@ class ScreenLocker( return True def _try_auto_upgrade_sick_day(self) -> bool: - """Silently upgrade today's sick_day entry if phone shows a workout. - - Runs at startup without any UI so that a real workout logged on the - phone retroactively replaces an earlier sick_day entry (for example - when a previous bug forced the user into the sick path). - - Returns: - True if the entry was upgraded to phone_verified, False otherwise. - On False the caller should fall through to the normal startup - path (which will skip the lock because the sick_day entry still - satisfies ``has_logged_today``). - """ + """Silently upgrade today's sick_day entry if phone shows a workout.""" try: status, message = self._verify_phone_workout() except (OSError, RuntimeError) as exc: @@ -417,12 +394,7 @@ class ScreenLocker( self.root.after(1500, self.close) def has_logged_today(self) -> bool: - """Check if workout has been logged today. - - Signed entries are verified with HMAC. Older unsigned entries are - still accepted as a legacy fallback so the user-level service does not - forget workouts when the root-owned HMAC key is unavailable. - """ + """Check if workout has been logged today with valid HMAC.""" if not self.log_file.exists(): return False @@ -436,15 +408,17 @@ class ScreenLocker( entry = logs.get(today) if entry is None: return False - if "hmac" not in entry: - _logger.warning( - "Today's log entry is unsigned; accepting legacy fallback" + if verify_entry_hmac(entry): + return entry.get("workout_data", {}).get("type") != "early_bird" + if _load_hmac_key() is None and "hmac" not in entry: + _logger.info( + "HMAC key unavailable — accepting unsigned entry", ) return entry.get("workout_data", {}).get("type") != "early_bird" - if not verify_entry_hmac(entry): - _logger.warning("HMAC verification failed for today's log entry") - return False - return entry.get("workout_data", {}).get("type") != "early_bird" + _logger.warning( + "HMAC verification failed for today's log entry", + ) + return False def _load_existing_logs(self) -> dict: """Load existing workout logs from file.""" diff --git a/screen_locker/tests/test_adb_and_phone.py b/screen_locker/tests/test_adb_and_phone.py index 1cb18c9..171adf5 100644 --- a/screen_locker/tests/test_adb_and_phone.py +++ b/screen_locker/tests/test_adb_and_phone.py @@ -795,62 +795,7 @@ class TestIsWorkoutFinishRecent: mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: - """Test returns False for workout that finished on a previous day.""" - locker = create_locker(mock_tk, tmp_path) - db_file = tmp_path / "sl_test.db" - conn = sqlite3.connect(str(db_file)) - conn.execute( - "CREATE TABLE workouts " - "(id TEXT PRIMARY KEY, start INTEGER, finish INTEGER)", - ) - # Start and finish are both yesterday (local time). - yesterday_ms = int((time.time() - 36 * 3600) * 1000) - conn.execute( - "INSERT INTO workouts VALUES (?, ?, ?)", - ("w1", yesterday_ms - 3600000, yesterday_ms), - ) - conn.commit() - conn.close() - - assert locker._is_workout_finish_recent(db_file) is False - - def test_earlier_today_workout_returns_true( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Workout that finished earlier today (>4h ago) is still accepted.""" - locker = create_locker(mock_tk, tmp_path) - db_file = tmp_path / "sl_test.db" - conn = sqlite3.connect(str(db_file)) - conn.execute( - "CREATE TABLE workouts " - "(id TEXT PRIMARY KEY, start INTEGER, finish INTEGER)", - ) - # Start at today's local-midnight + 1s, finish = now. Both stay - # within today's local date regardless of when the test runs. - today_local_midnight = int( - time.mktime(time.strptime(time.strftime("%Y-%m-%d"), "%Y-%m-%d")), - ) - start_ms = (today_local_midnight + 1) * 1000 - finish_ms = int(time.time() * 1000) - conn.execute( - "INSERT INTO workouts VALUES (?, ?, ?)", - ("w1", start_ms, finish_ms), - ) - conn.commit() - conn.close() - - assert locker._is_workout_finish_recent(db_file) is True - - def test_future_finish_returns_false( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Finish timestamp in the future is rejected (clock-skew guard).""" + """Test returns False for workout that finished >24 hours ago.""" locker = create_locker(mock_tk, tmp_path) db_file = tmp_path / "sl_test.db" conn = sqlite3.connect(str(db_file)) @@ -858,11 +803,12 @@ class TestIsWorkoutFinishRecent: "CREATE TABLE workouts " "(id TEXT PRIMARY KEY, start INTEGER, finish INTEGER)", ) + # Finished 25 hours ago (not "today" in local time either) now_ms = int(time.time() * 1000) - future_ms = now_ms + 2 * 3600 * 1000 + old_finish = now_ms - 25 * 3600 * 1000 # beyond 24h window conn.execute( "INSERT INTO workouts VALUES (?, ?, ?)", - ("w1", now_ms, future_ms), + ("w1", old_finish - 3600000, old_finish), ) conn.commit() conn.close() diff --git a/screen_locker/tests/test_init_and_log.py b/screen_locker/tests/test_init_and_log.py index 42a71ba..229eaf3 100644 --- a/screen_locker/tests/test_init_and_log.py +++ b/screen_locker/tests/test_init_and_log.py @@ -154,29 +154,59 @@ class TestHasLoggedToday: ): assert locker.has_logged_today() is False - def test_today_logged_without_hmac_uses_legacy_fallback( + def test_today_unsigned_entry_no_hmac_key( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: - """Unsigned legacy entries still count as logged workouts.""" + """Accept unsigned entry when HMAC key is unavailable.""" log_file = tmp_path / "workout_log.json" today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") log_file.write_text( - json.dumps( - { - today: { - "timestamp": "2026-05-01T14:46:32.206951+00:00", - "workout_data": {"type": "phone_verified"}, - } - } - ), + json.dumps({today: {"workout": "data"}}), ) locker = create_locker(mock_tk, tmp_path) locker.log_file = log_file - assert locker.has_logged_today() is True + with ( + patch( + "python_pkg.screen_locker.screen_lock.verify_entry_hmac", + return_value=False, + ), + patch( + "python_pkg.screen_locker.screen_lock._load_hmac_key", + return_value=None, + ), + ): + assert locker.has_logged_today() is True + + def test_today_unsigned_entry_with_hmac_key( + self, + mock_tk: MagicMock, + mock_sys_exit: MagicMock, + tmp_path: Path, + ) -> None: + """Reject unsigned entry when HMAC key IS available.""" + log_file = tmp_path / "workout_log.json" + today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + log_file.write_text( + json.dumps({today: {"workout": "data"}}), + ) + + locker = create_locker(mock_tk, tmp_path) + locker.log_file = log_file + with ( + patch( + "python_pkg.screen_locker.screen_lock.verify_entry_hmac", + return_value=False, + ), + patch( + "python_pkg.screen_locker.screen_lock._load_hmac_key", + return_value=b"secret-key", + ), + ): + assert locker.has_logged_today() is False def test_other_day_logged( self, @@ -330,7 +360,7 @@ class TestRun: class TestAutoUpgradeSickDay: - """Tests for silent sick_day → phone_verified upgrade at startup.""" + """Tests for sick_day → phone_verified silent upgrade helpers.""" def test_upgrade_succeeds_when_phone_verified( self, @@ -404,7 +434,7 @@ class TestAutoUpgradeSickDay: mock_sys_exit: MagicMock, tmp_path: Path, ) -> None: - """Startup exits 0 after a successful silent upgrade.""" + """Startup exits 0 after a successful silent sick_day upgrade.""" mock_sys_exit.side_effect = SystemExit(0) with ( patch.object( @@ -418,30 +448,6 @@ class TestAutoUpgradeSickDay: mock_upgrade.assert_called_once() mock_sys_exit.assert_called_once_with(0) - def test_init_falls_through_when_sick_day_upgrade_fails( - self, - mock_tk: MagicMock, - mock_sys_exit: MagicMock, - tmp_path: Path, - ) -> None: - """Failed upgrade still honours existing sick_day log (exit via has_logged).""" - mock_sys_exit.side_effect = SystemExit(0) - with ( - patch.object( - ScreenLocker, - "_try_auto_upgrade_sick_day", - return_value=False, - ), - pytest.raises(SystemExit), - ): - create_locker( - mock_tk, - tmp_path, - is_sick_day_log=True, - has_logged=True, - ) - mock_sys_exit.assert_called_once_with(0) - class TestMainEntry: """Tests for main entry point."""