diff --git a/.planning/v15-ios-handoff/09-recommended-flow.md b/.planning/v15-ios-handoff/09-recommended-flow.md new file mode 100644 index 000000000..912442e85 --- /dev/null +++ b/.planning/v15-ios-handoff/09-recommended-flow.md @@ -0,0 +1,622 @@ +--- +file: 09-recommended-flow.md +purpose: The iOS-side workflow recipe — first-time setup, contract-diff loop, parallel-dispatch discipline, pre-release Quality Gate checklist, mock-vs-live test strategy, CI surfaces, and the iOS-only gotchas that the server doesn't see. +when_to_read: Day 1 of iOS work. Re-read before every PR that touches a server-bound contract, before every parallel-dispatch session, and before tagging any v1.5.x release. +prerequisites: 00-philosophy.md, 01-repo-tour.md, 03-api-contracts.md, 08-locked-contracts.md +estimated_tokens: ~6000 +version_anchor: v1.4.25 / sha 49f71c92 +--- + +## TL;DR + +The iOS app is a thin client of a strict-Zod server, so the day-to-day workflow centres on three loops: a contract-diff loop that catches drift before it lands, a parallel-dispatch loop that lets multiple iOS sub-agents work in touch-disjoint slots without merge pain, and a Quality Gate loop that runs eight reviewers in parallel against `develop` before the release-merge to `main`. Mocks belong in unit tests, testcontainers in integration tests, and a staging Coolify instance in e2e. Everything else here is the why, the recipes, and the gotchas. + +## Decision tree — what to do now + +| Situation | First action | +| --- | --- | +| Cold-start, never run the dev server | § 1 First-time setup | +| Touching anything server-bound (DTO, endpoint URL, query params) | § 2 Contract-diff loop | +| Spawning more than one iOS sub-agent in parallel | § 3 Touch-disjoint discipline | +| Tagging a v1.5.x release | § 4 Marathon-pattern handoff + § 5 Quality Gate | +| Writing a new test | § 6 Mock vs live | +| Looking at red CI on a PR | § 7 CI workflow + § 8 iOS-only gotchas | + +--- + +## § 1 — First-time setup + +### 1.1 Clone + bootstrap + +```bash +# from a fresh shell +git clone git@github.com:MarcBombeck/HealthLog.git +cd HealthLog +git checkout develop # daily work goes here, never on main +nvm use 20 # Node 20+ — required by Next.js 16 +corepack enable # pnpm 10.31.0 ships via Corepack +pnpm install +pnpm db:generate # regenerate Prisma client +``` + +### 1.2 Postgres + the dev server + +```bash +docker compose up -d postgres # Postgres 16 on 5432 +cp .env.example .env.local # then edit DATABASE_URL + SESSION_SECRET +pnpm db:migrate:deploy # apply migrations 0001..0060 +pnpm dev # Next.js dev server on :3000 (Turbopack) +``` + +Health-check the boot: + +```bash +curl http://localhost:3000/api/health +# expect: {"data":{"status":"ok","worker":"running",...},"error":null} +``` + +### 1.3 Issue an API token for the iOS Simulator + +The iOS app uses Bearer auth. The fastest way to a working iOS build: + +```bash +# 1. Register a dev user (creates the cookie session) +curl -X POST http://localhost:3000/api/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email":"ios-dev@example.com","username":"ios-dev","password":"correct-horse-battery-staple"}' \ + -c cookies.txt + +# 2. Mint a wildcard-scope token (web Settings → API tokens does the same) +curl -X POST http://localhost:3000/api/tokens \ + -H 'Content-Type: application/json' \ + -b cookies.txt \ + -d '{"name":"iOS Simulator dev","permissions":["*"],"expiresInDays":365}' +# → returns { data: { token: "hlk_..." } } +``` + +Drop the `hlk_*` value into the iOS Simulator app's Settings → API base URL + token. Default API base for the Simulator is `http://localhost:3000` — the iOS app's network layer reads it from `UserDefaults` so multi-host operators (self-hosters) can point at their own Coolify instance. + +### 1.4 Apple Health permissions in the Simulator + +The iOS Simulator **cannot read Apple Health data** — HealthKit on Simulator is a stubbed framework. Two options: + +| Need | Path | +| --- | --- | +| Manual ad-hoc data | Tap the manual-entry sheet in the iOS app; it round-trips through `POST /api/measurements` (single) | +| Realistic HealthKit data shapes | Test on a physical iPhone with seeded HealthKit data, OR seed Postgres directly with `POST /api/measurements/batch` against the seeded fixtures | + +For batch-shape testing, the canonical fixture lives in `tests/integration/__fixtures__/apple-health-batch.json`. Replay it with: + +```bash +curl -X POST http://localhost:3000/api/measurements/batch \ + -H "Authorization: Bearer $HLK_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Client-Type: native" \ + -H "Idempotency-Key: $(uuidgen)" \ + --data-binary @tests/integration/__fixtures__/apple-health-batch.json +``` + +### 1.5 STOP HERE if… + +| If you only need… | …skip to… | +| --- | --- | +| To call ONE endpoint and verify the wire shape | `03-api-contracts.md` § matching route | +| To wire login + refresh in Swift | `05-auth-flows.md` § 2 | +| To debug a 422 from a batch | `08-locked-contracts.md` § 2 | + +--- + +## § 2 — The contract-diff loop + +Every iOS PR that touches an endpoint, a DTO, a query string, or an enum value MUST verify the contract before merge. Drift hides until prod. + +### 2.1 The 4-step loop + +``` +1. git fetch origin && git rebase origin/develop # absorb latest server changes +2. pnpm openapi:check # diff Zod registry vs committed YAML +3. (if 2 fails) pnpm openapi:generate # regenerate the YAML, commit if intentional +4. Regenerate iOS Swift codegen from docs/api/openapi.yaml +``` + +### 2.2 What `openapi:check` actually does + +```typescript +// from scripts/check-openapi.ts:6 +// Hard-fails on drift since v1.4.25 — the Zod registry is the +// source of truth for the public API contract that the v1.5 iOS +// Swift codegen consumes. +``` + +Two outcomes: + +| Result | What happened | Action | +| --- | --- | --- | +| Exit 0 | Zod registry == committed YAML | Safe; proceed | +| Exit 1 with diff | A server change drifted the Zod schemas without regenerating | Run `pnpm openapi:generate`, review the diff, commit if the change is intentional. If the diff includes a breaking shape change, raise it before merging | + +### 2.3 Swift codegen recipe + +```bash +# On the iOS side, mirror the server YAML into Swift +swift run swift-openapi-generator generate \ + --output-directory Sources/HealthLogAPI/Generated \ + --config openapi-generator-config.yaml \ + ../HealthLog/docs/api/openapi.yaml +``` + +Codegen-driven DTOs replace hand-written `Codable` structs. If a field that the iOS UI relies on disappeared from the regenerated source, the build breaks — exactly the failure mode you want. + +### 2.4 What to do when the diff is intentional + +``` +1. Open the server PR that introduced the drift. +2. Confirm the change is additive (server policy: additive across minor versions). +3. If the change is breaking (rare): coordinate the iOS rev-lock with the server tag. +4. Commit the regenerated YAML in the SAME PR that ships the iOS-side change. +``` + +Marc's rule: never ship an iOS PR that depends on un-merged server work. The server tag lands first; the iOS PR rebases against it. + +### 2.5 Where the contract-diff fire alarms go off + +| Symptom | Most likely cause | Fix | +| --- | --- | --- | +| iOS 422 with `meta.errorCode: "validation_failed"` on a field your DTO sends | iOS DTO has a key the server's Zod-strict parser rejects | Regenerate Swift codegen; remove the stale key | +| iOS 422 on a field your DTO does NOT send but the server now requires | Server added a required field; codegen catches it | Regenerate; populate the new field | +| iOS 200 with wrong-shaped data | Enum value changed spelling | Regenerate; the Swift enum now reflects new spelling | +| CI red on `openapi-drift` | A server hand-edit bypassed the registry | Server-side fix only — never patch the YAML by hand | + +--- + +## § 3 — Touch-disjoint discipline (parallel iOS sub-agents) + +Marc's release pattern depends on dispatching multiple sub-agents in parallel against touch-disjoint surfaces. The iOS workspace inherits the same convention. + +### 3.1 The rule + +> Two sub-agents working at the same time MUST NOT touch the same file. Period. + +If two slots both edit `Sources/HealthLogCoach/CoachView.swift`, you get merge conflicts that compound across the wave. The dispatcher pre-divides surfaces so this is impossible. + +### 3.2 Wave decomposition recipe + +``` +Goal: ship N independent iOS features in one session. + +Step 1 — Inventory the work. List every file each feature touches. +Step 2 — Find a partition. Group features so no file appears in two groups. +Step 3 — Dispatch in parallel. Each slot gets one group + a touch list. +Step 4 — Merge order. Slot 1 commits first, slot 2 rebases on slot 1, etc. +``` + +When a clean partition isn't possible (e.g. two features both touch `AppShell.swift`), serialise those two — one waits. + +### 3.3 Atomic-commit rule + +Each slot ships **one commit per logical change**, never a "WIP" or a squashed mega-commit. The release surfaces the commits as line-items in the CHANGELOG; one commit per item makes the changelog readable. + +| Acceptable | Not acceptable | +| --- | --- | +| `feat(coach): add streaming chat view` | `wip: coach + dashboard + auth` | +| `fix(auth): refresh on 401` | `various fixes` | +| `chore(deps): bump Alamofire to 5.10` | `update deps and refactor` | + +### 3.4 Marc-Voice commit-message style guide + +| Rule | Example | +| --- | --- | +| English, terse, professional | `fix(measurements): unblock batch ingest on 422` | +| Use Conventional Commits prefixes | `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `style`, `build`, `ci` | +| Imperative mood — "add" not "added" | `feat(coach): add streaming SSE parser` | +| Scope in parentheses, lowercase | `feat(insights): render dailyBriefing tile` | +| No AI tells | NEVER `Co-Authored-By: Claude`, NEVER `--no-verify`, NEVER reference "agent / marathon / phase / wave / session" in the message | +| No PII | Marc's name, health figures, BD-Zielbereich values, measurement counts never appear in commit messages | + +Counterexamples that would fail a review: + +``` +# BAD — agent-voice +fix(coach): patch streaming bug (Claude-assisted) + +# BAD — AI mention +feat(insights): add briefing tile (generated via Claude Opus) + +# BAD — marathon language +feat(auth): wave-3 refresh-token rotation + +# BAD — PII leak +fix(targets): restore Marc's 140/90 BD-Zielbereich after migration + +# GOOD +feat(auth): rotate refresh tokens on use +feat(coach): add SSE streaming view with provenance disclosure +fix(targets): persist user target range after edit +``` + +### 3.5 Branch model — `develop` is the daily target + +``` + feature/* ──┐ + ├──► develop ──► (release-merge via PR) ──► main ──► tag vX.Y.Z + fix/* ──┘ │ + └──► CHANGELOG entry pinned to the tag +``` + +The iOS app's GHCR-equivalent (App Store TestFlight) builds **from main only**. A dev push to develop never produces a TestFlight build — the GitHub Actions workflow trigger is `tags: 'v*'` on `main`. + +--- + +## § 4 — Marathon-pattern handoff (for v1.5.x release sessions) + +Marc-memory has a `release-marathon` skill that codifies the pattern. The iOS workspace inherits the rules. + +### 4.1 The non-negotiables + +| Rule | Why | +| --- | --- | +| NO `Co-Authored-By: Claude` trailer | Marc-Voice: every artefact reads as Marc's authorship | +| NO `--no-verify` on `git commit` or `git push` | Pre-commit hooks (lint, typecheck, test) gate every commit | +| NO `--no-edit` on `git rebase` | The `--no-edit` flag is not a valid rebase option | +| NO direct push to `main` | `main` only accepts release-merge PRs from `develop` | +| NO tagging by sub-agent | Marc tags after the release-merge PR lands | +| NO destructive git commands (`reset --hard`, `push --force` to main, `branch -D`, `clean -f`) unless Marc explicitly asks | Risk of losing other slots' work | + +### 4.2 Pre-flight rule + +Before opening any iOS PR in a marathon session, read the handoff doc (this directory) first. The wave-0 read budget is roughly: + +- `README.md` (this directory) +- `00-philosophy.md` +- `01-repo-tour.md` +- `08-locked-contracts.md` +- `09-recommended-flow.md` (this file) + +Total ~15k tokens. Skipping this surface is the most common cause of merge-blocking review feedback. + +### 4.3 Research-first for architecturally-new work + +If the iOS feature is genuinely new architecturally (e.g. APNs payload routing, HealthKit observer-query topology, Withings deep-link return), spin up a research note under `.planning/research/` BEFORE writing code. Benchmark Apple Health / Withings / Oura first, propose an ecosystem-fit plan, then implement. Marc's rule from `feedback_research_before_complex_features`. + +### 4.4 Phase reports + +Every completed phase writes a report at `.planning/phase-W-v1.5.--report.md`. The report captures: + +- Phase scope (the task description verbatim) +- What landed (commits + files) +- What deferred (with a one-line "why") +- Test deltas (unit / integration / e2e numbers before vs after) +- Open questions for Marc + +These are internal — never published to `docs/` or CHANGELOG. + +### 4.5 Decision-relay communication + +When a sub-agent hits a decision boundary that needs Marc's input (a UI taste call, an MDR-adjacent feature, a breaking-shape proposal), it pauses and writes a one-paragraph decision-relay note. It does NOT silently pick a direction. Examples that always relay: + +- "Should the Coach drawer use a SwiftUI `.sheet` or `.fullScreenCover`?" +- "Should iOS surface the Research Mode chart inline or behind a Settings link?" +- "Should refresh-token reuse trigger a silent re-login or a hard logout?" + +--- + +## § 5 — Pre-release Quality Gate checklist (W21-pattern) + +Marc's v1.4.25 W21 ran an 8-reviewer parallel pass before the release-merge. The iOS workspace mirrors it. + +### 5.1 The eight reviewers + +| Reviewer | What they check | Output | +| --- | --- | --- | +| **code-review** | Bug-spotting, error handling, off-by-ones, dead branches | `phase-W-code-review-findings.md` | +| **security** | Secret leaks, auth boundary, injection vectors, token storage | `phase-W-security-findings.md` | +| **design** | Touch targets ≥ 44×44 pt, contrast WCAG-AA, palette consistency | `phase-W-design-findings.md` | +| **senior-dev** | Architecture, layering, future-proofing, idiomatic Swift | `phase-W-senior-dev-findings.md` | +| **simplifier** | Dead code, redundant abstractions, over-engineered surfaces | `phase-W-simplifier-findings.md` | +| **product-lead** | User-facing acceptance criteria, copy quality, flow polish | `phase-W-product-lead-assessment.md` | +| **i18n-runtime-probe** | Hard-coded strings, locale-bundle parity, missing keys | `phase-W-i18n-runtime-findings.md` | +| **dead-code-scan** | Unused exports, unreferenced files, orphaned migrations | `phase-W-dead-code-findings.md` | + +### 5.2 The reconcile pass + +After the 8 reviewers land, run a **reconcile** pass that: + +1. Collates findings into severity buckets (C / H / M / L) +2. Maps each must-fix finding to a touch-disjoint Fix-* surface +3. Estimates commit count per surface +4. Dispatches fix sub-agents in parallel against those surfaces + +The reconcile plan lives at `.planning/phase-W-reconcile-plan.md` and is the single canonical source for "what's still pending before tag". Example: `.planning/phase-W21-reconcile-plan.md` (v1.4.25's W21 reconcile, 63 must-fix findings → 7 Fix-* surfaces). + +### 5.3 Severity rubric + +| Severity | Rule | +| --- | --- | +| **Critical** | Ship-blocker. Security leak, MDR-line crossing, data loss. Always applied before tag | +| **High** | Apply before tag unless a clean defer-rationale exists | +| **Medium** | Apply if the file is already open in another Fix-* slot (cheap reuse) | +| **Low** | Defer to the next patch unless trivially cheap | + +### 5.4 Pre-tag final-pass checklist + +Once Fix-* surfaces land: + +``` +[ ] pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build (server) +[ ] pnpm test:integration (server, testcontainers) +[ ] pnpm e2e (server, Playwright) +[ ] pnpm openapi:check (no drift) +[ ] Swift codegen runs cleanly against docs/api/openapi.yaml +[ ] xcodebuild test -scheme HealthLog (iOS unit + UI tests) +[ ] PII-grep against CHANGELOG + release notes: no Marc / 140/90 / BD-Zielbereich +[ ] AI-tell grep: no "Claude / agent / marathon / phase / wave / session" in user-facing artefacts +``` + +If every box ticks, open the release-merge PR develop → main. Marc reviews + tags. + +--- + +## § 6 — Mock vs hit-live test strategy + +Three test layers, three different data realities. + +### 6.1 The decision tree + +| Need | Layer | Backing | +| --- | --- | --- | +| Test a single function | Unit | Mocked Prisma (`vi.mock("@/lib/db")`) | +| Test a full HTTP path against a real DB | Integration | Testcontainers Postgres (`pnpm test:integration`) | +| Test a user flow across multiple pages | E2E | Playwright against a Coolify staging instance | +| Test the iOS app against the real server | E2E | iOS Simulator pointed at staging URL | + +### 6.2 Unit tests — what to mock + +```typescript +// from a typical Vitest unit test +import { vi, describe, it, expect } from "vitest"; +vi.mock("@/lib/db", () => ({ prisma: { measurement: { findMany: vi.fn() } } })); +vi.mock("@/lib/auth/session", () => ({ requireAuth: vi.fn().mockResolvedValue({ user: { id: "u1" } }) })); +``` + +Rules: + +- Mock Prisma; never hit a real DB +- Mock external HTTP (Withings, Anthropic, OpenAI) +- Stub time with `vi.useFakeTimers()` and pin to a specific ISO instant +- Never mock Zod — schemas must validate against real input shape + +### 6.3 Integration tests — testcontainers + +```bash +pnpm test:integration +# Boots a fresh Postgres 16 container, runs migrations, spawns a test instance, runs every spec, tears down. +``` + +These tests are the safety net against drift between Prisma client and the migrations. If `pnpm test` passes but `pnpm test:integration` fails, you have a schema-vs-client mismatch — almost always a migration rollback bug. + +### 6.4 E2E — staging only + +The iOS Simulator's e2e flow points at the Coolify staging URL (separate from production). Staging has: + +- A fresh Postgres + restored backup +- Marc's seeded test account +- Real AI providers (Anthropic key in env) +- Real Withings sandbox creds + +Never run iOS e2e against production. The smoke difference matters only for the dispatcher cron schedule — staging cron schedules use a `Europe/Berlin` clone but disable the offhost-backup queue (it shouldn't touch S3). + +### 6.5 iOS unit + UI tests + +```bash +# Unit tests — XCTest, mocked URLSession +xcodebuild test -scheme HealthLog -destination 'platform=iOS Simulator,name=iPhone 15' + +# UI tests — XCUITest against the iOS Simulator with a stubbed server +xcodebuild test -scheme HealthLogUITests -destination 'platform=iOS Simulator,name=iPhone 15' +``` + +Stub the server with a local `URLProtocol` subclass that intercepts every request. No physical server runs during iOS unit tests — the goal is to verify the iOS state machine in isolation. + +--- + +## § 7 — CI workflow surfaces + +Two CI surfaces matter: the server's GitHub Actions and the iOS-side GitHub Actions (or Xcode Cloud). They overlap deliberately. + +### 7.1 What the server CI runs on every PR + +| Workflow | Trigger | What it does | +| --- | --- | --- | +| `e2e.yml` | PR + push | Playwright against ephemeral Postgres | +| `integration.yml` | PR + push | Vitest integration (testcontainers) | +| `security.yml` | PR + daily | `pnpm audit`, secret scanning, OpenAPI drift gate (HARD-FAIL) | +| `docker-publish.yml` | Tag `v*` on `main` only | Multi-arch GHCR image build | +| `post-publish-verify.yml` | After GHCR build | Pulls the new image, runs health probes | + +The hard-flip gate to watch: `pnpm openapi:check` inside `security.yml`. If it fails, the PR cannot merge — even with admin override, Marc's policy is to fix the drift instead. + +### 7.2 What the iOS CI should run + +``` +1. xcodebuild test -scheme HealthLog # unit + UI tests +2. xcodebuild archive -scheme HealthLog -configuration Release # ship-able archive +3. Swift codegen drift check (regenerate, diff, fail if differ) +4. SwiftLint --strict +5. (Optional) accessibility audit on UI tests via XCTest +``` + +iOS CI does NOT need to run the server's tests. The contract is OpenAPI YAML; if YAML and Swift codegen agree, iOS doesn't care how the server tests itself. + +### 7.3 The cross-side handoff + +``` +Server PR → develop → CI green → review → merge +Server tag → main → CI green → GHCR build → Coolify staging +Server tag → main → docs/api/openapi.yaml frozen on the tag + │ + ▼ +iOS PR → develop → CI green (Swift codegen against frozen YAML) +iOS PR → develop → review → merge +iOS tag → main → CI green → TestFlight +``` + +iOS NEVER cross-references the server's `develop` branch's OpenAPI. iOS targets the latest server tag's YAML, period. This keeps both sides shippable independently. + +--- + +## § 8 — Common iOS-only gotchas + +These don't surface in any server log because the server never sees them. + +### 8.1 Apple Health permissions on Simulator + +``` +Symptom : HKHealthStore.requestAuthorization returns immediately, no prompt +Cause : iOS Simulator stubs the HealthKit framework; no real permission flow exists +Fix : Test on a physical device, OR seed Postgres via /api/measurements/batch +``` + +The iOS app should detect the Simulator and surface a "running on Simulator — manual entry only" banner in dev builds. + +### 8.2 Withings web-only OAuth vs iOS deep-link return + +``` +Symptom : User taps "Connect Withings" → opens SFSafariViewController → never returns to app +Cause : Withings redirect_uri is server-side (https://...example.com/api/withings/callback), + but the iOS app needs a deep-link back into the app after the cookie session + confirms the connect +Fix : Server-side handler at /api/withings/callback ends with a 302 redirect to a custom + URL scheme (e.g. healthlog://withings/connected), which iOS catches via Universal + Links (in production) or a custom URL scheme (in dev). The Apple Health bridge + does NOT have this issue — there's no OAuth involved. +``` + +Details in `02-server-architecture.md` § Withings integration. + +### 8.3 RESEARCH_MODE_DISCLAIMER_VERSION drift on server redeploy + +``` +Symptom : iOS user previously acknowledged the Research Mode disclaimer; after a server + redeploy, the chart is gated again with a "you must re-acknowledge" dialog +Cause : The constant in src/lib/medications/glp1-pk.ts moved forward (e.g. + "2026-05-14.1" → "2026-05-21.1") because the server team bumped it. +Fix : iOS reads the live currentDisclaimerVersion from GET /api/auth/me/research-mode + on every Coach turn and on every Research Mode chart render. NEVER cache the + version string client-side longer than one session. +``` + +See `08-locked-contracts.md` § 6. + +### 8.4 SSE Coach streaming via URLSession.bytes(for:), not long-polling + +```swift +// CORRECT — URLSession.bytes streams as the server emits +let (bytes, response) = try await URLSession.shared.bytes(for: request) +for try await line in bytes.lines { + if line.hasPrefix("data: ") { + let frame = String(line.dropFirst(6)) + // decode SSE event frame + } +} + +// WRONG — would buffer the entire response before any UI update +let (data, response) = try await URLSession.shared.data(for: request) +``` + +The Coach UX depends on token-by-token reveal. Buffered reads collapse to a "wait 4s, then everything appears at once" experience that defeats the entire point. See `14-coach-mental-model.md` § Evidence-grounding. + +### 8.5 The Idempotency-Key lifecycle on iOS + +``` +Step 1 : Generate UUID().uuidString once per logical write +Step 2 : Persist the key + body to disk +Step 3 : POST with Idempotency-Key header +Step 4 : On 2xx or non-retryable 4xx (400/415/422), drop the key +Step 5 : On 5xx or network failure, retry with the SAME key (24h replay window) +Step 6 : On 401, refresh the token, retry with SAME key +``` + +The most common iOS bug here: generating a fresh UUID on retry. That produces a duplicate write, not the idempotent replay. See `17-error-handling.md` § 3. + +### 8.6 Time zones in measurement timestamps + +``` +Symptom : An iOS sample at 23:50 in Pacific/Auckland lands in the wrong day on the dashboard +Cause : iOS sent the timestamp as a UTC string, the server day-bucketed it in UTC instead + of the user's TZ +Fix : Send the timestamp as ISO-8601 WITH offset (e.g. "2026-05-14T23:50:00+12:00"). + The server's userDayKey() resolver uses User.timezone to bucket, but the offset + on the wire is what locks the wall-clock moment. +``` + +### 8.7 The `X-Client-Type: native` header is mandatory on login + +``` +Symptom : iOS login succeeds but no refresh token comes back +Cause : Without X-Client-Type: native (or the UA prefix HealthLog-iOS), the server treats + the call as web and only sets a cookie +Fix : Always send X-Client-Type: native on /api/auth/login and /api/auth/register +``` + +See `05-auth-flows.md` § 2. + +### 8.8 Push notifications — the APNs token lifecycle + +``` +Step 1 : iOS requests notification permission +Step 2 : iOS receives an APNs device token (32 bytes hex) +Step 3 : iOS POSTs the token to /api/devices with platform: "ios" + apnsEnvironment + ("sandbox" for dev builds, "production" for App Store) +Step 4 : Server stores Device.apnsToken with a partial unique index +Step 5 : On every app launch, iOS re-POSTs the token (APNs tokens rotate silently) +Step 6 : On logout, iOS DELETEs the token via /api/devices/[id] +``` + +The most common bug: forgetting Step 5. APNs rotates tokens silently; stale tokens fail to deliver. The server's dispatcher cleans up tokens that fail twice in a row. + +### 8.9 Coolify staging vs production URLs + +``` +Production : https://healthlog.example.com (Marc's main instance) +Staging : https://staging.healthlog.example.com (refreshed nightly from prod backup) +Local : http://localhost:3000 (dev server) +``` + +Never run iOS e2e tests against production. Never run integration tests against staging. Local-Postgres-via-testcontainers for integration; staging-only for e2e. + +### 8.10 Six locales — string keys must exist in all of them + +``` +Symptom : iOS app shows the raw key "coach.refusal.glp1_dose" instead of the translation +Cause : The key was added to en.json but not to de.json / fr.json / es.json / it.json / pl.json +Fix : Add the key to all six bundles in the same PR. The server's i18n integrity test + blocks PRs that drop a key; the iOS side has no such gate — review carefully. +``` + +See `19-i18n-system.md` § 3. + +--- + +## § 9 — Common iOS PR shapes (cheat sheet) + +| PR type | Files typically touched | Reviewer focus | +| --- | --- | --- | +| New endpoint client method | `Sources/HealthLogAPI/Generated/*` (regen) + one new `*ViewModel.swift` | Contract-diff loop | +| New screen | One Swift file under `HealthLog/Views/`, one ViewModel, one string bundle update × 6 | Design + i18n | +| New HealthKit metric | `HealthKitClient.swift` + `MeasurementType` mirror + server-side `apple-health-mapping.ts` first | Server PR first, iOS PR second | +| Bug fix | Single file, single test | Single reviewer (code-review) | +| Dependency bump | `Package.swift` + lockfile | senior-dev — check for behaviour changes | + +--- + +## § 10 — STOP HERE if… + +| If your task is… | …skip the rest and read… | +| --- | --- | +| Just calling one endpoint | `03-api-contracts.md` § matching route | +| Wiring login + refresh | `05-auth-flows.md` | +| Building a Coach UI | `14-coach-mental-model.md` + `15-insights-architecture.md` | +| Building a dashboard tile | `11-web-ui-tour.md` § Dashboard + `12-design-system.md` | +| Understanding contract-locks before any code change | `08-locked-contracts.md` | + +Otherwise: continue to `10-research-pointers.md`. diff --git a/.planning/v15-ios-handoff/10-research-pointers.md b/.planning/v15-ios-handoff/10-research-pointers.md new file mode 100644 index 000000000..8853437c1 --- /dev/null +++ b/.planning/v15-ios-handoff/10-research-pointers.md @@ -0,0 +1,369 @@ +--- +file: 10-research-pointers.md +purpose: Cross-link map to the research and review notes already in .planning/research/ — the why behind every architecturally-load-bearing decision iOS-Claude will hit. +when_to_read: When the doc-pack says "read the equivalent research note", when designing a feature that touches Apple Health / Withings / GLP-1 / Health Score / source-priority, or before tagging a v1.5.x release. +prerequisites: 00-philosophy.md (the research-first directive) +estimated_tokens: ~3500 +version_anchor: v1.4.25 / sha 49f71c92 +--- + +## TL;DR + +Marc's rule from `feedback_research_before_complex_features`: research benchmarks the ecosystem before code lands. The artefacts live under `.planning/research/` and are the canonical "why" alongside the doc-pack's "what". Read the matching research note BEFORE building any architecturally-new iOS feature; reach for the W21 review notes BEFORE tagging a release. + +## How to use this file + +Every entry is `` → `` → `` → `` → ``. Status is **current** (still drives v1.4.26+) or **historical** (already absorbed into shipped code — read for context only). + +``` +PROJECT_ROOT/.planning/research/ ← all paths in this file are relative to here +``` + +## STOP HERE if… + +| If your task is… | …skip the rest and read… | +| --- | --- | +| Just need an API contract | `03-api-contracts.md` | +| Just need a refusal-probe shape | `08-locked-contracts.md` § 7 | +| Just need the Coach mental model | `14-coach-mental-model.md` | +| Need the v1.4.25 reconcile inventory | `.planning/phase-W21-reconcile-plan.md` (see § 4 below) | + +Otherwise: continue here. + +--- + +## § 1 — Ecosystem research (Apple Health, wearables, GLP-1) + +### `apple-health-ecosystem-scan.md` — 4113 words — current + +GitHub topic `apple-health` deep scan: 30 top repos by stars, license + tech + maturity per row. Catalogues the OSS landscape HealthLog could plug into (HealthGPT, react-native-healthkit, apple-health-grafana, healthkit-to-sqlite, SleepChartKit). + +Read this if iOS-Claude is about to: +- Pick an iOS HealthKit binding library +- Decide whether a SwiftUI native chart or a Recharts-WebView mirror is the right call +- Evaluate licence compatibility for any OSS Apple Health adapter + +### `apple-health-sync-deep-dive.md` — 4968 words — current + +iOS sync patterns + clean-room teardowns. The deeper version of the ecosystem scan; covers observer-query topology, HKAnchoredObjectQuery cursoring, background-delivery quirks, and the conflict-reconciliation patterns the open-wearables and healthkit-to-sqlite projects converged on. + +Read this if iOS-Claude is about to: +- Wire `HKObserverQuery` + `HKAnchoredObjectQuery` for incremental sync +- Decide between background-delivery and explicit foreground polling +- Reconcile HealthKit's "edited samples are new IDs" model with the server's externalId-based dedup + +### `glp1-feature-inspiration.md` — 12071 words — current — **CRITICAL** + +The single largest research artefact in the pack. EMA EPAR PDFs (Tirzepatide / Semaglutide / Liraglutide / Dulaglutide / Saxenda / Rybelsus) extracted near-verbatim; ASCPT pop-PK paper (psp4.13099) for the journal-of-record two-compartment math; clean-room teardown of the closed-source `my-glp-shot` app; **EU MDR 2017/745 + MDCG 2021-24 boundary analysis**. + +Read this if iOS-Claude is about to: +- Build ANY GLP-1-touching feature (medication list, injection-site map, dose-change history, plateau detection, Research Mode chart) +- Add or modify a GLP-1 drug in the catalog +- Touch the Coach refusal logic around drug levels (GROUND RULE 9/15) +- Surface a "weight projection" or "dose escalation" UI — those CROSS the MDR threshold and are explicitly out of scope + +The four do-not-builds (N1–N4 in the research) are the iOS-side's hard line: never project weight loss, never recommend escalation, never check drug-drug interactions, never import code/schema from `my-glp-shot` (no LICENSE = all rights reserved). + +### `glp1-injection-tracking.md` — 4488 words — historical + +The W4 first-pass research. Predecessor to `glp1-feature-inspiration.md` — covers the foundational landscape (treatmentClass enum proposal, injection-site map design, side-effect taxonomy, dose-change history schema). Mostly absorbed into shipped code by v1.4.25. + +Read this if iOS-Claude is about to: +- Trace the historical reasoning behind why `treatmentClass = GLP1` exists as a separate column +- Understand the injection-site rotation algorithm + +### `open-wearables-comparison.md` — 4075 words — current + +Side-by-side: Apple Health vs Withings vs HealthLog. Covers data-source coverage, sync mechanics, conflict resolution, and the unique design problems each platform solves. The source for the two-axis source-priority decision. + +Read this if iOS-Claude is about to: +- Design any UI that surfaces "which source provided this reading" +- Add a new metric type and need to know which sources can supply it +- Build a Settings → Sources screen + +### `withings-api-coverage.md` — 4648 words — historical + +Earlier Withings API audit (pre-W17). Coverage table of every Withings endpoint with what HealthLog ingested vs what it ignored. Largely absorbed; the W17 follow-up below supersedes it for activity + sleep specifics. + +Read this if iOS-Claude is about to: +- Need a historical "what did Marc consider but not ship" answer for Withings + +### `withings-plus-comparison.md` — 5306 words — historical + +Older competitive intel on Withings+ subscription. Captures the "what does the consumer app surface that HealthLog doesn't" gaps as of the time it was written. Useful as a feature-parity reference but not actionable for v1.5. + +Read this if iOS-Claude is about to: +- Pitch a feature parity argument with the Withings+ app + +--- + +## § 2 — UX + design research + +### `health-score-provenance-ux.md` — 3467 words — current + +The W8e research that shaped the Health Score provenance accordion. Covers the "trust surface" reasoning, the four-component decomposition, and the `aria-labelledby` pairing pattern. + +Read this if iOS-Claude is about to: +- Build a Health Score tile on iOS +- Design the per-component provenance disclosure UI + +### `insights-sub-pages-ux.md` — 4961 words — current + +The W4 Insights mother-page + sub-pages UX research. Apple Health vs Withings vs Oura tile patterns, hero+sub-tile composition, where the daily briefing sits. + +Read this if iOS-Claude is about to: +- Build the iOS Insights surface +- Decide between a single scrollable feed and per-metric tabs +- Design the Daily Briefing tile + +### `source-priority-two-axis.md` — 2742 words — current + +The W8c two-axis source-priority research. Why metric × device-type is the right shape (vs flat ladder), and how the iOS Settings → Sources screen surfaces both axes. + +Read this if iOS-Claude is about to: +- Build Settings → Sources on iOS +- Touch the `pickCanonicalSource()` resolver semantics in any way + +### `zielwerte-redesign.md` — 5600 words — historical + +The older target-zone redesign research. Mostly absorbed; surfaces the BD-Zielbereich UI thinking that ships at `/insights/zielwerte`. + +Read this if iOS-Claude is about to: +- Build the iOS target-zone editor +- Understand why "Zielwerte" is a distinct page (vs nested under Settings) + +--- + +## § 3 — Feature-specific research + +### `w8d-implementation-outline.md` — 5051 words — current + +Apple Health enum extensions spec. The implementation outline for the v1.4.25 W8d additions (AUDIO_EXPOSURE_ENV, AUDIO_EXPOSURE_HEADPHONE, TIME_IN_DAYLIGHT, plus the workout-batch endpoint). + +Read this if iOS-Claude is about to: +- Add a new MeasurementType to the iOS side +- Wire workout ingest + +### `w14b-onboarding-rebuild.md` — 3177 words — current + +The onboarding wizard redesign spec. Four-step state machine, race-safe step advance, the rationale for skipping the localStorage goals shim. + +Read this if iOS-Claude is about to: +- Build the iOS onboarding flow +- Touch any `/api/onboarding/*` endpoint + +### `w14c-native-coach-prompts.md` — 3616 words — current + +Multi-locale Coach prompt strategy. The reasoning behind native FR/ES/IT/PL prompts (vs the pre-v1.4.25 EN-with-language-footer approach), the YAML matrix structure, the parity test design. + +Read this if iOS-Claude is about to: +- Modify any Coach prompt or refusal copy +- Understand why the iOS side cannot localise refusal copy itself (it MUST come from the server) + +### `w16b-workout-ingest.md` — 2589 words — current + +Workout batch ingest research (W16b). Covers HKWorkout shape, the 5 MB ceiling, the `endedAt < startedAt` guard, and the cross-source dedup gap deliberately left for v1.5. + +Read this if iOS-Claude is about to: +- Implement workout ingest from HealthKit on iOS +- Hit a 413 from `/api/workouts/batch` + +### `w16c-pr-detection.md` — 2501 words — current + +PR detection research (W16c). The warm-up gate, the 6 workout slots, the silent-on-historical-backfill rule, the tie-handling logic. + +Read this if iOS-Claude is about to: +- Build the PR list screen +- Wire APNs for `PERSONAL_RECORD_ACHIEVED` notifications + +### `w17b-c-withings-activity-sleep.md` — 2653 words — current + +Withings v2 spec (activity sync + sleep stage segments). Covers the webhook subscription channels, the OAuth scope upgrade banner, and the hourly fallback cron schedule. + +Read this if iOS-Claude is about to: +- Surface the Withings scope upgrade banner on iOS +- Wire the Withings reconnect deep-link return + +--- + +## § 4 — Pre-tag W21 review findings (current v1.4.25 state) + +The 8-reviewer parallel pass that ran before the v1.4.25 release-merge. Each finding file is the unfiltered output of one reviewer; the consolidated reconcile plan is the canonical "what's still pending in v1.4.26". + +### `w21-code-review-findings.md` — 2340 words + +Audited: bug-spotting across the v1.4.25 diff. Caught the `endedAt < startedAt` workout-gate gap, the inventory state-machine bypass, and several quiet edge cases. + +Still pending in v1.4.26: +- Light-severity items deferred per the rubric (see reconcile plan) +- Refactor recommendations tagged as M-low priority + +### `w21-security-findings.md` — 2358 words + +Audited: secret leaks, auth boundary, injection vectors. Caught the critical Withings webhook secret-in-logs leak (Fix-J in the reconcile) + the GLP-1 endpoint missing Zod/audit/rate-limit (Fix-K). + +Still pending in v1.4.26: +- Medium-severity dependency upgrade recommendations +- Long-term auth-token rotation policy review + +### `w21-design-findings.md` — 2023 words + +Audited: touch targets ≥ 44×44 px, palette consistency, contrast WCAG-AA. Caught onboarding wizard buttons below floor, range-bar palette drift, PR-badge dark-mode contrast. + +Still pending in v1.4.26: +- Low-severity visual polish items (already documented as deferred) + +### `w21-senior-dev-findings.md` — 2466 words + +Audited: architecture + future-proofing. Flagged the `withRateLimit` positional-args drift (now option-bag), the `safeRequestProp` narrow-catch surface, several abstraction-needed surfaces. + +Still pending in v1.4.26: +- Module boundary cleanup recommendations +- Test infrastructure improvements + +### `w21-simplifier-findings.md` — 3340 words + +Audited: dead code, redundant abstractions. Caught the `goalsStorageKey` orphaned export + the `GoalsChipPicker` localStorage block with no consumer. + +Still pending in v1.4.26: +- Lower-priority dead-code removals deferred to next cleanup pass + +### `w21-product-lead-assessment.md` — 3394 words + +Audited: user-facing acceptance, copy quality, flow polish. Caught medication history page hard-coded "Zurück" back-button (now `t()`-routed), several copy polish items. + +Still pending in v1.4.26: +- Copy-tone refinements scoped as Low +- UX flow simplifications scoped as Medium-defer + +### `w21-i18n-runtime-findings.md` — 2624 words + +Audited: hard-coded strings, locale-bundle parity. Caught the medication history "Zurück" leak + several runtime-translatable-but-static strings. + +Still pending in v1.4.26: +- Long-tail copy that survives in admin-only surfaces +- Locale-pluralisation edge cases for non-DE/EN + +### `w21-dead-code-findings.md` — 2841 words + +Audited: unused exports, unreferenced files. Caught the same `goalsStorageKey` issue (cross-referenced with simplifier), plus older onboarding-spec leftovers. + +Still pending in v1.4.26: +- Confirmed-dead items waiting for the v1.4.26 cleanup pass + +--- + +## § 5 — Older review findings (W10 baseline) + +Historical context — the pre-v1.4.20 review pass that established the W21-style 8-reviewer pattern. + +| File | Words | Audited | +| --- | --- | --- | +| `w10-code-review-findings.md` | 1990 | First-pass bug audit | +| `w10-security-review-findings.md` | 1465 | First-pass security audit | +| `w10-design-review-findings.md` | 1521 | First-pass design audit | +| `w10-senior-dev-findings.md` | 1974 | First-pass architecture audit | +| `w10-simplifier-findings.md` | 1873 | First-pass simplification audit | +| `w10-product-lead-assessment.md` | 2592 | First-pass UX audit | +| `w10-i18n-runtime-gaps.md` | 1949 | i18n runtime probe baseline | +| `w10-dead-code-candidates.md` | 2124 | Dead-code baseline | +| `w10-reconcile-A-design-report.md` | 973 | Reconcile fix-out for design | +| `w10-reconcile-B-architecture-report.md` | 991 | Reconcile fix-out for architecture | +| `w10-reconcile-C-security-auth-report.md` | 715 | Reconcile fix-out for security/auth | +| `w10-reconcile-D-simplifier-report.md` | 479 | Reconcile fix-out for simplifier | + +These are historical — every must-fix item from the W10 pass landed across v1.4.20-v1.4.24. Read only when investigating the historical reasoning behind a v1.4.20-v1.4.24 change. + +--- + +## § 6 — The W21 reconcile plan + +### `.planning/phase-W21-reconcile-plan.md` — outside `research/` + +The canonical post-W21 reconcile inventory + the 7-Fix-* surface map. Collates 103 findings (2 Critical + 20 High + 41 Medium + 40 Low) from the 8 reviewers into 7 touch-disjoint Fix-* surfaces totalling ~22 commits. + +The 7 Fix-* surfaces (each touch-disjoint): + +| Surface | Scope | Severity | +| --- | --- | --- | +| **Fix-J** | Withings webhook log redaction (`src/lib/logging/`) | Critical | +| **Fix-K** | GLP-1 endpoint hardening + legacy webhook EOL counter | High | +| **Fix-L** | Design polish (touch targets, palette, contrast, dialog footer, native selects, back-button i18n) | High + Medium | +| **Fix-M** | Inventory state-machine + workout schema gate | Medium | +| **Fix-N** | `withRateLimit` option-bag standardisation + helper extraction | Medium | +| **Fix-O** | Audit-log retention + geo enrichment cleanup | Medium | +| **Fix-P** | Dead-code sweep + simplifier follow-ups | Medium | + +**iOS-Claude implication**: every item in the reconcile plan that touches a contract is already documented in `08-locked-contracts.md`. The reconcile plan itself is internal-only — never published. Read it BEFORE tagging v1.5.x to understand what v1.4.26 will likely close. + +--- + +## § 7 — Research-first directive (the meta rule) + +Marc-memory `feedback_research_before_complex_features`: + +> AI / insights / chart work runs through a research agent first. The agent benchmarks Apple Health / Withings / Oura, proposes an ecosystem-fit plan, then the implementation phase starts. + +For iOS-Claude this means: before any of these touches code, a `.planning/research/` note must exist: + +- New Coach surface or refusal change +- New Insight type or sub-page +- New chart family (sparkline, range bar, scatter, etc.) +- New ingest source (e.g. Garmin, Fitbit, Strava) +- New medical-adjacent feature (anything PK / dose / drug-interaction / projection) + +If the relevant note doesn't exist, file one before writing the iOS code. The research-first rule is the single most reliable predictor of which v1.X.Y features ship cleanly and which need an emergency follow-up patch. + +--- + +## § 8 — How to write a new research note + +Format convention (mirrors the existing notes): + +```markdown +# Research — v1.5.X + +**Scope.** One sentence on what's in / what's out. +**Read-only.** No code commits, no schema commits. +**Date.** YYYY-MM-DD +**Author.** Marc Bombeck + +## TL;DR +<2-3 sentence summary of the recommendation> + +## Section 1 — Ecosystem inventory + + +## Section 2 — Clinical / regulatory foundation (when relevant) + + +## Section 3 — App teardowns + + +## Section 4 — Boundary analysis (when relevant) + + +## Section 5 — Implementation outline + + +## Section 6 — Recommendations +<3-5 numbered recommendations the Marathon can dispatch from> + +## Section 7 — Do-not-builds + +``` + +Word-count target: 2000-5000 for a single-feature note, up to 12000 for a multi-feature deep-dive (like `glp1-feature-inspiration.md`). + +--- + +## § 9 — STOP HERE if… + +| If your task is… | …skip the rest and read… | +| --- | --- | +| Just need a contract refresher | `08-locked-contracts.md` | +| Just need the Coach pipeline | `14-coach-mental-model.md` | +| Just need to call an endpoint | `03-api-contracts.md` | +| Tagging v1.5.x | `09-recommended-flow.md` § 4 (marathon-pattern handoff) + `09-recommended-flow.md` § 5 (Quality Gate) | + +Otherwise: you have read every file in the v1.5 iOS handoff doc-pack. Marc UAT awaits.