fix(insights): make per-category status cards read-only so navigation never blocks on the LLM

Opening an Insights category page froze the whole UI: the per-metric
status card fired a cold, uncapped LLM fetch on mount with no client
abort, so the request hung up to the server's 60s ceiling and the
pending/resolve re-renders thrashed the tab strip and ate touch events.
Timeouts were never negatively cached, so every visit re-blocked.

Fix the class holistically:

- Bound every status GET on the client. The shared use-insight-status
  hook and the inline medication-compliance query now wrap fetch in an
  8s AbortController and stop retrying inline; navigation can no longer
  await an uncapped round-trip.

- Make the status routes read-only. On a cache miss the generator
  enqueues an out-of-band pg-boss job (insight-status-generate) and
  returns a preparing envelope; the client polls a bounded interval
  until the worker warms the cache. Models the dashboard snapshot's
  ready/preparing pattern. The queue is registered in the worker
  allQueues loop + a boss.work handler, with a guard test.

- Fix the nightly warm-pass regression: warm every supported locale
  (the client reads against the active UI locale, not the persisted
  User.locale) and warm on a cached comprehensive outcome too (its
  write still evicts the per-status caches). Add a short-TTL negative
  cache on the timeout path so a transient provider stall no longer
  makes every subsequent visit re-block or re-enqueue.

- Add loading.tsx for the insights segment + the root so any future
  RSC await streams a skeleton instead of blocking the transition.

- Stabilise the advisor regenerate callback so a status-query flip
  can't re-render the memoised tab strip mid-gesture.

New i18n key insights.assessmentPreparing across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 08:59:59 +02:00
1 parent 6a603ad587
commit 35800becbb
47 files changed
+1297 -75

No files matched your search

@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolveBloodPressureStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generateBloodPressureStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.blood-pressure-status" } });
+4
View File
@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolveBmiStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generateBmiStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.bmi-status" } });
@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolveMedicationComplianceStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generateMedicationComplianceStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.medication-compliance-status" } });
@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolveMoodStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generateMoodStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.mood-status" } });
@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolvePulseStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generatePulseStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.pulse-status" } });
@@ -23,9 +23,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
});
const locale = resolveWeightStatusLocale(resolved);
// v1.8.3 — read-only: serve the cache, enqueue generation out of band on
// a miss. The GET never awaits the provider, so opening /insights/<metric>
// can no longer pin the main thread behind a cold LLM round-trip.
const result = await generateWeightStatusForUser(user.id, {
locale,
force: false,
readOnly: true,
});
annotate({ action: { name: "insights.weight-status" } });