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