mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 22:27:12 +01:00
A manual workout logged at 00:02 CEST on 2026-09-13 was filed under 2026-09-12: every day-key derivation used datetime.now(tz=UTC), and at that instant UTC was still yesterday. The 09:18 lock check looked for 2026-09-13, found nothing, and locked the PC — for a workout that was on disk. The budget counter (a trailing window) counted it fine, which is why the status showed 1/2 used while the locker still locked. Any log between local midnight and 02:00 CEST hit this; it had happened before. - New screen_locker/_day.py: today_str() is the one definition of "what day is it" — the local calendar day the user sees on the clock, which is also what the phone app, RunnerUp verification, the status window and the freedays pool already used. All 14 UTC sites (log writer, lock predicates, explain chain, early-bird, wake state, shutdown/sick state, sick budget, manual CLI, phone verification) now go through it, writers and readers together so no new midnight window opens between them. - write_signed_entry dedups by workout_id across ALL days: a workout id is globally unique, and an entry moved to its correct day must not let its synced twin (still stamped with the old day) re-ingest as a duplicate. - scripts/rekey_log_entry.py: move a wrongly-filed entry to its real day (the HMAC never covered the day key, so it still verifies). - test_local_day_key.py freezes the clock at the exact failing instant. Replayed the real timeline (log at 00:02:35, evaluate at 09:18:53, weekly 3/5) against HEAD in a throwaway worktree vs this tree: HEAD -> logged under 2026-09-12, LOCK fixed -> logged under 2026-09-13, no lock (already_logged) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FcU5dCcUNHMAMzUNChdrrJ
98 lines
3.5 KiB
Python
Executable File
98 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
r"""Move one ``log.json`` entry to the day it was really logged on.
|
|
|
|
Until 2026-09-13 the PC filed workouts under the UTC day, so anything logged
|
|
between local midnight and 02:00 CEST landed on *yesterday*. The code now
|
|
keys on the local day (``screen_locker._day``); this script repairs an entry
|
|
that was filed under the wrong key before that fix.
|
|
|
|
The HMAC signs ``timestamp`` + ``workout_data`` + ``workout_id`` — never the
|
|
day key — so the entry moves verbatim and still verifies. Its ``workout_id``
|
|
is kept too: the write chokepoint dedups by id across all days, so the synced
|
|
copy (still stamped with the old day) is recognised rather than re-ingested.
|
|
|
|
Reads and writes ``log.json`` directly rather than importing ``screen_locker``,
|
|
like the other scripts here: recovery must work while the package is mid-edit.
|
|
|
|
Usage:
|
|
python3 scripts/rekey_log_entry.py --workout-id manual:2026-09-12T14:30 \\
|
|
--to 2026-09-13
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
LOG_FILE = Path(__file__).resolve().parent.parent / "screen_locker" / "log.json"
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _report(message: str) -> None:
|
|
"""Write one line to stdout (matches the other scripts in this directory)."""
|
|
sys.stdout.write(f"{message}\n")
|
|
|
|
|
|
def rekey(log: dict, workout_id: str, to_day: str) -> str:
|
|
"""Move the entry carrying ``workout_id`` under ``to_day`` in place.
|
|
|
|
Idempotent: an entry already under ``to_day`` is left alone. Raises
|
|
``LookupError`` when the id is not in the log at all — a silent no-op
|
|
there would let a caller revoke a sick day against nothing.
|
|
"""
|
|
for day, entries in log.items():
|
|
if not isinstance(entries, list):
|
|
continue
|
|
for entry in entries:
|
|
if entry.get("workout_id") != workout_id:
|
|
continue
|
|
if day == to_day:
|
|
return f" {workout_id}: already under {to_day} — nothing to do"
|
|
entries.remove(entry)
|
|
if not entries:
|
|
del log[day]
|
|
log.setdefault(to_day, []).append(entry)
|
|
return f" {workout_id}: moved {day} -> {to_day}"
|
|
msg = f"{workout_id} is not in the log"
|
|
raise LookupError(msg)
|
|
|
|
|
|
def main() -> int:
|
|
"""Re-key one entry and rewrite the log, or exit non-zero having changed nothing."""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--workout-id", required=True, help="entry's workout_id")
|
|
parser.add_argument("--to", required=True, help="YYYY-MM-DD to file it under")
|
|
parser.add_argument("--log-file", default=str(LOG_FILE))
|
|
args = parser.parse_args()
|
|
|
|
log_path = Path(args.log_file)
|
|
try:
|
|
log = json.loads(log_path.read_text())
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
_logger.exception("Cannot read %s", log_path)
|
|
_report(f"ERROR: cannot read {log_path}: {exc}")
|
|
return 1
|
|
try:
|
|
outcome = rekey(log, args.workout_id, args.to)
|
|
except LookupError as exc:
|
|
_logger.exception("Nothing changed")
|
|
_report(f"ERROR: {exc} — nothing changed")
|
|
return 1
|
|
try:
|
|
log_path.write_text(json.dumps(log, indent=2))
|
|
except OSError as exc:
|
|
_logger.exception("Could not write %s", log_path)
|
|
_report(f"ERROR: could not write {log_path}: {exc}")
|
|
return 1
|
|
_report(outcome)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
sys.exit(main())
|