Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
7fda8d5
Adds tests for screens
Devasy Jul 23, 2026
a3d61b9
Adds tests
Devasy Jul 23, 2026
71659af
Adds comprehensive tests
Devasy Jul 23, 2026
9732b9a
Adds new tests
Devasy Jul 23, 2026
87ce588
Updates test.yml to run on release branches
Devasy Jul 23, 2026
bb98883
Adds test and resolved the warnings and issues
Devasy Jul 23, 2026
8e745cc
Updates tests and minor bug fixes
Devasy Jul 23, 2026
f0fc3c2
Adds fixes for failing testsm and adds connection timeout safety for …
Devasy Jul 24, 2026
43e4dd3
Adds missing lines patch
Devasy Jul 24, 2026
3abd451
Updates the tests with analyse failures
Devasy Jul 24, 2026
9a55007
Updates tests and routine creator to use the common component
Devasy Jul 24, 2026
3b787a0
Updates flutter version and adds tests
Devasy Jul 24, 2026
4b85588
Adds major genui Feature and renderer
Devasy Jul 27, 2026
2a17656
chore: remove patch_so script
Devasy Jul 28, 2026
a790d6e
build: add --build-id=none for jni package in F-Droid metadata
Devasy Jul 28, 2026
e89e2ca
ci: add jni build-id sed step for future reproducible releases
Devasy Jul 28, 2026
85a48fd
feat: assisted pullups, deload-aware ML, handle-scoped PRs, sleeping …
Devasy Aug 5, 2026
62beb92
feat(genui): add A2UiProps alias-aware coercing property reader
Devasy Aug 5, 2026
36253fb
feat(genui): add A2UiSpec contract, A2UiNode and A2UiRegistry
Devasy Aug 5, 2026
85b472d
fix(genui): make A2UiRegistry throw on name/alias collisions
Devasy Aug 5, 2026
6a67623
feat(genui): add A2UiParser with fence, envelope and alias repair
Devasy Aug 5, 2026
116ef77
fix(genui): balanced-bracket JSON extraction and envelope singleton fix
Devasy Aug 5, 2026
c846369
feat(genui): inject A2UiTheme and extract shared panel chrome
Devasy Aug 6, 2026
ffa451e
test(genui): strengthen theme-injection and add A2UiPanel coverage
Devasy Aug 6, 2026
cb60d5f
feat(genui): add A2UiSeries as the shared categorical data shape
Devasy Aug 6, 2026
a426fa2
fix(genui): cover fallback path in A2UiSeries.extract, fix negative-m…
Devasy Aug 6, 2026
a0c3ffc
feat(genui): add StatCardSpec with typed props and trend synonyms
Devasy Aug 6, 2026
1ce4ae1
feat(genui): add MetricGaugeSpec with safe progress and null value
Devasy Aug 6, 2026
58a77f2
feat(genui): add DynamicChartSpec for line, bar and pie
Devasy Aug 6, 2026
243d907
feat(genui): add ScatterPlotSpec with point repair and safe bounds
Devasy Aug 6, 2026
43cae7f
feat(genui): add RadarChartSpec sharing the labels/series shape
Devasy Aug 6, 2026
d3c18b3
feat(genui): add DataListGroupSpec with row repair and optional title
Devasy Aug 6, 2026
5e2e2f1
feat(genui): add FilterChipsSpec with nullable active option
Devasy Aug 6, 2026
359ffd3
feat(genui): add GridContainerSpec, default registry and renderer
Devasy Aug 6, 2026
8d32d0d
feat(genui): generate the A2UI prompt section from the registry
Devasy Aug 6, 2026
fc7e843
refactor(genui): wire coach screen to the A2UI package, drop legacy r…
Devasy Aug 7, 2026
1b0b8cc
fix(genui): bracket negative-value ranges in DynamicChart line/bar axes
Devasy Aug 7, 2026
2287ff0
test(genui): cover all-negative bounds and malformed point entries
Devasy Aug 7, 2026
939349a
fix(genui): widen per-node children lookup back to components/element…
Devasy Aug 7, 2026
298126e
fix(genui): widen looksLikeUi to catch prose-prefixed fences, fix vac…
Devasy Aug 7, 2026
6561e95
refactor(genui): drop presentation payload from tools, add purity and…
Devasy Aug 7, 2026
5735d98
fix(genui): depth-agnostic purity regex, pin two silent-visual regres…
Devasy Aug 7, 2026
c0913bb
fix(genui): propagate registry through recursion, pin prompt drift, c…
Devasy Aug 7, 2026
8024efa
Merge branch 'r2.1.0' of https://github.com/Devasy/RepForge into feat…
Devasy Aug 7, 2026
1f327a5
docs: add design spec for Hive->SQLite migration + coach SQL query tool
Devasy Aug 7, 2026
7a0c18c
fix: persist assisted-load volume correctly, tighten exercise-handle …
Devasy Aug 7, 2026
f363a21
fix: bound sleep-analytics window, drop fabricated data, resolve musc…
Devasy Aug 7, 2026
8862e9d
fix(genui): pie negative-value filtering, overflow guard, stat-card u…
Devasy Aug 7, 2026
0ff6c2d
test: close vacuous-test gaps and pin already-fixed regressions
Devasy Aug 7, 2026
b35df0a
docs: add implementation plan for Hive->SQLite migration + coach SQL …
Devasy Aug 8, 2026
f22e1eb
chore: add sqflite dependencies for SQLite storage migration
Devasy Aug 8, 2026
18bfb86
feat: add SqliteStorageService with schema and workout session CRUD
Devasy Aug 8, 2026
8105d9d
fix: persist bodyWeightAtLog in SqliteStorageService sets table
Devasy Aug 8, 2026
6908914
feat: implement routine and target CRUD in SqliteStorageService
Devasy Aug 8, 2026
bd95d23
feat: implement muscle group and custom exercise CRUD in SqliteStorag…
Devasy Aug 8, 2026
34c485c
feat: implement settings, PR, training program, and conversation CRUD…
Devasy Aug 8, 2026
bf771d0
feat: implement export/import in SqliteStorageService, completing ISt…
Devasy Aug 8, 2026
06281e0
feat: add settings enumeration helper to StorageService for migration
Devasy Aug 8, 2026
93f0713
feat: add StorageMigrationService for one-time Hive-to-SQLite migration
Devasy Aug 8, 2026
8c9efcf
feat: resolve Hive-vs-SQLite storage backend in main() before runApp
Devasy Aug 8, 2026
6c82b4f
feat: add SqlQueryService for read-only SQL execution
Devasy Aug 8, 2026
6265901
feat: wire run_sql_query tool into CoachToolService
Devasy Aug 8, 2026
e1854d4
fix: fall back to fresh StorageService when app is constructed withou…
Devasy Aug 8, 2026
616ee4e
fix: block run_sql_query from reading settings/sqlite_master (credent…
Devasy Aug 8, 2026
07ae559
fix: prevent trailing SQL comment from breaking LIMIT wrapper
Devasy Aug 8, 2026
2b94d45
docs: warn model against SELECT * across joins in run_sql_query
Devasy Aug 8, 2026
14d8ec8
refactor: extract testable storage backend resolution logic; guard sq…
Devasy Aug 8, 2026
e4cface
docs: add design spec for syncing sleep/HR data into SQLite for coach…
Devasy Aug 11, 2026
d009807
docs: add implementation plan for syncing sleep/HR data into SQLite
Devasy Aug 11, 2026
4b607be
feat: add health_samples/sleep_sessions tables + upsert methods to Sq…
Devasy Aug 11, 2026
35952cc
fix: prevent run_sql_query from closing the app's shared database con…
Devasy Aug 11, 2026
ca12d74
fix: address Task 1 review findings
Devasy Aug 11, 2026
5569113
fix: replace Finalizer with deterministic tearDown cleanup
Devasy Aug 11, 2026
fdb7d88
feat: add HealthDataSyncService to pull sleep/HR data into SQLite
Devasy Aug 11, 2026
a35eed7
feat: sync health data into SQLite once per app launch
Devasy Aug 11, 2026
7f7504e
feat: add manual 'Sync coach data now' action to Profile screen
Devasy Aug 11, 2026
f59f749
feat: teach run_sql_query about the new health_samples/sleep_sessions…
Devasy Aug 11, 2026
71de154
fix: remove overly broad auto-close from init, add explicit close to …
Devasy Aug 11, 2026
6e9014d
fix: address final review findings for health sync + coach SQL tool
Devasy Aug 11, 2026
cc45fcb
fix: address migration/SQL-tool review findings from PR #66
Devasy Aug 15, 2026
3019728
Merge branch 'r2.1.0' into migrate/sqflite-db
Devasy Aug 15, 2026
a04517f
fix: address second round of CodeRabbit findings on PR #66
Devasy Aug 15, 2026
727b1b1
feat: make coach tool-call round limit configurable, surface invoked …
Devasy Aug 15, 2026
b664f20
Merge branch 'main' of https://github.com/Devasy/RepForge into migrat…
Devasy Aug 15, 2026
715ef8c
Merge branch 'r2.1.0' into migrate/sqflite-db
Devasy Aug 15, 2026
1719801
Merge branch 'r2.1.0' of https://github.com/Devasy/RepForge into migr…
Devasy Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,413 changes: 2,413 additions & 0 deletions docs/superpowers/plans/2026-08-08-sqlite-migration-and-coach-sql-tool.md

Large diffs are not rendered by default.

1,032 changes: 1,032 additions & 0 deletions docs/superpowers/plans/2026-08-11-health-data-sync-and-coach-sql.md

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
# Health Data Sync (Sleep + HR) into SQLite — Design Spec

**Date:** 2026-08-11
**Status:** Approved
**Feature area:** Storage layer (`lib/services/`) + AI Coach SQL tool (`lib/services/ai/`)

---

## 1. Problem

The AI Coach's `run_sql_query` tool (added in `docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`) can query workouts, sets, targets, and PRs directly — but health data (sleep stages, heart rate, resting HR, HRV) is fetched live from Health Connect on every request via `HealthConnectService`/`HealthHistoryManager` and is never persisted. This means the coach cannot join health data against workout data in a single SQL query (e.g. "average sleep the night before a PR attempt" or "HR trend across the last 8 weeks of leg day sessions") — each half of the question requires a separate tool call and the model has to reconcile the join itself, unreliably.

This spec adds three SQLite tables that mirror Health Connect data, plus a sync service that keeps them populated, so `run_sql_query` can join across workout and health data directly.

---

## 2. Goal

1. Persist sleep sessions (with stage breakdown) and HR-related samples (raw heart rate, resting heart rate, HRV RMSSD) into the same SQLite database `SqliteStorageService` already owns.
2. Keep this data reasonably fresh via sync-on-app-launch (throttled) plus a manual "Sync now" action — no background service.
3. Extend `run_sql_query`'s schema description so the coach can query and join the new tables.
4. Keep the existing live `get_health_metrics` coach tool as-is, for "right now" freshness the synced tables won't have until the next sync.

Non-goals: no background/periodic sync (WorkManager or equivalent), no downsampling/compaction of old raw samples, no changes to `IStorageService`'s method signatures (this feature is additive on `SqliteStorageService` directly, matching how `SqlQueryService` already bypasses that interface), no UI beyond one manual sync button.

---

## 3. Schema

Added to the same database `SqliteStorageService` manages, created in `onCreate` (and via a migration step for existing installs already past `onCreate` — see §6).

```sql
-- Raw heart rate, resting heart rate, and HRV RMSSD samples all share the
-- same {time, value} shape from Health Connect; one EAV-style table avoids
-- three near-identical tables and keeps the coach's query surface simple
-- ("WHERE type = 'heart_rate'") instead of three tables to remember.
CREATE TABLE health_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL, -- 'heart_rate' | 'resting_heart_rate' | 'hrv_rmssd'
timestamp TEXT NOT NULL, -- ISO8601
value REAL NOT NULL
);
CREATE UNIQUE INDEX idx_health_samples_unique ON health_samples(type, timestamp);
CREATE INDEX idx_health_samples_type_ts ON health_samples(type, timestamp);

CREATE TABLE sleep_sessions (
id TEXT PRIMARY KEY, -- synthetic: the start_ts ISO string
start_ts TEXT NOT NULL,
end_ts TEXT NOT NULL,
light_min INTEGER,
deep_min INTEGER,
rem_min INTEGER,
awake_min INTEGER
);
CREATE INDEX idx_sleep_sessions_start ON sleep_sessions(start_ts);

CREATE TABLE sleep_stage_intervals (
sleep_session_id TEXT NOT NULL REFERENCES sleep_sessions(id),
start_ts TEXT NOT NULL,
end_ts TEXT NOT NULL,
stage TEXT NOT NULL -- 'deep' | 'rem' | 'light' | 'awake'
);
CREATE INDEX idx_sleep_stage_session ON sleep_stage_intervals(sleep_session_id);
```

Sync watermarks (one ISO8601 timestamp per data stream, e.g. key `health_sync.heart_rate`) are stored as ordinary rows in the existing `settings` table — no new table needed for that.

`sleep_sessions.id` is derived from `start_ts` so re-syncing the same session (e.g. after a Health Connect correction) is a natural upsert target, not a duplicate.

---

## 4. `HealthSyncService`

New file: `lib/services/health_sync_service.dart`.

```dart
class HealthSyncService {
HealthSyncService(this._hc, this._db);

final IHealthConnectService _hc;
final SqliteStorageService _db;

Future<void> sync({bool force = false}) async { ... }
}
Comment thread
Devasy marked this conversation as resolved.
```

- **Throttle:** skip if the most recent sync (tracked via a `health_sync.last_run` watermark) was less than 30 minutes ago, unless `force: true`.
- **Per-stream incremental fetch with look-back:** for each of `sleep`, `heart_rate`, `resting_heart_rate`, `hrv_rmssd`: read that stream's watermark from `settings` (default `now - 90 days` if absent — the agreed backfill window). Fetch from `watermark - 3 days` through `now` — the 3-day look-back re-pulls recent data even though it was already synced, to catch late corrections Health Connect or the watch itself makes to recent records (e.g. a sleep session Health Connect revises the next morning). Anything before the look-back window is assumed final and is never re-fetched.
- **Upsert:**
- `health_samples`: `INSERT OR REPLACE` keyed by the `(type, timestamp)` unique index — naturally idempotent and self-correcting.
- `sleep_sessions` / `sleep_stage_intervals`: for each `SleepPeriod` in the fetch window, delete-then-reinsert `sleep_stage_intervals` for that session id and upsert the `sleep_sessions` row — same delete/reinsert-child-rows pattern the original migration spec already uses for `sets`/`exercise_logs`.
- After all four streams succeed, advance each stream's watermark to `now` and the `last_run` throttle marker to `now`.
- **Failure handling:** any exception (permission not granted, Health Connect unavailable, one stream fails) is caught per-stream — a failed stream's watermark is left untouched so the next sync retries it, and does not block the other streams or crash the caller. Matches the existing best-effort caching posture in `HealthHistoryManager._readCachedHrDay`.

### Wiring

Only constructed when the active backend is `SqliteStorageService` — mirrors the existing guard in `main.dart:191-192` (`_storageService is SqliteStorageService ? SqlQueryService(...) : null`). Health data has no meaning under the pre-migration Hive fallback path.

- `AppInitializer` calls `sync()` once after both `HealthConnectService` and `SqliteStorageService` are ready, fire-and-forget (does not block first frame).
- A "Sync now" button is added to the existing health-permissions area of the Profile screen, calling `sync(force: true)`.

---

## 5. Coach SQL Tool Update

`CoachToolService`'s embedded schema description (used by `run_sql_query`, §7 of the original migration spec) gets the three new tables appended in the same one-line-per-table/column format as the existing schema text, so the model can join them against `sessions`, `exercise_logs`, and `sets` without a separate discovery call.

`get_health_metrics` (the existing live Health Connect tool) is unchanged — it remains the source for "right now" data that the synced tables won't have until the next app-open or manual sync.

---

## 6. Migration for Existing Installs

Existing SQLite installs (already past `onCreate`) need the three new tables added without a fresh install. `SqliteStorageService.init()` bumps `_dbVersion` and adds an `onUpgrade` step that runs the `CREATE TABLE`/`CREATE INDEX` statements from §3 if the new tables don't already exist (`CREATE TABLE IF NOT EXISTS`, safe to run unconditionally on upgrade). No data migration needed — these are brand-new tables with no prior data to carry forward; the first post-upgrade sync populates them via the normal 90-day backfill path.

---

## 7. Testing

- **`HealthSyncService`** (new test file, in-memory DB via `sqflite_common_ffi` + a fake `IHealthConnectService`):
- First sync with no prior watermark backfills the full 90-day window.
- Second sync only re-fetches from `watermark - 3 days` onward (verify the fake service receives the narrower range).
- Re-running sync is idempotent: no duplicate rows in `health_samples` or `sleep_sessions`, and changed values from a "corrected" fake response overwrite the prior row.
- A sync attempted less than 30 minutes after the last one is skipped unless `force: true`.
- An exception thrown by the fake health service for one stream doesn't propagate, doesn't advance that stream's watermark, and doesn't block the other streams from syncing.
- **`SqliteStorageService`**: extend the existing test file to cover the new upsert methods and the `onUpgrade` path (open a v-1 schema DB, run `init()`, assert the new tables exist).
- **`run_sql_query`**: extend `sql_query_service_test.dart` with a join query across `sessions`, `sets`, `sleep_sessions`, and `health_samples`, confirming the schema and join work end-to-end.

---

## 8. Rollout Notes

- No new dependencies — reuses `sqflite`, `sqflite_common_ffi` (test), and the existing `IHealthConnectService`.
- No changes to `IStorageService`, `MockStorageService`, or any manager/provider — additive on `SqliteStorageService` only, same boundary `SqlQueryService` already uses.
- `CLAUDE.md`'s "6 boxes" / schema references would benefit from a follow-up doc note once this ships, but that's out of scope here (same deferral pattern as the original migration spec, §9).
94 changes: 92 additions & 2 deletions workout-logger/lib/main.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,23 @@
// Following Dependency Inversion Principle: we create concrete implementations
// here at the composition root and inject them into high-level modules.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

import 'package:hive_flutter/hive_flutter.dart';
import 'services/debug_log_buffer.dart';
import 'services/storage_service.dart';
import 'services/sqlite_storage_service.dart';
import 'services/storage_backend_resolver.dart';
import 'services/ai/sql_query_service.dart';
import 'services/ml_service.dart';
import 'services/ai/gemini_ai_service.dart';
import 'services/ai/coach_tool_service.dart';
import 'services/health_connect_service.dart';
import 'services/health_data_sync_service.dart';
import 'services/interfaces/storage_service_interface.dart';
import 'services/interfaces/ml_service_interface.dart';
import 'services/interfaces/health_connect_service_interface.dart';
Expand All@@ -32,6 +39,55 @@ import 'theme/a2ui_app_theme.dart';
import 'screens/home_screen.dart';
import 'screens/onboarding_screen.dart';

/// Resolved once in main() before runApp(). Read lazily by
/// WorkoutLoggerApp._storageService's static initializer, which only runs
/// on first access (during build()) — by then this is already set.
IStorageService? _resolvedStorageService;

/// One-time, flag-gated, reversible Hive -> SQLite cutover. See
/// docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md §6.
Future<void> _resolveStorageBackend() async {
// Hive stays initialized here even post-cutover: ApiService reads/writes
// an installation id directly against this settings box, independent of
// IStorageService. Do not remove this unconditional init.
await Hive.initFlutter();
final settingsBox = await Hive.openBox<String>('settings');
final alreadyMigrated = settingsBox.get(storageMigratedFlagKey) == 'true';

if (alreadyMigrated) {
final sqlite = SqliteStorageService();
try {
await sqlite.init();
} catch (e, st) {
debugPrint('SQLite init failed, staying on Hive: $e\n$st');
final hiveStorage = StorageService();
await hiveStorage.init();
_resolvedStorageService = hiveStorage;
return;
}
_resolvedStorageService = sqlite;
return;
}

final hiveStorage = StorageService();
await hiveStorage.init();
final sqliteStorage = SqliteStorageService();

try {
await sqliteStorage.init();
} catch (e, st) {
debugPrint('SQLite init failed, staying on Hive: $e\n$st');
_resolvedStorageService = hiveStorage;
return;
}

_resolvedStorageService = await resolveStorageBackend(
hiveStorage: hiveStorage,
sqliteStorage: sqliteStorage,
alreadyMigrated: false,
);
}

void main() async {
DebugLogBuffer.attach();
WidgetsFlutterBinding.ensureInitialized();
Expand All@@ -58,13 +114,15 @@ void main() async {
),
);

await _resolveStorageBackend();

runApp(const WorkoutLoggerApp());
}

class WorkoutLoggerApp extends StatelessWidget {
// Singleton instances created once at app startup
// This ensures the same instances are used throughout the app lifecycle
static final IStorageService _storageService = StorageService();
static final IStorageService _storageService = _resolvedStorageService ?? StorageService();
Comment thread
Devasy marked this conversation as resolved.
static final IMLService _mlService = MLService();
static final IHealthConnectService _healthConnectService = HealthConnectService();
static final ProgramManager _programManager = ProgramManager(_storageService);
Expand All@@ -83,6 +141,17 @@ class WorkoutLoggerApp extends StatelessWidget {
// Serves arbitrary-range sleep/HR data to the detail screens.
static final HealthHistoryManager _healthHistoryManager =
HealthHistoryManager(_healthConnectService, _storageService);
// Populates the SQLite health tables the coach's run_sql_query tool joins
// against workout data. Null under the pre-migration Hive fallback path —
// there's no live SQLite database file to sync into. Mirrors the
// sqlQuery ? ... : null guard used for CoachToolService below.
static final HealthDataSyncService? _healthDataSyncService =
_storageService is SqliteStorageService
? HealthDataSyncService(
healthConnectService: _healthConnectService,
storage: _storageService as SqliteStorageService,
)
: null;
static final GeminiAiService _geminiService =
GeminiAiService(storage: _storageService);
static final ConversationManager _conversationManager =
Expand DownExpand Up@@ -115,6 +184,7 @@ class WorkoutLoggerApp extends StatelessWidget {
ChangeNotifierProvider<PRManager>.value(value: _prManager),
ChangeNotifierProvider<ReadinessManager>.value(value: _readinessManager),
Provider<HealthHistoryManager>.value(value: _healthHistoryManager),
Provider<HealthDataSyncService?>.value(value: _healthDataSyncService),
// GeminiAiService is the single AI backend instance. It's a ChangeNotifier
// (settings UI watches isConfigured/model), so it's provided as such.
// Consumers that should depend on the abstraction (the coach ViewModel,
Expand All@@ -134,11 +204,16 @@ class WorkoutLoggerApp extends StatelessWidget {
),
),
// CoachToolService backs AI tool calls; reads from WorkoutProvider + PRManager.
// run_sql_query is only offered once the app has cut over to SQLite —
// it needs a live database file to open a read-only connection against.
Provider<CoachToolService>(
create: (ctx) => CoachToolService(
workoutProvider: ctx.read<WorkoutProvider>(),
prManager: ctx.read<PRManager>(),
healthHistory: ctx.read<HealthHistoryManager>(),
sqlQuery: _storageService is SqliteStorageService
? SqlQueryService((_storageService as SqliteStorageService).databasePath)
: null,
),
),
],
Expand DownExpand Up@@ -182,11 +257,16 @@ class _AppInitializerState extends State<AppInitializer> {
final api = context.read<ApiService>();
final gemini = context.read<GeminiAiService>();
final readiness = context.read<ReadinessManager>();
final healthDataSync = context.read<HealthDataSyncService?>();

try {
await provider.init();
await settings.init();
gemini.init(settings.geminiApiKey, model: settings.geminiModel);
gemini.init(
settings.geminiApiKey,
model: settings.geminiModel,
maxToolRounds: settings.geminiMaxToolRounds,
);
try {
await gemini.loadUsage();
} catch (e, st) {
Expand All@@ -206,6 +286,16 @@ class _AppInitializerState extends State<AppInitializer> {
// so the opt-in flag is loaded; never blocks or fails app init.
readiness.refresh();

// Fire-and-forget: populates the SQLite tables run_sql_query joins
// against. No-op under the pre-migration Hive fallback (null there).
// Errors are swallowed here since main.dart discards the returned
// Future — sync() has no caller to propagate a failure to.
unawaited(
healthDataSync?.sync().catchError(
(Object e, StackTrace st) => debugPrint('healthDataSync.sync failed: $e\n$st'),
),
);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Fire-and-forget analytics in background.
api.sendHeartbeat();
api.trackEvent('app_open');
Expand Down
27 changes: 25 additions & 2 deletions workout-logger/lib/models/models.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,6 +64,19 @@ class MuscleActivation {

// ==================== Exercise ====================

// Exercise IDs treated as bodyweight-assisted (e.g. an assisted-dip/pull-up
// machine). Computed once here so every consumer (the load panel, the input
// row, set persistence) agrees on which exercises count as "assisted".
const Set<String> _assistedBodyweightExerciseIds = {
'pull_ups',
'chin_ups',
'dips',
'push_ups',
};

bool isAssistedBodyweightExercise(String? exerciseId) =>
exerciseId != null && _assistedBodyweightExerciseIds.contains(exerciseId);

class Exercise {
final String id;
final String name;
Expand All@@ -72,7 +85,7 @@ class Exercise {
final bool isCustom; // User-created exercise
final List<String>? availableHandles; // Attachment/handle options e.g. ['Rope', 'Bar']

Exercise({
const Exercise({
required this.id,
required this.name,
required this.muscleActivations,
Expand DownExpand Up@@ -250,7 +263,7 @@ class ExerciseLog {
final String? notes;
final String? handle;

ExerciseLog({
const ExerciseLog({
required this.exerciseId,
required this.sets,
this.notes,
Expand DownExpand Up@@ -936,12 +949,16 @@ class ChatMessage {
final String role; // 'user' | 'model'
final String text;
final DateTime timestamp;
// Names of tools the model called (in order) while producing this reply.
// Null/empty for user messages and replies that used no tools.
final List<String>? toolCalls;

ChatMessage({
String? id,
required this.role,
required this.text,
DateTime? timestamp,
this.toolCalls,
}) : id = id ?? _uuid.v4(),
timestamp = timestamp ?? DateTime.now();

Expand All@@ -950,24 +967,30 @@ class ChatMessage {
'role': role,
'text': text,
'timestamp': timestamp.toIso8601String(),
if (toolCalls != null && toolCalls!.isNotEmpty) 'toolCalls': toolCalls,
};

factory ChatMessage.fromJson(Map<String, dynamic> json) => ChatMessage(
id: json['id'] as String?,
role: json['role'] as String,
text: json['text'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
toolCalls: (json['toolCalls'] as List?)?.cast<String>(),
);

ChatMessage copyWith({
Object? role = _sentinel,
Object? text = _sentinel,
Object? timestamp = _sentinel,
Object? toolCalls = _sentinel,
}) => ChatMessage(
id: id,
role: role == _sentinel ? this.role : role as String,
text: text == _sentinel ? this.text : text as String,
timestamp: timestamp == _sentinel ? this.timestamp : timestamp as DateTime,
toolCalls: toolCalls == _sentinel
? this.toolCalls
: toolCalls as List<String>?,
);
}

Expand Down
Loading
Loading