fix(insights,medications): close the staleness escape hatches and harden the review findings

Staleness can re-enter a generated text: measurement age is hashed as a
bucketed tier, so the fingerprint stays stable day to day but flips
exactly when a reading crosses the boundary the prompt caveat cares
about. The ingest debounce becomes a one-hour minimum gap with the
hash gate carrying the budget, so a notable morning reading is
narrated the same morning instead of after the next nightly run. A
manual regenerate now refills the status cards through the same
hash-gated pipeline — changed cards regenerate, unchanged cards
refresh free, no eviction touches the hash baselines. The user's
self-context joins the comprehensive fingerprint so a newly remembered
fact reaches the briefing, the sleep writer collapse covers every
caller that reads nights, the batched compliance build fans out
bounded instead of sequentially, and a failed compliance read renders
a quiet retry state in the exact skeleton footprint instead of
swallowing the error.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 22:49:42 +02:00
1 parent 3d95ace218
commit a2a995e429
25 files changed
+770 -97

No files matched your search

@@ -8,7 +8,10 @@ import {
Glp1MedicationCard,
type Glp1Medication,
} from "@/components/medications/glp1-medication-card";
import { MedicationComplianceBars } from "@/components/medications/card-parts/medication-compliance-bars";
import {
MedicationComplianceBars,
MedicationComplianceError,
} from "@/components/medications/card-parts/medication-compliance-bars";
import { MedicationStatusPill } from "@/components/medications/card-parts/medication-status-pill";
import { MedicationIntakeActions } from "@/components/medications/card-parts/medication-intake-actions";
import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges";
@@ -550,3 +553,87 @@ describe("compliance bars — fixed geometry (v1.16.6)", () => {
expect(skeleton(a)).toBe(skeleton(b));
});
});
/**
* v1.16.8 — when the batched compliance query FAILS (post-retry), the
* card swaps the loading skeleton for a quiet error fallback: same
* two-row footprint (no layout jump), no bars (a failed read must not
* paint a 0 % adherence), one muted notice line + a small retry that
* refetches the shared query. Without it the swallowed-error path left
* every card on a permanent skeleton with no recovery affordance.
*/
describe("compliance error fallback (v1.16.8)", () => {
function renderBodyCompliance(args: {
compliance: boolean;
complianceError?: boolean;
}) {
return render(
<MedicationCardBody
name="Ramipril"
dose="5 mg"
categoryLabel="Blood Pressure"
active
href="/medications/m1"
linkLabel="Open"
stateBadges={null}
headerActions={null}
windowStatus={null}
doseStatus="upcoming"
nextLine="Tomorrow, 08:00"
lastLine="Today, 07:30"
compliance={
args.compliance
? { rate7: 90, rate30: 88, streak: 0, shortDays: 7, longDays: 30 }
: null
}
complianceError={args.complianceError}
onRetryCompliance={() => {}}
currentCycle={null}
intakeLoading={null}
onRecordIntake={() => {}}
/>,
);
}
it("renders the notice line and the retry affordance, no progress bars", () => {
const html = render(<MedicationComplianceError onRetry={() => {}} />);
expect(html).toContain("Adherence could not be loaded");
expect(html).toContain("Retry");
expect(html).not.toContain('data-slot="progress"');
});
it("mirrors the skeleton's two-row footprint so the card height holds", () => {
const error = render(<MedicationComplianceError onRetry={() => {}} />);
// Two h-5 label lines + two h-2 bar-height slots — exactly the
// skeleton's reserved geometry, so failed/loading/loaded states all
// occupy the same space.
expect((error.match(/flex h-5 items-center/g) ?? []).length).toBe(2);
expect((error.match(/h-2 rounded/g) ?? []).length).toBe(2);
});
it("the body shows the error fallback instead of the skeleton on a failed query", () => {
const html = renderBodyCompliance({
compliance: false,
complianceError: true,
});
expect(html).toContain("Adherence could not be loaded");
expect(html).toContain("Retry");
// Not the aria-hidden loading skeleton.
expect(html).not.toContain('aria-hidden="true"><div class="space-y-1.5"');
});
it("the body keeps the skeleton while the query is merely in flight", () => {
const html = renderBodyCompliance({ compliance: false });
expect(html).not.toContain("Adherence could not be loaded");
});
it("resolved data still renders the bars even when a stale error flag lingers", () => {
// A successful refetch clears the failure visually: data wins.
const html = renderBodyCompliance({
compliance: true,
complianceError: true,
});
expect(html).toContain('data-slot="progress"');
expect(html).not.toContain("Adherence could not be loaded");
});
});
@@ -9,6 +9,7 @@ import { MedicationCardHeader } from "@/components/medications/MedicationCardHea
import { MedicationStatusPill } from "@/components/medications/card-parts/medication-status-pill";
import {
MedicationComplianceBars,
MedicationComplianceError,
MedicationComplianceSkeleton,
} from "@/components/medications/card-parts/medication-compliance-bars";
import { MedicationCycleStatus } from "@/components/medications/card-parts/medication-cycle-status";
@@ -95,6 +96,16 @@ export interface MedicationCardBodyProps {
longDays: number;
} | null;
/**
* True when the batched compliance query FAILED (post-retry). The body
* then renders the quiet same-footprint error fallback instead of the
* skeleton — a permanent skeleton reads as "still loading" forever,
* while a failed read needs a visible (but calm) retry affordance.
*/
complianceError?: boolean;
/** Refetch the shared compliance query (the error fallback's retry). */
onRetryCompliance?: () => void;
/** The open-cycle descriptor, or null when the display block is absent. */
currentCycle: CurrentCycle | null;
@@ -121,6 +132,8 @@ export function MedicationCardBody({
nextLine,
lastLine,
compliance,
complianceError = false,
onRetryCompliance,
currentCycle,
intakeLoading,
onRecordIntake,
@@ -183,7 +196,10 @@ export function MedicationCardBody({
<MedicationNextLastSlot next={nextLine} last={lastLine} />
{/* Compliance bars — always two rows; constant-height skeleton holds
the slot while the query is in flight so the grid row stays even. */}
the slot while the query is in flight so the grid row stays even.
A FAILED query swaps the skeleton for the same-footprint quiet
error fallback (no bars, one notice line + retry) instead of
sitting on the skeleton forever. */}
{active &&
(compliance ? (
<MedicationComplianceBars
@@ -193,6 +209,8 @@ export function MedicationCardBody({
shortDays={compliance.shortDays}
longDays={compliance.longDays}
/>
) : complianceError && onRetryCompliance ? (
<MedicationComplianceError onRetry={onRetryCompliance} />
) : (
<MedicationComplianceSkeleton />
))}
@@ -1,4 +1,4 @@
import { Flame } from "lucide-react";
import { Flame, RotateCw } from "lucide-react";
import { ComplianceInfoTip } from "@/components/medications/card-parts/compliance-info-tip";
import { Progress } from "@/components/ui/progress";
@@ -145,3 +145,45 @@ export function MedicationComplianceSkeleton() {
</div>
);
}
/**
* Quiet error fallback for the compliance block, shown when the batched
* `/api/medications/compliance` query failed after its retries. Mirrors
* the skeleton's two-row footprint exactly (same `h-5` label lines, same
* `h-2` bar slots, same gaps) so the card height never jumps between the
* loading, loaded, and failed states. No bars are drawn — a failed read
* must not paint a 0 % adherence — just one muted notice line and a small
* retry affordance that refetches the shared query for every card at once.
*/
export function MedicationComplianceError({
onRetry,
}: {
onRetry: () => void;
}) {
const { t } = useTranslations();
return (
<div className="space-y-2.5">
<div className="space-y-1.5">
<div className="flex h-5 items-center text-sm">
<span className="text-muted-foreground min-w-0 truncate">
{t("medications.complianceUnavailable")}
</span>
</div>
<div className="bg-muted/40 h-2 rounded" />
</div>
<div className="space-y-1.5">
<div className="flex h-5 items-center">
<button
type="button"
onClick={onRetry}
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs underline-offset-2 hover:underline"
>
<RotateCw className="h-3 w-3" aria-hidden="true" />
{t("common.retry")}
</button>
</div>
<div className="bg-muted/40 h-2 rounded" />
</div>
</div>
);
}
@@ -192,7 +192,13 @@ export function Glp1MedicationCard({
// v1.16.8 — same batched compliance source as the generic card: one
// `GET /api/medications/compliance` round trip shared by every card on
// the page (the per-id endpoint stays for the detail page's heatmap).
const { data: compliance } = useMedicationComplianceSummary(medication.id);
// `isError` swaps the compliance slot to the quiet retry fallback so a
// failed batch read never leaves the card on a permanent skeleton.
const {
data: compliance,
isError: complianceError,
refetch: refetchCompliance,
} = useMedicationComplianceSummary(medication.id);
// v1.4.37 W4b — same reminder-thresholds source as the generic
// medication card so the take-now / overdue / very-overdue pill
@@ -442,6 +448,8 @@ export function Glp1MedicationCard({
compliance={
compliance ? { rate7, rate30, streak, shortDays, longDays } : null
}
complianceError={complianceError}
onRetryCompliance={refetchCompliance}
currentCycle={display?.currentCycle ?? null}
intakeLoading={intakeLoading}
onRecordIntake={(skipped) => recordIntake(skipped, displayedSlot)}
@@ -131,7 +131,13 @@ export function MedicationCard({
// v1.16.8 — the per-card compliance read rides the ONE batched
// `GET /api/medications/compliance` round trip every card on the page
// shares (the per-id endpoint stays for the detail page's heatmap).
const { data: compliance } = useMedicationComplianceSummary(medication.id);
// `isError` swaps the compliance slot to the quiet retry fallback so a
// failed batch read never leaves the card on a permanent skeleton.
const {
data: compliance,
isError: complianceError,
refetch: refetchCompliance,
} = useMedicationComplianceSummary(medication.id);
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
@@ -386,6 +392,8 @@ export function MedicationCard({
compliance={
compliance ? { rate7, rate30, streak, shortDays, longDays } : null
}
complianceError={complianceError}
onRetryCompliance={refetchCompliance}
currentCycle={display?.currentCycle ?? null}
intakeLoading={intakeLoading}
onRecordIntake={(skipped) => recordIntake(skipped, displayedSlot)}