mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Extend the Fitbit/Google Health provider with the read path: the dataPoints.list client half, the metrics-bundle sync, the poll + backfill jobs, and the connection-management routes. Builds on the existing OAuth + credentials + token-refresh layer. - client: paginated `fetchDataPoints` walker over `safeFetch` keyed on `nextPageToken`, encoding each data type kebab-case in the path and snake_case in the filter predicate; defensive per-type mappers (finite + positive guards) for weight, body fat, daily SpO2, daily HRV, daily resting heart rate, daily respiratory rate, intraday heart rate, sleep-temperature derivations, and a height profile-seed extractor. `FITBIT_FIELD_MAP` + `mapping.md` are the source of truth. - mapping (no new MeasurementType): weight→WEIGHT, body-fat→BODY_FAT, SpO2→OXYGEN_SATURATION, HRV→HEART_RATE_VARIABILITY (SDNN slot, not HRV_RMSSD), resting-HR→RESTING_HEART_RATE, respiratory-rate→RESPIRATORY_RATE, heart-rate→PULSE, sleep-temp→WRIST_TEMPERATURE, height→User.heightCm when null. Steps/distance/energy/floors/VO2 + sleep + workouts stay for a later wave. - sync: `syncUserFitbit` orchestrator with the reauth short-circuit, the per-data-class 403 soft-skip (the six Restricted bundles grant independently), the all-soft-skipped success guard, and incremental-vs-full windows; `upsertFitbitMeasurements` keyed (userId,type,FITBIT,externalId) with a 24 h overlap for after-the-fact daily re-rolls, recomputing rollups and invalidating status caches. Google refresh tokens do not rotate, so the stored refresh token is kept unless a new one is returned. - jobs: hourly `fitbit-sync` poll (poll-only; Pub/Sub deferred), the self-converging boot backfill, and the daily OAuth-state sweep — every queue registered in both `allQueues` and a `boss.work` binding, staggered off the WHOOP/Withings crons. - routes: status / sync / disconnect mirroring the WHOOP shape. Tests cover the mappers, the casing invariant, `fetchDataPoints`, the orchestrator soft-skip guard, the idempotent upsert, the backfill discovery, and the queue registration; an integration test pins the upsert idempotency, the DAY rollup fold, and the Fitbit-vs-Withings per-source no-collapse against the Postgres testcontainer.
28 lines
914 B
TypeScript
28 lines
914 B
TypeScript
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { apiSuccess } from "@/lib/api-response";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { syncUserFitbit } from "@/lib/fitbit/sync";
|
|
import { NextRequest } from "next/server";
|
|
|
|
/**
|
|
* Manually trigger a Fitbit / Google Health sync for the current user (v1.12.0).
|
|
*
|
|
* Mirrors the WHOOP manual-sync route: incremental by default, full history when
|
|
* `{ fullSync: true }` is posted.
|
|
*/
|
|
export const POST = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "fitbit.sync" } });
|
|
|
|
let fullSync = false;
|
|
try {
|
|
const body = await request.json();
|
|
fullSync = body?.fullSync === true;
|
|
} catch {
|
|
// no body provided -> default incremental sync
|
|
}
|
|
|
|
const imported = await syncUserFitbit(user.id, { fullSync });
|
|
return apiSuccess({ imported, fullSync });
|
|
});
|