feat(insights): add on-demand full assessment warm

Add POST /api/insights/pregenerate so every AI assessment text — the
comprehensive insight, the seven specialised status cards, and every
data-bearing generic metric assessment — can be warmed for the calling
user on demand, in the active locale, instead of waiting for the nightly
cron or a lazy first-visit generation.

The route narrows userId from the session/Bearer, runs the assistant
surface gate, and enqueues a forced full-warm job onto the existing
insight-pregenerate queue, returning immediately. A short per-user bucket
(insights-warm:<userId>, one per three minutes) returns 429 on a tight
loop so the bypassed nightly 20h budget cannot be turned into a provider
cost amplifier.

The worker branches on the payload: a job carrying { userId, force }
runs forceWarmUser for that user, skipping the candidate scan and the
20h budget; a scheduled tick with no payload keeps its existing batch
behaviour. forceWarmUser reuses the existing comprehensive generator and
warm helpers — no new generation logic — and stays safe by construction:
the comprehensive generator no-ops without a provider, the generic warm
pass filters to metrics the user has rows for so an empty metric never
reaches the provider, and every write is idempotent.

The queue name and a generator-free enqueue helper move to
insight-pregenerate-shared.ts, mirroring the insight-status-generate
split, so the route enqueues without pulling the generator tree.

Regenerate the OpenAPI contract for the new route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit e10ae9bfeca544c7e5aecd9102459d97ddfad6db)
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 14:29:49 +02:00
1 parent d81ab69dc8
commit bf5052bb01
7 files changed
+483 -4

No files matched your search

+52
View File
@@ -1156,6 +1156,29 @@ const metricStatusResponse = z
"Generic per-metric assessment envelope. Identical shape to the seven specialised `*-status` cards so the `InsightStatusCard` consumes it unchanged. Read-only + stale-while-revalidate: a cache miss warms a generation out of band and serves the last-good text meanwhile.",
});
const insightsPregenerateRequest = z
.object({})
.meta({
id: "InsightsPregenerateRequest",
description:
"No body fields. The user is taken from the session / Bearer and the locale from the session; the warm covers every assessment for that user.",
});
const insightsPregenerateResponse = z
.object({
queued: z
.boolean()
.describe("True when the full warm was accepted and enqueued."),
locale: z
.enum(["de", "en"])
.describe("The locale the assessments are being warmed in."),
})
.meta({
id: "InsightsPregenerateResponse",
description:
"Acknowledgement that a full assessment warm was enqueued for the calling user. The generation runs out of band; the text lands in the read-only status routes.",
});
// v1.7.0 — unified dashboard first-paint snapshot. One GET that
// assembles every above-the-fold tile field in a single round-trip.
// Two-phase shape: `tiles` (fast, always present) + `extras` (thick,
@@ -2409,6 +2432,35 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
},
"/api/insights/pregenerate": {
post: {
tags: ["Insights"],
summary: "Warm all AI assessments for the calling user",
description:
"v1.8.7.1 — enqueue a full warm of every AI assessment for the authenticated user (comprehensive insight + the seven specialised status cards + every data-bearing generic metric assessment) in the active locale, so the read-only status GETs serve cached text instantly. Returns immediately; the generation runs out of band on the worker. Empty metrics and provider-less accounts never trigger an LLM call. Short anti-spam bucket (`insights-warm:<userId>`, one warm per 3 minutes) → 429 on a tight loop. Auth via cookie or Bearer; `userId` is taken from the session, never the body.",
requestBody: {
required: false,
content: {
"application/json": { schema: insightsPregenerateRequest },
},
},
responses: {
"200": {
description:
"Warm accepted and enqueued. The work runs on the worker; poll the read-only status routes for the text.",
content: {
"application/json": {
schema: dataEnvelope(
insightsPregenerateResponse,
"InsightsPregenerateResponseEnvelope",
),
},
},
},
...stdResponses,
},
},
},
"/api/insights/metric-status": {
get: {
tags: ["Insights"],