mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
fix(insights): bound the status preparing-poll to a fixed ceiling
The per-metric status cards and the inline medication-compliance query poll the read-only status route every 4 s while the payload reports preparing:true. A configured provider whose generation persistently fails leaves preparing pinned true, so an open Insights page would poll indefinitely — steady battery and network draw on a screen the user has walked away from. Cap the poll at 12 attempts (~48 s) through a shared nextStatusPollInterval helper both call sites route through. Past the ceiling the card settles on its existing static / empty state. The happy path is unchanged: a generation that lands within a couple of intervals clears preparing and stops the timer exactly as before, and a terminal no-provider payload never carried preparing so it stays terminal.
This commit is contained in:
1 parent
8dced00188
commit
65cfa2b83a
3 files changed
+92
-6
No files matched your search
@@ -5,6 +5,7 @@ import Link from "next/link";
|
||||
import { Loader2, Pill } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { nextStatusPollInterval } from "@/hooks/use-insight-status";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -64,11 +65,11 @@ interface MedicationComplianceStatusData {
|
||||
preparing?: boolean;
|
||||
}
|
||||
|
||||
// v1.8.3 — client ceiling + preparing-poll cadence, mirroring the shared
|
||||
// v1.8.3 — client ceiling on the round-trip, mirroring the shared
|
||||
// `use-insight-status` hook so the medication-compliance card never awaits
|
||||
// an uncapped LLM round-trip on navigation.
|
||||
// an uncapped LLM round-trip on navigation. The preparing-poll cadence and
|
||||
// its attempt ceiling come from the shared `nextStatusPollInterval` helper.
|
||||
const MED_STATUS_TIMEOUT_MS = 8_000;
|
||||
const MED_STATUS_POLL_MS = 4_000;
|
||||
|
||||
interface MedicationDailyData {
|
||||
expected: number;
|
||||
@@ -121,8 +122,14 @@ export default function InsightsMedikamentePage() {
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
retry: 0,
|
||||
// v1.8.4 — bounded preparing poll: stop after the shared attempt
|
||||
// ceiling so a persistently failing generation can't poll an open
|
||||
// page forever.
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.preparing ? MED_STATUS_POLL_MS : false,
|
||||
nextStatusPollInterval(
|
||||
query.state.data?.preparing,
|
||||
query.state.dataUpdateCount,
|
||||
),
|
||||
});
|
||||
|
||||
const medications = comprehensive?.medications ?? [];
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
nextStatusPollInterval,
|
||||
STATUS_POLL_MAX_ATTEMPTS,
|
||||
} from "../use-insight-status";
|
||||
|
||||
/**
|
||||
* v1.8.4 — the preparing poll must stop after a bounded number of
|
||||
* attempts. A provider can be configured yet have generation fail
|
||||
* persistently; without a ceiling the status card polls an open page
|
||||
* forever (battery / network waste). `nextStatusPollInterval` is the
|
||||
* shared decision both the hook and the inline medication-compliance
|
||||
* query route through, so locking its contract covers both sites.
|
||||
*/
|
||||
describe("nextStatusPollInterval — v1.8.4 poll ceiling", () => {
|
||||
it("does not poll a terminal payload (preparing absent/false)", () => {
|
||||
expect(nextStatusPollInterval(undefined, 1)).toBe(false);
|
||||
expect(nextStatusPollInterval(false, 1)).toBe(false);
|
||||
// A terminal `no-provider` / settled payload never carries preparing,
|
||||
// so it stops immediately regardless of attempt count.
|
||||
expect(nextStatusPollInterval(false, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps polling while preparing and below the cap", () => {
|
||||
expect(nextStatusPollInterval(true, 0)).toBeTypeOf("number");
|
||||
expect(nextStatusPollInterval(true, 1)).toBeTypeOf("number");
|
||||
expect(
|
||||
nextStatusPollInterval(true, STATUS_POLL_MAX_ATTEMPTS - 1),
|
||||
).toBeTypeOf("number");
|
||||
});
|
||||
|
||||
it("stops polling once the attempt cap is reached", () => {
|
||||
expect(nextStatusPollInterval(true, STATUS_POLL_MAX_ATTEMPTS)).toBe(false);
|
||||
expect(
|
||||
nextStatusPollInterval(true, STATUS_POLL_MAX_ATTEMPTS + 5),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a positive interval while it polls", () => {
|
||||
const interval = nextStatusPollInterval(true, 0);
|
||||
expect(interval).not.toBe(false);
|
||||
expect(interval as number).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,35 @@ const STATUS_TIMEOUT_MS = 8_000;
|
||||
*/
|
||||
const STATUS_POLL_MS = 4_000;
|
||||
|
||||
/**
|
||||
* v1.8.4 — hard ceiling on the preparing poll. The route is read-only and
|
||||
* a worker warms the cache out of band; a healthy generation lands within
|
||||
* a couple of intervals. If a provider is configured but generation
|
||||
* persistently fails, `preparing` stays true forever and the open page
|
||||
* would otherwise poll indefinitely (battery / network waste). Cap the
|
||||
* poll at ~12 attempts (≈ 48 s at the 4 s cadence) and then fall back to
|
||||
* the static empty / "no analysis yet" state. `dataUpdateCount` counts
|
||||
* every settled fetch — initial load plus each poll round — so once it
|
||||
* crosses the cap we stop scheduling the next interval.
|
||||
*/
|
||||
export const STATUS_POLL_MAX_ATTEMPTS = 12;
|
||||
|
||||
/**
|
||||
* Decide whether a `preparing` card should schedule its next poll.
|
||||
* Pure and shared between the `useInsightStatus` hook and the inline
|
||||
* medication-compliance query so both sites enforce the identical
|
||||
* ceiling. Returns the interval in ms while polling is warranted, or
|
||||
* `false` once the payload is terminal OR the attempt cap is reached.
|
||||
*/
|
||||
export function nextStatusPollInterval(
|
||||
preparing: boolean | undefined,
|
||||
dataUpdateCount: number,
|
||||
): number | false {
|
||||
if (!preparing) return false;
|
||||
if (dataUpdateCount >= STATUS_POLL_MAX_ATTEMPTS) return false;
|
||||
return STATUS_POLL_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric slugs the sub-pages render. Each slug is paired with the
|
||||
* Insights-status endpoint and the matching `queryKeys.*` factory so
|
||||
@@ -124,8 +153,13 @@ export function useInsightStatus(metric: InsightStatusMetric) {
|
||||
retry: 0,
|
||||
// v1.8.3 — poll only while the worker is preparing the assessment.
|
||||
// Returns false (no timer) for any terminal payload so a settled card
|
||||
// never re-fetches on an interval.
|
||||
// never re-fetches on an interval. v1.8.4 — also stop after the
|
||||
// attempt ceiling so a persistently failing generation can't poll an
|
||||
// open page forever.
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.preparing ? STATUS_POLL_MS : false,
|
||||
nextStatusPollInterval(
|
||||
query.state.data?.preparing,
|
||||
query.state.dataUpdateCount,
|
||||
),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user