mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Six provider syncs returned a bare count, which narrowed their failures away: a Nightscout window in which every SGV row was refused by a constraint returned the same 0 as a quiet night, and an Oura or Polar run whose collections all failed returned 0 too. The routes then answered 200 with a count and the settings card drew a green tick over it. Every sync now reports what it imported AND whether any part of it did not land, and the route resolves the outcome server-side so the web card and the native client cannot disagree. Google Health keeps its 502 for a run that failed and wrote nothing, but a run that failed after writing some resources is now a partial rather than a discarded result. The labs OCR commit carries the same resolved outcome.
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { apiSuccess, apiError } from "@/lib/api-response";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { resolveSyncOutcome } from "@/lib/outcome/written-outcome";
|
|
import { syncUserOura } from "@/lib/oura/sync";
|
|
|
|
/**
|
|
* Manually trigger an Oura sync for the current user (v1.32.28).
|
|
*
|
|
* Until now Oura only ever synced on its hourly cron tick, so a user who could
|
|
* see something was behind had no way to ask for a fresh pull.
|
|
*
|
|
* No body and no `fullSync` flag: `syncUserOura` has no full-history arm, and
|
|
* the window is not the caller's to choose — the sync derives it from the
|
|
* newest row it already holds, which is what makes a catch-up honest. Accepting
|
|
* a `lookbackDays` here would hand an unvalidated cost knob to the client.
|
|
*
|
|
* Rate-limited 5/60s per user, matching the hardened Fitbit / Google Health
|
|
* routes; a sync is a fan-out of upstream requests plus a batched write, so a
|
|
* tight retry loop must not be able to pin Prisma or burn the Oura budget.
|
|
*/
|
|
export const POST = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
annotate({ action: { name: "oura.sync" } });
|
|
|
|
const rl = await checkRateLimit(`oura-sync:${user.id}`, 5, 60_000);
|
|
if (!rl.allowed) {
|
|
return apiError("Too many sync requests", 429, {
|
|
errorCode: "rate_limited_self",
|
|
});
|
|
}
|
|
|
|
try {
|
|
const result = await syncUserOura(user.id);
|
|
return apiSuccess(resolveSyncOutcome(result));
|
|
} catch {
|
|
// The failure is already on the `oura` ledger, classified by the provider
|
|
// that owns the error shape. Answer generically — an upstream message can
|
|
// carry detail that has no business in a response body.
|
|
return apiError("Oura sync failed", 502);
|
|
}
|
|
});
|