A prior commit (6a7a7d8) accidentally captured only the _constants.py
half of an already-in-progress, uncommitted change removing
RUNNERUP_ACCEPTED_SPORTS -- leaving main broken (import error) since
_runnerup_verification.py and its 15 test files still referenced the
now-undefined constant. Committing the matching half: RunnerUp
entries are now judged purely by distance/duration, any sport
qualifies, matching how every other workout type is already treated.
Also: the new single-instance lock was not test-isolated, so every
ScreenLocker built across the suite contended for the same real
$XDG_RUNTIME_DIR/gatelock file -- only the first test won it, the
rest silently exited. Added to conftest.py's isolated-state table
(split into tests/_isolated_state.py to stay under the 250-line cap)
and added dedicated coverage for screen_locker/_instance.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
_runnerup_verification.py (312) -> _runnerup_tcx.py pull + parse TCX
_runnerup_backfill.py fill the log for a
date or a week
_ui_widgets.py (287) -> _ui_form_fields.py labelled fields,
focus, disable_paste
_sick_dialog.py (278) -> _sick_commitment.py next-day commitment
_ui_form_fields also takes disable_paste, since the fields are what use it;
_ui_widgets re-exports it for _manual_workout_widgets and _sick_dialog.
Two things had to move with the code rather than being left behind:
- conftest's _TK_MODULES gains _ui_form_fields, or its tkinter calls reach a
real display instead of the mock.
- pyproject's S314 waiver follows the ET.parse call it covers into
_runnerup_tcx.py. Same approved suppression, new home -- not a new one.
Test count 977 -> 978: _ui_form_fields is a new lock-surface module, so
test_no_popup_widgets safety-checks it too. 100% branch coverage held; pylint
back at 8.70 after fixing a genuinely undefined WidgetGroup annotation the
split exposed. `screen_lock --status` unchanged. Gate: 41 -> 28 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
MIN_RUN_DURATION_MINUTES and MIN_RUN_DISTANCE_KM were an AND (30+ min AND
5.0+ km), which rejected a fast 5 km run that took under 30 minutes. They
are now an OR at 40 min / 3.5 km: a run qualifies at either threshold
alone. Distance keeps its 5% GPS tolerance; duration, from the device
clock, does not.
Fixed one stale test whose fixture (1 min / 6 km) predated the OR change:
6 km alone now qualifies under the new rule, so the fixture needed to fail
BOTH branches to still exercise the "no file verified, return first
result" path it was written for.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdSp6Nbrs9xvsWkCMDN9a1
The auto-fill scan only ran once, at lock-screen login; a workout was
uncreditable for the rest of the day if the phone wasn't adb-visible at
that exact instant. Reuse the existing 15-minute workout-sync.timer to
retry it. Also upgrade the RunnerUp verification failure paths from
info to warning with the actual reason (no TCX export found, or root
DB access unavailable on this non-rooted device) instead of a vague or
invisible message.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SH7XBkpEqQX4KghHdgx9T
Check Phone only ever checked today's date, so a verified workout from
earlier in the week (e.g. yesterday's run, found once the phone connects
later) was never backfilled unless you ran the --status CLI. It now falls
back to the existing RunnerUp week-scanner plus a new StrongLifts
equivalent, still pulling and validating the real file via ADB before
crediting anything.
GPS distance tracking undershoots on tight turns/tree cover, so a run
within 5% of the 5.0 km minimum (e.g. 4.9 km) is now accepted instead of
silently rejected — duration is clock-based and gets no such leeway.
Also fixes a pre-existing bug in the --status CLI surfaced by this change:
its weekly-total summary derived counts from a per-day checkmark (max 1
per day) instead of screen_locker._weekly_check.count_weekly_workouts,
undercounting any day with more than one verified workout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWuV1nExiMfiNb9Gb7AQt
The workout log stored ONE workout per calendar day, so a second same-day
workout was silently dropped and could never earn credit.
Multiple workouts per day:
- workout_log.json: {date: entry} -> {date: [entry, ...]}. Legacy entries are
wrapped on read (_log_io.load_workout_log) so their HMACs stay valid — no
re-signing; every write normalizes the file, migrating it progressively.
- New write chokepoint (_log_mixin.write_signed_entry) APPENDS and dedups by a
stable workout_id, returning RecordResult so callers can tell a new workout
from a duplicate. Re-verifying the same workout is a no-op.
- count_weekly_workouts is anti-gaming: each VERIFIED workout counts
individually; self-reported manual entries count at most once per day. Four
real workouts in one day satisfy the week; four manual entries count as one.
- Reward: the day's first counted workout gets the base +2h; an additional
same-day VERIFIED workout gets +1h; an additional manual gets nothing. The
old "+1h when weekly_count > 4" gate is replaced — weekly volume is the
banked bonus's job and shouldn't be paid twice.
- Fixed: the unlock screen labelled the +1h "#{weekly_count - 4}", which can
render negative under the new rule. Removed the dead runnerup_manual type.
- Flutter: the manual budget counts distinct DAYS, matching the PC.
PC->phone sync (it had never worked once):
- push_pc_manuals pushed from a side-store written at log-time, so anything
logged before that feature existed could never publish. _manual_push now
derives the crdt log directly from workout_log.json — the single source of
truth — covering manual AND verified runs. The record id is the workout's own
workout_id (derived for legacy entries to match the phone's format) and the
HLC comes from the entry's own timestamp, so a re-push is byte-identical and
the repo never churns.
- The phone reads every kind (readMergedWorkoutPayloads) and lists the PC's
workouts under "SYNCED FROM PC". The manual budget still sees manual-only, so
a verified run can't consume a self-report slot.
- Settings claimed "Connected." whenever a token STRING existed. It now
verifies against the API on open, so a revoked token reports 401 instead of
showing a green lie.
Never fail silently:
- 30 swallowed exceptions across 19 files now log WARNING with the concrete
cause AND consequence — plus the token guards in _workout_sync that returned
[] in total silence, the exact bug that hid the dead sync for weeks.
- New pre-commit hook (scripts/check_silent_failures.py) fails any except that
neither re-raises nor logs loudly. ruff's S110 only catches `except: pass`;
the real pattern here was `except: log.info(); return []`. Documented in
CLAUDE.md.
Verified on real data: the Jul 13 run (pulled from its actual TCX over ADB) now
counts alongside that day's manual squash, and all three workouts are published
to kuhyx/syncs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fv8ULVt3Q67GwCFbM7doHB
- Refactor RunnerUp verification: extract RunnerUpDbMixin (_runnerup_db.py),
split _scan_and_fill_week_runnerup into a helper _try_fill_runnerup_for_date
to keep cyclomatic complexity ≤10
- Generalise TCX lookup to any date in the ISO week (was today-only); all gap
days Mon→today auto-filled on every startup and 08:30 timer firing
- Add _adjust_shutdown_time_by(): +1h per extra workout beyond the 4-workout
minimum, capped at midnight (hour=24)
- Add _shutdown_base.py: daily reset of shutdown config to a stored base so
the bonus doesn't silently accumulate across days
- Add _extra_benefits.py: streak tracking, skip credits (earn (n-4) credits
for 5+ workout weeks), early-bird extension to 09:00 for eligible weeks
- Add --status mode (_status.py): non-locking CLI view showing per-day
breakdown (✓/✗), RunnerUp auto-scan, bonus status, shutdown time, streak,
skip credits, and early-bird status
- Hook carrot into _check_non_verify_exits: bonus applied whenever auto-fill
pushes weekly count above the minimum
- Pass all pre-commit hooks (ruff, mypy, pylint, bandit, shellcheck,
codespell, max-file-length); 508 tests at 100% branch coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017auyHmf2ZwQcDAwXaSo7KX
Reads per-activity TCX exports that RunnerUp's File Synchronizer writes
to /sdcard/Documents/RunnerUp/ after each run — no root access required
and works over wireless ADB. The root DB-pull path is kept as an
automatic fallback for when no today's export file is found yet.
Setup required in RunnerUp once: Settings → Accounts → Add → File →
format=TCX, directory=Documents/RunnerUp.
TCX is preferred over GPX because TotalTimeSeconds and DistanceMeters
are pre-computed Lap elements, requiring no GPS Haversine calculation.
Multi-lap activities (pause/resume) are summed correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6oHAjRwhHEsLQCBnKrKki
Pulls RunnerUp's SQLite database directly from a rooted phone via ADB,
queries today's activity, and verifies it against configured thresholds
(30 min / 5 km minimum; Running, Orienteering, Treadmill accepted).
Handles WAL sidecar files to catch runs logged just moments before
connecting the phone.
Integration points:
- New RunnerUpVerificationMixin added to ScreenLocker base classes
- UI flow: phone check falls back to RunnerUp before showing failure
- Early-bird and sick-day auto-upgrade also try RunnerUp as fallback
- "runnerup_verified" added to COUNTED_WORKOUT_TYPES (counts toward
weekly minimum and earns the shutdown-time bonus)
- Debt clearing and commitment prompt both cover runnerup_verified
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6oHAjRwhHEsLQCBnKrKki