mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
feat(api): nullable deviceType on Measurement for two-axis source priority
Add an optional deviceType field to the POST /api/measurements/batch
payload schema. The iOS client maps each sample's HKDevice.model to a
canonical device class (watch | band | ring | phone | scale | other |
unknown); the server stores it on a new Measurement.device_type
column. The cross-source canonical picker (W8c) reads this column to
break Apple-Watch-vs-iPhone ties when the same source contributed
multiple devices for the same day.
Backward-compatible end-to-end:
- Schema field is optional; legacy iOS builds keep working unchanged.
- DB column is nullable; the migration is an additive ALTER TABLE
(Migration 0051, next free per v1.4.25 handoff section 4).
- The picker treats null as 'unknown' and only uses it as a
tiebreaker when a ranked device-type coexists in the bucket; with
a single source + null deviceType the row passes through as today.
OpenAPI: deviceType is documented on AppleHealthBatchEntry with
guidance on the HKDevice.model mapping. Batch endpoint description
gains a W8c note. iOS-Claude can lock the field name + enum from the
spec without re-reading source.
Tests: 45/45 source-priority tests pass; the batch route's existing
integration tests are untouched by the schema addition.
Pre-existing lint failure unrelated to this change:
src/components/i18n/maintainership-banner.tsx triggers a
react-hooks/set-state-in-effect warning; the file is currently
untracked and lives outside this Wave's scope.
This commit is contained in:
1 parent
27f06c1576
commit
024845e5bd
4 files changed
+52
-2
No files matched your search
@@ -34,6 +34,7 @@ import {
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import { mapAppleHealthEntry } from "@/lib/measurements/apple-health-mapping";
|
||||
import { validateMeasurementRange } from "@/lib/validations/measurement";
|
||||
import { deviceTypeEnum } from "@/lib/validations/source-priority";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
|
||||
const MAX_BATCH_ENTRIES = 500;
|
||||
@@ -47,6 +48,14 @@ const batchEntrySchema = z.object({
|
||||
sleepStage: z.number().int().min(0).max(20).optional(),
|
||||
externalId: z.string().min(1).max(120),
|
||||
externalSourceVersion: z.string().min(1).max(120).optional(),
|
||||
// v1.4.25 W8c — optional device-type tag. The iOS client maps the
|
||||
// `HKDevice.model` of each sample to one of the canonical device
|
||||
// classes (watch | band | ring | phone | scale | other | unknown).
|
||||
// Backward-compatible: every pre-W8c iOS build skips the field and
|
||||
// the row is stored with `deviceType = null`; the canonical picker
|
||||
// treats null as `unknown` and only uses it as a tiebreaker when a
|
||||
// ranked device-type coexists in the same daily bucket.
|
||||
deviceType: deviceTypeEnum.nullable().optional(),
|
||||
});
|
||||
|
||||
const batchPayloadSchema = z.object({
|
||||
@@ -156,6 +165,11 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
externalId: entry.externalId,
|
||||
externalSourceVersion: entry.externalSourceVersion ?? null,
|
||||
sleepStage: mapped.sleepStage ?? null,
|
||||
// v1.4.25 W8c — pass the iOS-supplied device-type through to
|
||||
// the row. Stays null for pre-W8c iOS builds; the canonical
|
||||
// picker treats null as `unknown` so legacy ingest keeps
|
||||
// working without a server-side default.
|
||||
deviceType: entry.deviceType ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user