fix(insights): serve the daily briefing read-only so the overview never blocks on the LLM

The hero strip, daily-briefing card and trends row all read the advisor
payload through `useInsightsAdvisorQuery`, which POSTed `/api/insights/generate`
without `force`. That POST generates inline on a cache miss, pinning the
page-load path on the full provider chain (up to the client's 8 s abort) before
the briefing paints — even though a read-only lift of `User.insightsCachedText`
costs nothing.

Add a read-only GET to the route that serves the cached payload immediately and,
when the cache is stale or missing and a provider is configured, enqueues an
out-of-band warm on the existing `insight-pregenerate` queue. The read mount now
uses the GET; user-initiated regeneration keeps the POST force path. Mirrors the
stale-while-revalidate posture of the per-metric status and period-narrative
reads — no provider call ever sits on a navigation request.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 16:44:46 +02:00
1 parent accd7c8514
commit 5e60eaf9c0
3 files changed
+174 -7

No files matched your search

@@ -73,12 +73,21 @@ async function fetchAdvisor(
);
let res: Response;
try {
res = await fetch("/api/insights/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options.force ? { force: true } : {}),
signal: controller.signal,
});
// Read path (no `force`): the GET serves the cached briefing read-only
// and enqueues an out-of-band warm on a stale / missing cache — it
// never blocks the page-load path on the provider chain. Only the
// user-initiated regenerate (`force`) POSTs to generate inline.
res = options.force
? await fetch("/api/insights/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ force: true }),
signal: controller.signal,
})
: await fetch("/api/insights/generate", {
method: "GET",
signal: controller.signal,
});
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// Graceful empty payload — the UI surfaces the empty / regen