fix(recovery): publish why the recovery page is empty from the server

The page worked out its empty note on the client from the integrations
status, so every client had to repeat the rule. The analytics summaries
slice now carries `recoveryEmpty` (general, oura_permission or
oura_no_resilience, null when any recovery type has a reading), decided
once in resolveRecoveryEmpty from the gated summaries and the stored
Oura grant. The page renders that value only, and its block list is
typed to the server's recovery types so the two cannot drift.

Each reason now reads as one sentence in every locale, and every reason
offers the same way on, the integrations settings.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 05:01:18 +02:00
1 parent 0ea8880923
commit 862361a224
20 files changed
+243 -171

No files matched your search

@@ -364,8 +364,12 @@ describe("GET /api/analytics", () => {
string,
{ lastSeenAt: string; daysAgo: number } | null
>;
recoveryEmpty: string | null;
};
};
// A weight-only account without Oura: the recovery page's empty reason
// is resolved here, not by the client.
expect(body.data.recoveryEmpty).toBe("general");
// The slim slice produced WEIGHT from the SQL pass; no chunked
// findMany was called.
expect(prisma.measurement.findMany).not.toHaveBeenCalled();
+4
View File
@@ -7,6 +7,7 @@ import { NO_STORE_BUT_BFCACHE } from "@/lib/http/cache-headers";
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import { summarize, type DataPoint } from "@/lib/analytics/trends";
import { computeSummariesSlice } from "@/lib/analytics/summaries-slice";
import { loadRecoveryEmpty } from "@/lib/insights/recovery-empty-read";
import { resolveEffectiveBpTargets } from "@/lib/analytics/effective-range";
import { bpTargetsEqual, getBpTargets } from "@/lib/analytics/bp-targets";
import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver";
@@ -136,6 +137,9 @@ export const GET = apiHandler(async (request?: Request) => {
...slim,
summaries: slimGated.summaries,
lastSeenByType: slimGated.lastSeenByType,
// Resolved here, from the gated summaries and the Oura connection, so
// a client renders the recovery page's empty note without deciding it.
recoveryEmpty: await loadRecoveryEmpty(user.id, slimGated.summaries),
});
slimRes.headers.set("Cache-Control", NO_STORE_BUT_BFCACHE);
return slimRes;