mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(insights): flag a stale briefing that has no provider to refresh it
An account with no AI provider configured still reads the last cached briefing — the read path serves it without ever touching a provider — so the days-old report rendered as current. The pregenerate cron runs fine; the cache simply can never refill without a provider. Report provider availability from the read GET and pair the briefing's relative age with a discreet 'this can't refresh — connect a provider' hint linking to Settings -> AI, on both the overview hero meta row and the briefing card footer. The hint only shows when a briefing is actually rendered; the empty-state already owns its own no-provider branch. Also align the briefing card footer caption to the 11px scale the adjacent period-in-review card uses, so the two overview footers read at one size.
This commit is contained in:
1 parent
2d5c4cfe46
commit
5cebc168df
12 files changed
+193
-18
No files matched your search
@@ -565,4 +565,50 @@ describe("GET /api/insights/generate — read-only advisor read", () => {
|
||||
// No warm enqueued → no convergence poll for the client to run.
|
||||
expect(body.data.revalidating).toBe(false);
|
||||
});
|
||||
|
||||
// v1.18.9 (#4) — the read path reports provider availability so the
|
||||
// insights surfaces can pair a stale cached briefing's honest age with
|
||||
// a connect-provider hint. The cache is served regardless of provider
|
||||
// state, so `hasProvider: false` is the only honest "can never refresh"
|
||||
// signal the client has.
|
||||
it("reports hasProvider: false alongside a served stale briefing", async () => {
|
||||
vi.mocked(hasUsableStatusProvider).mockResolvedValueOnce(false);
|
||||
const stale = new Date(Date.now() - 48 * 60 * 60 * 1000);
|
||||
const cached = { dailyBriefing: { paragraph: "old", keyFindings: [] } };
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
|
||||
insightsCachedAt: stale,
|
||||
insightsCachedText: JSON.stringify(cached),
|
||||
locale: "en",
|
||||
} as never);
|
||||
|
||||
const res = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/insights/generate"),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
data: { cached: boolean; hasProvider: boolean; revalidating: boolean };
|
||||
};
|
||||
// The stale briefing is still delivered (no provider needed to read).
|
||||
expect(body.data.cached).toBe(true);
|
||||
// But it can never refresh — no warm enqueued, hasProvider flagged.
|
||||
expect(body.data.hasProvider).toBe(false);
|
||||
expect(enqueueForceWarm).not.toHaveBeenCalled();
|
||||
expect(body.data.revalidating).toBe(false);
|
||||
});
|
||||
|
||||
it("reports hasProvider: true on a normal cached read", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
|
||||
insightsCachedAt: new Date(),
|
||||
insightsCachedText: JSON.stringify({
|
||||
dailyBriefing: { paragraph: "ok", keyFindings: [] },
|
||||
}),
|
||||
locale: "en",
|
||||
} as never);
|
||||
|
||||
const res = await (GET as unknown as (req: Request) => Promise<Response>)(
|
||||
new Request("http://localhost/api/insights/generate"),
|
||||
);
|
||||
const body = (await res.json()) as { data: { hasProvider: boolean } };
|
||||
expect(body.data.hasProvider).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -257,12 +257,22 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const isFresh =
|
||||
cachedAt !== null && Date.now() - cachedAt.getTime() < BRIEFING_FRESH_MS;
|
||||
|
||||
// v1.18.9 (#4) — resolve provider availability once, up front. The
|
||||
// read path never blocks on or generates through the provider, but it
|
||||
// must REPORT whether one exists: a provider-less account's cached
|
||||
// briefing freezes at the last successful generation (the pregenerate
|
||||
// cron can't refill it), and the UI presented that stale text as
|
||||
// current. Surfacing `hasProvider: false` lets the insights surfaces
|
||||
// pair the honest "Stand: vor X Tagen" age with a discreet
|
||||
// connect-an-AI-provider hint instead of implying the read is live.
|
||||
const hasProvider = await hasUsableStatusProvider(userId);
|
||||
|
||||
// Read-only: never block on the provider. Warm out of band only when the
|
||||
// cached briefing is stale / missing AND a provider is configured (a
|
||||
// provider-less account costs one cheap chain-resolve and shows the
|
||||
// empty / connect-AI state instead of a wasted enqueue).
|
||||
let revalidating = false;
|
||||
if (!isFresh && (await hasUsableStatusProvider(userId))) {
|
||||
if (!isFresh && hasProvider) {
|
||||
// Resolve the locale the caller is actually reading (cookie /
|
||||
// Accept-Language fall-back when `User.locale` is unset) and narrow
|
||||
// non-German to English — the same convention the nightly's
|
||||
@@ -285,7 +295,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const legacyPayload = isLegacyInsightPayload(cached);
|
||||
annotate({
|
||||
action: { name: "insights.generate.read" },
|
||||
meta: { cached: true, legacyPayload, revalidating },
|
||||
meta: { cached: true, legacyPayload, revalidating, hasProvider },
|
||||
});
|
||||
return apiSuccess({
|
||||
insights: cached,
|
||||
@@ -297,6 +307,10 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
// briefing lands instead of sitting on the stale payload for the
|
||||
// rest of the session.
|
||||
revalidating,
|
||||
// v1.18.9 (#4) — false when no AI provider is configured anywhere:
|
||||
// the served briefing can never refresh, so the UI pairs its age
|
||||
// with a connect-provider affordance.
|
||||
hasProvider,
|
||||
});
|
||||
} catch {
|
||||
// Invalid cache row — fall through to the empty payload below. The
|
||||
@@ -307,13 +321,15 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
annotate({
|
||||
action: { name: "insights.generate.read" },
|
||||
meta: { cached: false, revalidating },
|
||||
meta: { cached: false, revalidating, hasProvider },
|
||||
});
|
||||
return apiSuccess({
|
||||
insights: null,
|
||||
cached: false,
|
||||
legacyPayload: false,
|
||||
revalidating,
|
||||
// v1.18.9 (#4) — see the cached branch above.
|
||||
hasProvider,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user