Skip to content

Feat/android 17 - #59

Merged
Devasy merged 6 commits into
r2.1.0from
feat/android-17
Jul 23, 2026
Merged

Feat/android 17#59
Devasy merged 6 commits into
r2.1.0from
feat/android-17

Conversation

@Devasy

@Devasy Devasy commented Jul 15, 2026

Copy link
Copy Markdown
Owner
  • Upgrades the Android SDK version to 37 ie android 17
  • Upgrades the Java version to J17
  • Updates the Flutter build version to 3.44.4 from 3.41.6

Summary by CodeRabbit

  • Updates
    • Updated the Android build tooling and compile/target SDK versions.
    • Raised the Java and Kotlin compilation target to Java 17.
    • Updated the Flutter SDK requirement to version 3.44.4.
    • Updated Gradle wrapper and plugin versions to newer releases.
    • Bumped several Dart/Flutter dependencies and the Android launcher icons package.
  • Documentation
    • Refreshed README links and metadata to point to the new repository location.

Devasy added 3 commits July 11, 2026 19:15
…hain

- compileSdk + targetSdk: 36 → 37 (Android 17 / API 37)
- Removed compileSdkExtension (not needed for base API 37)
- Java source/target compatibility: VERSION_11 → VERSION_17
- Kotlin jvmTarget: 11 → 17
- Gradle wrapper: 8.12 → 8.14.1
- AGP: 8.9.1 → 8.11.1
- Kotlin Gradle Plugin: 2.1.0 → 2.2.20
- Enable android.builtInKotlin=true + android.newDsl=true
- Remove explicit id(kotlin-android) plugin (now injected by Flutter)
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The project updates its Flutter SDK constraint, Android build toolchain, and package dependencies. Android configuration adopts newer Gradle, Kotlin, Java, and SDK versions. Documentation and Android metadata now reference the RepForge repository.

Changes

Toolchain alignment

Layer / File(s) Summary
Flutter and Gradle foundation
workout-logger/pubspec.yaml, workout-logger/android/gradle/wrapper/gradle-wrapper.properties, workout-logger/android/settings.gradle.kts
Flutter, Gradle, Android Gradle Plugin, and Kotlin version constraints are updated.
Android build configuration
workout-logger/android/gradle.properties, workout-logger/android/app/build.gradle.kts
Built-in Kotlin and the new DSL are enabled; compilation and targeting move to Android SDK 37 with Java and Kotlin JVM targets set to 17.
Flutter dependency refresh
workout-logger/pubspec.yaml
intl, package_info_plus, file_picker, share_plus, and flutter_launcher_icons versions are updated.

Repository references

Layer / File(s) Summary
Repository link updates
README.md, workout-logger/fastlane/metadata/android/en-US/full_description.txt
README links, clone instructions, badges, developer profile, issue link, and Android metadata reference Devasy/RepForge.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly points to the Android 17 upgrade, which is the main theme of the PR.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workout-logger/android/app/build.gradle.kts`:
- Around line 5-22: Replace the kotlinOptions block in the android module
configuration with the Kotlin 2.x compilerOptions DSL, preserving the Java 17
JVM target. Do not re-add the org.jetbrains.kotlin.android plugin; keep the
existing built-in Kotlin setup and surrounding compileOptions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 555cf4b0-3fea-4724-aa58-626b533089c1

📥 Commits

Reviewing files that changed from the base of the PR and between 5624818 and 5f606b2.

⛔ Files ignored due to path filters (1)
  • workout-logger/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • workout-logger/android/app/build.gradle.kts
  • workout-logger/android/gradle.properties
  • workout-logger/android/gradle/wrapper/gradle-wrapper.properties
  • workout-logger/android/settings.gradle.kts
  • workout-logger/pubspec.yaml

Comment thread workout-logger/android/app/build.gradle.kts
@Devasy

Devasy commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai with this auto update tags do we have to remove the kotlin dep from the Buildgradle.kts or similar file ?
android.builtInKotlin
check for the other flag as well

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd build.gradle.kts -a
echo "---app build.gradle.kts---"
cat -n workout-logger/android/app/build.gradle.kts
echo "---settings.gradle.kts---"
cat -n workout-logger/android/settings.gradle.kts
echo "---gradle.properties---"
cat -n workout-logger/android/gradle.properties
echo "---search for kotlin references---"
rg -n "kotlin" workout-logger/android --iglob '*.kts' --iglob '*.properties'

Length of output: 7313


🤖 web_search executed:

Android Gradle Plugin android.builtInKotlin gradle property what does it do, does it remove need for kotlin-android plugin declaration

Length of output: 2459


🤖 web_search executed:

Android Gradle Plugin android.newDsl gradle property meaning AGP 8.x

Length of output: 6629


🤖 web_search executed:

Migrate to built-in Kotlin Android Studio - what is minimum AGP version required for android.builtInKotlin gradle property (AGP 8.x support)?

Length of output: 4403


🤖 web_search executed:

Does Flutter's dev.flutter.flutter-gradle-plugin automatically apply the org.jetbrains.kotlin.android plugin to app module, independent of AGP built-in Kotlin support?

Length of output: 4401


🤖 web_search executed:

Flutter "Built-in Kotlin migration for app developers" docs.flutter.dev - does Flutter's flutter-gradle-plugin apply kotlin-android plugin based on android.builtInKotlin property regardless of AGP version, and is AGP 9 required?

Length of output: 10136


@Devasy, good catch — I dug into this and here's the full picture:

Short answer: no, you don't need to remove the Kotlin plugin from settings.gradle.kts (or anything else) right now. Your current setup is actually correct for where you are in the migration.

Why:

  • android.builtInKotlin and android.newDsl are AGP 9.0+ features. Android's own docs state: "You need AGP 9.0 or higher to have built-in Kotlin support."
  • This PR only bumps AGP to 8.11.1 (in workout-logger/android/settings.gradle.kts), not to 9.0+.
  • I checked Flutter's Gradle plugin source directly (the isBuiltInKotlinEnabled check added in [AGP 9] Support Enabling Built-in Kotlin flutter/flutter#188543). It explicitly does:
    if (agpVersion == null || agpVersion.major < 9) {
        return false
    }
    So with AGP 8.11.1, Flutter's Gradle plugin ignores android.builtInKotlin=true entirely and falls back to auto-applying the legacy kotlin-android (Kotlin Gradle Plugin) itself — exactly like before.
  • That auto-apply still needs the org.jetbrains.kotlin.android plugin version to be resolvable, which is why settings.gradle.kts still declares:
    id("org.jetbrains.kotlin.android") version "2.2.20" apply false
    If you remove that declaration now, the build would break because FGP wouldn't be able to resolve/apply the Kotlin plugin version.

So, to summarize both flags:

Flag Status on AGP 8.11.1
android.builtInKotlin=true Inert — requires AGP 9.0+
android.newDsl=true Inert — also requires AGP 9.0+

They were added automatically by the Flutter migrator as forward-looking placeholders, and they don't cause any harm sitting in gradle.properties at 8.11.1. The app/build.gradle.kts comment ("kotlin-android is injected automatically by Flutter's built-in Kotlin support") is slightly misleading right now — technically it's injected by FGP's legacy auto-apply behavior, not true built-in Kotlin, since that isn't active yet.

If you want built-in Kotlin to actually activate, that would require a separate, larger effort: bumping AGP to 9.0+ in settings.gradle.kts, adopting the new DSL (ApplicationExtension/LibraryExtension instead of the android {} block extension functions), and checking plugin compatibility (e.g., android.applicationVariants usage in your app/build.gradle.kts for ABI version codes is part of the legacy variant API and would need migrating too). I'd recommend treating that as a follow-up PR rather than bundling it into this SDK/JDK/Flutter bump.

Let me know if you'd like me to open a follow-up issue to track the AGP 9 / built-in Kotlin migration.

(⁠•‾⁠_⁠•⁠) — curious to see how the AGP 9 migration shakes out when you're ready for it.


✏️ Learnings added
Learnt from: Devasy
URL: https://github.com/Devasy/RepForge/pull/59

Timestamp: 2026-07-15T15:47:23.404Z
Learning: In Devasy/RepForge's workout-logger Flutter app (workout-logger/android), `android.builtInKotlin` and `android.newDsl` gradle.properties flags only take effect when AGP >= 9.0.0. Flutter's Gradle plugin (FlutterPluginUtils.isBuiltInKotlinEnabled) explicitly forces built-in Kotlin to false when the detected AGP major version is below 9, falling back to auto-applying the legacy `org.jetbrains.kotlin.android` plugin. The project currently pins AGP to 8.11.1 in workout-logger/android/settings.gradle.kts, so these flags (added automatically by the Flutter migrator) are currently inert placeholders, and the `org.jetbrains.kotlin.android` plugin version declaration in settings.gradle.kts must be kept until an actual AGP 9.0+ upgrade is done.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@Devasy

Devasy commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai nice catch what would be the changes for moving to latest AGP? 9.00 or above, what changes in the gh action would come?

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e yml -e yaml . .github/workflows 2>/dev/null
echo "---workflows---"
for f in $(fd -e yml -e yaml . .github/workflows 2>/dev/null); do
  echo "=== $f ==="
  cat -n "$f"
done
echo "---app build.gradle.kts full---"
cat -n workout-logger/android/app/build.gradle.kts
echo "---settings.gradle.kts---"
cat -n workout-logger/android/settings.gradle.kts

Length of output: 16350


🤖 web_search executed:

Android Gradle Plugin (AGP) 9.0 release status 2026, stable release, minimum JDK version required, minimum Gradle version required

Length of output: 5339


🤖 web_search executed:

Android Gradle Plugin 9.0 new DSL migration guide ApplicationExtension LibraryExtension breaking changes variant API

Length of output: 8400


@Devasy, here's the breakdown based on the official AGP 9.0.1 release notes and compatibility tables:

1. Version bumps required

File Current Needed for AGP 9
workout-logger/android/settings.gradle.kts com.android.application 8.11.1 9.1.1+ (see note below, not just 9.0.1)
workout-logger/android/gradle/wrapper/gradle-wrapper.properties Gradle 8.14.1 Gradle ≥9.1.0 (AGP 9.0's own minimum)
JDK 17 17 (already satisfied — no change needed)

Important catch: The official compileSdk/AGP compatibility table lists API level 37 as requiring minimum AGP 9.1.1, not the base 9.0.1. Since this PR already sets compileSdk = 37 / targetSdk = 37 while pinning AGP to 8.11.1, you're technically already ahead of AGP's official support window for that API level (Gradle usually just emits a suppressible warning rather than failing, but worth being aware of). So if/when you move to AGP 9, target 9.1.1+ directly rather than 9.0.1.

2. settings.gradle.kts changes

  • Bump com.android.application to 9.1.1+.
  • Remove the org.jetbrains.kotlin.android plugin declaration entirely — AGP 9's release notes explicitly state "The org.jetbrains.kotlin.android plugin is not compatible with the new DSL." With android.builtInKotlin=true now actually activating (AGP ≥9), Kotlin compilation is handled internally.

3. app/build.gradle.kts changes

  • The android.applicationVariants.configureEach { ... } block used for your ABI-specific versionCodeOverride logic is exactly the deprecated API called out in the AGP 9 migration guide. It must be rewritten using the new androidComponents.onVariants(...) API — this is the biggest actual code change, not just a version bump.
  • The kotlinOptions { jvmTarget = ... } block is part of the legacy Kotlin Gradle Plugin DSL; with built-in Kotlin, JVM target config typically moves under kotlin { compilerOptions { jvmTarget = ... } } or an equivalent built-in-Kotlin DSL surface — worth double-checking against Flutter's migration docs once you're actually on AGP 9, since the exact API may still be evolving.
  • Any other legacy android {} block usage should still work for now (old classes are deprecated but present) unless you also flip android.newDsl=false as a safety valve during transition — but that would defeat the purpose of adopting android.newDsl=true.

4. Third-party plugin compatibility (the real risk)

  • Flutter's flutter-gradle-plugin needs to explicitly support AGP major version 9 (recall: if (agpVersion.major < 9) return false gating built-in Kotlin). AGP 9.0 only shipped in January 2026, so Flutter's stable-channel support maturity for it is worth verifying directly against your target Flutter version (3.44.4 per this PR) before committing to the bump — I don't have high confidence this has been battle-tested yet.
  • F-Droid build server compatibility for your signing-block-stripping trick (the comment in app/build.gradle.kts about signingConfig needing to stay on one line) should also be re-verified, since the new DSL/variant API could change how that block is structured internally.

5. GitHub Actions impact

Actually pretty minimal:

  • .github/workflows/release.yml already sets java-version: '17' via actions/setup-java@v4 — no change needed there (17 remains AGP 9's minimum too).
  • Neither workflow pins a Gradle or AGP version explicitly — both are driven by gradle-wrapper.properties / settings.gradle.kts, so the CI workflow YAML itself likely needs no changes. The version bump is fully contained to the Gradle/AGP config files.
  • .github/workflows/test.yml doesn't invoke Gradle/Android build directly (only flutter analyze/flutter test), so it's unaffected either way.

Recommendation

Given the DSL/variant-API rewrite (item 3) and the Flutter-compatibility unknown (item 4), I'd still treat this as a separate, carefully-tested follow-up PR rather than bundling it here. Want me to open a tracking issue for it?

(⁠`⁠_⁠´⁠) — this one's got some real teeth to it, happy to help scope the follow-up whenever you're ready.


✏️ Learnings added
Learnt from: Devasy
URL: https://github.com/Devasy/RepForge/pull/59

Timestamp: 2026-07-15T15:50:48.798Z
Learning: In Devasy/RepForge's workout-logger app (workout-logger/android/app/build.gradle.kts), the ABI-specific versionCode logic uses the legacy `android.applicationVariants.configureEach { ... }` API (casting output to `ApkVariantOutputImpl`) to set `versionCodeOverride` per ABI. This API is deprecated under AGP 9.0's new DSL/variant API model and must be migrated to the `androidComponents.onVariants(...)` API before upgrading AGP to 9.0+. Additionally, the official Android compileSdk/AGP compatibility table lists API level 37 as requiring minimum AGP 9.1.1 (not just 9.0.1), and AGP 9.0 requires minimum Gradle 9.1.0 and removes compatibility with the standalone `org.jetbrains.kotlin.android` plugin (which must be dropped from workout-logger/android/settings.gradle.kts) in favor of built-in Kotlin.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@Devasy

Devasy commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai open a separate issue for this, to be tracked in another pr

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="Devasy/RepForge"

body=$(cat <<'EOF'
## Summary

Follow-up to `#59`: migrate `workout-logger/android` to Android Gradle Plugin (AGP) 9.0+ to actually activate the `android.builtInKotlin` and `android.newDsl` gradle.properties flags that were added automatically by the Flutter migrator in `#59`. On the current AGP version (8.11.1), these flags are inert placeholders because Flutter's Gradle plugin only enables built-in Kotlin support when AGP major version >= 9.

## Background

- `workout-logger/android/gradle.properties` sets `android.builtInKotlin=true` and `android.newDsl=true`, but Flutter's Gradle plugin (`FlutterPluginUtils.isBuiltInKotlinEnabled`) explicitly forces this to `false` when AGP major version < 9.
- The project currently pins `com.android.application` and `org.jetbrains.kotlin.android` to 8.11.1 / 2.2.20 in `workout-logger/android/settings.gradle.kts`.
- Official AGP compileSdk compatibility tables list API level 37 (already targeted in `#59`) as requiring minimum AGP 9.1.1, not just 9.0.1.

## Required changes

1. **`workout-logger/android/settings.gradle.kts`**
   - Bump `com.android.application` plugin version to `9.1.1`+ (not just `9.0.1`, due to compileSdk 37 compatibility requirements).
   - Remove the `org.jetbrains.kotlin.android` plugin declaration entirely — AGP 9's new DSL is not compatible with the standalone Kotlin Android plugin; built-in Kotlin support handles this instead.

2. **`workout-logger/android/gradle/wrapper/gradle-wrapper.properties`**
   - Bump Gradle distribution to `>= 9.1.0` (AGP 9.0's minimum Gradle requirement).

3. **`workout-logger/android/app/build.gradle.kts`**
   - Rewrite the ABI-specific `versionCodeOverride` logic, currently implemented via the legacy `android.applicationVariants.configureEach { ... }` API (casting output to `ApkVariantOutputImpl`), using the new `androidComponents.onVariants(...)` variant API. This is the biggest non-trivial code change required.
   - Migrate the `kotlinOptions { jvmTarget = ... }` block to the built-in-Kotlin equivalent DSL (e.g., `kotlin { compilerOptions { jvmTarget = ... } }`), verifying against Flutter's built-in Kotlin migration docs at the time of the upgrade.

4. **Compatibility verification (risk items, not pure version bumps)**
   - Confirm Flutter's `flutter-gradle-plugin` (pinned via Flutter SDK `3.44.4`+ per `#59`) has stable-channel support for AGP major version 9 before committing to the bump.
   - Re-verify the F-Droid signing-block-stripping logic in `app/build.gradle.kts` (the comment about `signingConfig` needing to stay on one line) still holds under the new DSL/variant API.

## Affected files

- `workout-logger/android/settings.gradle.kts`
- `workout-logger/android/gradle/wrapper/gradle-wrapper.properties`
- `workout-logger/android/app/build.gradle.kts`
- `workout-logger/android/gradle.properties` (flags already in place from `#59`, will become active)

## Acceptance criteria

- [ ] AGP bumped to `9.1.1`+ in `settings.gradle.kts`, `org.jetbrains.kotlin.android` plugin declaration removed.
- [ ] Gradle wrapper bumped to `>= 9.1.0`.
- [ ] `android.applicationVariants.configureEach` ABI versionCode logic migrated to `androidComponents.onVariants(...)`.
- [ ] `kotlinOptions.jvmTarget` migrated to the built-in-Kotlin equivalent.
- [ ] Flutter Gradle plugin / Flutter SDK version confirmed compatible with AGP 9 on stable channel.
- [ ] F-Droid build (signing block stripping) verified to still work correctly.
- [ ] CI (`.github/workflows/release.yml`, `.github/workflows/test.yml`) confirmed to require no changes (JDK 17 already set via `actions/setup-java@v4`).

## References

- PR: https://github.com/Devasy/RepForge/pull/59
- Discussion: https://github.com/Devasy/RepForge/pull/59#issuecomment (see AGP 9 migration discussion thread)
- Requested by: `@Devasy`
EOF
)

issue_url=$(gh issue create \
  --repo "$repo" \
  --title "Migrate workout-logger/android to AGP 9.0+ for built-in Kotlin / new DSL support" \
  --body "$body" \
  --assignee "Devasy")

echo "Created issue: $issue_url"

Length of output: 4282


@Devasy done — I've opened #60 to track the AGP 9.0+ / built-in Kotlin migration as a follow-up, with the required changes, affected files, and acceptance criteria captured from our discussion. It's assigned to you and links back to this PR.

(⁠`⁠ヘ⁠´⁠)⁠凸 tracking this properly now — good luck when you tackle it!

@Devasy
Devasy changed the base branch from main to r2.1.0 July 23, 2026 10:28
@Devasy
Devasy merged commit 38b723b into r2.1.0 Jul 23, 2026
2 checks passed
@Devasy
Devasy deleted the feat/android-17 branch July 23, 2026 10:28
This was referenced Aug 7, 2026
Devasy added a commit that referenced this pull request Sep 1, 2026
* Feat/android 17 (#59)

* feat: add localized summaries, app metadata, and signing block information to F-Droid repository data

* chore: upgrade Android SDK 16→17, Java 11→17, Gradle/AGP/Kotlin toolchain

- compileSdk + targetSdk: 36 → 37 (Android 17 / API 37)
- Removed compileSdkExtension (not needed for base API 37)
- Java source/target compatibility: VERSION_11 → VERSION_17
- Kotlin jvmTarget: 11 → 17
- Gradle wrapper: 8.12 → 8.14.1
- AGP: 8.9.1 → 8.11.1
- Kotlin Gradle Plugin: 2.1.0 → 2.2.20
- Enable android.builtInKotlin=true + android.newDsl=true
- Remove explicit id(kotlin-android) plugin (now injected by Flutter)

* chore: update pubspec.lock (transitive dependency bumps)

* chore: update repo name and username references to RepForge and Devasy

* upadtes the build gradle kts file to match the review comment

* Adds pubspec yaml

---------

Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>

* Upgrades bottom nav bar Upgrade Bottom Navigation Bar (#61)

* feat: add localized summaries, app metadata, and signing block information to F-Droid repository data

* chore: upgrade Android SDK 16→17, Java 11→17, Gradle/AGP/Kotlin toolchain

- compileSdk + targetSdk: 36 → 37 (Android 17 / API 37)
- Removed compileSdkExtension (not needed for base API 37)
- Java source/target compatibility: VERSION_11 → VERSION_17
- Kotlin jvmTarget: 11 → 17
- Gradle wrapper: 8.12 → 8.14.1
- AGP: 8.9.1 → 8.11.1
- Kotlin Gradle Plugin: 2.1.0 → 2.2.20
- Enable android.builtInKotlin=true + android.newDsl=true
- Remove explicit id(kotlin-android) plugin (now injected by Flutter)

* chore: update pubspec.lock (transitive dependency bumps)

* chore: update repo name and username references to RepForge and Devasy

* upadtes the build gradle kts file to match the review comment

* Adds pubspec yaml

* Enhances the bottom nav bar

* fixes out bulging issue

* Updates the bottom navbar UI, and then adds build size reuction params

* Adds build script and upgrades the release workflow

* Adds tests

* updates acc to review comments

* Adds gitignore and updates codecov yaml

* updated comments according to review comments

---------

Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>

* Feat/increase coverage test screens (#63)

* Adds tests for screens

* Adds tests

* Adds comprehensive tests

* Adds new tests

* Updates test.yml to run on release branches

* Adds test and resolved the warnings and issues

* Updates tests and minor bug fixes

* Adds fixes for failing testsm and adds connection timeout safety for health connector

* Adds missing lines patch

* Updates the tests with analyse failures

* Updates tests and routine creator to use the common component

* Updates flutter version and adds tests

---------

Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>

* Feat/genui (#64)

* Adds tests for screens

* Adds tests

* Adds comprehensive tests

* Adds new tests

* Updates test.yml to run on release branches

* Adds test and resolved the warnings and issues

* Updates tests and minor bug fixes

* Adds fixes for failing testsm and adds connection timeout safety for health connector

* Adds missing lines patch

* Updates the tests with analyse failures

* Updates tests and routine creator to use the common component

* Updates flutter version and adds tests

* Adds major genui Feature and renderer

* chore: remove patch_so script

* build: add --build-id=none for jni package in F-Droid metadata

* ci: add jni build-id sed step for future reproducible releases

* feat: assisted pullups, deload-aware ML, handle-scoped PRs, sleeping HR tool

Batches several in-flight features that were sitting uncommitted:

- Bodyweight/assisted pullup volume: (BW - assist + extra) * reps
- MLService reads the past 3 sessions and recovers from a deload week
  using the pre-deload baseline instead of the deload trough
- PRManager scopes records per handle variation (Rope vs Bar)
- CoachToolService.get_sleeping_hr_analytics: p5/p25/mean, stdev,
  variance and linear trend over the last N nights
- GenUI parser tolerates numeric StatCard values, loose trend words and
  Markdown code fences

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiProps alias-aware coercing property reader

Foundation for the genui refactor: a never-throwing view over raw
component prop maps that resolves keys by exact match, normalized
match (case/underscore/hyphen/space-insensitive), then semantic
alias, and coerces values to typed accessors with documented
fallbacks instead of throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiSpec contract, A2UiNode and A2UiRegistry

Adds the four-in-one component contract (A2UiSpec) that lets each UI
component name itself, parse its own props, build its own widget and
document itself for the LLM prompt on one object, plus the
A2UiRegistry lookup table that replaces the old allowedA2UiComponents
set and two parallel switch statements. Includes an A2UiTheme skeleton
(filled in by Task 4) and A2UiNode, the parsed-tree node type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): make A2UiRegistry throw on name/alias collisions

Code review found that A2UiRegistry's constructor loop silently
resolved canonical-name/alias collisions (last-writer-wins for names,
first-writer-wins for aliases), which would produce unreachable specs
or dropped aliases with no signal as more components are registered in
later tasks. The constructor now throws a StateError identifying both
colliding specs for any of: two specs sharing a canonical name, an
alias colliding with another spec's canonical name, or two specs
sharing an alias. Adds three regression tests using a new configurable
_NamedFakeSpec fake.

Also documents (doc-comment only, no behavior change) that
A2UiNode.children is not defensively copied, per the review's Minor
finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiParser with fence, envelope and alias repair

Adds the single gate that decides whether an LLM reply is a UI payload
or ordinary prose, and turns UI payloads into an A2UiNode tree. Handles
markdown fences, prose-wrapped JSON, flat vs props-wrapped shapes,
bare-array/envelope auto-wrapping into GridContainer, and recursive
children, without ever throwing.

Also promotes A2UiProps._asStringKeyed to a public static
A2UiProps.stringKeyed so the parser can re-key decoded JSON maps
without an awkward part-of coupling between the two libraries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): balanced-bracket JSON extraction and envelope singleton fix

_extractJson previously sliced from the first { to the last }, which
broke on any stray brace in surrounding prose (e.g. "add reps
{optional}"). Replace with a scan that tries jsonDecode on every
balanced {..}/[..] span found via a depth counter that correctly skips
brackets inside string literals, preferring the longest successful
decode as the actual payload.

Also fix _wrap's unconditional single-child collapse: an explicit
envelope key ({"components":[...]}) is a deliberate container request
and must still produce a GridContainer with one child, while a bare
top-level array with one item keeps collapsing since it's ambiguous
between "a list of one" and "just one component."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): inject A2UiTheme and extract shared panel chrome

Adds A2UiThemeProvider (InheritedWidget, falls back to A2UiTheme.dark)
and the panel/title/empty-state/legend widgets every component spec
will share, plus lib/theme/a2ui_app_theme.dart mapping RepForge's real
design tokens onto A2UiTheme. This is the only file where the two
systems meet - lib/genui/ still imports nothing app-specific.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(genui): strengthen theme-injection and add A2UiPanel coverage

The injection test compared against repforgeA2UiTheme, which is
field-for-field identical to the A2UiThemeProvider.of fallback
(A2UiTheme.dark), so it passed even if the InheritedWidget lookup were
broken. Inject a fixture with distinct values instead, and assert a
sibling context still falls back to the default. Also add direct
coverage for A2UiPanel's padding, decoration, and child rendering,
previously only exercised indirectly via A2UiEmptyPanel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiSeries as the shared categorical data shape

A2UiSeries.extract() and maxValue() give line/bar/pie and radar chart
components one common {name, values} shape to consume, so a model that
learns {labels, series} once can drive all four components.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): cover fallback path in A2UiSeries.extract, fix negative-max bug

Address code review findings on A2UiSeries:
- Add tests pinning down the series->values fallback when every series
  entry drops to empty/unparseable values, and when series is an empty
  list — the risky path the brief called out but left untested.
- Rename the misleading 'reads the axes alias' test; it only exercised
  stringified-number coercion inside series values, not alias resolution.
- Fix maxValue() to track whether any value has been seen instead of
  seeding with 0.0, so all-negative series report their true max
  instead of silently clamping to 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add StatCardSpec with typed props and trend synonyms

Establishes the pattern for Tasks 7-13: a typed props record, an
A2UiSpec bundling name/aliases/doc/parseProps/buildWidget, and
never-throwing parsing that degrades to documented fallbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add MetricGaugeSpec with safe progress and null value

Fixes the validator/renderer contradiction where a String value was
accepted but cast to num, and the min == max NaN sweep angle bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add DynamicChartSpec for line, bar and pie

Adds the most-used and most complex A2UI component so far, covering
line/bar/pie rendering over the shared {labels, series} shape with
never-throwing prop parsing and label padding to prevent out-of-range
axis lookups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add ScatterPlotSpec with point repair and safe bounds

Adds paired x/y observation plotting with an optional correlation badge,
following the Task 6-8 A2UiSpec pattern. Malformed points are dropped
rather than throwing, and bounds widen degenerate axes so fl_chart never
sees a zero-span range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add RadarChartSpec sharing the labels/series shape

Task 10 of the a2ui/genui refactor: RadarChart consumes the same
{labels, series} shape as DynamicChart, with `axes` kept as a
backward-compatible alias for `labels`. Every series is truncated
or zero-padded to labels.length at parse time so fl_chart's radar
never sees a mismatched entry count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add DataListGroupSpec with row repair and optional title

Adds a titled list-of-rows component with a defensive row-extraction
fallback chain: named fields, bare scalars, first-stringifiable-value
fallback, and silent drop of rows with nothing displayable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add FilterChipsSpec with nullable active option

Renders a decorative, non-interactive row of scope chips (e.g. "7d /
30d / 90d") and fixes the old renderer's `activeOption as String`
crash by matching case-insensitively and falling back to null instead
of throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add GridContainerSpec, default registry and renderer

Task 13: assembles all eight leaf components into defaultA2UiRegistry,
adds the GridContainerSpec layout wrapper, the public A2UiRenderer
widget, and the lib/genui/a2ui.dart barrel file that will be the only
import path the rest of the app uses going forward.

fix(genui): make structural children lookup exact, not alias-resolved

Cross-task fix to a2ui_parser.dart (a Task 3 file), discovered during
Task 13 registry integration. A2UiParser._parseChildren and
_declaresChildren resolved the structural `children` key through
A2UiProps' alias-aware lookup(), which treats `items` as an alias for
`children`. That collided with DataListGroupSpec, whose own canonical
data-row key is also `items`: a DataListGroup node's `items` list of
{primaryText, ...} maps was mistaken for child components, none of
them parsed as one, and the whole node was then discarded as an
emptied-out container. Reading the literal `children` key only fixes
this and matches the precision _envelopeKeys already had (it does not
include `items` as a synonym for `children` either).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): generate the A2UI prompt section from the registry

Replaces hand-written component-schema prose in the coach system
prompt with a section generated from defaultA2UiRegistry, so the
vocabulary advertised to the model can never drift from what the
parser/renderer actually support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(genui): wire coach screen to the A2UI package, drop legacy renderer

Replaces private _CoachMessageContent with a public, stateful
CoachMessageContent that memoizes parsing per text value and shows a
"Building dashboard..." placeholder for partial JSON while streaming,
instead of letting raw braces scroll past or losing prose on a mixed
reply. Wraps the app root in A2UiThemeProvider(theme: repforgeA2UiTheme)
so the renderer picks up RepForge's design tokens. Deletes the
superseded lib/genui/a2ui_component.dart and lib/genui/a2ui_renderer.dart,
and drops test/new_features_test.dart's GenUI Component Resilience Tests
group, whose two cases are already covered more thoroughly by
test/genui/a2ui_parser_test.dart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): bracket negative-value ranges in DynamicChart line/bar axes

minY was hardcoded to 0 while maxY derived from the true series max, so an
all-negative dataset (e.g. [-10, -5, -3]) produced a visible axis range of
[0, 1] with every real data point falling outside it — a silent blank
chart despite valid, non-empty data. Adds A2UiSeries.minValue mirroring
the existing maxValue, and a shared _yBounds helper used by both _line and
_bar so the two renderers can't diverge on axis math. Also covers
multi-series label padding, which was previously only exercised through
series[0].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(genui): cover all-negative bounds and malformed point entries

Task 9 review flagged that ScatterPlotProps.bounds had no regression pin
for all-negative-coordinate spreads (same failure class as Task 8's
DynamicChartSpec axis bug) and that point-parsing had no test for
structurally invalid entries (nested objects, raw lists). Adds both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): widen per-node children lookup back to components/elements/content

Follow-up to the Task 13 a2ui_parser.dart fix: restricting the per-node
_parseChildren/_declaresChildren lookup to the literal 'children' key
was narrower than intended. It regressed 'components'/'elements'/
'content' as per-node child-list keys, which never collided with
anything (only 'items' did, via DataListGroup's own canonical data key).
A payload like {"component":"GridContainer","props":{"columns":1,
"components":[...]}} resolved fine before the original bug and silently
rendered blank (zero children, no null fallback) after the first fix,
since _declaresChildren no longer recognized 'components' as a
children-declaring key either.

Adds a _childKeys constant (children/components/elements/content,
still excluding items) mirroring _envelopeKeys' existing tolerance, and
routes both _parseChildren and _declaresChildren through a shared
_firstChildList literal (non-alias) lookup over that key set.

Adds regression tests in a2ui_renderer_test.dart: per-node
components/elements/content resolve to real children, and items stays
excluded at the per-node level.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): widen looksLikeUi to catch prose-prefixed fences, fix vacuous memoization test

looksLikeUi only checked whether the text, after stripping a *leading*
fence, started with `{`/`[`. A model that writes a sentence before
opening a fenced payload (e.g. "Here is your data:\n```json\n{...")
fell through undetected, so CoachMessageContent showed the raw partial
JSON instead of the streaming placeholder -- the exact symptom this
task exists to fix. Now also treats an unclosed ``` fence found
anywhere in the streamed-so-far text as a UI signal, while plain prose
with no JSON or fence anywhere still returns false.

Also fixes the memoization regression test in
test/screens/ai_coach_genui_test.dart: the second observation was
taken after a bare `tester.pump()`, which doesn't mark the element
dirty and never actually calls build() again, so the test could not
distinguish memoized parsing from a widget that never rebuilds at all.
It now pumps a second CoachMessageContent instance with identical text
at the same tree location, which reuses the existing State and
genuinely triggers didUpdateWidget/build.

Adds regression tests for both the prose-prefixed-fence case and the
plain-prose-no-json case in test/genui/a2ui_parser_test.dart, plus a
widget-level test in test/screens/ai_coach_genui_test.dart confirming
the placeholder (not raw JSON) renders end-to-end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(genui): drop presentation payload from tools, add purity and fuzz suites

The sleeping-HR analytics tool was hand-constructing an A2UI DynamicChart
payload directly, leaking presentation decisions into the data layer.
Replace `genui_chart_props` with neutral `labels`/`series` keys so the
prompt — not the tool — decides how to present the data.

Add two permanent guard suites: a2ui_purity_test.dart proves lib/genui/
never imports app-specific code (theme/models/services/screens) and its
component renderers never cast raw model data; a2ui_robustness_test.dart
fuzzes the parser and renderer against ~26 hostile/malformed LLM payloads
to confirm nothing throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): depth-agnostic purity regex, pin two silent-visual regressions

Review of the previous commit found the purity test's forbidden-import
check was depth-blind: its literal needle list only covered one and two
../ hops, but components live three levels below lib/, so a real
../../../theme/... import passed undetected. Replace it with a regex
that matches any number of ../ hops (or a package:repforge/ prefix),
covering import and export directives alike, and add a self-test that
proves the regex catches every relevant depth/form without touching real
source files.

Also widen the no-raw-casts check to include bool/Object/dynamic, make
the components-directory scan recursive, and pin down the two historical
silent-visual regressions (Task 8's chart axis-bounds clamp, Task 13's
GridContainer child-key aliasing) with positive assertions in the fuzz
suite, since neither throws and the existing no-throw checks structurally
can't catch either.

Reword analyze_health_workout_correlation's tool declaration to drop
direct component names, closing the same presentation-leak class this
task already fixed for the sleeping-HR tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): propagate registry through recursion, pin prompt drift, close review findings

Final whole-branch review fix wave for the A2UI genui refactor:

- A2UiRenderer's registry override used to be silently dropped past one
  level of nesting because GridContainerSpec recurses via bare
  A2UiRenderer(node: ...) calls. Mirror the existing theme-injection
  pattern with a new A2UiRegistryProvider InheritedWidget so an explicit
  registry override at any level propagates ambiently to everything below
  it (explicit param > inherited provider > defaultA2UiRegistry fallback).
- Pin the hand-written "WHICH COMPONENT TO REACH FOR" prose in
  gemini_context_builder.dart against silent drift: every component name
  it mentions must resolve in defaultA2UiRegistry, and the registry's
  spec count is asserted directly.
- Delete A2UiProps.object()/has() — confirmed zero call sites.
- Repurpose the orphaned Task 3 scaffolding test
  (a2ui_parser_stub_test.dart, redundant with a2ui_parser_test.dart) into
  a2ui_custom_registry_test.dart, the regression coverage the registry-
  propagation fix needed.
- Add scanned-file-count floors to the purity test's two directory scans
  so an empty/unreachable directory can't produce a vacuous pass.
- Document FilterChips' SizedBox.shrink() as a deliberate exception to
  the plan's "always A2UiEmptyPanel" rule (decorative chrome, not data).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add design spec for Hive->SQLite migration + coach SQL query tool

* fix: persist assisted-load volume correctly, tighten exercise-handle scoping

- WorkoutSet now snapshots bodyweight/assist/extra at logging time instead
  of recomputing effective load from the CURRENT profile bodyweight on every
  read, which was silently corrupting historical volume whenever a user
  updated their weight. ExerciseLog.totalVolume and the workout_flow_screen
  logging path thread the snapshot through.
- Exercise-handle matching (workout_provider) now requires an exact handle
  match whenever a handle is set, falling back to legacy behavior only when
  no exact match exists — a null-handle log was previously matching ANY
  requested handle, surfacing the wrong variation's "last session" data.
- Handle selector no longer visually pre-selects an unpersisted handle, and
  setExerciseHandle no longer retroactively relabels already-logged sets.
- Assisted-load display values now respect the user's unit preference; the
  assisted-exercise classification is computed once and shared instead of
  drifting between two separate predicates.
- Body-weight input (settings_provider) now rejects non-finite/non-positive
  values on both the load and set paths, falling back to 70.0 when invalid.
- ml_service: deload-recovery reasoning no longer hardcodes "kg" regardless
  of unit settings; recovery detection now requires the comparison session
  to be recent and uses effective (not raw) load for assisted exercises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: bound sleep-analytics window, drop fabricated data, resolve muscle groups by id

- get_sleeping_hr_analytics clamps the model-provided days window instead of
  looping unbounded; get_health_metrics now honors the requested days window
  instead of always querying one week, and both its and the correlation
  tool's declarations no longer advertise fields (resting HR, readiness)
  that aren't actually backed by implementation.
- analyze_health_workout_correlation no longer fabricates synthetic sleep
  data points to pad out insufficient real pairs — returns the existing
  insufficient-data error instead, so correlation/regression/chart output is
  never partly made up.
- get_muscle_group_volume now resolves requested names to ids via
  _resolveMuscleGroup and compares ids (also aggregating secondary muscle
  activations) instead of raw display-name substring matching.
- CoachToolService's optional HealthHistoryManager is now a named parameter.
- gemini_ai_service: daily-quota classification narrowed to actual
  daily-limit identifiers so minute-scale rate limits go through normal
  retry-delay handling instead of being misclassified as daily exhaustion;
  function-call ids are now preserved and matched into their responses;
  the fallback path now builds a thinkingConfig compatible with whichever
  model was actually selected. Mirrored in scripts/test_gemini_api.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): pie negative-value filtering, overflow guard, stat-card unit match

- DynamicChart's pie mode now filters to positive values before computing
  percentages/sections (preserving original index alignment with labels and
  series colors), falling back to an empty panel when nothing positive
  remains, instead of rendering a nonsense chart from negative/zero data.
- A2UiPanelTitle's trailing label is now Flexible with maxLines/ellipsis so
  a long model-provided string can't overflow the row.
- StatCard's unit-already-present check now requires a trailing-suffix
  match instead of any substring, fixing a false positive like unit "s"
  matching inside value "10 reps".
- MetricGauge's arc painter now also compares `track` in shouldRepaint, so
  a background-color-only change still triggers a repaint.
- A2UiTheme.seriesColor asserts a non-empty palette before the modulo index
  that would otherwise throw on one.
- A2UiParser: props/outer-children now merge (props wins on conflict) so a
  model writing children as a sibling of props isn't silently dropped; adds
  a whole-text jsonDecode fast path ahead of the balanced-span scan.
- A2UiRenderer logs the unresolved component name via the app's existing
  debugPrint/kDebugMode convention before falling back to an empty widget.
- a2ui_app_theme now imports A2UiTheme via the public genui barrel instead
  of an internal src path.
- CI: the release workflow's linker-patch step now requires and quotes
  PUB_CACHE, restricts the patch to resolved jni-*/src/CMakeLists.txt
  targets, is idempotent against re-runs, and fails the build instead of
  silently continuing when no target is found or patching fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: close vacuous-test gaps and pin already-fixed regressions

Fixes tests that would pass identically whether the behavior they claim to
verify was correct or broken:
- stat_card_test's pump() helper now actually threads its props argument
  into the rendered node (it previously always rendered empty props).
- new_features_test's assisted-pullups case now uses distinguishable
  weight/assistWeight values, so the test fails if the wrong field is used.

Tightens two guardrail-class tests to actually detect what they claim to:
- a2ui_prompt_test's worked-example extraction is now bounded to the region
  after the "WORKED EXAMPLE:" marker via balanced-brace matching, instead of
  the last '}' anywhere in the whole prompt.
- a2ui_purity_test's forbidden-import regex now also guards lib/data/.
- a2ui_robustness_test's negative-axis assertion now requires minY to
  actually bracket the dataset's true minimum, not just be below -10.
- a2ui_theme_test's panel-decoration finders are scoped to the panel under
  test rather than the first Container anywhere in the tree.

Adds regression coverage pinning fixes already shipped in prior commits:
DynamicChart pie's negative-value filtering, StatCard's unit-suffix match,
and CoachToolService's days-window/insufficient-data/muscle-id fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: address CodeRabbit review findings on PR #64 (feat/genui)

Fixes real findings from PR #64's own review, ahead of merging into
r2.1.0, so the sqflite-migration branch (which currently carries these
genui files unmerged) won't reintroduce them as merge conflicts.

- a2ui_theme: seriesColor() now falls back to accent on an empty
  seriesPalette instead of only asserting (release builds strip
  asserts, so this was still a release-mode divide-by-zero)
- coach_tool_service: removed the synthetic "readiness_score" metric
  from analyze_health_workout_correlation — it was a made-up
  70-100 formula derived from sleep duration, presented as if it were
  an independent measured health signal in statistical output
- coach_tool_service, main.dart: CoachToolService constructor now uses
  named parameters (3+ args); updated every call site
- workout_provider: getRecommendations no longer passes the
  exercise-wide growth model into a handle-scoped recommendation,
  since _growthModels isn't trained per-handle and would mix
  variations (e.g. "Rope pushdown" trend bleeding into "Bar pushdown")
- test_gemini_api.py: post_generate_content_with_retry could fall off
  the end returning None after a quota-fallback on the final attempt,
  despite its dict return type; restructured so every path returns or
  raises
- test coverage: legend-absence assertions for single-series/pie
  charts, NaN/Infinity scatter-point coordinates, stable payload-based
  test names in the robustness suite, hoisted regex in the purity
  test, const constructor, and a corrected self-contradictory comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Devasy Patel <110348311+Devasy23@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test: drop coverage for ApiService/SettingsScreen removed by main merge

Merging main brought in the telemetry removal (ApiService and the
orphaned settings_screen.dart are gone). r2.1.0 had its own test
coverage for both that main never had - api_service_test.dart,
screens/settings_screen_test.dart, and the SettingsScreen-only half
of userflow_settings_and_storage_test.dart all targeted code that no
longer exists, so they're deleted. test_harness.dart drops its
ApiService provider registration, which nothing consumes anymore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Migrate storage from Hive to SQLite + add coach SQL query tool (#66)

* Adds tests for screens

* Adds tests

* Adds comprehensive tests

* Adds new tests

* Updates test.yml to run on release branches

* Adds test and resolved the warnings and issues

* Updates tests and minor bug fixes

* Adds fixes for failing testsm and adds connection timeout safety for health connector

* Adds missing lines patch

* Updates the tests with analyse failures

* Updates tests and routine creator to use the common component

* Updates flutter version and adds tests

* Adds major genui Feature and renderer

* chore: remove patch_so script

* build: add --build-id=none for jni package in F-Droid metadata

* ci: add jni build-id sed step for future reproducible releases

* feat: assisted pullups, deload-aware ML, handle-scoped PRs, sleeping HR tool

Batches several in-flight features that were sitting uncommitted:

- Bodyweight/assisted pullup volume: (BW - assist + extra) * reps
- MLService reads the past 3 sessions and recovers from a deload week
  using the pre-deload baseline instead of the deload trough
- PRManager scopes records per handle variation (Rope vs Bar)
- CoachToolService.get_sleeping_hr_analytics: p5/p25/mean, stdev,
  variance and linear trend over the last N nights
- GenUI parser tolerates numeric StatCard values, loose trend words and
  Markdown code fences

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiProps alias-aware coercing property reader

Foundation for the genui refactor: a never-throwing view over raw
component prop maps that resolves keys by exact match, normalized
match (case/underscore/hyphen/space-insensitive), then semantic
alias, and coerces values to typed accessors with documented
fallbacks instead of throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiSpec contract, A2UiNode and A2UiRegistry

Adds the four-in-one component contract (A2UiSpec) that lets each UI
component name itself, parse its own props, build its own widget and
document itself for the LLM prompt on one object, plus the
A2UiRegistry lookup table that replaces the old allowedA2UiComponents
set and two parallel switch statements. Includes an A2UiTheme skeleton
(filled in by Task 4) and A2UiNode, the parsed-tree node type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): make A2UiRegistry throw on name/alias collisions

Code review found that A2UiRegistry's constructor loop silently
resolved canonical-name/alias collisions (last-writer-wins for names,
first-writer-wins for aliases), which would produce unreachable specs
or dropped aliases with no signal as more components are registered in
later tasks. The constructor now throws a StateError identifying both
colliding specs for any of: two specs sharing a canonical name, an
alias colliding with another spec's canonical name, or two specs
sharing an alias. Adds three regression tests using a new configurable
_NamedFakeSpec fake.

Also documents (doc-comment only, no behavior change) that
A2UiNode.children is not defensively copied, per the review's Minor
finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiParser with fence, envelope and alias repair

Adds the single gate that decides whether an LLM reply is a UI payload
or ordinary prose, and turns UI payloads into an A2UiNode tree. Handles
markdown fences, prose-wrapped JSON, flat vs props-wrapped shapes,
bare-array/envelope auto-wrapping into GridContainer, and recursive
children, without ever throwing.

Also promotes A2UiProps._asStringKeyed to a public static
A2UiProps.stringKeyed so the parser can re-key decoded JSON maps
without an awkward part-of coupling between the two libraries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): balanced-bracket JSON extraction and envelope singleton fix

_extractJson previously sliced from the first { to the last }, which
broke on any stray brace in surrounding prose (e.g. "add reps
{optional}"). Replace with a scan that tries jsonDecode on every
balanced {..}/[..] span found via a depth counter that correctly skips
brackets inside string literals, preferring the longest successful
decode as the actual payload.

Also fix _wrap's unconditional single-child collapse: an explicit
envelope key ({"components":[...]}) is a deliberate container request
and must still produce a GridContainer with one child, while a bare
top-level array with one item keeps collapsing since it's ambiguous
between "a list of one" and "just one component."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): inject A2UiTheme and extract shared panel chrome

Adds A2UiThemeProvider (InheritedWidget, falls back to A2UiTheme.dark)
and the panel/title/empty-state/legend widgets every component spec
will share, plus lib/theme/a2ui_app_theme.dart mapping RepForge's real
design tokens onto A2UiTheme. This is the only file where the two
systems meet - lib/genui/ still imports nothing app-specific.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(genui): strengthen theme-injection and add A2UiPanel coverage

The injection test compared against repforgeA2UiTheme, which is
field-for-field identical to the A2UiThemeProvider.of fallback
(A2UiTheme.dark), so it passed even if the InheritedWidget lookup were
broken. Inject a fixture with distinct values instead, and assert a
sibling context still falls back to the default. Also add direct
coverage for A2UiPanel's padding, decoration, and child rendering,
previously only exercised indirectly via A2UiEmptyPanel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add A2UiSeries as the shared categorical data shape

A2UiSeries.extract() and maxValue() give line/bar/pie and radar chart
components one common {name, values} shape to consume, so a model that
learns {labels, series} once can drive all four components.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): cover fallback path in A2UiSeries.extract, fix negative-max bug

Address code review findings on A2UiSeries:
- Add tests pinning down the series->values fallback when every series
  entry drops to empty/unparseable values, and when series is an empty
  list — the risky path the brief called out but left untested.
- Rename the misleading 'reads the axes alias' test; it only exercised
  stringified-number coercion inside series values, not alias resolution.
- Fix maxValue() to track whether any value has been seen instead of
  seeding with 0.0, so all-negative series report their true max
  instead of silently clamping to 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add StatCardSpec with typed props and trend synonyms

Establishes the pattern for Tasks 7-13: a typed props record, an
A2UiSpec bundling name/aliases/doc/parseProps/buildWidget, and
never-throwing parsing that degrades to documented fallbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add MetricGaugeSpec with safe progress and null value

Fixes the validator/renderer contradiction where a String value was
accepted but cast to num, and the min == max NaN sweep angle bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add DynamicChartSpec for line, bar and pie

Adds the most-used and most complex A2UI component so far, covering
line/bar/pie rendering over the shared {labels, series} shape with
never-throwing prop parsing and label padding to prevent out-of-range
axis lookups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add ScatterPlotSpec with point repair and safe bounds

Adds paired x/y observation plotting with an optional correlation badge,
following the Task 6-8 A2UiSpec pattern. Malformed points are dropped
rather than throwing, and bounds widen degenerate axes so fl_chart never
sees a zero-span range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add RadarChartSpec sharing the labels/series shape

Task 10 of the a2ui/genui refactor: RadarChart consumes the same
{labels, series} shape as DynamicChart, with `axes` kept as a
backward-compatible alias for `labels`. Every series is truncated
or zero-padded to labels.length at parse time so fl_chart's radar
never sees a mismatched entry count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add DataListGroupSpec with row repair and optional title

Adds a titled list-of-rows component with a defensive row-extraction
fallback chain: named fields, bare scalars, first-stringifiable-value
fallback, and silent drop of rows with nothing displayable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add FilterChipsSpec with nullable active option

Renders a decorative, non-interactive row of scope chips (e.g. "7d /
30d / 90d") and fixes the old renderer's `activeOption as String`
crash by matching case-insensitively and falling back to null instead
of throwing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): add GridContainerSpec, default registry and renderer

Task 13: assembles all eight leaf components into defaultA2UiRegistry,
adds the GridContainerSpec layout wrapper, the public A2UiRenderer
widget, and the lib/genui/a2ui.dart barrel file that will be the only
import path the rest of the app uses going forward.

fix(genui): make structural children lookup exact, not alias-resolved

Cross-task fix to a2ui_parser.dart (a Task 3 file), discovered during
Task 13 registry integration. A2UiParser._parseChildren and
_declaresChildren resolved the structural `children` key through
A2UiProps' alias-aware lookup(), which treats `items` as an alias for
`children`. That collided with DataListGroupSpec, whose own canonical
data-row key is also `items`: a DataListGroup node's `items` list of
{primaryText, ...} maps was mistaken for child components, none of
them parsed as one, and the whole node was then discarded as an
emptied-out container. Reading the literal `children` key only fixes
this and matches the precision _envelopeKeys already had (it does not
include `items` as a synonym for `children` either).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(genui): generate the A2UI prompt section from the registry

Replaces hand-written component-schema prose in the coach system
prompt with a section generated from defaultA2UiRegistry, so the
vocabulary advertised to the model can never drift from what the
parser/renderer actually support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(genui): wire coach screen to the A2UI package, drop legacy renderer

Replaces private _CoachMessageContent with a public, stateful
CoachMessageContent that memoizes parsing per text value and shows a
"Building dashboard..." placeholder for partial JSON while streaming,
instead of letting raw braces scroll past or losing prose on a mixed
reply. Wraps the app root in A2UiThemeProvider(theme: repforgeA2UiTheme)
so the renderer picks up RepForge's design tokens. Deletes the
superseded lib/genui/a2ui_component.dart and lib/genui/a2ui_renderer.dart,
and drops test/new_features_test.dart's GenUI Component Resilience Tests
group, whose two cases are already covered more thoroughly by
test/genui/a2ui_parser_test.dart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): bracket negative-value ranges in DynamicChart line/bar axes

minY was hardcoded to 0 while maxY derived from the true series max, so an
all-negative dataset (e.g. [-10, -5, -3]) produced a visible axis range of
[0, 1] with every real data point falling outside it — a silent blank
chart despite valid, non-empty data. Adds A2UiSeries.minValue mirroring
the existing maxValue, and a shared _yBounds helper used by both _line and
_bar so the two renderers can't diverge on axis math. Also covers
multi-series label padding, which was previously only exercised through
series[0].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(genui): cover all-negative bounds and malformed point entries

Task 9 review flagged that ScatterPlotProps.bounds had no regression pin
for all-negative-coordinate spreads (same failure class as Task 8's
DynamicChartSpec axis bug) and that point-parsing had no test for
structurally invalid entries (nested objects, raw lists). Adds both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): widen per-node children lookup back to components/elements/content

Follow-up to the Task 13 a2ui_parser.dart fix: restricting the per-node
_parseChildren/_declaresChildren lookup to the literal 'children' key
was narrower than intended. It regressed 'components'/'elements'/
'content' as per-node child-list keys, which never collided with
anything (only 'items' did, via DataListGroup's own canonical data key).
A payload like {"component":"GridContainer","props":{"columns":1,
"components":[...]}} resolved fine before the original bug and silently
rendered blank (zero children, no null fallback) after the first fix,
since _declaresChildren no longer recognized 'components' as a
children-declaring key either.

Adds a _childKeys constant (children/components/elements/content,
still excluding items) mirroring _envelopeKeys' existing tolerance, and
routes both _parseChildren and _declaresChildren through a shared
_firstChildList literal (non-alias) lookup over that key set.

Adds regression tests in a2ui_renderer_test.dart: per-node
components/elements/content resolve to real children, and items stays
excluded at the per-node level.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): widen looksLikeUi to catch prose-prefixed fences, fix vacuous memoization test

looksLikeUi only checked whether the text, after stripping a *leading*
fence, started with `{`/`[`. A model that writes a sentence before
opening a fenced payload (e.g. "Here is your data:\n```json\n{...")
fell through undetected, so CoachMessageContent showed the raw partial
JSON instead of the streaming placeholder -- the exact symptom this
task exists to fix. Now also treats an unclosed ``` fence found
anywhere in the streamed-so-far text as a UI signal, while plain prose
with no JSON or fence anywhere still returns false.

Also fixes the memoization regression test in
test/screens/ai_coach_genui_test.dart: the second observation was
taken after a bare `tester.pump()`, which doesn't mark the element
dirty and never actually calls build() again, so the test could not
distinguish memoized parsing from a widget that never rebuilds at all.
It now pumps a second CoachMessageContent instance with identical text
at the same tree location, which reuses the existing State and
genuinely triggers didUpdateWidget/build.

Adds regression tests for both the prose-prefixed-fence case and the
plain-prose-no-json case in test/genui/a2ui_parser_test.dart, plus a
widget-level test in test/screens/ai_coach_genui_test.dart confirming
the placeholder (not raw JSON) renders end-to-end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(genui): drop presentation payload from tools, add purity and fuzz suites

The sleeping-HR analytics tool was hand-constructing an A2UI DynamicChart
payload directly, leaking presentation decisions into the data layer.
Replace `genui_chart_props` with neutral `labels`/`series` keys so the
prompt — not the tool — decides how to present the data.

Add two permanent guard suites: a2ui_purity_test.dart proves lib/genui/
never imports app-specific code (theme/models/services/screens) and its
component renderers never cast raw model data; a2ui_robustness_test.dart
fuzzes the parser and renderer against ~26 hostile/malformed LLM payloads
to confirm nothing throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): depth-agnostic purity regex, pin two silent-visual regressions

Review of the previous commit found the purity test's forbidden-import
check was depth-blind: its literal needle list only covered one and two
../ hops, but components live three levels below lib/, so a real
../../../theme/... import passed undetected. Replace it with a regex
that matches any number of ../ hops (or a package:repforge/ prefix),
covering import and export directives alike, and add a self-test that
proves the regex catches every relevant depth/form without touching real
source files.

Also widen the no-raw-casts check to include bool/Object/dynamic, make
the components-directory scan recursive, and pin down the two historical
silent-visual regressions (Task 8's chart axis-bounds clamp, Task 13's
GridContainer child-key aliasing) with positive assertions in the fuzz
suite, since neither throws and the existing no-throw checks structurally
can't catch either.

Reword analyze_health_workout_correlation's tool declaration to drop
direct component names, closing the same presentation-leak class this
task already fixed for the sleeping-HR tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): propagate registry through recursion, pin prompt drift, close review findings

Final whole-branch review fix wave for the A2UI genui refactor:

- A2UiRenderer's registry override used to be silently dropped past one
  level of nesting because GridContainerSpec recurses via bare
  A2UiRenderer(node: ...) calls. Mirror the existing theme-injection
  pattern with a new A2UiRegistryProvider InheritedWidget so an explicit
  registry override at any level propagates ambiently to everything below
  it (explicit param > inherited provider > defaultA2UiRegistry fallback).
- Pin the hand-written "WHICH COMPONENT TO REACH FOR" prose in
  gemini_context_builder.dart against silent drift: every component name
  it mentions must resolve in defaultA2UiRegistry, and the registry's
  spec count is asserted directly.
- Delete A2UiProps.object()/has() — confirmed zero call sites.
- Repurpose the orphaned Task 3 scaffolding test
  (a2ui_parser_stub_test.dart, redundant with a2ui_parser_test.dart) into
  a2ui_custom_registry_test.dart, the regression coverage the registry-
  propagation fix needed.
- Add scanned-file-count floors to the purity test's two directory scans
  so an empty/unreachable directory can't produce a vacuous pass.
- Document FilterChips' SizedBox.shrink() as a deliberate exception to
  the plan's "always A2UiEmptyPanel" rule (decorative chrome, not data).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add design spec for Hive->SQLite migration + coach SQL query tool

* fix: persist assisted-load volume correctly, tighten exercise-handle scoping

- WorkoutSet now snapshots bodyweight/assist/extra at logging time instead
  of recomputing effective load from the CURRENT profile bodyweight on every
  read, which was silently corrupting historical volume whenever a user
  updated their weight. ExerciseLog.totalVolume and the workout_flow_screen
  logging path thread the snapshot through.
- Exercise-handle matching (workout_provider) now requires an exact handle
  match whenever a handle is set, falling back to legacy behavior only when
  no exact match exists — a null-handle log was previously matching ANY
  requested handle, surfacing the wrong variation's "last session" data.
- Handle selector no longer visually pre-selects an unpersisted handle, and
  setExerciseHandle no longer retroactively relabels already-logged sets.
- Assisted-load display values now respect the user's unit preference; the
  assisted-exercise classification is computed once and shared instead of
  drifting between two separate predicates.
- Body-weight input (settings_provider) now rejects non-finite/non-positive
  values on both the load and set paths, falling back to 70.0 when invalid.
- ml_service: deload-recovery reasoning no longer hardcodes "kg" regardless
  of unit settings; recovery detection now requires the comparison session
  to be recent and uses effective (not raw) load for assisted exercises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: bound sleep-analytics window, drop fabricated data, resolve muscle groups by id

- get_sleeping_hr_analytics clamps the model-provided days window instead of
  looping unbounded; get_health_metrics now honors the requested days window
  instead of always querying one week, and both its and the correlation
  tool's declarations no longer advertise fields (resting HR, readiness)
  that aren't actually backed by implementation.
- analyze_health_workout_correlation no longer fabricates synthetic sleep
  data points to pad out insufficient real pairs — returns the existing
  insufficient-data error instead, so correlation/regression/chart output is
  never partly made up.
- get_muscle_group_volume now resolves requested names to ids via
  _resolveMuscleGroup and compares ids (also aggregating secondary muscle
  activations) instead of raw display-name substring matching.
- CoachToolService's optional HealthHistoryManager is now a named parameter.
- gemini_ai_service: daily-quota classification narrowed to actual
  daily-limit identifiers so minute-scale rate limits go through normal
  retry-delay handling instead of being misclassified as daily exhaustion;
  function-call ids are now preserved and matched into their responses;
  the fallback path now builds a thinkingConfig compatible with whichever
  model was actually selected. Mirrored in scripts/test_gemini_api.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(genui): pie negative-value filtering, overflow guard, stat-card unit match

- DynamicChart's pie mode now filters to positive values before computing
  percentages/sections (preserving original index alignment with labels and
  series colors), falling back to an empty panel when nothing positive
  remains, instead of rendering a nonsense chart from negative/zero data.
- A2UiPanelTitle's trailing label is now Flexible with maxLines/ellipsis so
  a long model-provided string can't overflow the row.
- StatCard's unit-already-present check now requires a trailing-suffix
  match instead of any substring, fixing a false positive like unit "s"
  matching inside value "10 reps".
- MetricGauge's arc painter now also compares `track` in shouldRepaint, so
  a background-color-only change still triggers a repaint.
- A2UiTheme.seriesColor asserts a non-empty palette before the modulo index
  that would otherwise throw on one.
- A2UiParser: props/outer-children now merge (props wins on conflict) so a
  model writing children as a sibling of props isn't silently dropped; adds
  a whole-text jsonDecode fast path ahead of the balanced-span scan.
- A2UiRenderer logs the unresolved component name via the app's existing
  debugPrint/kDebugMode convention before falling back to an empty widget.
- a2ui_app_theme now imports A2UiTheme via the public genui barrel instead
  of an internal src path.
- CI: the release workflow's linker-patch step now requires and quotes
  PUB_CACHE, restricts the patch to resolved jni-*/src/CMakeLists.txt
  targets, is idempotent against re-runs, and fails the build instead of
  silently continuing when no target is found or patching fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: close vacuous-test gaps and pin already-fixed regressions

Fixes tests that would pass identically whether the behavior they claim to
verify was correct or broken:
- stat_card_test's pump() helper now actually threads its props argument
  into the rendered node (it previously always rendered empty props).
- new_features_test's assisted-pullups case now uses distinguishable
  weight/assistWeight values, so the test fails if the wrong field is used.

Tightens two guardrail-class tests to actually detect what they claim to:
- a2ui_prompt_test's worked-example extraction is now bounded to the region
  after the "WORKED EXAMPLE:" marker via balanced-brace matching, instead of
  the last '}' anywhere in the whole prompt.
- a2ui_purity_test's forbidden-import regex now also guards lib/data/.
- a2ui_robustness_test's negative-axis assertion now requires minY to
  actually bracket the dataset's true minimum, not just be below -10.
- a2ui_theme_test's panel-decoration finders are scoped to the panel under
  test rather than the first Container anywhere in the tree.

Adds regression coverage pinning fixes already shipped in prior commits:
DynamicChart pie's negative-value filtering, StatCard's unit-suffix match,
and CoachToolService's days-window/insufficient-data/muscle-id fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add implementation plan for Hive->SQLite migration + coach SQL tool

* chore: add sqflite dependencies for SQLite storage migration

* feat: add SqliteStorageService with schema and workout session CRUD

* fix: persist bodyWeightAtLog in SqliteStorageService sets table

* feat: implement routine and target CRUD in SqliteStorageService

* feat: implement muscle group and custom exercise CRUD in SqliteStorageService

* feat: implement settings, PR, training program, and conversation CRUD in SqliteStorageService

* feat: implement export/import in SqliteStorageService, completing IStorageService

* feat: add settings enumeration helper to StorageService for migration

* feat: add StorageMigrationService for one-time Hive-to-SQLite migration

* feat: resolve Hive-vs-SQLite storage backend in main() before runApp

* feat: add SqlQueryService for read-only SQL execution

* feat: wire run_sql_query tool into CoachToolService

* fix: fall back to fresh StorageService when app is constructed without going through main()

* fix: block run_sql_query from reading settings/sqlite_master (credential exposure)

SELECT * FROM settings or sqlite_master passed all existing run_sql_query
validation and would leak the migrated Gemini API key into model context
and persisted chat history. Add a second denylist of restricted table/
schema identifiers, checked the same way as the existing forbidden-keyword
list, plus a substring guard against SQLite's pragma_* table-valued
functions.

* fix: prevent trailing SQL comment from breaking LIMIT wrapper

A model-submitted query ending in a `--` line comment swallowed the
wrapper's closing paren when concatenated onto one line, producing an
avoidable syntax error. Put the closing `) LIMIT ?` on its own line.

Also finishes staging test/sql_query_service_test.dart, which now covers
both this fix (trailing-comment query succeeds) and the settings/
sqlite_master restricted-table rejections from the previous commit.

* docs: warn model against SELECT * across joins in run_sql_query

sqflite's row maps are keyed by column name, so a natural join query like
"SELECT * FROM sessions s JOIN exercise_logs l ON ..." silently drops
duplicate columns (e.g. id, notes) from one side with no error. Steer the
model's generated SQL toward explicit aliased columns instead.

* refactor: extract testable storage backend resolution logic; guard sqliteStorage.init()

- lib/main.dart: sqliteStorage.init() was outside the try/catch on the
  path every existing user hits on first launch after this update —
  disk-space/sandbox/SQLite-build failures propagated out of main()
  before runApp(), so the app never booted even though the working Hive
  storage right above it was fine. Now guarded with its own fallback to
  Hive. Also documents why Hive.initFlutter() stays unconditional post-
  cutover: ApiService reads/writes an installation id directly against
  this settings box, independent of IStorageService.
- lib/services/storage_backend_resolver.dart (new): extracts the
  Hive-vs-SQLite decision (migrate-or-fallback, flag write) out of
  main.dart's untestable _resolveStorageBackend into a pure, directly
  testable top-level function.
- test/storage_backend_resolver_test.dart (new): covers the two
  real-world paths every user takes — already-migrated relaunch, and
  fresh-install migration success. The forced-migration-failure case is
  intentionally omitted; there's no way to make
  StorageMigrationService.migrate() throw with SqliteStorageService's
  current public API without adding production surface purely for
  testability, and that path is exercised indirectly by
  storage_migration_service_test.dart.

* docs: add design spec for syncing sleep/HR data into SQLite for coach SQL joins

Lets run_sql_query join workout data against sleep/HR history instead of
requiring separate live Health Connect tool calls per question.

* docs: add implementation plan for syncing sleep/HR data into SQLite

Five-task TDD plan: schema + upsert methods, HealthDataSyncService,
launch-time wiring, manual sync button, and the coach's schema description.

* feat: add health_samples/sleep_sessions tables + upsert methods to SqliteStorageService

- Add schema v2 with three new tables: health_samples, sleep_sessions, sleep_stage_intervals
- Add upsertHealthSamples() and upsertSleepSessions() methods for health data sync
- Add onUpgrade callback for v1->v2 schema migration
- Use temporary files for in-memory test databases to support read-only connections
- All tests passing (35/35)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: prevent run_sql_query from closing the app's shared database connection

openReadOnlyDatabase(path) with the default singleInstance:true returns the
app's existing shared connection when called against the same path as
SqliteStorageService's live database, so the coach's per-query
finally { db.close() } was tearing down the app's only connection after
the first query. Pass singleInstance:false to force a genuinely separate
connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address Task 1 review findings

- Remove _isTestDatabase path-substring flag; production init() no longer
  branches on test-fixture path content
- Remove the unconditional health-schema fallback loop that made onUpgrade
  untested/redundant; onCreate and onUpgrade are now the only paths that
  create the health tables
- Revert IF NOT EXISTS back to plain CREATE TABLE/CREATE INDEX, matching
  the existing schema statement convention
- Use a const list spread (..._healthSchemaStatements) instead of a
  duplicated inline copy in _schemaStatements
- :memory: overrides still resolve to temp files (needed for read-only
  secondary connections in tests), but now via an explicit Finalizer-based
  cleanup keyed on the constructor's _databasePathOverride parameter
  rather than sniffing the resulting path string

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: replace Finalizer with deterministic tearDown cleanup

- Remove Finalizer mechanism and unused imports (dart:async)
- Remove _tempDatabasePath and _generatedTempPath fields
- Simplify init() to convert :memory: to temp files without tracking
- Add deterministic tearDown() in test to close database and delete temp files
- Verified: no temp file leaks, all 35 tests passing

Closes: finding #5 from previous review

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: add HealthDataSyncService to pull sleep/HR data into SQLite

* feat: sync health data into SQLite once per app launch

Wires HealthDataSyncService into the composition root, guarded to
only exist post-SQLite-cutover (mirrors the CoachToolService sqlQuery
guard). Fired fire-and-forget from AppInitializer._initializeApp()
alongside readiness.refresh() so it never blocks app startup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: add manual 'Sync coach data now' action to Profile screen

Lets the user force a Health Connect -> coach SQLite sync on demand
from the Health Connect section, instead of waiting for the next
app launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat: teach run_sql_query about the new health_samples/sleep_sessions tables

Extends the schema description in CoachToolService's run_sql_query
declaration with health_samples, sleep_sessions, and
sleep_stage_intervals so the coach LLM knows these tables exist and
can join against them. Adds a test asserting the description text
mentions the new tables (nothing else would catch a typo/omission
there), plus a regression test for the join shape the coach will run.

* fix: remove overly broad auto-close from init, add explicit close to upgrade test

- Remove auto-close block from init() that was closing database for any
  explicit file path, breaking coach_tool_service_test and other callers
- Add explicit await upgraded.close() in upgrade test before file deletion
- Regression: coach_tool_service_test now passes again
- All related tests verified: sqlite_storage_service (35), coach_tool_service (11),
  health_data_sync_service (6), sql_query_service (10)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address final review findings for health sync + coach SQL tool

- Skip syncing a health stream entirely when its HealthReadType isn't
  granted, and leave its watermark untouched — prevents watermarks
  from silently advancing to `now` on first launch before the user
  has opted into Health Connect, which was breaking the 90-day
  backfill for essentially every user.
- Store health_samples/sleep_sessions timestamps as local time
  (.toLocal() before .toIso8601String()) to match the local-naive
  convention used by `sessions.date`, fixing day-bucketing joins for
  non-UTC users.
- Wrap the already-migrated SQLite init() branch in main.dart with a
  Hive fallback, mirroring the fresh-migration branch, so a partial
  upgrade failure can't crash app startup.
- Add IF NOT EXISTS to the health-schema DDL so a retried onUpgrade
  after a partial failure doesn't blow up on already-created tables.
- Add missing tearDown to health_data_sync_service_test.dart to stop
  leaking temp db files, guard a profile_screen snackbar with mounted
  for consistency, and reset _initialized on close() so a
  close()+init() cycle actually reopens the connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address migration/SQL-tool review findings from PR #66

Fixes CodeRabbit findings scoped to the hive->sqflite migration and
coach SQL tool work on this branch (genui and docs findings deferred
to their own branches):

- gemini_ai_service: rebuild generationConfig.thinkingConfig after a
  daily-quota model fallback, so the retried request matches whichever
  model it's about to hit instead of the previous model's shape
- health_data_sync_service: named constructor/_syncSamples params;
  guard grantedReadTypes() so a Health Connect failure doesn't abort
  the whole sync instead of degrading per-stream
- ml_service: recommendSets now falls back to the first non-empty
  pastSessions entry when lastSession is empty, instead of returning
  no recommendations
- sqlite_storage_service: guard close() against a never-initialized
  db; filter getCustomExercises() by is_custom; order exercise_logs/
  sets by rowid instead of the synthetic text id, which sorted "_10"
  before "_2" and silently misordered sets/exercises past 9 per group
- workout_provider: removeLastSet preserves the exercise log's handle;
  handle-fallback lookups only match legacy handle-less logs instead
  of any handle
- test_gemini_api.py: clamp the parsed retry delay to match the Dart
  implementation's bounds
- add coverage: 11+ set/exercise ordering, migration-failure fallback
  path, training-program/growth-rate migration, and the id/type-only
  storage-service call sites

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: address second round of CodeRabbit findings on PR #66

Fixes real findings from the fresh review CodeRabbit ran after…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant