diff --git a/.gitignore b/.gitignore index 7467f62..05456d8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ Thumbs.db .agents/ .impeccable/ artifacts/ +# Browser automation session snapshots and console logs +.playwright-cli/ # Local CI artifact downloads and rendered previews output/ PROJECT.md diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebdc75d..53a8add 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,9 +37,9 @@ android { minSdk = 26 targetSdk = 36 versionCode = providers.gradleProperty("IRONLOG_VERSION_CODE").orNull?.toIntOrNull() - ?: 11 + ?: 12 versionName = providers.gradleProperty("IRONLOG_VERSION_NAME").orNull - ?: "0.1.0-pre-alpha.10" + ?: "0.1.0-pre-alpha.11" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/ironlog/app/data/repository/ExerciseRepository.kt b/app/src/main/java/com/ironlog/app/data/repository/ExerciseRepository.kt index a0920d5..4b42faa 100644 --- a/app/src/main/java/com/ironlog/app/data/repository/ExerciseRepository.kt +++ b/app/src/main/java/com/ironlog/app/data/repository/ExerciseRepository.kt @@ -49,14 +49,14 @@ class ExerciseRepository( observeQuery { exercisesBox.query().build() }.map { rows -> filterRows(rows, filters) } suspend fun getExercisesSnapshot(filters: ExerciseFilters = ExerciseFilters()): List = - withContext(Dispatchers.IO) { filterRows(exercisesBox.all, filters).map(::mapExerciseRowToLegacyShape) } + withContext(Dispatchers.IO) { catalogRows(filters) } suspend fun getCompactCatalogMarkdown(): String = withContext(Dispatchers.IO) { val canonical = context?.let { catalogMarkdownFile(it) } if (canonical != null && canonical.exists()) { return@withContext canonical.readText() } - val generated = buildCompactCatalogMarkdown(filterRows(exercisesBox.all, ExerciseFilters()).map(::mapExerciseRowToLegacyShape)) + val generated = buildCompactCatalogMarkdown(catalogRows()) canonical?.let { it.parentFile?.mkdirs() it.writeText(generated) @@ -80,8 +80,6 @@ class ExerciseRepository( requireNonEmpty(input.equipment, "equipment") requireNonEmpty(input.category, "category") val normalized = normalizeExerciseNameKey(name) - val duplicate = exercisesBox.query(ExerciseEntity_.normalizedName.equal(normalized)).build().use { it.find() } - if (duplicate.isNotEmpty()) error("Exercise with this name already exists.") // Validate every child before the parent is written so a bad contribution cannot leave // behind a partially-created exercise. @@ -111,6 +109,7 @@ class ExerciseRepository( secondaryMusclesJson = input.secondaryMuscles?.let { org.json.JSONArray(it).toString() } } store.runInTx { + requireUniqueName(normalized) exercisesBox.put(created) val muscleCreates = input.muscles.orEmpty().map { m -> ExerciseMuscleEntity().apply { @@ -131,22 +130,26 @@ class ExerciseRepository( suspend fun updateExercise(id: String, input: UpdateExerciseInput): ExerciseEntity = withContext(Dispatchers.IO) { requireNonEmpty(id, "id") - val exercise = findExerciseByUidOrThrow(id) - val now = System.currentTimeMillis() - if (!input.name.isNullOrEmpty()) { - val updatedName = normalizeExerciseName(input.name) - exercise.name = updatedName - // Use normalizeExerciseName first to get a non-null String, then call the - // String overload of normalizeExerciseNameKey (underscore format) — consistent - // with createCustomExercise which also stores in underscore format. - exercise.normalizedName = normalizeExerciseNameKey(updatedName) + val updatedName = input.name?.let { normalizeExerciseName(it).also { name -> requireNonEmpty(name, "name") } } + input.primaryMuscle?.let { requireNonEmpty(it.trim(), "primary_muscle") } + input.equipment?.let { requireNonEmpty(it.trim(), "equipment") } + input.category?.let { requireNonEmpty(it.trim(), "category") } + lateinit var exercise: ExerciseEntity + store.runInTx { + exercise = findExerciseByUidOrThrow(id) + if (updatedName != null) { + val normalized = normalizeExerciseNameKey(updatedName) + requireUniqueName(normalized, exercise.objectBoxId) + exercise.name = updatedName + exercise.normalizedName = normalized + } + input.primaryMuscle?.let { exercise.primaryMuscle = it.trim() } + input.equipment?.let { exercise.equipment = it.trim() } + input.category?.let { exercise.category = it.trim() } + input.notes?.let { exercise.notes = it } + exercise.updatedAt = System.currentTimeMillis() + exercisesBox.put(exercise) } - if (!input.primaryMuscle.isNullOrEmpty()) exercise.primaryMuscle = input.primaryMuscle - if (!input.equipment.isNullOrEmpty()) exercise.equipment = input.equipment - if (!input.category.isNullOrEmpty()) exercise.category = input.category - if (input.notes != null) exercise.notes = input.notes.ifEmpty { "" } - exercise.updatedAt = now - exercisesBox.put(exercise) syncCompactCatalogMarkdown() exercise } @@ -184,7 +187,7 @@ class ExerciseRepository( suspend fun buildExerciseAlternatives(exerciseId: String, filters: ExerciseFilters = ExerciseFilters()): SubstitutionEngine.AlternativesResult = withContext(Dispatchers.IO) { requireNonEmpty(exerciseId, "exerciseId") - val exercises = exercisesBox.all.map(::mapExerciseRowToLegacyShape) + val exercises = catalogRows() val target = exercises.find { it.id == exerciseId || it.exerciseId == exerciseId } ?: return@withContext SubstitutionEngine.AlternativesResult.EMPTY SubstitutionEngine.buildExerciseAlternatives(target, exercises, filters, filters.limit ?: 12) @@ -197,8 +200,33 @@ class ExerciseRepository( internal fun findExerciseByName(name: String): ExerciseEntity? = exercisesBox.query(ExerciseEntity_.name.equal(name)).build().use { it.findFirst() } - internal fun mapExerciseRowToLegacyShape(row: ExerciseEntity): LegacyExerciseShape { - val primary = if (row.primaryMuscle.isNotBlank()) listOf(row.primaryMuscle) else emptyList() + /** Called inside the owning write transaction; duplicate legacy rows are not rewritten. */ + private fun requireUniqueName(normalized: String, exceptId: Long = 0) { + val duplicate = exercisesBox.query(ExerciseEntity_.normalizedName.equal(normalized)).build().use { query -> + query.find().any { it.objectBoxId != exceptId } + } + if (duplicate) error("Exercise with this name already exists.") + } + + /** Batch-load relationships once, avoiding a lazy relation query per catalog entry. */ + private fun catalogRows(filters: ExerciseFilters = ExerciseFilters()): List = store.callInReadTx { + mapExerciseRows(filterRows(exercisesBox.all, filters)) + } + + private fun mapExerciseRows(rows: List): List { + val primaryByUid = musclesBox.all.filter { it.role.equals("primary", true) }.groupBy { it.exerciseUid } + return rows.map { row -> mapExerciseRowToLegacyShape(row, primaryByUid[row.uid].orEmpty().map { it.muscle }) } + } + + private fun metadataStrings(json: String?): List = json?.let { + runCatching { + val array = org.json.JSONArray(it) + (0 until array.length()).mapNotNull { index -> (array.opt(index) as? String)?.trim()?.takeIf(String::isNotBlank) }.distinct() + }.getOrElse { emptyList() } + }.orEmpty() + + internal fun mapExerciseRowToLegacyShape(row: ExerciseEntity, primaryRelationships: List = emptyList()): LegacyExerciseShape { + val primary = (listOf(row.primaryMuscle) + primaryRelationships).map(String::trim).filter(String::isNotBlank).distinct() val category = row.category.ifBlank { "strength" } val trackingType = ExerciseTrackingTypeNormalizer.normalize( name = row.name, @@ -212,23 +240,18 @@ class ExerciseRepository( name = row.name, primaryMuscles = primary, primaryMuscle = row.primaryMuscle.ifBlank { null }, - secondaryMuscles = row.secondaryMusclesJson?.let { json -> - runCatching { - val arr = org.json.JSONArray(json) - (0 until arr.length()).map { arr.getString(it) } - }.getOrElse { emptyList() } - } ?: emptyList(), + secondaryMuscles = metadataStrings(row.secondaryMusclesJson), equipment = row.equipment.ifBlank { "Other" }, category = category, trackingType = trackingType, isCustom = row.isCustom, - aliases = emptyList(), + aliases = metadataStrings(row.aliasesJson), isBodyweight = row.isBodyweight || row.equipment.lowercase() == "bodyweight", movementPattern = row.movementPattern, difficulty = row.difficulty, apparatus = row.apparatusJson, equipmentDetail = row.equipmentDetail, - sourceTags = emptyList(), + sourceTags = metadataStrings(row.sourceTagsJson), notes = row.notes, ) } @@ -244,7 +267,7 @@ class ExerciseRepository( private fun syncCompactCatalogMarkdown() { val ctx = context ?: return val file = catalogMarkdownFile(ctx) - val rows = filterRows(exercisesBox.all, ExerciseFilters()).map(::mapExerciseRowToLegacyShape) + val rows = catalogRows() file.parentFile?.mkdirs() file.writeText(buildCompactCatalogMarkdown(rows)) } diff --git a/app/src/test/java/com/ironlog/app/data/repository/ExerciseRepositoryIntegrityTest.kt b/app/src/test/java/com/ironlog/app/data/repository/ExerciseRepositoryIntegrityTest.kt new file mode 100644 index 0000000..fade5c4 --- /dev/null +++ b/app/src/test/java/com/ironlog/app/data/repository/ExerciseRepositoryIntegrityTest.kt @@ -0,0 +1,71 @@ +package com.ironlog.app.data.repository + +import com.ironlog.app.data.model.CreateExerciseInput +import com.ironlog.app.data.model.UpdateExerciseInput +import com.ironlog.app.data.objectbox.ExerciseEntity +import com.ironlog.app.data.objectbox.ExerciseMuscleEntity +import com.ironlog.app.data.objectbox.MyObjectBox +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import org.junit.Assert.* +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class ExerciseRepositoryIntegrityTest { + @get:Rule val temporary = TemporaryFolder() + private fun input(name: String) = CreateExerciseInput(name = name, primaryMuscle = "Chest", equipment = "Cable", category = "strength") + + @Test fun `concurrent normalized creates persist exactly one identity`() = runBlocking { + val store = MyObjectBox.builder().directory(temporary.newFolder()).build() + try { + val repo = ExerciseRepository(boxStore = store) + val results = coroutineScope { + (0 until 32).map { index -> async { runCatching { repo.createCustomExercise(input(if (index % 2 == 0) " Cable Press " else "Cable Press")) } } }.awaitAll() + } + assertEquals(1, results.count { it.isSuccess }) + assertEquals(1L, store.boxFor(ExerciseEntity::class.java).count()) + } finally { store.close() } + } + + @Test fun `blank and colliding renames fail without modifying stored row`() = runBlocking { + val store = MyObjectBox.builder().directory(temporary.newFolder()).build() + try { + val repo = ExerciseRepository(boxStore = store) + val first = repo.createCustomExercise(input("Cable Press")) + repo.createCustomExercise(input("Cable Fly")) + for (name in listOf("", " ", " cable fly ")) { + assertTrue(runCatching { repo.updateExercise(first.uid, UpdateExerciseInput(name = name, notes = "should not save")) }.isFailure) + val unchanged = repo.getExerciseById(first.uid) + assertEquals("Cable Press", unchanged.name) + assertEquals("", unchanged.notes) + assertEquals(first.updatedAt, unchanged.updatedAt) + } + repo.updateExercise(first.uid, UpdateExerciseInput(name = " CABLE PRESS ", notes = "valid edit")) + assertEquals("valid edit", repo.getExerciseById(first.uid).notes) + } finally { store.close() } + } + + @Test fun `catalog preserves aliases source tags and all primary muscle relationships`() = runBlocking { + val store = MyObjectBox.builder().directory(temporary.newFolder()).build() + try { + val row = ExerciseEntity().apply { + uid = "multi"; name = "Compound Press"; primaryMuscle = "Chest"; equipment = "Cable" + aliasesJson = "[\"Cable Compound\",\" Alternate press \"]"; sourceTagsJson = "[\"seed\",\"compound\"]" + secondaryMusclesJson = "[\"Triceps\"]" + } + store.boxFor(ExerciseEntity::class.java).put(row) + store.boxFor(ExerciseMuscleEntity::class.java).put(listOf("Chest", "Shoulders").map { name -> ExerciseMuscleEntity().apply { exercise.target = row; exerciseUid = row.uid; muscle = name; role = "primary"; contributionFraction = 0.5 } }) + val mapped = ExerciseRepository(boxStore = store).getExercisesSnapshot().single() + assertEquals(listOf("Chest", "Shoulders"), mapped.primaryMuscles) + assertEquals(listOf("Cable Compound", "Alternate press"), mapped.aliases) + assertEquals(listOf("seed", "compound"), mapped.sourceTags) + assertEquals(listOf("Triceps"), mapped.secondaryMuscles) + val exported = ImportExportRepository(store, temporary.newFolder()).exportDatabase().getJSONObject("data") + assertEquals(2, exported.getJSONArray("exercise_muscles").length()) + assertEquals(2, org.json.JSONArray(exported.getJSONArray("exercises").getJSONObject(0).getString("aliases_json")).length()) + } finally { store.close() } + } +} diff --git a/docs/reviews/2026-10-08-cloud-transport-evidence.md b/docs/reviews/2026-10-08-cloud-transport-evidence.md new file mode 100644 index 0000000..444620f --- /dev/null +++ b/docs/reviews/2026-10-08-cloud-transport-evidence.md @@ -0,0 +1,14 @@ +# Browser cloud transport evidence — 8 October 2026 + +Official sources checked during whole-plan implementation: +- OpenAI API overview: https://developers.openai.com/api/reference/overview +- OpenAI TypeScript SDK browser support: https://github.com/openai/openai-node#requirements +- Google API key security: https://ai.google.dev/gemini-api/docs/api-key +- Anthropic TypeScript SDK browser support: https://github.com/anthropics/anthropic-sdk-typescript#requirements +- OpenRouter REST fetch example: https://openrouter.ai/docs/quickstart + +OpenAI and Anthropic SDKs document opt-in browser support while warning about credential exposure. Google advises server-side protection of production keys. This app must never distribute a project/provider credential in its bundle. A user's optional own credential can be entered explicitly for a session and used only for an action they request; the UI must disclose that browser scripts can access it. Configuration can be portable, credentials cannot. No arbitrary-URL credential proxy is authorized or required. + +Provider presets are configuration, not a guarantee that an endpoint accepts the deployed browser origin. Transport failures (including CORS) must preserve manual prompt/paste, offer retry or configuration changes, and never claim generation succeeded. Custom endpoints need HTTPS (loopback HTTP only for local development), no URL credentials/query keys, no redirects carrying auth, bounded responses, timeout/cancellation and obsolete-result suppression. Requests must use headers for keys, avoid logs/raw provider error echo, and exclude photos/unrelated historical data. Built-in mode makes zero requests. + +Use native protocols/presets as the parity reference; let users edit current model IDs. The above verification does not prove every region, provider plan, model or live paid account works. Tests use synthetic responses; no owner key or paid request is used in QA. diff --git a/docs/reviews/2026-10-08-native-exercise-integrity.md b/docs/reviews/2026-10-08-native-exercise-integrity.md new file mode 100644 index 0000000..a93ce18 --- /dev/null +++ b/docs/reviews/2026-10-08-native-exercise-integrity.md @@ -0,0 +1,12 @@ +# Native exercise integrity — 8 October 2026 + +Confirmed against real temporary ObjectBox stores, not source-string assertions: +1. 32 concurrent creates for whitespace/case-normalized identity could persist duplicates. +2. Blank and colliding renames were accepted, allowing invalid catalog identity. +3. Catalog mapping discarded aliases/source tags and reduced multiple primary relationships to the scalar first muscle. + +All three regression tests failed before implementation. After the change all three pass, including persisted-row rollback, exact-one create and export metadata checks. The duplicate check and parent/child write share one write transaction. Rename validates before mutation, re-reads in the transaction and excludes only the edited identity. Catalog reads batch primary relationships in one read transaction; historical workout snapshots and ObjectBox schema IDs are untouched. + +Focused final command on JDK21: :app:testDebugUnitTest --tests com.ironlog.app.data.repository.ExerciseRepositoryIntegrityTest --no-daemon; BUILD SUCCESSFUL in34s, 3 tests /0 failures /0 errors. Full gates and signed release follow integration. + +ObjectBox diagnostics: the initial red and first green run emitted cached read-cursor destruction warnings during JVM teardown. Final focused run after catalog read transaction emitted none; this is a narrow observation, not a claim that every repository/whole-suite teardown is fixed. Official lifecycle documentation identifies cached per-thread readers and closeThreadResources: https://objectbox.io/docfiles/java/current/io/objectbox/BoxStore.html . Retain the full-suite diagnostics investigation until the final gate. diff --git a/docs/reviews/2026-10-08-push-navigation-parity.md b/docs/reviews/2026-10-08-push-navigation-parity.md new file mode 100644 index 0000000..43208aa --- /dev/null +++ b/docs/reviews/2026-10-08-push-navigation-parity.md @@ -0,0 +1,56 @@ +# Push navigation parity — 2026-10-08 + +## Native ownership evidence + +Source: `app/src/main/java/com/ironlog/app/navigation/AppNavigator.kt` in this worktree. `NavHost` registers `Tabs` at line 232. The five-page `Tabs` implementation starts at line 819. Its floating active-workout pill is rendered at lines 989–1007 and its tab bar at lines 1009 onward. Consequently neither control belongs to root destinations pushed above `Tabs`. Root screens use `popBackStack()` rather than recreating a tab destination. The web now follows that ownership for its dedicated detail routes. + +| Web route | Native counterpart / evidence | Shell owner | Direct-entry Back fallback | +| --- | --- | --- | --- | +| `home` | Tabs/Home | Tabs | — | +| `plans` | Tabs/Plans | Tabs | — | +| `log` | Tabs/Log | Tabs | — | +| `stats` | Tabs/Stats | Tabs | — | +| `settings` | Tabs/Settings | Tabs | — | +| `workout` | ActiveWorkout variants, lines 243–316 | Root | Home | +| `plan/:id` | PlanEditor, lines 317–334 | Root | Plans | +| `library` | ExerciseLibrary, lines 335–345 | Root; feature owns header | Settings | +| `exercise/:identity` | ExerciseProgress, lines 359–364; opened from Stats and Library | Root | Stats | +| `body` | BodyWeight, lines 365–395 | Root | Stats | +| `calendar` | WorkoutCalendar, lines 396–404; opened from Stats | Root | Stats | +| `history/new[/:date]` | HistoricalWorkout, lines 405–413 | Root | Log | +| `measurements` | BodyMeasurements, lines 414–435 | Root | Stats | +| `analytics` | VolumeAnalytics, lines 436–441 | Root | Stats | +| `recovery` | RecoveryMap, lines 442–447 | Root | Home | +| `intelligence` | TrainingIntelligence, lines 448–460 | Root | Home | +| `photos` | ProgressPhotos, line 476; opened from Log and Stats | Root | Stats | +| `ledger` | statusWindow, lines 590 onward; opened from Home and Stats | Root | Home | +| `history/:id` | Native HistoryCard expands inside HistoryScreen; web has a dedicated detail page | Web root adaptation | Log | +| `research` | Web-specific research page | Web root adaptation | Home | + +Actual recorded origins override these direct-entry fallbacks. For example, Library → Exercise Progress returns to Library; a photo route opened from Log returns to Log. Fallback routes do not claim that native itself has browser deep-link parents. + +## Shell and history behavior + +`App.tsx` now renders the bottom navigation and resume pill only on the five tab routes. The pill floats above the bottom bar. Tab content with an active session reserves space for both controls. Root content reserves ordinary bottom spacing plus its safe-area inset, without a hidden-tab gap. Library's own header handles the top inset once. + +The shell Back action, local plan Back action, and browser Back/Forward share the plan editor's discard prompt. Keeping edits retains the mounted draft. Explicit discard replays the requested destination. Successful save, confirmed deletion and duplication release the guard before their existing exit. Save failures retain the guard and draft; optimistic plan revision checking remains in `savePlanIfUnchanged`. + +Browser entries carry an index, epoch and scroll position. Indexed cancellation returns to the original editor entry; confirmed navigation traverses the existing chain. Return navigation restores the source document scroll position. Tab-to-tab motion still follows tab ordering; root pushes and returns now report their own forward/back direction rather than inheriting the last tab swipe direction. + +Unknown legacy entries cannot reveal traversal distance or direction. At that boundary the router retains the requested destination and pushes a replacement editor entry in a new epoch. This keeps the unsaved draft editable and makes explicit discard reach the destination. **The unknown forward tail is replaced at this exceptional boundary.** It is not possible to promise exact preservation of an arbitrary preexisting unindexed chain. Indexed navigation retains its chain. A restoration that cannot deliver its expected event falls back after 300 ms; it never silently discards the draft. Completion of restoration requires the matching index, epoch and editor route. + +## Verification performed + +- TDD route matrix: 15 dedicated detail routes hide tab controls and resume pill; five tab routes retain both; persisted active workout survives detail navigation. +- TDD regressions reproduced shell Back bypass, incorrect Calendar/Exercise Progress deep-link parents, missing source scroll recording, missing push/return direction, unindexed predecessor Back and a stalled restoration receiving an unrelated matching-index event. +- Focused Vitest run: **45 tests pass** across push-navigation, navigation-parity, existing plan-editor parity and existing motion parity suites. +- TypeScript check and production build pass. Build still reports existing font-resolution and large-chunk warnings. +- Real Chrome and WebKit CLI workflows on synthetic data at 390 × 844: browser Back cancellation/discard, Forward cancellation/discard, refreshed plan guard, saved-plan reload, successful save exit, active-workout resume, pill/bar geometry, detail footer and source-scroll restoration to 200 px pass. +- Chrome and WebKit legacy-boundary CLI workflows: pre-subscription unindexed Back cancellation/discard, forward reopen, and unindexed future Forward discard pass. +- Wider eight-suite run: 66 tests passed and the first journeys unit test timed out under parallel load; the journeys suite passed all nine tests when run alone. No source changes were made to mask that timeout. + +Ignored reproducible browser helpers and screenshots are under `web/output/playwright/push-navigation-*`. Browser QA uses production assets and synthetic training records. WebKit reports its existing unsupported `interactive-widget` viewport-key warning; no app exception was observed in these checks. + +## Limits + +This slice did not run native builds, a native emulator, physical-device QA or installed iOS/PWA navigation. Native ownership above is source evidence; native runtime evidence is coordinated separately. Native HistoryCard expands inline while the web retains its preexisting dedicated history detail route. Settings destinations and modal editors that remain inside a tab are unchanged. Scroll position is restored; retained list search/filter state is outside this shell slice. diff --git a/docs/reviews/2026-10-08-remaining-parity-execution.md b/docs/reviews/2026-10-08-remaining-parity-execution.md new file mode 100644 index 0000000..42c6c62 --- /dev/null +++ b/docs/reviews/2026-10-08-remaining-parity-execution.md @@ -0,0 +1,36 @@ +# Remaining parity execution ledger — 8 October 2026 + +Baseline: c5ace518, signed .10/code11. Whole-plan authorization includes implementation, validation, GitHub and deployment. Native source in this repository is authoritative; unrelated UnifiedPort work stays preserved. + +| Slice | Status | Acceptance | +|---|---|---| +| Root navigation | Implemented and verified | Route ownership, detail chrome, unsaved editor interception, browser Back/Forward and scroll restoration; 320px/390px/desktop runtime | +| Native exercise integrity | Implemented; focused tests and both reviews passed | Transactional duplicate/create/rename, metadata fidelity, real ObjectBox regressions | +| Home and interaction motion | Implemented and verified | Measured density/geometry, whole gradient cycle, native plan/day/active card controls; drag/swipe/sheets evidence | +| Plan controls and intelligence | Implemented and verified | Editor pointer reorder, deload action, today directive, progression rationale | +| History editing and sharing | Implemented and verified | Saved date/time/duration/rating, bulk delete/share, chronology and mutation safeguards | +| AI review and optional cloud | Implemented and verified with synthetic responses | Active gym context, unresolved review lock/rematch, provider/model settings, explicit mode, requests/cancel/retry, summaries/debrief; no live credential certification | +| Analytics | Implemented and verified | ISO week/program ranges, display-unit chart values, radar/prior overlay, muscle drill, share | +| Portability | Implemented and verified | Complete calibration/provenance/fixed baseline clock, native legacy formats, Strong/Hevy/OpenWeight reviewed import; canonical precedence; credential redaction | +| Existing body/photos/onboarding | Verified | Reload/retry, goals, date comparison, notes, selected delete, byte export/share fallback and atomic onboarding completion | +| Release/workspace | Signed update verified; final browser/publication gates in progress | 766 native tests passed, one manual benchmark skipped/lint0errors; 342 web tests; signed .11/code12 retained-data emulator update; full browser/offline/CI/deployment completion follows | + +Physical Safari/Home Screen and OEM device certification cannot be replaced by emulation. Record those environmental limits separately from implemented browser-capable features. Android-only integrations remain excluded. No all-parity claim from source inspection alone. + +## Resumed implementation + +The owner revoked the pause and requested completion of the entire remaining checklist. All implementation slices above are now reconciled against source and runtime evidence. See [remaining parity validation](2026-10-11-remaining-parity-validation.md), the final Home/cloud/portability runtime reports, and [workspace inventory](2026-10-11-workspace-inventory.md). The native source remains authoritative; unrelated work is preserved. Provider key casing, explicit built-in/cloud gating, cloud activation and per-new-proof goal-mode badges were closed during final quality review. Code quality review approved all final fixes. + +## Historical explicit pause checkpoint + +Paused at the user's request on 8 October 2026. Branch: feat/remaining-parity-closure. Nothing from this batch has been pushed, released or deployed; public web and signed .10 APK remain the prior published version. + +Completed locally: full-screen detail shell ownership, active pill/footer spacing, plan dirty-navigation guards including browser/hash boundary recovery, scroll metadata/return restoration, and transactional native exercise identity/metadata fixes. Navigation has 45 focused tests, TypeScript/production build, separate spec/quality reviews and Chrome/WebKit 390px workflows. Native has 3 real-store regressions (all red before, all green after), separate spec/quality reviews and unchanged schema IDs. Full integrated web/native gates remain pending. + +Known navigation adaptation: unknown legacy/unindexed history boundaries preserve the draft/destination but replace the unknown forward tail. 320px and desktop runtime acceptance still needs completion after resumption. A wider parallel unit run had one journeys timeout; all9 journeys passed alone. Final gates should run serially to distinguish resource pressure. + +Motion evidence captured before changing code: signed .10/code11 emulator, 1080x2400, density420 (2.625), system font scale1, Athlete, Full Body Beginner3x/week, active FULL BODY A synthetic session. Ignored evidence output/android/parity-native-cycle.mp4 and parity-native-phase-01..12.png captures the full12-second cycle; parity-native-home.png and parity-native-resume.png show controls. SwiftShader emulator attempts exited; host graphics remained stable with no crash-buffer entries. Emulator stopped safely; its synthetic data was retained. Home controls and gradient density changes are NOT implemented yet. + +Audit confirmed additional real gaps (all pending above): editor exercise pointer drag/deload; saved-history timestamp/duration/rating and bulk/share; AI unresolved import lock/review plus active-gym equipment; optional cloud settings/transport/summaries/debrief; local directive and conservative target/effort-based progression evidence; analytics ISO WEEK/program window, lb chart labels, radar/prior/drill/share; full calibration/provenance portability and legacy SQLite-export JSON/AsyncStorage plus Strong/Hevy/OpenWeight import workflows. Existing body/photo/onboarding source and53 focused tests across9 suites were checked; actual runtime/export/failure checks still required. SQLite legacy means exported JSON schema, not a raw database engine requirement. + +Root workspace preservation: UnifiedPort was not edited or reset. Signing/configuration files were not read, moved or tracked. Existing ignored Vercel staging was retained after its earlier approval rejection. Added .playwright-cli ignore to keep diagnostic snapshots out of publication. Provider research evidence is documentation only; no provider requests/keys were used. diff --git a/docs/reviews/2026-10-11-cloud-final-runtime.md b/docs/reviews/2026-10-11-cloud-final-runtime.md new file mode 100644 index 0000000..ca3fbc3 --- /dev/null +++ b/docs/reviews/2026-10-11-cloud-final-runtime.md @@ -0,0 +1,40 @@ +# Cloud AI final runtime verification — 2026-10-11 + +Executed 2026-10-11 22:03–22:08 UTC (2026-10-12 Europe/Warsaw), against Vite at `http://localhost:5183/Ironlog/app/`, using the isolated Playwright CLI session `cloudfinal`. + +Only a synthetic profile, synthetic gym and synthetic plan were used. Provider requests targeted an HTTPS `.invalid` endpoint and were intercepted by Playwright. The only credential entered was an explicitly synthetic test token. No live provider request, real key, user database, private configuration, source edit or broad test/build was involved. + +## Results + +| Check | Runtime result | +| --- | --- | +| Fetch models | Explicit click produced the intercepted model list containing `synthetic-model`. The password field cleared after the synthetic token entered the in-memory key store. | +| Test connection | Explicit click displayed `Connection responded.` for the intercepted text response. | +| HTTP error | Intercepted HTTP 401 displayed the bounded provider status guidance; the provider response body was not echoed. A corresponding browser HTTP error entry is expected. | +| Cancellation | Cancel restored enabled request controls, removed the pending control and showed no error. A held response fulfilled after cancellation did not display connection success. | +| Endpoint credential isolation | Changing the base URL disabled Test connection when no key existed for that endpoint. Restoring the original endpoint restored access to its session key. | +| Settings persistence | Saved Custom provider, HTTPS base URL, model and OpenAI format survived reload. | +| Credential persistence | Serialized every IndexedDB table and checked for the synthetic token: absent. Reload retained settings while Test connection became disabled, demonstrating that the session key had been forgotten. | +| Active gym equipment | A synthetic active gym with unavailable Cable and Machine initialized the quiz with those checkboxes unchecked and Barbell checked. | +| Manual AI review | A JSON plan containing `Runtime Unknown Press 472` previewed as one unresolved exercise. Import remained disabled and the database contained zero plans. | +| Rematch and explicit import | Matching the unresolved row to existing Bench Press enabled Import reviewed plan. Explicit import saved exactly one synthetic plan containing the resolved exercise and returned to Plans. | + +No blocking runtime issue was found in these exercised flows. + +## Limits + +This run does not establish live provider availability, current model IDs, provider billing, production browser CORS behavior or provider-specific interoperability. The stream/JSON limits and strict Anthropic body validation are covered by the focused automated verification separately; they were not repeated in this browser pass. Cloud generation retry, all insight surfaces, timeout expiry, IndexedDB quota/write failures and importing after a simulated failed database write were not exercised here. The active-gym fixture was seeded directly into the isolated database rather than created through the gym editor. Verification used a desktop Chromium session; mobile layout and other browser engines were not rechecked. + +## Coaching-mode and activation follow-up — 2026-10-11 22:21–22:23 UTC + +Used a second isolated Playwright CLI session, `cloudfinalextra`, against the same Vite server and only synthetic data. No live provider requests were sent; an HTTPS `.invalid` interception was registered for the synthetic provider. + +- A profile explicitly set to built-in mode hid Ask Cloud AI actions despite retaining a cloud configuration. +- A saved lowercase native-style `openai` provider displayed the correct `OpenAI` option in Cloud AI settings. An in-browser synthetic Android backup encode/decode roundtrip also exported lowercase native provider representation and decoded the provider to `OpenAI`. The file-picker import UI was not repeated. +- Selecting Cloud AI mode, entering an explicitly synthetic session token and saving persisted `cloud_ai`. Reload showed Cloud AI mode selected. The plan quiz exposed Create with Cloud AI while that mode was active. +- The `ai_activated` badge was durably recorded after cloud mode plus provider/model and synthetic key were saved. It survived reload, Forget session keys and switching back to built-in mode. +- Saving built-in mode retained the model/base URL/provider configuration. After the profile update settled, Ask Cloud AI actions were hidden; the quiz cloud-generation button was absent. Reload showed built-in mode selected with the retained synthetic model. + +One immediate DOM count sampled before the asynchronous profile save/rerender settled still observed the old cloud controls. Subsequent settled and reload checks confirmed the correct built-in state; this was a verification timing issue, not a persistent UI finding. + +This follow-up did not exercise goal-mode workout-proof accrual, midflight coaching-mode cancellation, a live provider, or native file-picker imports. No additional blocker was found. diff --git a/docs/reviews/2026-10-11-home-final-runtime.md b/docs/reviews/2026-10-11-home-final-runtime.md new file mode 100644 index 0000000..af8fe2e --- /dev/null +++ b/docs/reviews/2026-10-11-home-final-runtime.md @@ -0,0 +1,17 @@ +# Home final runtime closure + +Verified on 2026-10-12 with Playwright CLI and Chromium against the fresh Vite server at `http://127.0.0.1:5183/Ironlog/app/`. The new isolated `home-final-oct12` session used only synthetic browser-local fixtures; no user browser data or application source was changed. + +- Actual mouse click on the next visible year row changed 1995 to 1996. Keyboard Home then ArrowUp changed 1900 to 1901, confirming the corrected spinbutton direction. +- Actual mouse-wheel scrolling wrapped below 1900 to 2026 and above 2026 to 1900, with the selected value centered after settling. +- Home at 320, 411 and 1280 CSS pixels had document width equal to viewport width. DailyProof remained contained and displayed the expected first-proof state. The 320px Home screenshot was visually inspected; cards, action text and navigation were readable without overlap. +- DailyProof's Start workout action scrolled to the workout card. Selecting carousel Day C stored `dayId=firstC`. Canceling the discard confirmation preserved the active workout; confirming discard returned Home to the saved plan card. +- Chromium CDP touch input swiped Home to Plans. A vertical touch drag moved the second plan to the top; the stored order became `second, first`, the active plan became `second`, and Home displayed the new active plan. The resulting plan screenshot was visually inspected. +- The Home round start control opened Day A of the newly active plan. DailyProof displayed the active Day A session, and its Resume workout action reopened that session. +- Both completed browser scripts captured zero application `pageerror` events. + +Artifacts under ignored `web/output/playwright/`: `home-final-review.js`, `home-final-touch.js`, `home-final-wheel.png`, `home-final-320.png`, `home-final-411.png`, `home-final-1280.png`, and `home-final-touch-plans.png`. The wheel and touch-plan screenshots were also visually inspected. Initial automation attempts required correcting helper argument/locator mistakes; the completed runs passed. + +Limits: this is Chromium development-server evidence with simulated touch, not physical-device or production-build verification. It exercises first-proof and active-session DailyProof states, not every coaching branch. Broader automated tests and release checks remain outside this pass. + +Final source reconciliation: Kotlin `showDailyProofAction` displays the primary button only for SETUP/RECOVER_SMART. The web now follows that rule; first-proof/active states use the workout card's controls rather than an extra Daily Proof button. A component regression verifies the single round resume control. The earlier extra Daily Proof action observations above describe the pre-reconciliation browser pass, not the final control hierarchy. Durability now uses `timerStartedAt`, including deletion of the earliest/all sets and reload/completion. diff --git a/docs/reviews/2026-10-11-home-motion.md b/docs/reviews/2026-10-11-home-motion.md new file mode 100644 index 0000000..0da16b4 --- /dev/null +++ b/docs/reviews/2026-10-11-home-motion.md @@ -0,0 +1,36 @@ +# Home and interaction motion — 2026-10-11 + +Implemented in the isolated IronLogWeb worktree. No native changes, builds, publication, or commits. + +## Source trace and delivered behavior + +Read native `HomeScreen.kt` StartWorkoutCard (781 onward), ActiveWorkoutTimerText, `ui/CardGradient.kt`, onboarding `InfiniteNumberWheelSheet.kt`, theme type tokens, and WorkoutRepository first-set timer comments. Compared actual signed native `output/android/parity-native-home.png`, `parity-native-resume.png`, and cycle phase frames against browser captures. Native capture: 1080 × 2400, density 2.625, system font scale 1. + +- Home now displays the plan title, goal badge, day count/average, 48px round start/resume action, recommended day first, horizontally scrolling day cards that directly start their chosen day, protected training rhythm explanation, and START WITHOUT PLAN. +- Active card displays workout name, IN PROGRESS, running first-logged-set timer (zero before any sets), resume, and discard confirmation. Uses existing transactional start/discard APIs; failure leaves confirmation open. Completed history remains untouched. No-plan card can start an empty session. +- Shine uses raw native physical coordinates (-600 → 1800, end +900), resolved physical card height, and 5s forward +5s reverse linear SVG animation. ResizeObserver maps CSS dimensions through browser DPR into the SVG viewBox. Switch off unmounts the layer; reduced motion removes SMIL animation while retaining its static initial brush. +- NumberWheel uses five visible 48px snapping rows, cyclic scrolling through three repeated ranges with middle-range recentering, native touch scrolling, mouse dragging, and bounded keyboard/direct numeric alternatives. Visual row clicks select their value. Numeric bounds remain explicit in spinbutton semantics. + +Ownership: `web/src/features/Home.tsx`, `web/src/ui/NativeCardShine.tsx`, NumberWheel and play/resume icon entries in `web/src/ui/components.tsx`, Home/wheel portions of `web/src/styles.css`, `web/tests/home-motion-closure.test.tsx`, updated native-label/height expectation in `web/tests/motion-parity.test.tsx`. + +## Verification + +`npm run check` passes. Focused Vitest: Home closure + existing motion + navigation parity, 25 tests pass. New integration checks start a nonrecommended day, cancel/confirm discard without altering history, verify zero before first set, and exercise cyclic scrolling alongside bounded keyboard/numeric entry. + +Playwright CLI (no added Playwright specs), isolated `home-motion` session, development port 5182. Ignored browser evidence under `web/output/playwright/`: + +- `home-motion-320.png`, `home-motion-411.png`, `home-motion-1280.png`: settled Home; document scrollWidth equals viewport width at each size; start action remains 48px. Card widths 292/371/560px. Carousel clips/scrolls within its card. +- `home-motion-resume.png`, `home-motion-discard-sheet.png`: first-set-free resume timer at 00:00 and confirmation sheet, canceled to retain session. +- `home-shine-phase-00.png` through `home-shine-phase-10.png`, `home-shine-measurements.txt`: DPR2.625 full cycle sampled at each second. x1 measured -600/-120/360/840/1320/1800/1320/840/360/-120/-600; x2 always +900. ViewBox dimensions match measured CSS card ×2.625, with tiny float rounding at reversal. Phase colors are visibly diagonal and return symmetrically; native screenshots are an unsynchronized cycle so exact frame pixel diffs are not claimed. +- `home-shine-reduced.png`, `home-shine-off.png`: runtime assertions find zero animate elements in reduced motion and zero shine layer when disabled. +- `home-number-wheel-320.png`: actual wheel scrolling moves birth year 1995 →1998 after 144px scroll; Home/ArrowUp remain bounded at1900. + +Browser contexts copy only local synthetic test data. Native stored fitness data is untouched. + +## Limits and remaining concerns + +Home timer reconstructs the first logged timestamp rather than persisting a durable timer-start field. Removing the earliest/all logged sets can move/reset its origin. Existing Workout header and completion duration use startedAt; full native first-set duration parity requires a coordinated store/schema/Workout change and is not claimed here. + +Native and browser title line wrapping still differ because of platform font/layout metrics; card structure, source sizes, brush geometry, and actionable controls are matched, not claimed pixel-identical. Existing Daily Proof text/mascot differs from the signed native screenshot and was left outside this focused card/motion slice. + +Reviewed existing App tab swipe: it ignores horizontally scrollable children, protects inputs, performs threshold/velocity navigation and route animation. Existing plan-list drag animates displaced rows with Web Animations and supports keyboard/persisted ordering. Existing modal Sheet opens as native HTML dialog with focus trap/backdrop dismissal and CSS entry animation; reduced-motion CSS disables animations. Existing motion tests pass, but this slice did not add a real touch tab-swipe or plan drag width matrix; that runtime evidence remains pending. No changes to those already functioning paths. diff --git a/docs/reviews/2026-10-11-plan-intelligence.md b/docs/reviews/2026-10-11-plan-intelligence.md new file mode 100644 index 0000000..b501021 --- /dev/null +++ b/docs/reviews/2026-10-11-plan-intelligence.md @@ -0,0 +1,32 @@ +# Plans, Intelligence and progression parity closure — 2026-10-11 + +## Native sources + +- `UnifiedPort/app/src/main/java/com/ironlog/app/domain/intelligence/ProgressionRecommendationEngine.kt`: target completion, effort minimum, special/mixed-load hold, policy-based load cap, percent-1RM branch, one-rep fallback. +- `UnifiedPort/app/src/main/java/com/ironlog/app/domain/intelligence/ProgressionLoadability.kt`: both baseline and smallest plate increment must be exactly loadable. +- `UnifiedPort/app/src/main/java/com/ironlog/app/domain/intelligence/ProgressionPolicyResolver.kt` and existing web `program-intelligence.ts`: exercise override → plan rules → global preference → conservative default. +- `UnifiedPort/app/src/main/java/com/ironlog/app/ui/screens/intelligence/TrainingIntelligenceScreen.kt`: TodayDirective priorities, cyclic next planned day, scheduled deload recommendation, quick actions. +- `UnifiedPort/app/src/main/java/com/ironlog/app/ui/screens/plans/PlanEditorScreen.kt`: confirmed deload clone uses `maxOf(2, sets / 2)`, unchanged rep/rest targets and 60%-weight note. + +## Acceptance + +- Plan exercise handles accept mouse and touch pointers, capture the pointer, reorder the current day with stable exercise identity, animate neighboring rows, visually mark the dragged row and restore original order on pointer cancellation. Existing up/down and across-day controls remain available. Edits persist only on explicit Save plan. +- Generate Deload Day first opens confirmation; cancel changes nothing. Generate adds a draft with fresh IDs, same exercises and native minimum-two half-set rule. Source day remains unchanged. Web preserves extra fields such as warmup/superset metadata in the clone. +- Today's directive reproduces native neural-fatigue → no mapped weekly sets → slowing PR trend → train as planned priorities. Current recovery estimate and recent (48-hour) pain flags remain visible. The recommended session cycles after the most recent completed plan-day, excluding active/discarded/future sessions. Starting uses the store's existing active-workout guard; an existing workout is resumed instead of replaced. +- Scheduled deload displays 60% working load, about 40% fewer sets and unchanged rep ranges, and links to the plan editor. +- Progression uses the latest comparable session's full working-set evidence and current targets. Rep ranges use their upper endpoint before progressing. Incomplete targets, absent effort, near-limit effort, special sets and mixed loads hold. Both RPE/RIR use their stricter margin. Exercise/plan/global policy controls limits. Barbell increases require exact loadability with current plate inventory; otherwise the engine proposes one controlled rep. Unknown/timed/assisted tracking receive no unsupported recommendation. +- Why this suggestion exposes source date, target, working/effort counts, policy provenance, margin/cap, available increment and missing evidence. Use suggestion only fills inputs: no set, workout or plan is automatically saved. + +## Validation + +- `npm --prefix web run check`: passed after integration with root changes. +- Focused Vitest: progression advice, plan editor, engine, program intelligence and tracking UI; 33 tests passed. +- Playwright CLI with isolated `plan-intel` browser at local Vite port 5183: real mouse pointer reorder, confirmed deload generation and explicit save, directive/scheduled deload display, progression rationale, apply input without changing stored active-workout JSON, and resume action. Chromium CDP touch events at 412 × 915 also reordered the editor correctly. +- Ignored helpers and screenshots: `output/playwright/plan-intelligence-runtime.js`, `progression-runtime.js`, `touch-runtime.js`; `plan-editor-before.png`, `plan-editor-touch-reorder.png`, `intelligence-directive-deload.png`, `workout-progression-rationale.png`, `workout-progression-inputs.png`. Rationale screenshot visually inspected for readable contained dialog. + +## Limits + +- Heuristics reproduce native coaching intent; they are not a physiological model. Pain does not silently select or overwrite a plan. +- Plate inventory applies to barbell/EZ-bar load recommendations. Dumbbell/machine increments use the native kg/lb default because no dumbbell-stack inventory is modeled. +- Browser touch evidence is Chromium input simulation, not physical-device QA. No native files, release configuration, signing data or deployment were changed. +- Runtime fixture seeding happened only in the isolated local browser database. Initial concurrent Vite updates logged duplicate-root HMR errors; reloading recovered, and completed flows passed. This does not constitute a production release-build assessment. diff --git a/docs/reviews/2026-10-11-portability-final-runtime.md b/docs/reviews/2026-10-11-portability-final-runtime.md new file mode 100644 index 0000000..7593290 --- /dev/null +++ b/docs/reviews/2026-10-11-portability-final-runtime.md @@ -0,0 +1,35 @@ +# Portability final browser verification + +Executed 2026-10-12 Europe/Warsaw (2026-10-11 UTC) against fresh Vite source at `http://127.0.0.1:5183/Ironlog/app/`. + +Used Playwright CLI in a newly created named Chromium session, `portability-closure`. All browser records, import files, photo bytes and credential markers were synthetic. No user browser session, private configuration, actual API key or phone data was accessed. No application source was edited and no broad build/test gates were run. + +## Results + +| Check | Evidence | Result | +| --- | --- | --- | +| Strong CSV preview and review gate | A two-row CSV with `Weight (lb)`, quoted comma and quoted newline showed `1 workouts · 2 sets`; append was disabled until the review checkbox was selected. | Pass | +| Reviewed append | Imported one completed session with two sets. 100 lb became 45.35923699997481 kg; 105 lb became 47.62719884997355 kg. Notes retained both the comma and newline. | Pass | +| Preserve existing data | Compared the entire active workout and measurement records before and after append. Both remained identical. | Pass | +| Duplicate import | Repeated the same CSV through preview/review/append. UI showed `These workouts are already present; nothing was changed.` Completed workout count remained 1, active count remained 1, and the existing measurement remained present. | Pass | +| Legacy SQLite preview and cancel | Preview showed 0 plans, 1 workout, 0 measurements and 0 photos. Closing the replacement dialog preserved the existing imported workout and measurement. | Pass | +| Legacy SQLite commit | UI replacement restored `Legacy Runtime`, 12 training-age months and 20 baseline pushups; profile stayed onboarded. | Pass | +| Canonical native JSON commit | Restored a native-format JSON without a web extension through the real file picker and replacement dialog. Profile was onboarded; 24 months / four days per week rebuilt 417 baseline sessions; pushups were 30; native lowercase `strength` remained valid. | Pass | +| Native badges and recovery | Native first-workout date 1735689600000 survived restore with `native_import` origin. Recovery week `2026-W40` with positive count survived; zero-count `2026-W39` was excluded. | Pass | +| Actual downloaded ZIP roundtrip | Seeded a synthetic 68-byte PNG photo; downloaded a complete browser ZIP through the UI; deleted that photo; selected the downloaded ZIP through the file picker and committed replacement. The photo returned with all 68 bytes exactly equal. | Pass | +| Credential exclusion | Synthetic markers in a direct credential field, an array token field and stringified JSON authorization field were absent from the restored ZIP profile. Ordinary array and stringified preferences survived. | Pass | +| Stale extension precedence | Browser-evaluated codecs on a synthetic snapshot with stale extension baseline (24 months, seed 1000) and changed canonical calibration (0 months, updated-at 2000). Decoded baseline had 0 estimated sessions and seed 2000. Canonical first-workout/comeback dates and `native_import` origins survived subsequent baseline migration. | Pass | + +No remaining portability blocker was observed in these checks. This is a Chromium source-runtime verification; it does not establish Safari, mobile-device, or built-distribution behavior. + +## Reproduction artifacts + +Ignored runtime artifacts are under `web/output/playwright/`: + +- `portability-closure-seed.js`: synthetic browser seed. +- `portability-append.js`: CSV interaction harness. Its final duplicate-warning locator matched both the top alert and dialog alert, so the final database verification used an explicit browser evaluation; the application append and duplicate flows themselves completed successfully. +- `portability-legacy.json`, `portability-restore.js`, `portability-legacy-preview.png`: legacy preview/cancel/commit fixture, assertions and screenshot. +- `portability-native.json`, `portability-native-restore.js`, `portability-native-restored.png`: canonical native fixture, assertions and screenshot. +- `portability-browser.zip`, `portability-zip.js`, `portability-zip-restored.png`: actual UI download, byte/credential assertions and screenshot. + +CLI snapshots and console records are in ignored `web/.playwright-cli/`. Browser console records contained React development-tool information; no uncaught application error appeared during these flows. diff --git a/docs/reviews/2026-10-11-remaining-parity-validation.md b/docs/reviews/2026-10-11-remaining-parity-validation.md new file mode 100644 index 0000000..5278a69 --- /dev/null +++ b/docs/reviews/2026-10-11-remaining-parity-validation.md @@ -0,0 +1,36 @@ +# Remaining parity validation + +Implementation tree: `Z:/KOTLIN/IronLogWeb`, branch `feat/remaining-parity-closure`, baseline `c5ace518`. The dirty UnifiedPort checkout and publication checkout remain untouched. The explicit pause was revoked by the owner; publication is authorized by the subsequent requests. + +## Behavior and evidence + +| Area | Implemented and exercised | +|---|---| +| Navigation | Detail routes own their header and omit root tabs/resume chrome. Actual 320px and 1440px Back/Forward cancel/discard, refresh, explicit save, retained active workout and 200px scroll restoration passed. Unknown unindexed boundaries retain the draft/destination while replacing the unknown forward tail. | +| Home | Native physical-pixel brush coordinates over a measured SVG viewport, full ten-second cycle, shine toggle and reduced motion; native carousel and start/resume/discard controls. Daily Proof state text/art follows native priorities; primary CTA is visible only for setup/recovery, matching `showDailyProofAction`. Wheel click, cyclic scrolling and standard spinbutton keys; touch tabs and plan drag/activation. See Home motion/runtime reports. | +| Plans/intelligence | Draft pointer reorder with mouse/touch and cancellation; explicit deload clone with native set/load guidance. Directive, scheduled deload, cyclic next session and conservative progression with target/effort/policy/plate evidence. Advice applies only to inputs. Comparable duplicate exercise occurrences use their stable slot identity. | +| History | Date/time/duration/rating/name/note edits preserve unchanged exact timestamps. Captured revisions reject stale drafts. Bulk selection retains revisions and deletes atomically, preserving active sessions. PNG shares use supported file sharing or a real download. | +| Analytics | ISO WEEK versus rolling ranges; native program horizon; contribution-weighted fine muscles/radar and prior window; drilldowns/weekly consistency; display-unit chart values. 320px summary cells fit their text, all range controls work, PNG downloads work, confirmed PR reset preserves history. | +| AI | Active-gym equipment, unresolved preview lock and explicit rematching, typed generation/retry review, provider/model/session-key settings, explicit built-in/cloud mode, minimized requests and cancellation. Native lowercase provider keys map to web names. Cloud activation requires a valid session key/config and cloud opt-in. No paid/live-provider request was made. See cloud runtime and transport evidence. | +| Portability | Canonical calibration/badge/recovery rows win over stale extensions; baseline values are rebuilt while preserving their fixed seed clock, and native badge origins prevent cleanup from deleting native evidence. Native/legacy JSON and reviewed Strong/Hevy/OpenWeight append; stable repeated-import IDs; recursively redacted object/array/stringified credentials. Actual ZIP restore retained exact synthetic photo bytes. Goal modes count only new credited workout IDs; native-compatible evidence/markers are portable. | +| Body/photos/onboarding | Pound input stores canonical kg, goals survive reload, measurements/goal persistence; date pair uses latest photo, slider/viewer/note reload/selected delete/photo ZIP/share download. Partially saved onboarding survives reload. Injected final-profile write failure rolls back the starter plan and leaves step9; retry stores exactly one plan and completes step10. | +| Native integrity | Transactional normalized exercise creation/rename checks and metadata snapshots, with three real ObjectBox regressions and unchanged schema IDs. | + +Root CLI helpers and screenshots are ignored under `web/output/playwright/`: `final-navigation-320.js`, `final-navigation-1440.js`, `final-body-runtime.js`, `final-onboarding-runtime.js`, `final-stats-runtime.js`; corresponding photo, body, onboarding, analytics and share images/archives. Helpers use separate synthetic browser stores. Browser automation mistakes were corrected before completed passing runs; they are not counted as application failures. Native evidence is ignored under `output/android/`. + +## Integrated gates + +- Web: 68 unit/component files, 342 tests passed after final domain/UI changes; TypeScript and production build passed. Scoped output verifier passed 185 files. The build retains its known large-chunk and public-font URL warnings; paths are checked at runtime/output. +- Android: 767 JVM tests across181 report files: 766 passed, one manual benchmark skipped, no failures/errors; lint0 errors,162 warnings,19 hints. These pre-existing warnings are not described as resolved. +- Signed minified APK: `0.1.0-pre-alpha.11`, code12, package `com.ironlogpro.app`, SDK36/min26. Build succeeded. v2 signature valid, one signer; certificate SHA256 `93970f38b16f3a10e958cdd1339b6f2ca0616cef65595fc53f1d1123a373bba7`. +- APK SHA256: `310ec88b4a0cac83a446a3ea3062e1de220841b8c952cf9492bff902f3e9e4eb`. +- Update QA: API36.1 retained synthetic AVD, `adb install -r` succeeded from .10/code11. Original firstInstallTime `2026-10-05 23:45:09` retained; Athlete/Full Body Beginner/active FULL BODY A survived. MainActivity resumed, crash buffer empty; screenshot inspected. +- Full55 browser scenarios, two origin-stopped scenarios, public deployment and release checks are still running/pending. Do not infer those results from the above. + +Browser acceptance exposed feedback covering Back. An initial inline placement fixed that but Linux CI traces then showed feedback arriving between captured drag coordinates and pointer-down, shifting the plan handles; its inherited centering animation also clipped the inline banner. Final feedback is a centered, non-blocking overlay above navigation/actions, so it neither shifts layout nor captures underlying gestures. Dismiss remains interactive; success feedback clears after five seconds and errors remain explicit. Drag regressions assert real pointer capture and stable handle position when feedback is dismissed. The complete workout/history/re-entry and plan-drag checks pass in both local engines; final CI reruns the full suite. The old detail-page test now uses Back. A recovery fixture uses a fixed Wednesday clock and matching set timestamps instead of depending on the wall-clock Monday boundary. + +## Environmental limits + +Desktop Chromium/WebKit and emulated touch do not certify physical Safari/Home Screen, VoiceOver/TalkBack or OEM behavior. Native font rendering and browser font metrics are not literally pixel-identical. No provider credentials/billing/CORS plan was available for live integration certification. Native cached Ledger event records cannot be replayed as web event identities; web recomputes available evidence and preserves durable unlocks/provenance. Android photo URIs cannot convey bytes. Android Health Connect, foreground services/widgets/on-device AI and guaranteed lock-screen background alarms remain outside browser capability. This is a tested pre-alpha preview, not Play Store production readiness or a bug-free guarantee. + +Final Linux unit run exposed the onboarding integration test querying the next heading before asynchronous IndexedDB/snapshot completion under CI load. The step helper now waits for the committed button to unmount (bounded five seconds), then checks the next heading and the final calibration. No production onboarding behavior changed. The full local 342-test suite passes; fresh CI remains the publication gate. diff --git a/docs/reviews/2026-10-11-workspace-inventory.md b/docs/reviews/2026-10-11-workspace-inventory.md new file mode 100644 index 0000000..9944111 --- /dev/null +++ b/docs/reviews/2026-10-11-workspace-inventory.md @@ -0,0 +1,17 @@ +# Workspace and artifact inventory + +The implementation lives in the isolated `IronLogWeb` worktree. `UnifiedPort` retains its unrelated dirty development work; `_github_publish_ironlog` retains its own main checkout. No reset, wholesale copy, history rewrite, signing-key rotation or physical-phone installation was performed. + +Source remains in `app/` and `web/`; durable evidence/review records are in `docs/reviews/`. Browser helpers, generated snapshots/screenshots and synthetic downloads are ignored under `web/output/playwright/` and `web/.playwright-cli/`. Android captures/AVDs stay ignored under `output/android/`; stable signed distribution copies go under `output/release-pre-alpha.*`. + +Inventory of root ignored output folders during closure: + +| Folder | Files | Approximate MB | Disposition | +|---|---:|---:|---| +| android | 87 | 8675.7 | Retained QA AVD and reproducible native evidence; no user data | +| playwright | 25 | 1.7 | Retained older runtime evidence | +| release-pre-alpha.10 | 5 | 51.3 | Retained signed update baseline | +| releases | 3 | 51.3 | Retained previous distribution copy | +| vercel-source | 995 | 78.8 | Retained prior staging; previous deletion was automatically rejected | + +The earlier staging deletion rejection is not bypassed using another command. No recursive deletion/move was needed to ship this work. Private signing/local configuration stays local and ignored; no secrets were printed or added to the review. Native compiled source/schema is unchanged by organizing diagnostics. Historical quarantine references are not a production source set and are retained rather than guessed disposable. diff --git a/web/docs/acceptance.md b/web/docs/acceptance.md index 904549f..bea9838 100644 --- a/web/docs/acceptance.md +++ b/web/docs/acceptance.md @@ -1,12 +1,12 @@ # Web acceptance record -Status: implementation and hardening in progress; **not fully accepted**. The owner requested direct repository/preview publication on 31 August 2026. Deployment remains gated by CI; publication must not be described as native parity or physical-iPhone acceptance. All test data is synthetic. No APK, phone data or private backup was modified. +Current status: remaining browser-capable checklist implementation and runtime checks are complete; final release/browser/CI/deployment gates are recorded in [remaining parity validation](../../docs/reviews/2026-10-11-remaining-parity-validation.md). Publication is a tested preview, not physical-device certification or literal cross-platform pixel identity. All test data is synthetic; private phone/browser data remains untouched. Dated sections below are historical checkpoints. ## Implemented journeys - Native identity, current monochrome logo, all twelve themes, shared landing/app selection, native art and 24-role palette extraction. - Lazy-launched working app inside a phone frame, with a separate full-screen mobile entry sharing saved data. Onboarding step changes reset scroll/focus; the landing is not a desktop dashboard. -- Saved onboarding steps, basic calibration/profile, goals and coaching preferences, starter selection, zero initial rewards. +- Saved onboarding steps, full calibration/profile, goals and coaching preferences, starter selection and truthful provisional rewards; zero-history defaults remain at zero. - Templates, custom plans/exercises, editing, duplication, ordering, active selection, canonical and legacy JSON plans, notes and supersets. - Persisted start/resume/finish/discard, stable set IDs, edit/delete/order, effort and notes, explicit warmup queues, rest deadlines, plate diagram and custom substitutions with session/plan scope. - Editable History, date filtering, PRs, exercise/volume analytics, body measurements, photo storage/comparison, recovery check-ins/map, Ledger, durable badges and weekly Recovery Circuit. @@ -69,7 +69,9 @@ Screenshots and traces are local, ignored artifacts under `web/output/playwright These are local automated checks. They do not close real iPhone Safari/Home Screen, physical Android, browser notification/background execution, Health Connect, widgets or native photo/camera behavior. -## Publication gates still open +## Historical September publication gates + +This list records the September state. Implemented browser-capable feature gaps and later publication results are reconciled in [remaining parity validation](../../docs/reviews/2026-10-11-remaining-parity-validation.md). Physical-device/platform limits remain separate from completed implementation. 1. Desktop WebKit's `context.setOffline(true)` reload returns an internal engine error. A minimal vanilla service worker reproduces the failure without IronLog/Workbox. The application acceptance transport now stops its own actual HTTP origin, keeping the reload/fresh-page/log/finish assertions and requiring an uncached request to fail. The app-independent emulation diagnostic remains unchanged and nonzero on failure. This isolates an emulation-path limitation, not a precise upstream defect, and does not prove shipping Safari or device-level airplane-mode behavior. 2. Real iPhone Safari and installed Home Screen testing: launch/offline/lock/unlock/termination, storage separation, backup/import, safe areas, keyboard, VoiceOver, photo pickers and timer resumption. Desktop WebKit does not satisfy this gate. @@ -85,7 +87,7 @@ Only `web/dist` is uploaded by the separate web workflow. Android CI is retained Use a new test browser profile with synthetic plans. Never clear the owner's existing app or browser storage. -- Onboard from scratch, select a theme and plan, restart, verify zero rewards until eligible proof. +- Onboard from scratch, select a theme and plan, restart; verify truthful self-reported baseline rewards or zero-history defaults, and separate eligible logged proof. - Import a noted plan, create a custom exercise, swap session-only then plan-level; check names/tracking/notes after resume. - Log/edit/delete sets and effort; queue warmups, lock/unlock, skip/log explicitly; inspect 65kg barbell view and lb conversion. - Finish once, verify History/Ledger/recovery; delete/edit retained sets and check recomputation. diff --git a/web/docs/domain-parity.md b/web/docs/domain-parity.md index 4dfa604..6778168 100644 --- a/web/docs/domain-parity.md +++ b/web/docs/domain-parity.md @@ -12,16 +12,15 @@ This implementation is local-first. It does not contact native app storage, cont - `RecoveryReadinessEngine.kt`: effort/type/duration dose factors, native exercise factors, two-phase fatigue decay, bounded probabilistic session combination, manual 48-hour wellness offset and pain flags. All muscle template, anchor, library and region constants were mechanically extracted from `MuscleContributionEngine.kt` into `src/data/native-muscles.json`. - `TrainingIntelligenceEngine.generateWarmupSets`: 40/55/70/85%, 8/5/3/1 reps, nearest 2.5kg, bar floor and duplicate removal. At or below the bar weight there are no lower loadable warmups. Targets are pending, never credited until explicitly logged. - `WorkoutSuggestionEngine.kt`: token-aware region matching, partial-coverage neutral weighting, stable earliest-day tie breaking. -- `ActiveWorkoutScreen.buildProgressionSuggestion`: previous top weight +2.5kg / +5lb or bodyweight +2 reps. These are suggestions, not prescriptions; web excludes warmup and cardio rows. +- Progression uses comparable tracking/equipment occurrences, retained target/effort evidence, resolved exercise/plan/global policy, bounded increments and exact barbell loadability. Advice explains its source and changes inputs only after an explicit action. It remains a coaching heuristic. - Native badge thresholds are evaluated from credited sessions and verified PRs. Unlocks are persisted additively by `reconcileBadges`; deleting history does not remove already-earned badges. ## Known limits (do not claim complete Android parity) -- Web now retains immutable session exercise snapshots, primary-muscle arrays and explicit contribution maps used by recovery. It resolves broad and case-insensitive muscle labels through the native region vocabulary. Native fuzzy review-candidate ranking and every advanced library field are still not ported. -- Exact IDs, normalized names and exact aliases resolve deterministically. The native fuzzy resolver and its review-candidate ranking are not ported. Unresolved imported names remain visible for review. +- Web retains immutable session exercise snapshots, multi-primary metadata, contribution maps and native region vocabulary. Exact IDs/names/aliases plus confidence-ranked fuzzy candidates are supported; unresolved or ambiguous imported exercises require explicit review rather than fabricated metadata. - Finite physical plate quantities, odd-spare warnings and bounded plate calculation are supported. An unnamed current finite setup is exported as an active native gym named `Current setup`; unlimited-pair profiles remain a web-only compatibility case. -- Cloud-AI-activation and historical goal-mode-usage badge triggers are not invented. Imported existing unlocks are retained. No Health Connect biometric blending exists on web. -- Native cached Ledger events, onboarding calibration baselines, advanced preferences and all historical settings are not fully restored by the Android codec. Current web XP is deterministically rebuilt from available history. +- Cloud activation requires explicit cloud mode, a configured model/validated endpoint and its memory-only session key. Goal-mode badges accumulate canonical modes only when a newly credited workout appears. Durable unlocks survive key removal/history deletion; mode evidence and its workout marker are portable. No Health Connect biometric blending exists on web. +- Native calibration, provenance, baseline reconstruction, badge dates/origins, recovery credit and supported preferences are restored. Canonical rows override stale extension copies. Cached native Ledger events are not replayed as browser event identities: web XP is rebuilt from available history. Retain original backups for unsupported native settings and event history. - Android JSON includes photo URIs but not photo bytes. Those references are reported as skipped; only web ZIP backups preserve actual web photo blobs. Web ZIP preserves the complete web snapshot and applies compressed/expanded size limits before parsing. - Android v1 exports retain the required `ironlog_watermelon_export` literal and relational table names. A `webExtension` retains web-only session/check-in/gym fields when roundtripping through this web codec; Android ignores this extension. Android may drop it when re-exporting. - Native backup measurement fields are bodyweight, waist, chest, arm and thigh. Other web measurement types survive web ZIP / webExtension but are not native relational rows. @@ -55,4 +54,8 @@ Verification for this slice: 24 unit/component files, 138 tests, production buil The five primary routes now follow the current Compose screen hierarchy and navigation geometry. Web Home derives its 30-day session count, working-set count, average duration and weekly volume from the same credited-proof and typed-tracking rules already used by the domain engine. -The web `Create with AI` entry is a deterministic local plan builder over the native template catalog. It does not claim to run Gemini Nano or contact a cloud model. The generated plan is fully editable and persists through the same transactional plan path as library and blank plans. +The September local builder description below was superseded by the manual prompt/catalog/paste review workflow and optional explicit Cloud AI generation. Browser Gemini Nano is not provided. Provider settings are portable; credentials are memory-only and excluded from every backup. Built-in mode hides cloud insights/generation while preserving the configuration. Significant plan changes are reviewed before atomic import. + +## Current closure evidence + +The [remaining parity validation](../../docs/reviews/2026-10-11-remaining-parity-validation.md) records the current implementation, runtime evidence, release gates and environmental limits. Earlier dated verification counts below are historical, not current acceptance. No literal cross-platform pixel identity, live-provider availability, physical Safari/OEM certification or guaranteed background execution is claimed. diff --git a/web/src/App.tsx b/web/src/App.tsx index 36cd4db..26c0891 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -24,7 +24,7 @@ import { VolumeAnalytics } from "./features/stats/VolumeAnalytics"; import { BodyMeasurements, BodyWeight } from "./features/body/BodyComposition"; import { ProgressPhotos } from "./features/body/ProgressPhotos"; import { Research } from "./research"; -import { backFrom } from "./ui/navigation"; +import { backFrom, isTabRoute, subscribeNavigation } from "./ui/navigation"; const tabs = ["Home", "Plans", "Log", "Stats", "Settings"]; const tabRoutes = tabs.map((tab) => tab.toLowerCase()); const detailTitles: Record = { @@ -49,6 +49,11 @@ export function App() { ); const [error, setError] = useState(""); const [status, setStatus] = useState(""); + useEffect(() => { + if (!status) return; + const timeout = window.setTimeout(() => setStatus(""), 5000); + return () => window.clearTimeout(timeout); + }, [status]); const [busy, setBusy] = useState(false); const running = useRef(false); const [now, setNow] = useState(Date.now()); @@ -71,7 +76,7 @@ export function App() { useLayoutEffect(() => { // Focus the committed route before it can be interacted with. A deferred // animation frame can otherwise steal focus between Enter's key events. - window.scrollTo(0, 0); + window.scrollTo(0, Number(history.state?.ironlogScrollY) || 0); main.current?.focus({ preventScroll: true }); }, [route]); useEffect(() => { @@ -83,15 +88,14 @@ export function App() { ); }, []); useEffect(() => { - const hash = () => { - const nextRoute = location.hash.replace(/^#\//, "") || "home"; + const unsubscribeNavigation = subscribeNavigation((nextRoute, direction) => { const from = tabRoutes.indexOf(routeRef.current); const to = tabRoutes.indexOf(nextRoute); if (from >= 0 && to >= 0 && from !== to) { routeDirection.current = to > from ? "next" : "previous"; - } + } else routeDirection.current = direction; setRoute(nextRoute); - }; + }); const visible = () => { if (document.visibilityState === "visible") { setNow(Date.now()); @@ -104,14 +108,13 @@ export function App() { const theme = (e: StorageEvent) => { if (e.key === "ironlog-theme") applyTheme(currentTheme()); }; - window.addEventListener("hashchange", hash); document.addEventListener("visibilitychange", visible); window.addEventListener("pageshow", visible); window.addEventListener("online", network); window.addEventListener("offline", network); window.addEventListener("storage", theme); return () => { - window.removeEventListener("hashchange", hash); + unsubscribeNavigation(); document.removeEventListener("visibilitychange", visible); window.removeEventListener("pageshow", visible); window.removeEventListener("online", network); @@ -229,7 +232,7 @@ export function App() { ); const active = data.workouts.find((w) => w.status === "active"); - const detail = !["home", "plans", "log", "stats", "settings"].includes(route); + const detail = !isTabRoute(route); const selectedTabRoute = route === "library" ? "settings" : route.startsWith("plan/") ? "plans" : route.startsWith("history/") || route === "calendar" @@ -320,7 +323,7 @@ export function App() { ) : ( <> -
+
{detail && route !== "library" && (
)} - {active && route !== "workout" && ( + {active && !detail && (
- {route !== "library" &&