fix(whoop): a dead token no longer reports a successful sync

Port the hard-fail ledger the Fitbit transport already carries. A revoked WHOOP
grant makes every resource short-circuit and return zero rows without throwing,
so the cycle read as clean: it stamped success, un-parked the reconnect state,
reset the failure streak, and the hourly cohort hammered the dead refresh token
on every tick. Record the three dead-token null-return paths on the ledger and
fold that verdict into both drivers — the full orchestrator and the per-resource
cron path that runs most often — so a dead-token cycle refuses the success
stamp and the park holds.

Return an imported-plus-failed verdict from the orchestrator and gate the
history backfill on it: an interrupted full-history walk now throws for a
pg-boss retry instead of being stamped complete, and a connection parked at
reauth returns without burning the retry budget. Thread the same verdict into
the one-shot sleep-timeline backfill.

Also catch a write-path throw on both drivers so a persistent measurement-write
failure surfaces on the status ledger instead of leaving a stale-green pill, and
widen the cycle overlap to the seven-day re-score window the other collections
use so a late-revised day's strain and energy are re-fetched rather than
stepped past.
This commit is contained in:
Marc-André Bombeck committed 2026-07-24 20:05:41 +02:00
1 parent 8be7458c7a
commit 9a69e4e557
14 files changed
+672 -65

No files matched your search

@@ -30,7 +30,7 @@ describe("POST /api/whoop/sync", () => {
beforeEach(() => vi.clearAllMocks());
it("triggers an incremental sync by default", async () => {
sync.mockResolvedValue(7);
sync.mockResolvedValue({ imported: 7, failed: false });
const res = (await (
POST as unknown as (r: NextRequest) => Promise<{ data: unknown }>
)(req({}))) as { data: { imported: number; fullSync: boolean } };
@@ -40,7 +40,7 @@ describe("POST /api/whoop/sync", () => {
});
it("honours fullSync: true", async () => {
sync.mockResolvedValue(99);
sync.mockResolvedValue({ imported: 99, failed: false });
const res = (await (
POST as unknown as (r: NextRequest) => Promise<{ data: unknown }>
)(req({ fullSync: true }))) as { data: { fullSync: boolean } };
+1 -1
View File
@@ -27,6 +27,6 @@ export const POST = apiHandler(async (request: NextRequest) => {
// no body provided -> default incremental sync
}
const imported = await syncUserWhoop(user.id, { fullSync });
const { imported } = await syncUserWhoop(user.id, { fullSync });
return apiSuccess({ imported, fullSync });
});