mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(copy): make delete confirmations, sleep-debt labels, and the glucose band honest
Three surfaces where the words no longer matched the code, plus the missing web control for a preference the server already honours. Delete confirmations. Four dialogs promised permanence on surfaces that render an Undo: the measurements and mood bulk bodies, the lab-result body, and the illness-episode body (which said "cannot be undone" sixty lines below the code that fires the Undo toast). All four now use the same sentence the single-row measurement and mood dialogs already used. A sweep of every other confirmation in the tree found the rest accurate — the biomarker cascade, the mood-tag purge, and the account purge keep their warning because those deletes really are unrecoverable. Sleep debt. v1.19.0 replaced the summed shortfall with a running balance that credits catch-up sleep; the labels still described the old sum, so the word contradicted the number for anyone who slept in after a short night. Corrected in all six locales, plus the places the old semantics had propagated: the Coach system prompt, the OpenAPI field descriptions the native client reads, and four module docblocks. Glucose reference band. `hasDiabetes` selects the tighter reference band and grounds the Coach, but no web surface set it — a web-only account was pinned to the non-diabetic scale with no way out. The control goes on Targets, above the per-metric editor, because it answers the same question that page answers and the two are ordered: it picks the default band, an override below beats it. The copy says what it is — a reference band, not a diagnosis, never inferred from a reading, no clinical action taken on it. The passkey list was reported as always reading "never used". It does not reproduce: the column is selected by the list route and stamped on every verified assertion, both since v1.23.0. Pinned with tests on both halves so the contract cannot quietly regress.
This commit is contained in:
1 parent
b0021a8258
commit
e9d28b399e
21 files changed
+601
-61
No files matched your search
@@ -22,8 +22,11 @@ function hoursMinutes(totalMinutes: number): {
|
||||
/**
|
||||
* v1.17.0 — sleep-debt headline.
|
||||
*
|
||||
* The cumulative shortfall between sleep need and sleep obtained over the
|
||||
* rolling window. Under the night threshold it shows the calm `partial`
|
||||
* The outstanding balance between sleep need and sleep obtained over the
|
||||
* rolling window — a night below need adds to it, a night above need pays it
|
||||
* down (v1.19.0 replaced the summed shortfall with this running balance; the
|
||||
* copy follows the arithmetic). Under the night threshold it shows the calm
|
||||
* `partial`
|
||||
* state ("still learning — N more nights") and never asserts a total off thin
|
||||
* data. Mobile-first: a single readable figure + one line of grounded context.
|
||||
*/
|
||||
|
||||
@@ -267,6 +267,21 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).not.toContain("settings.sections.thresholds.");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> carries the glucose reference band control", () => {
|
||||
// The declared diabetes opt-in (`PATCH /api/auth/me/diabetes`) selects the
|
||||
// tighter glucose band and grounds the Coach, but shipped with no web
|
||||
// control at all — a web-only account could never reach it. It belongs on
|
||||
// Targets: same question ("which range judges my reading") as the editor
|
||||
// below it, and the copy must stay honest about being a reference band
|
||||
// rather than a diagnosis.
|
||||
const html = renderFramed("thresholds", <ThresholdsSection />);
|
||||
expect(html).toContain("Glucose reference band");
|
||||
expect(html).toContain("Use the diabetes goal band");
|
||||
expect(html).toContain("not a diagnosis");
|
||||
expect(html).toContain('data-slot="glucose-reference-disclaimer"');
|
||||
expect(html).not.toContain("settings.glucoseReference.");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> resolves the German title", () => {
|
||||
const html = renderFramed("thresholds", <ThresholdsSection />, "de");
|
||||
// v1.8.7.1 — German title is "Zielwerte" (Targets only).
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Droplet, Info } from "lucide-react";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet, apiPatch } from "@/lib/api/api-fetch";
|
||||
|
||||
/**
|
||||
* Glucose reference band — the web control for the declared diabetes opt-in
|
||||
* (`GET/PATCH /api/auth/me/diabetes`).
|
||||
*
|
||||
* WHY IT LIVES ON TARGETS
|
||||
* -----------------------
|
||||
* The flag answers exactly the question this page answers — "which range is my
|
||||
* reading judged against" — and the page already owns the four glucose target
|
||||
* rows. Putting it anywhere else would split one concept across two surfaces.
|
||||
* The card sits ABOVE the per-metric editor because the two controls are
|
||||
* ordered: this one picks the default band, a per-metric override below then
|
||||
* beats it (`src/lib/targets/glucose-targets.ts`).
|
||||
*
|
||||
* WHAT IT DOES NOT DO
|
||||
* -------------------
|
||||
* The flag is a display preference, never a diagnosis, and is never inferred
|
||||
* from a reading. It selects the band the glucose surfaces and the Coach's
|
||||
* reference grounding read; the app takes no clinical action on it. The copy
|
||||
* says so at the point of the switch rather than in a help page.
|
||||
*
|
||||
* Note: `GET /api/user/thresholds` (the editor below) reports the general
|
||||
* bands regardless of this flag — the flag resolves in the targets pipeline
|
||||
* (`src/lib/targets/build-response.ts`), which is why the mutation invalidates
|
||||
* `insightsTargets()` rather than `userThresholds()`.
|
||||
*/
|
||||
|
||||
interface DiabetesPrefShape {
|
||||
hasDiabetes: boolean;
|
||||
}
|
||||
|
||||
export function GlucoseReferenceCard() {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: queryKeys.diabetesPref(),
|
||||
queryFn: async () => apiGet<DiabetesPrefShape>("/api/auth/me/diabetes"),
|
||||
});
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationFn: async (hasDiabetes: boolean) =>
|
||||
apiPatch<DiabetesPrefShape>("/api/auth/me/diabetes", { hasDiabetes }),
|
||||
onSuccess: (next) => {
|
||||
queryClient.setQueryData(queryKeys.diabetesPref(), next);
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.insightsTargets(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const enabled = data?.hasDiabetes ?? false;
|
||||
|
||||
return (
|
||||
<SettingsCard>
|
||||
<SettingsCardHeader
|
||||
icon={Droplet}
|
||||
title={t("settings.glucoseReference.title")}
|
||||
description={t("settings.glucoseReference.description")}
|
||||
/>
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<Label htmlFor="glucose-reference-diabetes" className="text-sm">
|
||||
{t("settings.glucoseReference.enable")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="glucose-reference-diabetes"
|
||||
checked={enabled}
|
||||
disabled={toggle.isPending || data == null}
|
||||
onCheckedChange={(v) => toggle.mutate(v)}
|
||||
className="mt-0.5 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-sm">{t("settings.glucoseReference.explainer")}</p>
|
||||
|
||||
<div
|
||||
data-slot="glucose-reference-disclaimer"
|
||||
className="border-border bg-muted/40 flex items-start gap-2.5 rounded-lg border-l-2 p-3"
|
||||
>
|
||||
<Info
|
||||
className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-foreground/80 text-xs leading-relaxed">
|
||||
{t("settings.glucoseReference.disclaimer")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{toggle.isError ? (
|
||||
<p className="text-destructive text-sm" role="alert">
|
||||
{t("settings.glucoseReference.error")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { GlucoseReferenceCard } from "@/components/settings/glucose-reference-card";
|
||||
import { ThresholdsEditorSection } from "@/components/settings/thresholds-editor-section";
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,14 @@ import { ThresholdsEditorSection } from "@/components/settings/thresholds-editor
|
||||
* the names stop clashing.
|
||||
*/
|
||||
export function ThresholdsSection() {
|
||||
// Per-metric threshold ranges; the heading is supplied by the route frame.
|
||||
return <ThresholdsEditorSection id="thresholds" />;
|
||||
// The glucose reference band sits ABOVE the per-metric editor: it picks the
|
||||
// DEFAULT band a glucose reading is judged against, and a per-metric
|
||||
// override in the editor below then beats it. Same question, one page —
|
||||
// splitting the two across surfaces would hide the precedence.
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<GlucoseReferenceCard />
|
||||
<ThresholdsEditorSection id="thresholds" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user