Skip to content

[P1/high] feat(web): terms-clean health-capture foundation (Capacitor + HealthSource seam) — vires-ops#37#71

Draft
cipher813 wants to merge 3 commits into
mainfrom
feat/health-capture-capacitor-37
Draft

[P1/high] feat(web): terms-clean health-capture foundation (Capacitor + HealthSource seam) — vires-ops#37#71
cipher813 wants to merge 3 commits into
mainfrom
feat/health-capture-capacitor-37

Conversation

@cipher813

Copy link
Copy Markdown
Contributor

Priority: P1 · Complexity: high

Part of the route-tracking arc (vires-ops EPIC #34); implements the buildable, terms-clean foundation of vires-ops#37 (automatic ruck capture). Not Closes — the native capture core is out-of-CI (see Remaining below).

What this delivers

Health capture is built at the correct architectural layer: just another route-derivation mode, reusing the existing SI RouteStatsapplyStats pipeline rather than a parallel path.

  • web/capacitor.config.ts — wraps the existing Vite PWA (webDir: dist); the PWA still builds/runs standalone. Adds @capacitor/core (dep) + @capacitor/cli (dev).
  • web/src/lib/health.ts — the TypeScript seam: HealthSource interface, an SI normalizer for untrusted native records (normalizeWorkout), a workout → RouteStats projection, a Capacitor-native source that bridges a Health plugin, and an unavailable web fallback selected by Capacitor.isNativePlatform().
  • web/src/components/RouteCapture.tsx — a native-only Health route mode listing recent hike/walk workouts; on selection it prefills distance/elevation/duration through the same applyStats used by trail/draw/GPX. The tab is hidden unless healthSource.isAvailable(), so the standalone web PWA is byte-unaffected.
  • routeMode.ts / api.ts / api/schemas/workout.py — new health RouteSource; the backend RouteSource literal is widened to accept it (a 1-value enum change, not a load-path change — SI conversion + metabolic_cost_kj are identical regardless of source, so POST /api/workouts/activity behavior is unchanged).

Note on issue anchors

The issue references RuckForm.tsx / logRuck / POST /api/workouts/ruck; those were refactored into the generic ActivityForm.tsx / RouteCapture.tsx / logActivityPOST /api/workouts/activity seam. This PR targets the real code. The issue also asks for read-only prefill; the established route-capture UX prefills editable fields ("edit anything that looks off") — kept consistent here.

Validated locally (green)

  • Frontend (Node 20.19): npm run build ✓, npm run lint (oxlint) ✓, npm run test (vitest) ✓ — 131 pass, +7 new (health.test.ts).
  • Backend (Py 3.12): ruff check ✓, pytest ✓ — 365 pass, +1 new (test_activity_accepts_health_route_source).

Remaining — why DRAFT (out of this environment)

The actual on-device capture cannot be built or validated on a headless Linux box; it needs Apple/Android tooling + a device:

  1. npx cap add ios / npx cap add android native project generation.
  2. The Health plugin native code — a HealthKit workout query (iOS) + a Health Connect read (Android), mapping to the SI { distanceM, elevationGainM, durationS } this seam already consumes.
  3. On-device permission prompt + reading real workouts.

To validate the buildable part: cd web && npm ci && npm run build && npm run lint && npm run test (Node ≥20.19) and, from the repo root, pytest.

groom-bot and others added 2 commits July 2, 2026 16:27
…pluggable HealthSource + Import-from-Health (vires-ops#37)

Lays the terms-clean automatic-capture architecture that replaces the
Strava path (vires-ops#36, closed for its AI-use prohibition): read the
user's own on-device hike/walk workouts from Apple HealthKit / Android
Health Connect, so there is no third-party terms restriction on feeding
the coach (unblocks load-accounting).

Architecture (built at the correct layer — health capture is just another
route-derivation mode, reusing the existing SI RouteStats pipeline):

- web/capacitor.config.ts: wraps the existing Vite PWA (webDir=dist); the
  PWA still builds/runs standalone. Adds @capacitor/core (dep) + cli (dev).
- web/src/lib/health.ts: the TypeScript seam — HealthSource interface, an
  SI normalizer (untrusted native records -> HealthWorkout), a
  workout->RouteStats projection, a Capacitor-native source (bridges the
  `Health` plugin) and an unavailable web fallback selected by platform.
- web/src/components/RouteCapture.tsx: a native-only "Health" route mode
  that lists recent workouts and prefills distance/elevation/duration via
  the SAME applyStats path; tab hidden unless a health source isAvailable(),
  so the standalone PWA is unaffected.
- routeMode/api: `health` RouteSource; backend RouteSource literal widened
  to accept it. The load path (SI conversion + metabolic cost) is identical
  regardless of source — no change to POST /api/workouts/activity behavior.

Validated locally: web build + oxlint + vitest (131 pass, +7 new);
backend ruff + pytest (365 pass, +1 new). The native ios/android projects
and the HealthKit / Health Connect plugin native code require Xcode / the
Android SDK + a device and are the remaining out-of-CI work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cipher813 cipher813 added the gate:decision Draft: awaiting a human merge/scope decision (no external blocker) label Jul 3, 2026
@ne-groomer ne-groomer Bot added gate:live-run and removed gate:decision Draft: awaiting a human merge/scope decision (no external blocker) labels Jul 8, 2026
@cipher813 cipher813 added gate:device Draft: needs on-device/hardware validation not available in CI and removed gate:live-run labels Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate:device Draft: needs on-device/hardware validation not available in CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant