A real StrongLifts session on 2026-08-10 never reached this machine, so it
had to be logged as a manual workout -- spending a manual budget slot on a
workout that was genuinely verified. The session was in Firebase the whole
time; four separate defects each hid it.
- pull_synced_workout() read a hardcoded devices/phone/log.json. Since
ffc6c72 the phone pushes under a per-install uuid, so that directory has
been frozen since 2026-07-17 and the PC kept returning a stale session
from 07-27 as if it were current. Both readers now walk EVERY device
directory; reinstalls keep minting new ones, so no single path can be
right for long.
- A session is identified by SHAPE (it has `exercises`), not by a `kind`
field the phone never writes. That filter is why the one reader that did
walk every directory discarded real sessions, and it also kept PC-origin
runnerup/phone_verified records from being handed to a caller expecting
a session.
- FirebaseSyncError is a SIBLING of GitHubSyncError, not a subclass, so
`except GitHubSyncError` never caught it and a DNS failure took the whole
locker down (status=1/FAILURE, then a restart loop). Catch the common
parent; a pull failure degrades to a warning.
- A GitHub token is no longer required to sync. Firebase has been primary
since eb4ff01, so a Firebase-only machine was reporting "sync is OFF"
while syncing fine.
Sync also only ran once per locker start, so a workout finished after login
was invisible until the next login. workout-sync.timer runs the new headless
--sync-only every 15 minutes; remove_systemd.sh now removes every unit the
installer creates, not just two of them.
workout_app: the 2026-08-05 reinstall wiped progression because the DB seeds
factory defaults before the backup is restored, and the first write then
exported those defaults over the only off-device copy. Storage permission is
now settled before the DB opens, a defaults-only DB refuses to overwrite a
richer backup, and the three swallowed storage errors say what broke.
Sessions that exist only in Firebase/GitHub are restored into local history,
so a reinstalled app has the history it actually earned rather than starting
at whatever survived. Matched on (date, start_time), so re-syncing never
duplicates and local rows always win.
Adds a target-reps editor: progression can only ever RAISE reps, so a rep
target a defaults re-seed set wrong could not be corrected from the UI at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JS5V44jkHZiH1wC7BH5h5n
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
Both surfaces of this repo move together, because they share one prefix:
screen_locker (the PC, which reads what the phone logged) and workout_app (the
phone, which writes).
Firebase becomes the primary backend when it is set up, with GitHub kept as a
mirror so a device that has not moved yet is still seen. MirrorSyncClient reads
the union of both, so a workout logged against either backend still counts --
which is what lets the PC and the phone cut over on different days.
The PC's two pulls are read-only, so they need only the client swapped.
_manual_push does a real sync_log and now carries a revision cache, as does
every push in workout_app: without it each push re-uploads the whole log and
each pull re-downloads every peer's, regardless of change.
remote_client is public rather than _private because _manual_push imports it;
one decision point beats two copies that can drift.
The config file is checked before constructing anything, so an unconfigured
machine never reaches the network -- without that guard the PC's sync tests
asserted against live Firebase data instead of their own fakes, which is how
this was caught.
screen_locker: 904 tests at 100% branch coverage.
workout_app: 299 tests at 100% line coverage; pinned v0.1.0 -> v0.6.0 across
three minor versions with no API drift. Four assertions moved off `.single`,
which now finds two writes: the log, then the revision published after it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
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>
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
- 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
Breaks belong after each rep (circle tap), not removed entirely.
Restored break_banner, audioplayers, vibration, and break_end.mp3 asset.
Break triggers on every circle tap except the last one; 3 min on success,
5 min on failure; sound + vibration fires when the countdown ends.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full-featured workout tracker with session persistence, auto A/B cycling,
warmup weights (4/5 of target), settings weight stepper, history + progress
graph, HTTP sync server, and crash-safe active session resume.
Removed per-set break timers per user preference. Dropped audioplayers and
vibration dependencies; updated permission_handler to 12.x to eliminate
two of three KGP build warnings (shared_preferences_android is an upstream issue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>