mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Refs #778. Two honesty gaps from the first-run report: the web onboarding presented Apple Health like a web connector although the connection only completes in the iPhone app, and the initial backfill gave no sign of progress, so a working first sync looked broken. The onboarding source step now says plainly that Apple Health is set up in the iPhone app after onboarding, without dead-ending the flow. The integration card gains a server-authoritative progress summary — rows accepted, oldest reading reached, and an honest flowing/waiting line derived from the last accepted batch. Nothing the server does not know is shown: no percentage, no ETA, no invented throttle state; a quiet stretch reads as waiting for the phone, not as an error.
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/**
|
|
* First-run Apple Health sync progress (issue #778).
|
|
*
|
|
* The iOS app drives the HealthKit backfill; the server only ever sees the
|
|
* batches that actually arrive. During a first run nothing on the web said
|
|
* how far that had come, so a backfill that was quietly working looked
|
|
* broken. This module computes the figures the server genuinely holds — how
|
|
* many rows carry the `APPLE_HEALTH` source and how far back in time they
|
|
* reach — and nothing more: the phone's throttle state, its remaining queue,
|
|
* and any ETA live on the device and are deliberately not invented here.
|
|
*
|
|
* Both the live batch ingest and the one-shot export.zip import write
|
|
* `source = APPLE_HEALTH`, so the count is "Apple Health records in
|
|
* HealthLog", not "records from one specific pipe" — which is the number a
|
|
* user watching a first run actually wants to see grow.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
|
|
export interface AppleHealthSyncProgress {
|
|
/** Measurement + workout rows carrying the `APPLE_HEALTH` source. */
|
|
recordsAccepted: number;
|
|
/**
|
|
* Earliest instant reached (`measuredAt` for measurements, `startedAt` for
|
|
* workouts), as an ISO string; null when nothing has arrived.
|
|
*/
|
|
oldestMeasuredAt: string | null;
|
|
}
|
|
|
|
/**
|
|
* Two grouped aggregates (count + MIN), mirroring the freshness reads in
|
|
* `./metric-freshness.ts` which cover the same two tables for the same
|
|
* reason: workouts are Apple Health data too, but never `Measurement` rows.
|
|
*/
|
|
export async function getAppleHealthSyncProgress(
|
|
userId: string,
|
|
): Promise<AppleHealthSyncProgress> {
|
|
const [measurements, workouts] = await Promise.all([
|
|
prisma.measurement.aggregate({
|
|
where: { userId, deletedAt: null, source: "APPLE_HEALTH" },
|
|
_count: { _all: true },
|
|
_min: { measuredAt: true },
|
|
}),
|
|
// `Workout` rows are hard-deleted, so there is no tombstone predicate.
|
|
prisma.workout.aggregate({
|
|
where: { userId, source: "APPLE_HEALTH" },
|
|
_count: { _all: true },
|
|
_min: { startedAt: true },
|
|
}),
|
|
]);
|
|
|
|
const minima = [measurements._min.measuredAt, workouts._min.startedAt].filter(
|
|
(d): d is Date => d instanceof Date,
|
|
);
|
|
const oldest =
|
|
minima.length > 0
|
|
? new Date(Math.min(...minima.map((d) => d.getTime())))
|
|
: null;
|
|
|
|
return {
|
|
recordsAccepted: measurements._count._all + workouts._count._all,
|
|
oldestMeasuredAt: oldest?.toISOString() ?? null,
|
|
};
|
|
}
|