mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 22:53:53 +01:00
screen_locker pushed manual workouts as "pc" and the workout app as "phone". A role constant collides the moment a second machine takes the same role, and after a reinstall the new install inherits the previous one's CRDT identity. Both halves now mint a uuid once and persist it: the PC in ~/.local/share/screen_locker/.device_id, the app under SharedPreferences 'crdt.nodeId' -- the same key every sibling app uses. The old constant survives as the legacy id and is passed to sync_log / syncLog, so devices/<legacy>/ is skipped as this device's own rather than re-downloaded and re-merged as a peer's on every tick. It drops to None once that path is reclaimed. Note the *read* loop in _workout_sync deliberately still merges every device including this one: the PC needs its own manual workouts in the merged view, so "skip own" would be wrong there. Only the push path and the HLC node id move. The test fixture redirecting the new id file is added in the same change that introduces the constant -- without it the first test that syncs mints a uuid into the real state dir and this machine's live sync identity is decided by whichever test ran first. Verified: ~/.local/share/screen_locker/ held no .device_id after a full suite run. Verified against the live backend, not just mocks: ran the real push_pc_workouts, which minted 57582b3b and pushed 9 records. In the RTDB the new device's log is a strict superset of both old paths -- 0 of devices/pc/'s 10 records and 0 of devices/phone/'s 9 are missing from it. Python: 907 tests, 100% branch coverage. workout_app: 313 tests, analyzer clean. Pins bumped to crdt-sync-v0.6.0 / crdt_sync_dart-v0.7.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZH89WEqAjuJT8WZdqc7AP
165 lines
8.6 KiB
Python
165 lines
8.6 KiB
Python
"""Constants for the screen locker module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from gatelock.log_integrity import DEFAULT_HMAC_KEY_FILE
|
|
|
|
# Single-sourced from gatelock so the literal key path can't drift.
|
|
HMAC_KEY_FILE = DEFAULT_HMAC_KEY_FILE
|
|
SICK_LOCKOUT_SECONDS = 120 # base 2 minutes wait when sick (escalates with usage)
|
|
PHONE_PENALTY_DELAY_DEMO = 10
|
|
PHONE_PENALTY_DELAY_PRODUCTION = 100
|
|
# Penalty added to phone-penalty timer when ADB / phone unavailable
|
|
# (so unplugging phone does not become an easy escape into sick mode).
|
|
NO_PHONE_EXTRA_LOCKOUT_SECONDS = 480 # extra 8 minutes on top of base
|
|
# Sick day rate-limiting (rolling windows). Once any window is exhausted
|
|
# the "I'm sick" button disappears entirely.
|
|
SICK_BUDGET_PER_7_DAYS = 1
|
|
SICK_BUDGET_PER_30_DAYS = 3
|
|
SICK_BUDGET_PER_90_DAYS = 10
|
|
# Each sick day in the trailing 30 days doubles the wait countdown.
|
|
SICK_LOCKOUT_MULTIPLIER_PER_RECENT = 2
|
|
# Minimum chars in the freeform sick justification.
|
|
SICK_JUSTIFICATION_MIN_CHARS = 120
|
|
# How many past sick justifications to show on the dialog (read-only).
|
|
SICK_HISTORY_REVIEW_COUNT = 10
|
|
# 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.
|
|
SICK_COMMITMENT_PENALTY_DAYS = 2
|
|
# How long the commitment prompt stays visible after a workout unlock.
|
|
COMMITMENT_PROMPT_TIMEOUT_SECONDS = 15
|
|
# Manual (unverified) workout rate-limiting — looser than sick days since
|
|
# activities like table tennis recur more often than illness. Once either
|
|
# window is exhausted the "Log Manual Workout" option disappears entirely.
|
|
MANUAL_WORKOUT_BUDGET_PER_7_DAYS = 2
|
|
MANUAL_WORKOUT_BUDGET_PER_30_DAYS = 10
|
|
# Minimum start-to-end duration for a manual entry to be accepted.
|
|
MANUAL_WORKOUT_MIN_DURATION_MINUTES = 20
|
|
# Minimum chars for the "what was done" activity-details field.
|
|
MANUAL_WORKOUT_DESCRIPTION_MIN_CHARS = 40
|
|
# Minimum chars for each reflection field (what went well / to improve / feeling).
|
|
MANUAL_WORKOUT_REFLECTION_MIN_CHARS = 20
|
|
MANUAL_WORKOUT_RPE_MIN = 1
|
|
MANUAL_WORKOUT_RPE_MAX = 10
|
|
ADB_TIMEOUT = 15
|
|
# Workout app JSON candidate paths on the phone, in the order the app prefers
|
|
# when writing (see sync_service.dart). The app writes to the primary /sdcard/
|
|
# path first and only falls back to its app-external files dir if /sdcard/ is
|
|
# not writable, so the locker must check both — primary first.
|
|
WORKOUT_APP_JSON_REMOTES = (
|
|
"/sdcard/workout_result.json",
|
|
"/storage/emulated/0/Android/data/com.kuhy.workout_app/files/workout_result.json",
|
|
)
|
|
# Port the workout app's HTTP server listens on (no ADB/developer-options needed).
|
|
WORKOUT_HTTP_PORT = 8765
|
|
MIN_WORKOUT_DURATION_MINUTES = 60
|
|
RUNNERUP_PACKAGES = ("org.runnerup", "org.runnerup.free")
|
|
RUNNERUP_DB_SDCARD_TMP = "/sdcard/.runnerup_tmp_verification.db"
|
|
MIN_RUN_DURATION_MINUTES = 30
|
|
MIN_RUN_DISTANCE_KM = 5.0
|
|
# GPS distance tracking undershoots on tight turns/tree cover/urban canyons;
|
|
# a run within 5% of the minimum is accepted rather than rejected on
|
|
# measurement noise. Duration comes from the device clock, not GPS, so it
|
|
# gets no such leeway.
|
|
RUNNERUP_DISTANCE_TOLERANCE = 0.05
|
|
RUNNERUP_ACCEPTED_SPORTS: frozenset[int] = frozenset({0, 3, 5})
|
|
# 0=RUNNING, 3=ORIENTEERING, 5=TREADMILL
|
|
MAX_CLOCK_SKEW_SECONDS = 300 # 5 minutes max time skew from NTP
|
|
EARLY_BIRD_START_HOUR = 5
|
|
EARLY_BIRD_END_HOUR = 8
|
|
EARLY_BIRD_END_MINUTE = 30
|
|
HEAT_SKIP_TEMP_THRESHOLD: int = 33 # °C — above this the heat-skip dialog is offered
|
|
HEAT_SKIP_CITY: str = "Warsaw"
|
|
SHUTDOWN_CONFIG_FILE = Path("/etc/shutdown-schedule.conf")
|
|
# Helper script path (relative to this file)
|
|
ADJUST_SHUTDOWN_SCRIPT = Path(__file__).resolve().parent / "adjust_shutdown_schedule.sh"
|
|
# State file to track sick day usage and original config values
|
|
SICK_DAY_STATE_FILE = Path(__file__).resolve().parent / "sick_day_state.json"
|
|
# Persistent sick-day history (rate-limit, debt, commitments, justifications).
|
|
# Distinct from SICK_DAY_STATE_FILE which is a one-day shutdown-config snapshot.
|
|
SICK_HISTORY_FILE = Path(__file__).resolve().parent / "sick_history.json"
|
|
# JSON list of ISO date strings ("YYYY-MM-DD") for which the screen lock is skipped.
|
|
SCHEDULED_SKIPS_FILE = Path(__file__).resolve().parent / "scheduled_skips.json"
|
|
# State file tracking streak, skip credits, and early-bird extension weeks.
|
|
EXTRA_BENEFITS_FILE = Path(__file__).resolve().parent / "extra_benefits_state.json"
|
|
# State file storing the base (pre-bonus) shutdown hours and last reset date.
|
|
SHUTDOWN_BASE_FILE = Path(__file__).resolve().parent / "shutdown_base.json"
|
|
# Self-expiring marker: "logged in during today's early-bird window, still
|
|
# waiting to see if a real workout shows up." Not a workout_log.json entry —
|
|
# it's a same-day pending flag, checked against its own "date" field.
|
|
EARLY_BIRD_PENDING_FILE = Path(__file__).resolve().parent / "early_bird_pending.json"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wake-alarm integration (originally from wake_alarm._constants / _state).
|
|
# These must match the values used by the companion wake_alarm service.
|
|
# ---------------------------------------------------------------------------
|
|
# Days on which the wake alarm fires (0=Mon … 6=Sun).
|
|
ALARM_DAYS: frozenset[int] = frozenset({0, 4, 5, 6})
|
|
# How many hours after midnight the alarm triggers (configurable in wake_alarm).
|
|
WAKE_AFTER_HOURS: int = 8
|
|
# Path to the rtcwake binary.
|
|
RTCWAKE_BIN: str = "/usr/sbin/rtcwake"
|
|
# State file written by wake_alarm; read here to check for workout skip.
|
|
WAKE_STATE_FILE = (
|
|
Path(__file__).resolve().parent.parent / "wake_alarm" / "wake_state.json"
|
|
)
|
|
|
|
# Directories where RunnerUp writes per-activity TCX exports (File Synchronizer).
|
|
# Listed in preference order; both resolve to the same path on most devices.
|
|
RUNNERUP_EXPORT_DIRS: tuple[str, ...] = (
|
|
"/sdcard/Documents/RunnerUp",
|
|
"/storage/emulated/0/Documents/RunnerUp",
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sync (phone workout data) — Workstream C, built on the shared crdt-sync
|
|
# library. GitHub is used purely as dumb file storage via the REST Contents
|
|
# API (not a git clone), same pattern as diet_guard's cross-device sync. The
|
|
# phone app pushes to devices/<SYNC_PHONE_DEVICE_ID>/log.json; the PC reads
|
|
# every device's log and pushes its OWN manual workouts to devices/pc/log.json
|
|
# (see _manual_push) — which needs a token with contents:write, not read-only.
|
|
# ---------------------------------------------------------------------------
|
|
SYNC_REPO_OWNER: str = "kuhyx"
|
|
SYNC_REPO_NAME: str = "syncs"
|
|
# The phone's pre-uuid device id. Still read (the phone may not have migrated
|
|
# yet), but no longer the only path the reader knows about -- it discovers
|
|
# every device directory.
|
|
SYNC_PHONE_DEVICE_ID: str = "phone"
|
|
# The id this machine pushed manual workouts under before per-install uuids.
|
|
# Kept so the sync layer still recognises devices/pc/ as its own and does not
|
|
# re-merge its own history as a peer's; drop to None once that path is gone.
|
|
SYNC_LEGACY_DEVICE_ID: str | None = "pc"
|
|
# This machine's sync id, minted once and persisted. A fixed "pc" collides as
|
|
# soon as a second machine takes the same role, and a reinstall would inherit
|
|
# the previous install's CRDT identity.
|
|
SYNC_DEVICE_ID_FILE: Path = (
|
|
Path.home() / ".local" / "share" / "screen_locker" / ".device_id"
|
|
)
|
|
# A fine-grained GitHub PAT, scoped to just SYNC_REPO_NAME's contents. The
|
|
# user creates this once via github.com and saves it here, mode 600. Never
|
|
# committed — this path is outside the repo entirely. Unlike diet_guard,
|
|
# this file being absent is a normal, expected state (sync is optional here).
|
|
SYNC_TOKEN_FILE: Path = Path.home() / ".config" / "screen_locker" / "sync_token"
|
|
|
|
# Revision cache for cross-device sync. Without it every push re-uploads and
|
|
# every pull re-downloads regardless of change, which is the traffic the
|
|
# Firebase free tier's monthly budget depends on not happening.
|
|
SYNC_STATE_FILE: Path = (
|
|
Path.home() / ".local" / "share" / "screen_locker" / "sync_state.json"
|
|
)
|
|
SYNC_TIMEOUT_SECONDS: float = 10.0
|
|
|
|
# Sync runs from the morning routine, which is started right after boot and
|
|
# right after resume — often seconds before networking is actually up. Such a
|
|
# run used to fail outright, and reported it as a permissions problem
|
|
# (2026-07-20: six workouts "FAILED" at 08:04 with the network still down, then
|
|
# pushed fine by hand minutes later on the same token). So retry before giving
|
|
# up. Exponential backoff from SYNC_RETRY_DELAY_SECONDS gives 2s + 4s + 8s =
|
|
# ~14s of patience — enough for DHCP/DNS to come up, without making a genuinely
|
|
# offline run drag the whole morning routine.
|
|
SYNC_RETRY_ATTEMPTS: int = 4
|
|
SYNC_RETRY_DELAY_SECONDS: float = 2.0
|