mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
Six Apple Health signals leave the deferred list and ride POST /api/measurements/batch and the export.zip import end to end: EXERCISE_MINUTES, STAND_MINUTES and STAND_HOURS (the exercise and stand rings), MINDFUL_MINUTES, STATE_OF_MIND_VALENCE (iOS 18 State of Mind, -1..1) and AFIB_BURDEN (AFib History, percent of the week). The ring types and mindful minutes are cumulative, so the daily rollup is their sum; AFib burden is scaled strictly from Apple's fraction and a value above 1 is refused rather than guessed at. The archive import takes mindful minutes from the session's dates and skips idle stand hours. Every registry carries them: units, ranges, labels in all locales, categories, chart tokens, report catalogue (mindful minutes and the valence fenced as sensitive), signal registry with FHIR codings, and MCP exposure for the three ring types only. Workout GPS tracks are now an opt-in, off by default (PATCH /api/auth/me/workout-routes, a switch on Data & Privacy). While it is off, the workout batch route and the export import keep every workout and drop its track with the same per-entry status. With it on, the import reads the referenced GPX files from the archive one bounded member at a time, thins each to the point cap and stores it sealed. The climb a track carries is written to Workout.elevationM at write time, so the workout insight no longer opens the track, and the AI egress guard now freezes the set of files that read route geometry.
38 lines
1.4 KiB
TypeScript
38 lines
1.4 KiB
TypeScript
/**
|
|
* Metres climbed, derived from a workout's GPS track at write time.
|
|
*
|
|
* The workout batch route fills `Workout.elevationM` from a kept track when
|
|
* the source sent no elevation of its own, so the figure lives on the
|
|
* workout row. Readers, the workout insight among them, take the column and
|
|
* never open the track: no AI path reads route geometry (the egress guard
|
|
* freezes the readers).
|
|
*/
|
|
|
|
/**
|
|
* Total metres climbed, summed from a GeoJSON LineString's altitude channel.
|
|
*
|
|
* Only positive deltas count — this is climb, not net elevation change, which
|
|
* is the figure a rider recognises. Returns null when the geometry carries no
|
|
* altitude channel at all (Withings ships static GPX without one), so the
|
|
* caller can fall back to the denormalised column instead of reporting a
|
|
* confident zero.
|
|
*/
|
|
export function routeClimbM(geometry: unknown): number | null {
|
|
if (!geometry || typeof geometry !== "object") return null;
|
|
const coords = (geometry as { coordinates?: unknown }).coordinates;
|
|
if (!Array.isArray(coords)) return null;
|
|
|
|
let climb = 0;
|
|
let previous: number | null = null;
|
|
let seen = 0;
|
|
for (const point of coords) {
|
|
if (!Array.isArray(point) || point.length < 3) continue;
|
|
const alt = point[2];
|
|
if (typeof alt !== "number" || !Number.isFinite(alt)) continue;
|
|
seen++;
|
|
if (previous !== null && alt > previous) climb += alt - previous;
|
|
previous = alt;
|
|
}
|
|
return seen >= 2 ? Math.round(climb) : null;
|
|
}
|