Commit Graph

101 Commits

Author SHA1 Message Date
0c6f6957a8 Wire gatelock.LockConfig into every window, not just the lock overlay
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
self._colors = LockConfig(...) is now built unconditionally in
ScreenLocker.__init__ (previously only the fullscreen lock path built a
local `config`), and every raw hex literal across status_view.py,
_ui_flows.py, _ui_flows_relaxed.py, _sick_dialog.py,
_manual_workout_dialog.py, _manual_workout_widgets.py, and
_temperature_status_mixin.py now reads from it instead -- replacing
ad hoc greens/oranges/reds (#00ff00, #0066cc, #ff4444, ...) with the
shared success/accent/danger/warning/muted tokens. Also fixes the
verify-window and status-window paths that still hardcoded #1a1a1a for
the background.

This was the prerequisite the on-fill fix (previous commit) actually
needed: _window_setup.py/_ui_widgets.py/_heat_skip.py reference
self._colors, which nothing in the codebase constructed outside the
fullscreen lock path until this commit.

Verified: 859/859 tests, 100% coverage.
2026-07-21 21:45:59 +02:00
96701134c6 Fix stale adaptive_icon_background left over from the warm-palette icon regen
pubspec.yaml's adaptive_icon_background is a separate literal from
python_pkg/app_icons' own style.BACKGROUND -- the icon regen updated
every generated PNG but missed this one, so the actual on-device home
screen icon (Android's adaptive icon, not the legacy mipmap) stayed on
the old cool charcoal. Caught on the physical device, not by file checks.

testsAndMisc's generator now syncs this automatically going forward.
2026-07-21 21:42:59 +02:00
6629260256 Fix on-fill text contrast on filled buttons, bump gatelock pin
_ui_widgets.py's _button() always set fg=self._colors.fg (near-white)
regardless of bg, so any accent/success/warning/danger-filled button
under-contrasted (measured 2.2-3.6:1 vs the 4.5:1 floor). Added
_fg_for(bg) so fg is derived from bg instead of hardcoded -- fixes every
current _button() call site structurally, not per-instance. Found and
fixed 2 more standalone instances of the same bug outside the shared
helper (_window_setup.py's demo-close button, _heat_skip.py's heat-skip
button).

Bumps gatelock to the released v0.1.1 carrying the same on_fill token
plus the warm palette rework. Regenerated workout_app's launcher icon
assets to match.

Verified: gatelock 50/50 + screen_locker 859/859 tests, both 100%
coverage; pre-commit (incl. pylint against the real v0.1.1 install) green.
2026-07-21 21:17:14 +02:00
541db3701d Add a design-rules audit against the new safe-design-rules checklist
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Report-only findings for a future cleanup pass; no code changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:25:20 +02:00
7eca3f4145 Pin the StrongLifts week-scan tests to a fixed Wednesday
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
test_fills_gap_for_past_week_day and test_rescanning_an_already_filled_day
built their fixture date as date.today() - 1 day and asserted it was "this
week". _try_fill_stronglifts_for_week accepts week_start <= entry <= today,
and on a Monday week_start IS today — so yesterday (Sunday) belongs to the
previous ISO week and both tests failed. They passed Tue-Sun and failed every
Monday, which is exactly when they were noticed: they blocked an unrelated
push.

Pin "now" to Wednesday 2026-07-22 with the _FakeDatetime pattern already used
in test_status_part2, so the fixture day is Tuesday — comfortably inside the
week whatever day the suite actually runs. Verified under faketime on Mon,
Tue, Fri and Sun: 8 passed each time.

The neighbouring "last week" case needs no pinning: 8 days back always
precedes a week start, on any weekday.

Also covers the remaining branch in _try_fill_stronglifts_for_week — an
in-week JSON that fails content validation must not be written. It was
unreachable on a Monday, so the file had never hit 100% on one; the suite is
now 859 passed / 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 08:57:25 +02:00
b0d9165a79 Retry sync past the boot/resume network race, and stop blaming the token
The morning routine is started by systemd right after boot and right after
resume, so its sync ran seconds before DHCP/DNS were up. Every such run failed
outright — and said this:

    Workout sync push FAILED for 6 workout(s): network error reading
    screen-locker-sync/devices — a 403 here means the token lacks
    contents:write on kuhyx/syncs

Both halves of that were wrong. It was not a 403, and the token was fine: the
same six workouts pushed successfully by hand minutes later on the same token,
which reports push=true on the repo. The message cost a real investigation
chasing a permissions bug that did not exist.

- _sync_retry.with_sync_retry wraps the three sync call sites in a bounded
  retry (4 attempts, 2s/4s/8s backoff — ~14s, enough for the network to come
  up, short enough not to drag the routine). Verified offline in a network
  namespace: three retries, then an honest give-up.
- Any GitHubSyncError retries, deliberately including RepoNotFoundError.
  Classifying transient vs permanent would couple us to which exception
  crdt_sync chains where; a few wasted seconds on a genuinely bad token is the
  cheaper mistake.
- The push failure log now reports what actually happened. The contents:write
  hint survives only for RepoNotFoundError, which is the one error that
  actually implicates the repo or the token.

Retry at the application layer rather than ordering the unit after
network-online.target: the same race hits on resume, where boot ordering does
not apply, and these are user units that cannot depend on a system target.

The tests' autouse no-sleep fixture patches the module-local `sleep` name
rather than `_sync_retry.time.sleep` — the latter mutates the shared time
module object and silently disabled sleep for the whole suite, breaking an
unrelated temperature timeout test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 08:46:37 +02:00
135545ad1e workout_app: add trailing newline to generated colors.xml
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
flutter_launcher_icons writes the file without one, which trips the
end-of-file-fixer hook on push.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:04:35 +02:00
a3997321ba workout_app: replace the stock Flutter launcher icon
Adopt the shared icon family (python_pkg/app_icons in testsAndMisc): a
charcoal #1B1D21 field carrying a crimson barbell glyph. Crimson rather than
this app's indigo seed, because dufs_client already owns indigo and the two
sit side by side in the launcher; crimson is the app's own accent colour
(Color(0xFFFF4444)), so the icon still matches the UI.

Adds the adaptive-icon layers the project never had, including an Android 13
monochrome layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 16:58:53 +02:00
9172b1ec8a Fix CI-only failure: avoid dating a test entry "today" without HMAC
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
has_logged_today treats an unsigned today-dated entry as "logged" only
when no HMAC key is configured on the machine (CI has none; this dev
box has a real key), so the test's precondition diverged between
environments. Reach the new weekly minimum via Jan 1-4 instead of
adding a same-day entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 13:56:44 +02:00
298daf028d Give manual workouts full effect parity; raise minimum to 5, double budget
Synced manual workouts never earned shutdown/debt credit (only weekly
count), which silently missed pushing shutdown time for a same-day
workout logged on the phone. Manual workouts now go through the same
credit logic as any other workout type, count individually toward the
weekly total and rate budget (no more once-per-day collapse), and stack
same-day shutdown bonuses like verified workouts. Also bumps the weekly
minimum from 4 to 5 and doubles the monthly manual-workout budget to 10.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 13:50:39 +02:00
57c3f30e7b Add change explanation: check-phone-week-aware
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ERWuV1nExiMfiNb9Gb7AQt
2026-07-18 07:08:11 +02:00
c277325a3e Make Check Phone week-aware; add RunnerUp GPS tolerance; fix CLI weekly count
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
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
2026-07-18 06:55:55 +02:00
8cb1ce0be8 Add change explanation: multi-workout-day-and-sync
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Walkthrough of e92a74c..65380df: why the day-keyed log had one slot per
day, the anti-gaming counting rule (verified stack, manual caps at 1/day),
the workout_id dedup and the legacy-entry bug it missed, deriving the sync
push from workout_log.json, and the no-silent-failures hook.

Includes a micro-world for the counting rule: add verified/manual workouts
to days and watch the weekly count and the lock decision, including the
4-manual cheat the cap exists to stop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fv8ULVt3Q67GwCFbM7doHB
2026-07-17 11:05:41 +02:00
65380df425 Mirror the in-progress session so an app-data wipe can't lose your set
The active session (which set you are on, and the reps done so far) lived
only in app-private SQLite. An uninstall — or `pm clear`, which sounds
harmless and is not — wipes that, so a user standing mid-workout loses the
exact set and rep they were on. `backup.json` did not help: it mirrors only
COMPLETED history. This is not hypothetical; it happened, mid-workout, and
the state had to be re-entered by hand.

Persistence granularity was already right (saveActiveSession fires on every
set tap, untap, warmup and break change, storing `tapped` + `doneReps`).
Only the LOCATION was wrong, so this mirrors it the same way the sync token
already is:

- BackupService.exportActiveSession/readActiveSession keep
  /sdcard/WorkoutTracker/active_session.json; a null export clears it.
- StorageService.saveActiveSession mirrors on every write;
  loadActiveSession falls back to the mirror and re-seeds the table when it
  is empty — exactly the state after a wipe; clearActiveSession clears the
  mirror too, so a finished workout is never resurrected.
- Both reads are defensive: a truncated/non-object mirror (battery death
  mid-set) is ignored rather than bricking the resume.

Tested by simulating the wipe in the suite (resetForTesting + fresh init
with the mirror intact), not on a real device holding real data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fv8ULVt3Q67GwCFbM7doHB
2026-07-17 09:57:54 +02:00
b026dfd074 Recover the sync token from backup when the stored one is rejected
SyncSettings.load() prefers the keystore and only consults the external
backup when the keystore is EMPTY. So a stale keystore entry — one left by
an earlier authorization — shadows a perfectly good backup indefinitely:
every sync 401s and the user is told to re-authorize for nothing. This is
not hypothetical; it is the most likely explanation for the phone's 401,
whose token turned out to be healthy (GET /user 200, scope repo) once read
from the backup.

- SyncSettings.recoverFromBackup(rejectedToken) re-seeds the keystore from
  the backup and returns the recovered token, or null when there is nothing
  to recover (no backup, empty, or the same token that was just rejected).
- settings_screen retries verification once via the backup before demanding
  re-auth, and says so ("recovered the saved token from backup") instead of
  nagging.
- workout_sync_service retries the read once from the backup, so history
  self-heals without the user having to visit Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fv8ULVt3Q67GwCFbM7doHB
2026-07-17 09:45:12 +02:00
79879263ba Support multiple workouts per day; fix the dead sync; ban silent failures
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
2026-07-17 09:22:31 +02:00
e92a74c982 Add change explanation: flutter-coverage-to-100
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Interactive walkthrough of the coverage grind that unblocked the push
gate: the FakeAsync vs real-loop tension, the runAsync tap technique,
lazy-ListView coverage, and justified coverage:ignore. Includes a
zone-simulator micro-world and a debiased quiz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 14:42:04 +02:00
a0923ce694 Fix flutter analyze infos for the push gate
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Document the now-public ServerAddressTile.addresses field, wrap the prod
DB-path ignore in a start/end block to avoid an >80-char line, and reflow
a pre-existing >80-char comment in sync_service. flutter analyze is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 14:28:45 +02:00
79c7d8d1c6 Cover workout_screen.dart to 100%
Add live-workout interaction tests: set-tap breaks + Skip, warmup break,
rep decrement recompute, long-press reset, in-workout threshold change,
finish -> summary dialog, reset-confirm clears the session, and restoring
an in-progress break through to _onBreakFinished. Interaction taps run
under runAsync so their sqflite writes/break timers stay on the real loop.
coverage:ignore the Android/iOS-only Vibration.vibrate call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 14:26:12 +02:00
a27efefefc Cover history_screen.dart to 100%
Add an exercise drill-down test (dropdown select -> streak stats card,
weight chart, exercise calendar nav, session tiles with warmup/reps
variants) and a same-date test for the chart painter's tRange==0 branch.
coverage:ignore the _load exercise-state branch (unreachable — selection
goes through _pickExercise, not _load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 14:10:21 +02:00
e14cdf9997 Cover settings_screen.dart to 100%
Add tests for the threshold selectors (success + fail persistence), the
debounced weight write, and three device-flow branches that were untested:
token-save failure (throwing secure storage), verification failure, and
the transient pending "verifying" status tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 14:03:21 +02:00
5fe4222dfd Cover home_screen.dart to 100%
Cover the post-navigation reload paths (return from workout via Resume,
return from settings), the resume-button closure, and the empty-address
placeholder. Expose ServerAddressTile so its no-address fallback can be
unit-tested directly (the test host always has network interfaces).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 13:57:10 +02:00
b76d156ae3 Cover storage_service.dart to 100%
Migration (v1->v3), reps-increment at weight cap, and restore-from-backup
paths were untested. Add targeted unit tests plus a file-db test hook
(resetForTesting dbPath) so onUpgrade actually runs. coverage:ignore the
prod DB path and the defensive null-state branch (unreachable — seeding
guarantees a state row).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
2026-07-13 13:49:38 +02:00
a5ea2ff206 Cover home_screen navigation/resume flows toward 100%
Add widget tests for the manual-workout icon navigation, starting a workout
(navigates to WorkoutScreen), and auto-resume of a saved active session.
home_screen down to 4 uncovered lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 13:32:01 +02:00
0d0e6e4481 Cover services + app bootstrap toward 100% flutter coverage
Pre-existing coverage gaps, unrelated to the manual-workout feature, blocking the
100% pre-push gate:

- widget_test: exercise WorkoutApp's app-lifecycle handler (paused/detached/
  resumed/inactive/hidden) via handleAppLifecycleStateChanged.
- http_server_service: add a resetForTesting seam and test the bind-in-use
  SocketException swallow plus the /workout HTTP endpoint (no-data 404, data 200,
  unknown-path 404). coverage:ignore the /sdcard-file-read branch (only reachable
  on a real device).
- backup_service: coverage:ignore the Android permission wrappers and the
  defensive readSyncToken catch.
- main(): coverage:ignore the platform-channel app bootstrap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 13:27:18 +02:00
5b3b15f73c Mark Android-only external-storage blocks coverage:ignore
getExternalStorageDirectory() is unavailable on the Linux test host, so the
Platform.isAndroid fallback blocks in sync_service/http_server_service can't be
exercised in CI. Wrap them in coverage:ignore (honored by flutter test
--coverage) so the 100% gate doesn't demand coverage of an Android-only path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 13:18:19 +02:00
77151c0013 Manual-workout phone sync P5: phone form + budget + push
The phone can now log an off-app workout that syncs to the PC and counts toward
the unlock.

- lib/screens/manual_workout_screen.dart: the form, mirroring the PC manual
  form (sport TT/other + sport-specific fields, times, location/transport/cost/
  reservation phone, RPE slider, techniques/warm-up/pain, reflections). Shows
  the shared 2/week + 5/month budget (pull-only, computed over the merged
  cross-device record set) and refuses to submit when exhausted. On submit it
  validates with validateManualWorkout, builds the shared sync record, and
  pushes it to devices/phone/log.json.
- workout_sync_service.dart: pushManual(record) and readMergedManualPayloads()
  (pull-only budget read — never writes).
- models/manual_workout.dart: ManualBudget + countManualBudget over the merged
  payloads, windowed by each payload's own date.
- home_screen.dart: a "Log manual workout" app-bar action.

All new lib code is 100% covered (screen widget tests via an injected fake sync
service; sync-service MockClient tests; model unit tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 13:14:42 +02:00
6f2ec1fa86 Manual-workout phone sync P4: Dart model + validation
Dart port of the Python manual-workout model so the phone can build the same
signed-on-ingest sync payload the PC form produces.

- lib/models/manual_workout.dart: ManualWorkoutDraft (mirrors the Python
  dataclass), validateManualWorkout (ports validate_manual_workout: required
  fields, HH:MM end-after-start, min duration, RPE range, per-sport rules,
  reflection length), buildEntry/buildSyncPayload (byte-for-byte equivalent of
  the Python build_entry + kind/date), manualSyncRecordId, buildManualRecord.
  The MANUAL_WORKOUT_* constants are inlined here (a const-only file has no
  coverable lines and trips the 100%-coverage file-presence guard), replacing
  models/manual_workout_constants.dart.
- test/models/manual_workout_test.dart: the cross-language wire fixture (an
  identical literal to test_manual_workout.py::TestSyncWireFormat — this catches
  key-name/format drift between the two serializers) plus full validation
  coverage. 29 tests; manual_workout.dart is 100% covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 12:55:05 +02:00
7722260eeb Fix workout_app service tests on non-Android hosts
Pre-existing failures unrelated to the manual-workout feature, exposed once the
pre-push flutter gate started running the Dart suite:

- http_server_service / sync_service: getExternalStorageDirectory() throws
  UnsupportedError (an Error, not an Exception) on non-Android platforms, so the
  `on Exception` fallbacks didn't catch it and it escaped, failing start() and
  writeWorkoutResult(). External storage is Android-only, so guard the call with
  Platform.isAndroid instead of trying to catch a platform Error.
- sync_service_test: the "updates HttpServerService" assertion expected compact
  JSON ("workout_type":"A") but the payload is pretty-printed
  ("workout_type": "A"); fixed the expectation to match.

Full workout_app suite (191 tests) now passes on this host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 12:47:55 +02:00
3de2928967 Fix workout_app widget tests hanging on sqflite-ffi DB ops
Pre-existing: any DB call (saveSession/getExerciseState/setExerciseWeight) made
directly in a testWidgets body runs in the fake-async zone and hangs sqflite-ffi
for ~3-4 min, then crashes the isolate ("Cannot close sink while adding stream"),
aborting the whole file. This blocked the pre-push flutter gate for any Dart-
touching commit. Two fixes:

- Wrap every in-body DB call in tester.runAsync(() => ...) so it runs on the real
  event loop (setUp already does; the passing tests did their reads inside the
  pump helper's runAsync). history/home/settings screen tests.
- StorageService._open: open the test ':memory:' DB with singleInstance:false so
  each test gets a fresh empty DB. sqflite's default singleInstance returned the
  same cached in-memory DB and resetForTesting never closed it, so rows piled up
  across tests and pushed the asserted widget off-screen ("Found 0 widgets").
  Production keeps the default single shared instance.

188 workout_app tests now pass (was aborting after ~4 min). Unrelated to the
manual-workout feature; needed to unblock its Dart commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 12:42:42 +02:00
233536d275 Manual-workout phone sync P3: PC pushes its manual entries to pc/log.json
Two-way sync: the PC now publishes its own manual workouts so the phone can see
them for the shared budget. Reverses the old "the PC never pushes" invariant.

- _manual_push.py (new): a small crdt-sync store of PC-originated manuals
  (manual_sync_log.json, next to workout_log.json). record_pc_manual() appends
  the form's entry (wrapped as the shared kind/date payload under a stable
  manual: id, monotonic HLC); push_pc_manuals() runs one sync_log tick to
  devices/pc/log.json. Best-effort: no token, empty store, or a 403 from a
  read-only token is swallowed so the local form still works.
- _manual_workout_dialog.py: record the manual locally on form save (fast, both
  hosts) — published on the next lock startup.
- screen_lock.py: push_pc_manuals() runs in the startup sync step (also retries
  any manual a previous push failed to publish).
- Updated the now-false "PC only ever reads, never pushes" docstrings in
  _workout_sync.py and _constants.py; noted the contents:write PAT requirement.

A PC-pushed record round-trips through the same ingestion decoder, so it is
ingestable by any device. Full suite green at 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 11:29:41 +02:00
870a65015f Manual-workout phone sync P2: PC ingestion (multi-record + manual branch)
Ingest manual/off-app workouts synced from the phone (or another device) into
workout_log.json so they count toward the weekly-minimum unlock. No phone UI
yet — fully driven by a crafted sync log.

- _workout_sync.py: pull_all_manual_records() merges every device log under the
  sync prefix (phone, pc, …), keeping the highest-HLC copy per stable record id;
  and the session-verify path (_latest_payload) now skips kind="manual_workout"
  records so a manual can never be treated as a StrongLifts session.
- _manual_sync.py (new): reconstruct_draft() rebuilds a ManualWorkoutDraft from a
  synced payload (raw inputs only — never trusting the phone's derived fields);
  ingest_manual_records() re-validates with the same validate_manual_workout the
  local form uses, enforces the rate budget, then HMAC-signs and writes each
  under its own date. Idempotent (dedup by sync_record_id) and non-destructive
  (never clobbers an existing counted day).
- _log_mixin.py: extract write_signed_entry(log_file, date, workout_data) so the
  live save path and ingestion share one date-keyed signer; _load_existing_logs
  now delegates to the shared _read_logs.
- screen_lock.py: run _ingest_synced_manual_workouts() at startup, beside the
  RunnerUp auto-fill, before the lock-decision early-exits.

Trust: a phone can at most add a budget-limited honour-system manual_workout,
never a phone_verified one; the PC re-validates and signs on ingest. Full suite
green at 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 11:15:07 +02:00
f4d5337f25 Manual-workout phone sync P1: wire format + shared constants + fixture
First phase of syncing phone-logged manual/off-app workouts to the PC so they
count toward the unlock. This phase only establishes the cross-language wire
contract; no behavior change to existing flows.

- _manual_workout.py: build_sync_payload(draft, date) = build_entry fields +
  a `kind="manual_workout"` discriminator + the workout's own `date`; and
  manual_sync_record_id(date, start) -> stable `manual:<date>T<HH:MM>` id.
  A StrongLifts session payload has no `kind`, so an ingesting PC can route on
  it and never mistake a manual for a verified session.
- test_manual_workout.py: TestSyncWireFormat pins the exact literal payload as
  the cross-language contract (the Dart workout_app test will reproduce the
  same literal byte-for-byte; neither side's own round-trip test catches drift
  from the other).
- workout_app/lib/models/manual_workout_constants.dart: hand-maintained mirror
  of the 7 MANUAL_WORKOUT_* constants + the kind value (Python stays source of
  truth; the shared fixture is the drift tripwire).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 10:52:00 +02:00
60d8c3361b Redesign manual-workout form: two-column grid, drop phone-only fields
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
The manual-workout evidence form (shown on the lock screen and the status
window via the shared ManualWorkoutDialogMixin) was a cramped, narrow
centered column of tiny inputs with large empty margins, and included two
fields that make no sense to fill from a locked PC.

- Remove the vestigial "Google Maps link" (location_maps_link) and
  "Screenshot path" (proof_screenshot_path) fields from the form, the
  ManualWorkoutDraft dataclass, and build_entry(). Both were written to
  workout_log.json but never read by any code. The now-empty "Evidence"
  section is gone too. reservation_phone is kept.
- Lay the form out as a two-column grid that fills ~90% of the screen
  (the fullscreen container was shrink-wrapping to its widest child) with
  larger labels/inputs; reflection boxes span full width. Activity-specific
  fields now sit directly under their section header.
- Extract the low-level field/grid builders into a new
  ManualWorkoutFormWidgetsMixin (_manual_workout_widgets.py) so the dialog
  module stays under the 400-line cap; the dialog mixin inherits it, so the
  host classes need no change.

Tests updated for the removed fields and the pack->grid sport swap; added a
unit test for the grid cursor's odd-row bump. Full suite green at 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
2026-07-13 10:09:18 +02:00
5390f2043c Add CI-mirror pre-push gate + requirements-sync check
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Keeps CI green by closing the local↔CI gap that caused this session's
red runs: the installed pre-commit hook only checks staged files and the
dev env already has every dep, while CI runs `pre-commit run --all-files`
and installs a fresh env from requirements.txt.

- scripts/check_requirements_sync.py: static hook (pre-commit stage) that
  AST-scans the package's third-party imports and fails if any isn't
  declared in requirements.txt — the exact ModuleNotFoundError class that
  reddened three repos. Fast, runs at commit time.
- scripts/ci_mirror.sh: pre-push gate that reproduces both CI workflows —
  a requirements.txt-only venv (hash-gated, rebuilt only when
  requirements.txt changes) running `pre-commit run --all-files` +
  `pytest`. Replaces the old pytest-coverage pre-push hook (subsumed by
  the clean-venv pytest). A red result blocks the push; `git push
  --no-verify` overrides.
- .ci-mirror-venv/ gitignored; pre-push hook installed.

Verified: mirror runs green (747 passed, 100% cov); dep-sync check
catches a removed crdt-sync; hash-gate reuses the venv on repeat runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:42:45 +02:00
23b9bd5cde Install Flutter in pre-commit CI so flutter-analyze can run
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
The pre-commit workflow has been red on every run since 2026-07-04: it
executes the flutter-analyze hook but never installed Flutter, so the
hook failed command-not-found each time (Dart was never actually
analyzed in CI). Add subosito/flutter-action pinned to 3.44.6 (matching
the version the app is developed against, Dart SDK ^3.12.2) plus a
`flutter pub get` step so `flutter analyze` resolves packages. The
sibling flutter-test-coverage hook is pre-push-only and does not run in
this default-stage CI job, so analyze is the only Flutter dependency
here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:23:42 +02:00
8429cf04d5 Fix ScreenLocker test-factory log_file isolation flake
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
create_locker (+ _relaxed_day / _early_bird variants) patched
Path.resolve to return tmp_path, but ScreenLocker.__init__ computes
script_dir = Path(__file__).resolve().parent — the .parent turned the
intended per-test tmp_path into tmp_path.parent, pytest's session-shared
temp base. Every factory-built locker therefore shared ONE
workout_log.json, so any test that wrote a log polluted it and
test_saves_pending_marker's `assert not locker.log_file.exists()`
passed or failed purely by random ordering (pytest-randomly).

The flake was invisible until now because CI died at conftest import
(missing crdt_sync) before any test ran; fixing that import in the prior
commit exposed it. Reproduced deterministically at CI seed 2701400184.

Fix: resolve() now returns tmp_path / "screen_locker" so .parent is the
unique per-test tmp_path — each locker gets its own log_file. Also stops
tests writing workout_log.json into the real source tree. Verified 747
passed across seeds 2701400184/1/42/999/123456 (was 1 failed at the CI
seed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:20:01 +02:00
53140a96e2 Fix pre-existing CI failures (crdt_sync dep, dead type:ignore, ruff)
CI ran on 399f981 before these fixes landed; both workflows were red:

- Tests: ModuleNotFoundError: No module named 'crdt_sync'. CI installs
  requirements.txt (not pyproject deps), which lacked the crdt-sync git
  dependency the Workstream-C sync introduced. Add crdt-sync + mcp>=1.9
  to requirements.txt (mirrors diet-guard's fix).
- pre-commit no-noqa: ~45 `# type: ignore[attr-defined]` comments across
  _auto_upgrade/_runnerup_db/_status/_heat_skip flagged repo-wide by the
  no-noqa hook. The mypy hook already runs with
  --disable-error-code=attr-defined, so every one of these ignores was
  suppressing an already-disabled check — dead weight. Removed them; mypy
  and pytest (747 passed, 100% cov) both stay green. Same for the test's
  redundant `# type: ignore[override]` (override never fired).
- pre-commit ruff: FBT003 in _heat_skip.py (tk positional bools, its API)
  and C901/PLR0912 in _status.py run_status (branchy weekly pipeline by
  design) — added per-file-ignores with rationale, matching the existing
  ignores for the sibling validation-chain modules.
- Added crdt-sync/mcp/pydantic to the pylint hook's additional_dependencies
  so pylint can resolve the new imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
2026-07-11 17:05:03 +02:00
399f9815c4 Add phone-workout GitHub sync via shared crdt_sync (Workstream C)
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Sync the workout log across devices using the shared crdt-sync /
crdt_sync_dart CRDT transport (same scheme as diet_guard).

- Python: new _workout_sync.py + _temperature_status_mixin.py; wire sync
  into screen_lock.py / status_view.py; add the crdt-sync git dependency
  and test_sync_fixtures / test_workout_sync suites.
- Dart (workout_app): new workout_sync_service.dart, github_device_auth.dart,
  sync_settings.dart with tests + fakes; settings/workout screens updated.
- docs: workstream-d-shared-crdt-transport.md.

Verification: pytest 747 passed at 100% coverage; flutter analyze clean;
flutter test passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
2026-07-11 00:06:59 +02:00
494cc74bb5 Add read-only MCP server exposing workout/lock status
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Expose the screen-locker's compliance and lock-decision state as read-only
MCP tools (screen_locker._mcp) so Claude Code and its subagents can query
workout status without shelling out to the CLI or opening the Tk window.

- _mcp.py: FastMCP stdio server. Read-only tools get_status, get_summary,
  explain_lock, get_flags. Import leaf over stdout-free status functions
  (gather_status, format_summary_line, explain_lock_decision, predicates);
  logs to stderr. NO write/action tools by design — workouts are logged only
  from RunnerUp-verified TCX data, never from a caller's claim. No tool
  exposes the sync token or any HMAC key.
- .mcp.json + scripts/setup_mcp.sh: registration via a dedicated
  ~/.venvs/screen-locker-mcp interpreter; mcp is an optional-dep.
- 100% coverage of the new module; verified end-to-end over stdio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQGQj8Ne6TriBExv91nG8v
2026-07-10 21:28:09 +02:00
64d253bf91 Point gatelock at the consolidated utils monorepo
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
gatelock was folded into github.com/kuhyx/utils as a subdirectory;
update the pip git URL across requirements.txt, pyproject.toml, and
.pre-commit-config.yaml, and note the screen-locker-sync -> syncs
consolidation in the Workstream C planning doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 14:16:16 +02:00
d9119a6582 Add manual (unverified) workout logging with evidence form
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Adds a rate-limited manual-workout entry point (StatusWindow + locked
retry screen) for activities like table tennis that ADB/RunnerUp can't
verify, gated by a 2/7d + 5/30d budget and a detailed evidence form
rather than free text. Full credit toward the weekly minimum, debt
clearing, and commitment prompt, matching phone/RunnerUp verification.

Also stops tracking personal runtime state (workout_log.json,
sick_history.json, extra_benefits_state.json, scheduled_skips.json,
shutdown_base.json, early_bird_pending.json) in git — these are
regenerated locally and never belong in a public/private repo's
history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbPcvAvt1JhiRzEyPDE5dA
2026-07-05 20:08:52 +02:00
58a33938e3 fix: resolve shutdown-schedule canonical path via guardctl dynamically
Hardcoding /usr/local/share/locked-shutdown-schedule.conf broke once the
hosts/shutdown-schedule file-guard instances moved to guard-lib's own
canonical-path layout. Resolve it at runtime via `guardctl file-guard
canonical-path` instead, and fail loudly if the guard-lib instance isn't
installed rather than silently reading/writing the wrong path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFNiYQQgSLAkiBXswyimPq
2026-07-04 11:48:39 +02:00
7bb43422f2 Fix silent skip-credit bypass; replace with weekly shutdown-time bonus
The screen locker skipped enforcement on 2026-07-03 without ever showing
a lock: a banked skip credit (earned from a prior 5+/week streak) was
consumed automatically with no confirmation and no visible log. Reworked
the whole reward mechanic instead of just gating it, since banking a
"skip a future workout" credit works against maximizing weekly workouts:

- Removed skip credits entirely (has_skip_credit/consume_skip_credit and
  the confirmation dialog built to gate them). The only same-day skip
  paths left are heat_skip and sick_day, both requiring a genuine reason.
- Extra workouts (5+/week) now bank shutdown-time-later hours for the
  following week instead — comfort, not reduced enforcement. Reuses the
  existing _adjust_shutdown_time_by and reset_to_base_if_new_day's
  previously-discarded return value as the once-per-day gate.
- early_bird and sick_day no longer pollute workout_log.json. early_bird
  is a same-day pending marker now stored in its own self-expiring,
  HMAC-signed file; sick_day is sourced entirely from sick_history.json
  (already the real source of truth). Fixes an accidental-safety gap
  where "already took a sick day today" only halted startup by luck.
- Cleaned up 3 stale non-workout entries already in workout_log.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdTccgbK7624kfoaV6CtXS
2026-07-03 15:27:08 +02:00
185848bbe2 Set heat-skip threshold to 33°C based on historical workout data
Highest recorded strength workout day in Warsaw was 32°C (2026-06-26),
so 33°C is the first temperature above any completed workout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QCx1roriuXzFgrzFXtugb
2026-06-29 11:27:48 +02:00
2729c2a880 Add heat-skip feature and fix early-bird 9:00 re-check gap
Heat skip: if Warsaw temperature >= 32°C at locker startup, a fullscreen
dark-themed dialog asks the user to confirm skipping. Temperature is always
fetched from wttr.in automatically (user cannot self-report). Fail-closed:
API unavailable → no dialog, normal lock. Placed before skip-credit
consumption so credits are preserved when heat skip is used instead.
Logs a heat_skip entry (with temperature) to workout_log.json; does not
count toward weekly minimum. Visible in --status as "Heat skips (month)".

Early-bird gap fix: the re-check timer now fires at both 08:30 (normal
5:00–8:30 window) and 09:05 (extended 5:00–9:00 window earned by 5+
workout weeks). Previously the 08:30 run would see the window still active
on extended weeks and never re-check after 9:00.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QCx1roriuXzFgrzFXtugb
2026-06-29 11:23:19 +02:00
63489b07b2 Add comprehensive test suite, backup service, and linting to workout app
- Add sqflite_common_ffi + very_good_analysis; tighten analysis_options
- Add BackupService for JSON export/import of exercise state
- Add full test coverage: models, screens, services, widgets
- Add scripts/check_flutter_coverage.sh to enforce 100% line coverage
- Add docstrings to ExerciseState fields and storage service
- Minor fixes across screens, widgets, and sync/HTTP services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VuiPt6GPWkxpLbJFrnfy8U
2026-06-28 08:11:43 +02:00
eec0662ef9 Add auto-fill RunnerUp scan, carrot bonuses, and --status interface
- 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
2026-06-28 08:08:35 +02:00
890604cad9 Add TCX file-based RunnerUp verification (no root, works over WiFi)
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
2026-06-25 17:58:06 +02:00
763c58787f Add RunnerUp automatic run verification via ADB DB pull
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
2026-06-25 17:50:24 +02:00