mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
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:
1 parent
05cff531e4
commit
53e87ba345
11 files changed
+632
-254
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user