perf: consolidate integration status query, add mood-insights SWR, defer below-fold charts

Settings → Integrations fired five status round-trips on every mount —
the consolidated /api/integrations/status alongside the four per-provider
/api/<provider>/status queries. Extend the consolidated endpoint to carry
every field the four cards render (Withings activity scope, WHOOP/Fitbit
backfill state, moodLog webhook secret + entry count) and drive all four
cards off the single envelope. The legacy per-provider routes stay for the
iOS/test callers; the web cards no longer hit them.

Mood-insights cache was a plain 60 s TTL that was hard-evicted on every
mood write, so an active logger re-paid the multi-second cold compute on
every entry. Add stale-while-revalidate to ServerCache (wrapSwr / cachedSwr
+ a markStaleByPrefix that collapses the fresh TTL without dropping the
value), give the moodInsights bucket a 10-minute stale window, route the
read through cachedSwr, and switch the mood-write invalidation from a hard
evict to a mark-stale so the prior aggregate serves immediately while a
single background recompute warms a fresh one.

Defer the eager Recharts on the mood-insights surface. The trajectory
forecast card pulled Recharts into the initial chunk of every
trajectory-eligible metric page though the main chart is already deferred;
split its fan band into trajectory-fan.tsx behind next/dynamic. The three
below-fold mood mini-charts (distribution / weekday / time-of-day) were
static imports under an already-dynamic hero line chart; defer them with
next/dynamic. Each loader paints a skeleton sized to the chart's own band
so the chunk arrives without a layout shift; the charts stay Recharts and
visually identical.

(cherry picked from commit 2b627893ab6b4d7bc457fc18e6e5585b048190e1)
This commit is contained in:
Marc-André Bombeck committed 2026-06-05 03:25:22 +02:00
1 parent 05cff531e4
commit 53e87ba345
11 files changed
+632 -254

No files matched your search

+38 -6
View File
@@ -8,10 +8,12 @@
* credentials configured, token expiry, OAuth-connected; moodLog:
* credentials configured, enabled flag)
*
* Combining both into one fetch removes a third round-trip from the
* Settings page. The legacy /api/withings/status and
* /api/integrations/moodlog/status remain for the existing UI/tests
* — we add this in addition rather than rewriting them.
* This is the single fetch the Settings → Integrations cards read off
* — it carries every field the four cards render (Withings activity
* scope, WHOOP/Fitbit backfill state, moodLog webhook secret + entry
* count) so the per-card /api/<provider>/status round-trips are gone
* from the web. The legacy per-provider routes stay for the iOS/test
* callers; the web cards no longer hit them.
*/
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
@@ -22,6 +24,8 @@ import {
getPersistentFailureThreshold,
type IntegrationKey,
} from "@/lib/integrations/status";
import { hasActivityScope } from "@/lib/withings/client";
import { readMoodLogSecret } from "@/lib/moodlog-secret";
export const dynamic = "force-dynamic";
@@ -38,6 +42,7 @@ export const GET = apiHandler(async () => {
withingsConn,
whoopConn,
fitbitConn,
moodLogEntryCount,
] = await Promise.all([
getIntegrationStatus(user.id, "withings"),
getIntegrationStatus(user.id, "moodlog"),
@@ -56,6 +61,7 @@ export const GET = apiHandler(async () => {
moodLogApiKeyEncrypted: true,
moodLogEnabled: true,
moodLogLastSyncedAt: true,
moodLogWebhookSecret: true,
},
}),
prisma.withingsConnection.findUnique({
@@ -64,6 +70,7 @@ export const GET = apiHandler(async () => {
tokenExpiresAt: true,
lastSyncedAt: true,
createdAt: true,
scope: true,
},
}),
prisma.whoopConnection.findUnique({
@@ -72,6 +79,7 @@ export const GET = apiHandler(async () => {
tokenExpiresAt: true,
lastSyncedAt: true,
createdAt: true,
backfillCompletedAt: true,
},
}),
prisma.fitbitConnection.findUnique({
@@ -80,8 +88,13 @@ export const GET = apiHandler(async () => {
tokenExpiresAt: true,
lastSyncedAt: true,
createdAt: true,
backfillCompletedAt: true,
},
}),
// v1.7.0 sync — exclude tombstoned rows from the entry count.
prisma.moodEntry.count({
where: { userId: user.id, deletedAt: null },
}),
]);
const now = Date.now();
@@ -104,13 +117,24 @@ export const GET = apiHandler(async () => {
tokenExpired: withingsConn
? withingsConn.tokenExpiresAt.getTime() <= now
: null,
// v1.4.25 W5d — `scope` is the comma-separated OAuth scope string;
// `hasActivityScope` is the derived flag the reconnect banner reads.
// Null `scope` = legacy connection that predates activity-scope reads.
scope: withingsConn?.scope ?? null,
hasActivityScope: hasActivityScope(withingsConn?.scope ?? null),
} satisfies IntegrationViewModel & WithingsExtras,
{
...moodLogStatus,
configured:
!!dbUser?.moodLogUrlEncrypted && !!dbUser?.moodLogApiKeyEncrypted,
// moodLog "configured" tracks the URL alone (the API key is
// optional for the webhook-only path), matching the legacy
// /api/integrations/moodlog/status contract the card relied on.
configured: !!dbUser?.moodLogUrlEncrypted,
enabled: dbUser?.moodLogEnabled ?? false,
legacyLastSyncedAt: dbUser?.moodLogLastSyncedAt?.toISOString() ?? null,
// V3 audit STILL-V2-C-2: stored secret is AES-GCM encrypted at rest;
// decrypt for the settings page (legacy plaintext is also handled).
webhookSecret: readMoodLogSecret(dbUser?.moodLogWebhookSecret ?? null),
entryCount: moodLogEntryCount,
} satisfies IntegrationViewModel & MoodLogExtras,
{
...whoopStatus,
@@ -124,6 +148,7 @@ export const GET = apiHandler(async () => {
tokenExpired: whoopConn
? whoopConn.tokenExpiresAt.getTime() <= now
: null,
backfillCompleted: whoopConn ? !!whoopConn.backfillCompletedAt : null,
} satisfies IntegrationViewModel & WhoopExtras,
{
...fitbitStatus,
@@ -137,6 +162,7 @@ export const GET = apiHandler(async () => {
tokenExpired: fitbitConn
? fitbitConn.tokenExpiresAt.getTime() <= now
: null,
backfillCompleted: fitbitConn ? !!fitbitConn.backfillCompletedAt : null,
} satisfies IntegrationViewModel & FitbitExtras,
],
});
@@ -157,12 +183,16 @@ interface WithingsExtras {
legacyLastSyncedAt: string | null;
tokenExpiresAt: string | null;
tokenExpired: boolean | null;
scope: string | null;
hasActivityScope: boolean;
}
interface MoodLogExtras {
configured: boolean;
enabled: boolean;
legacyLastSyncedAt: string | null;
webhookSecret: string | null;
entryCount: number;
}
interface WhoopExtras {
@@ -172,6 +202,7 @@ interface WhoopExtras {
legacyLastSyncedAt: string | null;
tokenExpiresAt: string | null;
tokenExpired: boolean | null;
backfillCompleted: boolean | null;
}
interface FitbitExtras {
@@ -181,4 +212,5 @@ interface FitbitExtras {
legacyLastSyncedAt: string | null;
tokenExpiresAt: string | null;
tokenExpired: boolean | null;
backfillCompleted: boolean | null;
}
+7 -2
View File
@@ -1,7 +1,7 @@
import { apiSuccess } from "@/lib/api-response";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { cached, caches, type ServerCache } from "@/lib/cache/server-cache";
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import {
fetchMoodAggregates,
type MoodAggregates,
@@ -19,11 +19,16 @@ export const dynamic = "force-dynamic";
* once per 60-s cache window, the browser only ever receives pre-shaped
* data (no raw mood rows). This follows the v1.8.3 anti-freeze posture
* — a cheap cached read, never a synchronous LLM call.
*
* v1.12.1 — stale-while-revalidate. On an expired or freshly-marked-
* stale bucket (a mood write marks rather than evicts) the prior
* aggregate is served immediately and a single background recompute
* warms a fresh one, so an active logger never re-pays the cold compute.
*/
export const GET = apiHandler(async () => {
const { user } = await requireAuth();
const result = await cached(
const result = await cachedSwr(
caches.moodInsights as ServerCache<MoodAggregates>,
user.id,
() => fetchMoodAggregates(user.id),