One-screen live run UI (icons and digits), map slot via mapBuilder, upload badge, sandbox e2e, Pixel 6a inset test, dufs provisioning script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
Marker: twins (Pixel_6a_ infix) listed once, app-authored files never. Backup guard: a recorded run no longer lifts the workout-history clobber guard. runGuarded: a throw outside per-file handling logs and sets failed. Silent-failure hook clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
Foreground service (dataSync+location) started after access, numbers-only notification, kilometre buzz and chime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
One-time, phone-only import of /RunnerUp/processed over dufs WebDAV as display-only runs, then a dotfile marker PUT for the PC. Never PUTs a tcx. Icon-only status badge in Settings, shared DufsCredentials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
runs/run_points were in neither backup.json nor the user export. App-made runs go in the backup (history returns from the PC), all runs in export v3; merge keyed by id or hash, idempotent. The backup clobber guard now also protects a runs-only backup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
Synthetic 4 km loop generated in Dart, replayed at 1x/10x/60x; main imports the dufs login and retries pending uploads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
State machine over PositionSource + GpsFilter with pause segments, pending-run WebDAV PUT queue, dufs login import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
RunMap.route/.live (flutter_map, OSM tiles cached on disk,
collapsed icon-only attribution, one warning when tiles fail),
RunDetailScreen (full-bleed map + numbers, no scrolling), a runs
strip in the history, and day+sport dedupe that hides the PC's
runnerup_verified entry once the phone has the run itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
Pure-Dart TCX codec pinned to the PC's RunnerUp parser by a shared
golden fixture (Dart byte-compare + pytest through the real verifier),
GPS filters and moving-time maths mirroring the PC walk rule, runs /
run_points tables (schema v9), PositionSource seam over geolocator,
location permissions and foreground service type, map/cache deps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZ9CKZsNxtD5Jr9bYD
Pin @emnapi core/runtime to stable 1.11.3: astro's peers
otherwise resolve to 2.0.0-beta.2 and astro check crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZd3smuxDGmEgXkYP8
2811f78 hid the sandbox from the launcher by removing its LAUNCHER
category. flutter_foreground_task builds its notification from
getLaunchIntentForPackage, which then returned null; the NPE meant
startForeground never ran and Android killed the app with
ForegroundServiceDidNotStartInTimeException on the first set tap and
on every resume. A separate MAIN+INFO filter (checked before LAUNCHER)
restores the intent and leaves the launcher entry hidden. The daily
flavor keeps LAUNCHER and was never affected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
dopamine-ux 05: selectionClick on recording a set or warmup,
mediumImpact + generated chime (music_theory, pinned tag) on
finish, dialog arrival on the slow motion token, AppDuration /
AppCurve in theme.dart, streak card counts up, all of it
collapsing under the OS reduced-motion flag. One Settings
"Sound" switch silences the chime and the break-end cue
(foreground service included, via a silent channel); haptics
stay. Removes the finished dopamine-ux-05 prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
The manual form takes a workout date from today to today+7 (never past). The budget is read as of that date: the fullest 7-/30-day window containing it, past and planned entries alike. History lists future manual entries under PLANNED (off the calendar) and deletes them until their day via a crdt-sync tombstone. Re-logging onto a tombstoned id is refused; a failed push keeps the form open with the reason. Shared fixture manual_budget_merge.json gains a 'planned' scenario (top-level expectations unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015mmTBiMpvyfLuEAXc6nDYT
web: react/react-dom 19.3.1, vitest + coverage-v8 5.0.4, jsdom 30.1.3,
user-event 14.6.8, typescript-eslint 8.71.2. Lockfile is now
web/pnpm-lock.yaml (packageManager pnpm@12.14.0); hooks and CI use pnpm
with --frozen-lockfile. Node 24.22.0 in workflows; workout_app sqflite
2.4.4+2 / sqflite_common_ffi 2.4.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
"Deload now" in the exercise settings sheet runs exactly the step a fail
streak would take (targetAfterFailure), resets both streaks, and records a
progression_events row (schema v7, backed up and restored with the rest).
No manual step up: the target still only climbs through finished workouts.
Mid-workout it re-targets the exercise for the current session: untapped
sets move to the new reps, tapped sets keep theirs, and the whole exercise
is recorded at the new weight at Finish. From Settings it cancels pending
weight/reps debounce writes so they cannot undo the deload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KejiqD7y4NXRiPBKQeWHSy
- coverage 7.16.1 -> 7.16.2 (dependency freshness).
- workout_screen.dart (253) -> _allSetsCompleted/_isPaused move into
workout_screen_breaks.dart next to _stateOf.
- storage_service_modes_test.dart (289) -> setExerciseSettings and
per-mode progression groups move to storage_service_settings_test.dart.
Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Warmup is always 6 reps (kWarmupReps): even, one constant.
- Per exercise: rest after success / fail / warmup (0:30-10:00, 15 s
steps; defaults 3:00/5:00/3:00). Schema v6 NOT NULL DEFAULT columns,
synced in the Firebase progression record (lenient for older records).
The notification carries the fail rest so -1 rep re-cuts to it.
- Settings: PER EXERCISE list replaces PROGRESSION THRESHOLDS; each row
opens the workout tile's sheet and pushes progression when it closes.
Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Per exercise: weight / reps / double progression (n=12, m=6), one pure
rule shared by Finish and every preview; streaks and next step shown on
the tile. Warmup is the first circle of the set row and can be turned off.
- Pause an exercise for n days (default 14): shown as paused, recorded as
FAILED every workout so it steps down; skipped by the notification.
- App bar removed: Reset/Finish live in the rest strip, no rest labels.
- Schema v5 (progression_mode, reps_high, reps_low, has_warmup, paused_until),
synced through the Firebase progression record; NULL warmup = plan default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF
`flutter test` runs on a Linux host, so credentialStore()'s Platform.isLinux
branch wrote $HOME/.config/screen_locker/firebase_auth.json -- the refresh
token the screen-locker daemon signs in with. firebase_backend_google_test's
"stores the account under the email Firebase reports" case overwrote it with
the fixture token 'refresh' on every local run; the daemon only survived by
self-healing from a cached token.
Dart:
- lib/services/desktop_config_dir.dart: one resolver, desktopConfigRoot(),
for every desktop path (the credential file's read/write/delete and the
~/.config/crdt-sync account read). @visibleForTesting override; under
FLUTTER_TEST it throws StateError if the root is, or is inside, the real
$HOME/.config -- so a test that bypasses the override fails loudly instead
of writing. HOME unset now yields null instead of a cwd-relative .config.
- The crdt-sync read resolves the root outside its catch-all, so the guard
cannot be swallowed as "not connected".
- test/flutter_test_config.dart gives every test its own temp config root.
- The Google sign-in test now asserts the token landed in that temp root.
- path_provider is unbound under flutter test (MissingPluginException), so
the getApplicationSupportDirectory callers cannot reach live dirs.
Python (tests only): isolate_sync_token now patches every import-time
binding of SYNC_TOKEN_FILE (_constants, _manual_push, _sync_status were
missed), and test_home_isolation scans all runtime modules for any Path
still bound inside the real ~/.config/screen_locker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFwA9nymhUdTrVF65kigTV
The dependency-freshness gate (and the pre-commit workflow, which runs the
same hook) went red on these five upstream releases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188kZdx3bb56QM8JFUnbHuM
The rest banner used to be inserted into the column only while a break ran,
so its arrival took height from the exercise list, shifted every tile down
and pushed the last one into scroll range. The strip is now a permanently
reserved 64 dp slot (muted "Rest / 00:00" while idle, Skip keeps its space),
the list is a non-scrolling column that scales down as a whole if it would
not fit (0.96 on the Pixel 6a with four tiles, pinned by a widget test at
the phone's logical size with the status bar simulated), and the activity is
locked to portrait. Verified on the real phone: only the tapped circle's
pixels change when a rest starts.
Verifying that needed a workout screen that today's already-logged workout
did not offer, so the app gained a `sandbox` product flavor beside `daily`:
com.kuhy.workout_app.sandbox installs side by side with its own data, refuses
every network connection at the socket layer, starts no LAN server for the
PC to pull from, keeps its /sdcard files beside the daily build's, wears a
red SANDBOX ribbon, and offers a SANDBOX section in Settings (wipe, mark
today not done, rest length in seconds). MainActivity answers the flavor and
carries a `WorkoutSandbox` logcat trace of every tap, break, save and
navigation. On Linux the same mode is WORKOUT_SANDBOX=1, driven under Xvfb by
scripts/run_sandbox_xvfb.sh. CI builds --flavor daily; .phone-deploy-flavor
tells phone_deploy.sh the same.
Set circles and the home icon buttons carry semantics labels so they can be
addressed by element (and by screen readers).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbBZqUaNWyhHYfzz6zGC7q
The between-set break never finished once the phone's screen was off. Three
independent causes (worked through on-device on 2026-09-12; left
uncommitted awaiting the go-ahead, committed here as-is plus one pin bump):
1. The countdown was not wall-clock: `_breakRemaining--` on a
Timer.periodic, which Android throttles and Doze suspends in a
backgrounded isolate, so the counter stalled and _onBreakFinished()
never fired. Now a BreakClock deadline on the wall clock.
2. An expired break was silently dropped on restore (`if (remaining > 0)`
only) — now cued and logged.
3. No notification infrastructure at all: no package, no
POST_NOTIFICATIONS / FOREGROUND_SERVICE / WAKE_LOCK.
Now: flutter_foreground_task runs the countdown in a service isolate that
survives BACK and the screen lock; flutter_local_notifications shows an
ongoing "Workout A · break m:ss / Next: …" notification with −1 rep / Done
actions and a "Break over" alert on its own channel (raw break_end sound,
vibration). Button presses go through a persisted BreakIntent queue the
screen drains on resume, so a press with the app killed is not lost.
Finish and Reset stop the service; a startup reaper handles the crash case.
Verified on the phone with the screen off and locked: service alive and
ticking while Dozing, both notifications on the lock screen, channel
importance 5 with the break_end sound. Not verifiable from adb (receiver is
RECEIVER_NOT_EXPORTED): an actual action-button tap, and a true recents
swipe — see stronglift_replacement/TODO-workout-app-v2.md.
Also fixed en route: the break-end sound had never played in any widget
test (AssetSource needs path_provider, which nothing faked, and the failure
was swallowed by catchError); the queue's prune wrote back a stale list.
This commit additionally:
- takes the freshness gate's bump flutter_local_notifications 22.3.1;
- restores web/package.json to HEAD (the tree held a stale pre-bump copy);
- gives scripts/check_flutter_coverage.sh a FLUTTER_TEST_CONCURRENCY knob:
the default one-isolate-per-core run peaks over 4 GiB and is OOM-killed
under the resource cap. Default behaviour unchanged.
Gate: flutter analyze clean, 511 tests, 3187/3187 lines (100%), pre-commit
green. Not re-installed on the phone for the 22.3.1 pin: no device attached
at commit time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7vgacLhb4WJjxuVj2QLEP
Pre-push ci-mirror was red at HEAD on exactly these four; the lock was
regenerated with the pinned Flutter 3.47.4 (which also moves
flutter_secure_storage_platform_interface to 2.1.0 transitively).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcU5dCcUNHMAMzUNChdrrJ