mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Mostly table column normalisation under docs/audit/ + planning, object- literal expansion in src/lib/ai/coach/snapshot.ts after the W4 F6 Apple Health metric block landed, and small wrap/unwrap line adjustments in the device + APNs routes / tests touched across W3/W4/W5 sessions. No behavioural change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.0 KiB
9.0 KiB
Wave 2 — v1.4.23 backend foundation report
Status: shipped on develop (2026-05-11).
Scope: F1 (MeasurementType extension), F2 (MeasurementSource
expansion), F3 (POST /api/measurements/batch) plus the
analytics-side sleep-stage aggregation the new schema unblocks.
What landed
Six atomic commits
- **
feat(schema)— Apple Health measurement types + sleep stages- dedup index.**
prisma/schema.prismagains seven additiveMeasurementTypevalues,APPLE_HEALTHjoinsMeasurementSource, a newSleepStageenum mirrors the existingGlucoseContextpattern, andMeasurementpicks up nullablesleepStage+externalSourceVersioncolumns plus a composite unique index(user_id, type, source, external_id). Migration0036_apple_health_measurement_typesis strictly additive — no row mutations.
- dedup index.**
feat(measurements)— Apple Health identifier mapping helper. Newsrc/lib/measurements/apple-health-mapping.tstranslates each HealthKit identifier into the row shape theMeasurementmodel expects. Mirrors the WithingsMEASURE_TYPE_MAPbut keyed by string identifier. Unit conversion (0..1 fraction → percent for oxygen saturation and body fat) sits per-entry next to the type. 16 unit tests, including the sleep-stage codepoint map.feat(api)— batch measurement ingest for iOS Apple Health.POST /api/measurements/batchaccepts ≤500 entries per call, wraps inwithIdempotency()for HTTP-level retry safety, and relies on the composite unique index for per-entry dedup. Returns per-entry status (inserted/duplicate/skipped) so the iOS client can advance its sync cursor accurately. Six integration tests undertests/integration/measurements-batch.test.ts.feat(ui)— Apple Health source badge + restore-route enum sync. Dracula-pink chip in the measurements list,measurements.sourceAppleHealthtranslation keys (EN + DE), admin backup-restore guard updated for the new enum values. Three regression-guard tests for the badge.feat(analytics)— per-stage sleep aggregation./api/analyticsaggregates SLEEP_DURATION rows per Berlin day before summarising so multi-stage nights surface as one datapoint. New optionalsleepStagesblock exposes the per-stage minute breakdown over the trailing 30 days. Two integration tests.docs(planning)— STATE.md tick + this report.
Test count delta
- Unit tests: 2111 → 2130 (+19; 16 mapping + 3 badge regression).
- Integration tests: 89 → 97 (+8; 6 batch ingest + 2 sleep stage).
pnpm typecheck,pnpm lint,pnpm test --run, andpnpm test:integrationare all green between every commit.
Migration name + SQL summary
prisma/migrations/0036_apple_health_measurement_types/migration.sql.
ALTER TYPE measurement_type ADD VALUE IF NOT EXISTS …× 7 (the seven new HealthKit-shaped values).ALTER TYPE measurement_source ADD VALUE IF NOT EXISTS 'APPLE_HEALTH'.CREATE TYPE sleep_stage AS ENUM (…).ALTER TABLE measurements ADD COLUMN external_source_version TEXT, ADD COLUMN sleep_stage sleep_stage(both nullable).- CHECK constraint scoping
sleep_stageto SLEEP_DURATION rows (mirrors the glucose_context constraint from migration 0021). CREATE UNIQUE INDEX measurements_user_id_type_source_external_id_key ON measurements (user_id, type, source, external_id)— Apple Health batch dedup key. Postgres treats NULL externalId rows as distinct, so manual entries don't collide.
The legacy (user_id, type, measured_at, source) unique index stays
in place as the manual-UI "no duplicate at the same wall-clock"
guard.
W1 recommendations adopted verbatim
| W1 recommendation | Status |
|---|---|
Source enum value = APPLE_HEALTH (not HEALTHKIT) |
Adopted |
Reuse existing ACTIVITY_STEPS for HK step count |
Adopted |
Reuse existing SLEEP_DURATION, shift unit hours → minutes |
Adopted |
Per-stage sleep rows with new sleep_stage column |
Adopted |
apple-health-mapping.ts keyed by HK string identifier |
Adopted |
| Apple ships 0..1 fraction for SpO2 + body fat → × 100 | Adopted |
BODY_TEMPERATURE enum value alongside the other 6 |
Adopted |
Decisions made beyond W1's scope
- Per-entry status as
inserted | duplicate | skippedwith a documentedreasonfield for skips (unmappable_identifier,value_out_of_range). The iOS client checkpoints pastinsertedANDduplicateand surfacesskippedfor diagnostics. W1 didn't pin the exact wire shape; I picked the names that read clearest in the iOS-client perspective. - Plausibility-range guard runs per-entry, never fails the batch.
A single bogus sensor reading becomes a
skippedentry rather than crashing the whole upload. The validator inherits HealthLog's existingvalidateMeasurementRange()thresholds; new ranges were added for HRV (1–200 ms), resting HR (25–220 bpm), active energy (0–10 000 kcal), flights (0–1000), distance (0–200 km), VO2 max (5–100 mL/(kg·min)), body temperature (28–45 °C). - Skip-duplicates reconciliation when
createManyraces a concurrent retry. The endpoint performs a pre-flight existence check, then usescreateMany({ skipDuplicates: true }). If the count returned bycreateManydoesn't match the planned-insert count (a race), the endpoint re-reads the DB and downgrades the affected per-entry statuses frominsertedtoduplicateso the response stays truthful. sleepStagesblock returnsnullwhen the user has no stage-tagged rows rather than an empty object. The UI consumer can then keep painting the totals-only path verbatim.
What I couldn't ship and why
Nothing in scope was dropped. Three adjacent items are explicitly deferred per the brief:
- APNs scaffolding (Wave 3 F4) stays untouched here — the brief
noted I should be aware of the
apnsEnvironmentdecision but not implement it. - OpenAPI generator + Coach schema extension (Wave 4 F5/F6) —
outside this wave's scope. F6 wants the new metric tokens in the
sourceMetricenum;chart-tokens.tsalready accepts them so the AI surface can reference them, but the Coach prompt + provenance ratchet ships in W4. - Doctor PDF rendering for the new HealthKit metrics —
intentionally excluded for v1.4.23 (see
measurement-type-enum-coverage.test.ts); ships in v1.5 alongside the iOS app's first paying-customer sync once layout + reference ranges are agreed.
Open questions for the maintainer about the iOS DTO contract
- DTO rename
HEALTHKIT→APPLE_HEALTH. Server-side is now locked. The iOS repo (~/Projects/healthlog-iOS/) needs theMeasurementDTO.swiftenum value rename in lock-step. The maintainer's encoded sim payloads should be cleared after the rename (one-line in the iOS dev README). Confirm the maintainer will land this in the same release window — once a TestFlight build ships, the rename becomes a breaking change. externalIdshape. Server treats it as an opaque string ≤120 chars. The iOS contract should pinHKSample.uuid.uuidStringas the value, NOT a synthesised "type-id::date" composite — otherwise two iOS devices syncing the same Apple-Cloud sample would generate differentexternalIds and the dedup index can't collapse them.sleepStagecodepoint domain. Server accepts integers 0–20 to leave headroom for Apple's future additions; the documented iOS-16+ codepoints are 0–5. If the iOS DTO ships an enum, confirm it serialises as the numeric codepoint rather than a string label so the server-sideAPPLE_HEALTH_SLEEP_STAGE_MAPdoes the translation.- Pre-conversion vs post-conversion on the wire. Server currently expects Apple's native units verbatim (0..1 fraction for SpO2 / body fat, kcal for active energy, etc.) and converts to DB units inside the mapping. Confirm the iOS app does NOT pre-convert on its side — otherwise we double-multiply and SpO2 lands at 9 700.
- Behaviour when iOS pushes a sample with a HealthKit identifier
the server doesn't know yet (e.g. a hypothetical new metric Apple
adds in iOS 18). Today the server marks the entry as
skipped/unmappable_identifier. Does the iOS app drop those samples from its sync cursor (one-shot retry on next server release with the new mapping), or does it park them for re-upload after the next app update? Recommendation: park-for-retry so a server-side mapping addition automatically backfills.
Verification commands
pnpm typecheck # green
pnpm lint # green (only pre-existing warnings)
pnpm test --run # 2130 passed (was 2111)
pnpm test:integration # 97 passed (was 89)
Six commit SHAs on develop are visible via git log origin/develop..HEAD once pushed (W2 sequence is 95209eb, 30c0f76,
5cc0a00, 86a80c5, 5470649 + this docs commit).