mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 11:43:09 +02:00
Apply focus-mode, screen-locker, and steam backlog updates
This commit is contained in:
parent
721afe5f0e
commit
1d9eddb70e
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user