style(prettier): sweep formatting across the tree

Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 11:05:40 +02:00
1 parent c6e3ac69e8
commit 3048dd6637
72 files changed
+508 -606

No files matched your search

+3 -7
View File
@@ -115,8 +115,7 @@ test.describe("achievements page renders all four card states", () => {
metric: "totalTakenIntakes",
category: "medication",
titleKey: "achievements.badges.intakeTotal1.title",
descriptionKey:
"achievements.badges.intakeTotal1.description",
descriptionKey: "achievements.badges.intakeTotal1.description",
icon: "Pill",
format: "count",
target: 1,
@@ -133,8 +132,7 @@ test.describe("achievements page renders all four card states", () => {
metric: "moodDayStreak",
category: "mood",
titleKey: "achievements.badges.moodStreak7.title",
descriptionKey:
"achievements.badges.moodStreak7.description",
descriptionKey: "achievements.badges.moodStreak7.description",
icon: "Smile",
format: "days",
target: 7,
@@ -209,9 +207,7 @@ test.describe("achievements page renders all four card states", () => {
// Hidden-locked — opaque placeholder visible. The real title /
// description must NOT appear in the rendered HTML.
await expect(
page.getByText("Hidden achievement").first(),
).toBeVisible();
await expect(page.getByText("Hidden achievement").first()).toBeVisible();
const html = await page.content();
expect(html).not.toContain("Night owl");
expect(html).not.toContain("between 02:00");
+3 -3
View File
@@ -61,9 +61,9 @@ test.describe("/admin/api-tokens mobile no-overflow", () => {
// The mobile card-list must be visible and the desktop table must
// be display:none at this viewport.
await expect(
page.getByTestId("admin-tokens-mobile-list"),
).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("admin-tokens-mobile-list")).toBeVisible({
timeout: 10_000,
});
const dims = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
+4 -4
View File
@@ -99,7 +99,9 @@ test.describe("Settings → AI provider dropdown UX (B2)", () => {
// 2c. Switch to Anthropic → Anthropic form rendered.
await select.selectOption("anthropic");
await expect(page.getByTestId("ai-provider-config-anthropic")).toBeVisible();
await expect(
page.getByTestId("ai-provider-config-anthropic"),
).toBeVisible();
// 2d. Switch to Local → Local form rendered.
await select.selectOption("local");
@@ -108,9 +110,7 @@ test.describe("Settings → AI provider dropdown UX (B2)", () => {
// 3. Fallback chain card surfaces with the four chain rows.
await expect(page.getByTestId("ai-fallback-chain")).toBeVisible();
for (const slug of ["codex", "openai", "anthropic", "local"]) {
await expect(
page.locator(`[data-chain-row="${slug}"]`),
).toBeVisible();
await expect(page.locator(`[data-chain-row="${slug}"]`)).toBeVisible();
}
});
});
+10 -13
View File
@@ -76,19 +76,16 @@ test.describe("chart overlay controls", () => {
page,
}) => {
let putRequestCount = 0;
await page.route(
"**/api/dashboard/chart-overlay-prefs",
async (route) => {
if (route.request().method() === "PUT") {
putRequestCount += 1;
}
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { saved: true }, error: null }),
});
},
);
await page.route("**/api/dashboard/chart-overlay-prefs", async (route) => {
if (route.request().method() === "PUT") {
putRequestCount += 1;
}
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: { saved: true }, error: null }),
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
+1 -3
View File
@@ -42,9 +42,7 @@ test.describe("sidebar Admin entry no-expansion", () => {
// No sub-section anchors inside the sidebar — those belong to
// `<AdminShell>`'s in-page nav, not the global sidebar.
const subLinks = sidebar.locator(
"a[href^='/admin/']:not([href='/admin'])",
);
const subLinks = sidebar.locator("a[href^='/admin/']:not([href='/admin'])");
await expect(subLinks).toHaveCount(0);
});
+1 -6
View File
@@ -83,12 +83,7 @@ export const GET = apiHandler(async () => {
// for the dashboard tile in a single pair-and-count pass. The
// headline (`bpInTargetPct`) reuses the 30-day result so we don't
// re-pair the same data twice. See lib/analytics/bp-in-target.ts.
const windows = computeBpInTargetWindows(
sysData,
diaData,
bpTargets,
now,
);
const windows = computeBpInTargetWindows(sysData, diaData, bpTargets, now);
bpInTargetPct = windows.last30Days?.pct ?? null;
bpInTargetPct7d = windows.last7Days?.pct ?? null;
bpInTargetPct30d = windows.last30Days?.pct ?? null;
@@ -76,8 +76,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
await tx.user.update({
where: { id: user.id },
data: {
dashboardWidgetsJson:
normalized as unknown as Prisma.InputJsonValue,
dashboardWidgetsJson: normalized as unknown as Prisma.InputJsonValue,
},
});
},
+2 -2
View File
@@ -98,8 +98,8 @@ export const PUT = apiHandler(async (request: NextRequest) => {
// visibility / order; chart prefs are PUT through their own route
// (`/api/dashboard/chart-overlay-prefs`) and would otherwise be
// wiped here on a subsequent layout save.
let mergedChartOverlayPrefs: ChartOverlayPrefsMap | undefined =
parsed.data.chartOverlayPrefs as ChartOverlayPrefsMap | undefined;
let mergedChartOverlayPrefs: ChartOverlayPrefsMap | undefined = parsed.data
.chartOverlayPrefs as ChartOverlayPrefsMap | undefined;
if (mergedChartOverlayPrefs === undefined) {
const existing = await prisma.user.findUnique({
where: { id: user.id },
+86 -89
View File
@@ -473,94 +473,94 @@ export const GET = apiHandler(async (request: NextRequest) => {
auditEvents,
moodEntries,
] = await Promise.all([
prisma.measurement.findMany({
where: {
userId,
measuredAt: { gte: startDate, lte: now },
source: { not: "IMPORT" },
type: {
in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"],
prisma.measurement.findMany({
where: {
userId,
measuredAt: { gte: startDate, lte: now },
source: { not: "IMPORT" },
type: {
in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"],
},
},
select: {
type: true,
value: true,
measuredAt: true,
},
}),
prisma.medicationIntakeEvent.findMany({
where: {
userId,
source: { not: "IMPORT" },
scheduledFor: { gte: startDate, lte: now },
},
select: {
medicationId: true,
scheduledFor: true,
takenAt: true,
skipped: true,
},
}),
prisma.medication.findMany({
where: { userId },
select: {
id: true,
schedules: {
select: {
windowStart: true,
windowEnd: true,
daysOfWeek: true,
},
},
select: {
type: true,
value: true,
measuredAt: true,
},
}),
prisma.passkey.findMany({
where: {
userId,
createdAt: { gte: startDate, lte: now },
},
select: {
createdAt: true,
},
}),
prisma.auditLog.findMany({
where: {
userId,
createdAt: { gte: startDate, lte: now },
action: {
in: [
"auth.login.passkey",
"auth.login.password",
"bugreport.submit",
// v1.4.18 — hidden Easter-egg triggers
"doctor-report.generate",
"doctor-report.pdf.generate",
"settings.locale.update",
],
},
}),
prisma.medicationIntakeEvent.findMany({
where: {
userId,
source: { not: "IMPORT" },
scheduledFor: { gte: startDate, lte: now },
},
select: {
medicationId: true,
scheduledFor: true,
takenAt: true,
skipped: true,
},
}),
prisma.medication.findMany({
where: { userId },
select: {
id: true,
schedules: {
select: {
windowStart: true,
windowEnd: true,
daysOfWeek: true,
},
},
},
}),
prisma.passkey.findMany({
where: {
userId,
createdAt: { gte: startDate, lte: now },
},
select: {
createdAt: true,
},
}),
prisma.auditLog.findMany({
where: {
userId,
createdAt: { gte: startDate, lte: now },
action: {
in: [
"auth.login.passkey",
"auth.login.password",
"bugreport.submit",
// v1.4.18 — hidden Easter-egg triggers
"doctor-report.generate",
"doctor-report.pdf.generate",
"settings.locale.update",
],
},
},
select: {
action: true,
createdAt: true,
},
}),
// v1.4.18 — mood entries feed the new mood badges + the
// entry-streak/consistent-month engagement metrics. Synced from
// moodLog.app or entered directly; we intentionally include all
// sources because consistency-of-tracking is what the badge
// rewards.
prisma.moodEntry.findMany({
where: {
userId,
moodLoggedAt: { gte: startDate, lte: now },
},
select: {
date: true,
score: true,
moodLoggedAt: true,
},
}),
]);
},
select: {
action: true,
createdAt: true,
},
}),
// v1.4.18 — mood entries feed the new mood badges + the
// entry-streak/consistent-month engagement metrics. Synced from
// moodLog.app or entered directly; we intentionally include all
// sources because consistency-of-tracking is what the badge
// rewards.
prisma.moodEntry.findMany({
where: {
userId,
moodLoggedAt: { gte: startDate, lte: now },
},
select: {
date: true,
score: true,
moodLoggedAt: true,
},
}),
]);
const schedulesByMedicationId = new Map(
medications.map((med) => [med.id, med.schedules]),
@@ -820,10 +820,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
// discovered set. Hidden achievements that are still locked are
// counted toward `totalCount` so the user sees they exist.
const visibleUnlocked = visibleAchievements.filter((a) => a.unlocked);
const visibleEarned = visibleUnlocked.reduce(
(acc, a) => acc + a.points,
0,
);
const visibleEarned = visibleUnlocked.reduce((acc, a) => acc + a.points, 0);
const visibleTotalPoints = visibleAchievements.reduce(
(acc, a) => acc + a.points,
0,
+6 -1
View File
@@ -1,7 +1,12 @@
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiError, apiSuccess, getClientIp, safeJson } from "@/lib/api-response";
import {
apiError,
apiSuccess,
getClientIp,
safeJson,
} from "@/lib/api-response";
import { auditLog } from "@/lib/auth/audit";
import { prisma } from "@/lib/db";
import { Prisma } from "@/generated/prisma/client";
@@ -15,7 +15,10 @@ vi.mock("@/lib/api-handler", () => ({
session: { id: "s-1" },
})),
HttpError: class HttpError extends Error {
constructor(public status: number, message: string) {
constructor(
public status: number,
message: string,
) {
super(message);
}
},
@@ -22,10 +22,7 @@ vi.mock("next/navigation", () => ({
}));
import { I18nProvider } from "@/lib/i18n/context";
import {
ADMIN_SECTIONS,
AdminShell,
} from "../admin-shell";
import { ADMIN_SECTIONS, AdminShell } from "../admin-shell";
function renderShell(props: {
active?: (typeof ADMIN_SECTIONS)[number]["slug"];
@@ -97,9 +97,7 @@ describe("ApiTokenOverviewSection — responsive", () => {
// forcing a scroll container — that was the v1.4.15 bug Marc
// re-reported. Confirm there's no overflow-x-auto INSIDE the
// mobile <ul>.
const mobileMatch = html.match(
/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/,
);
const mobileMatch = html.match(/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/);
expect(mobileMatch).not.toBeNull();
expect(mobileMatch![1]).not.toContain("overflow-x-auto");
});
@@ -123,9 +121,7 @@ describe("ApiTokenOverviewSection — responsive", () => {
it("mobile timestamp lines wrap (no nowrap on the meta paragraphs)", () => {
const html = render();
const mobileMatch = html.match(
/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/,
);
const mobileMatch = html.match(/<ul[^>]*md:hidden[^>]*>([\s\S]*?)<\/ul>/);
expect(mobileMatch).not.toBeNull();
// The Last-used + Created lines use <p class="text-[11px]…"> — no
// whitespace-nowrap so a long German date+time can flow onto a
+5 -6
View File
@@ -132,12 +132,9 @@ export function AiQualitySection() {
</div>
<div className="overflow-x-auto">
<table
className="w-full text-sm"
data-slot="ai-quality-table"
>
<table className="w-full text-sm" data-slot="ai-quality-table">
<thead>
<tr className="text-muted-foreground border-border border-b text-left text-xs uppercase tracking-wider">
<tr className="text-muted-foreground border-border border-b text-left text-xs tracking-wider uppercase">
<th className="py-2 pr-3 font-medium">
{t("admin.aiQuality.colSeverity")}
</th>
@@ -183,7 +180,9 @@ export function AiQualitySection() {
{bucket.promptVersion}
</td>
<td className="py-2 pr-3 tabular-nums">{bucket.helpful}</td>
<td className="py-2 pr-3 tabular-nums">{bucket.notHelpful}</td>
<td className="py-2 pr-3 tabular-nums">
{bucket.notHelpful}
</td>
<td
className={`py-2 font-semibold tabular-nums ${helpfulRateColour(
bucket.helpfulRate,
+140 -138
View File
@@ -49,156 +49,158 @@ export function SystemStatusSection() {
<div className="space-y-6">
<HostMetricsChart />
<div className="bg-card border-border rounded-xl border p-6">
<div className="flex items-center gap-2">
<Server className="text-primary h-5 w-5" />
<div className="text-lg font-semibold">{t("admin.systemStatus")}</div>
</div>
{status ? (
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatusItem
icon={Database}
label={t("admin.database")}
value={
status.database === "connected"
? t("admin.databaseConnected")
: t("admin.databaseError")
}
className={
status.database === "connected"
? "text-dracula-green"
: "text-destructive"
}
/>
<StatusItem
icon={Clock}
label={t("admin.startedAt")}
value={formatDateTime(status.startTime)}
/>
<StatusItem
icon={Users}
label={t("admin.users")}
value={String(status.counts.users)}
/>
<StatusItem
icon={Activity}
label={t("admin.measurementsCount")}
value={fmt.integer(status.counts.measurements)}
/>
<StatusItem
icon={Key}
label={t("admin.activeTokens")}
value={String(status.counts.activeTokens)}
/>
<StatusItem
icon={Globe}
label={t("admin.activeSessions")}
value={String(status.counts.activeSessions)}
/>
<StatusItem
icon={Cog}
label={t("admin.workerStatus")}
value={
status.worker.running
? t("admin.workerRunning")
: t("admin.workerStopped")
}
className={
status.worker.running ? "text-dracula-green" : "text-destructive"
}
/>
{status.worker.lastReminderCheck && (
<div className="flex items-center gap-2">
<Server className="text-primary h-5 w-5" />
<div className="text-lg font-semibold">{t("admin.systemStatus")}</div>
</div>
{status ? (
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatusItem
icon={Bell}
label={t("admin.lastReminderCheck")}
value={formatDateTime(status.worker.lastReminderCheck)}
icon={Database}
label={t("admin.database")}
value={
status.database === "connected"
? t("admin.databaseConnected")
: t("admin.databaseError")
}
className={
status.database === "connected"
? "text-dracula-green"
: "text-destructive"
}
/>
<StatusItem
icon={Clock}
label={t("admin.startedAt")}
value={formatDateTime(status.startTime)}
/>
<StatusItem
icon={Users}
label={t("admin.users")}
value={String(status.counts.users)}
/>
)}
{status.integrations.umami && (
<StatusItem
icon={Activity}
label="Umami"
label={t("admin.measurementsCount")}
value={fmt.integer(status.counts.measurements)}
/>
<StatusItem
icon={Key}
label={t("admin.activeTokens")}
value={String(status.counts.activeTokens)}
/>
<StatusItem
icon={Globe}
label={t("admin.activeSessions")}
value={String(status.counts.activeSessions)}
/>
<StatusItem
icon={Cog}
label={t("admin.workerStatus")}
value={
status.integrations.umami.enabled
? t("common.active")
: t("common.disabled")
status.worker.running
? t("admin.workerRunning")
: t("admin.workerStopped")
}
className={
status.integrations.umami.enabled
status.worker.running
? "text-dracula-green"
: "text-destructive"
}
/>
)}
{status.integrations.glitchtip && (
<StatusItem
icon={AlertTriangle}
label="GlitchTip"
value={
status.integrations.glitchtip.enabled
? t("common.active")
: t("common.disabled")
}
className={
status.integrations.glitchtip.enabled
? "text-dracula-green"
: "text-destructive"
}
/>
)}
{status.integrations.webPush && (
<StatusItem
icon={BellRing}
label={t("admin.integrationWebPush")}
value={t("admin.configured")}
className="text-dracula-green"
/>
)}
{status.integrations.bugReport && (
<StatusItem
icon={Bug}
label={t("admin.integrationBugReport")}
value={t("admin.configured")}
className="text-dracula-green"
/>
)}
</div>
) : isError ? (
// P19: surface load failures inline instead of leaving the
// section spinning forever. The hook throws on non-OK
// responses, so isError is true on 500s, network errors, etc.
// v1.4.16 Wave-C MED — pair the alert with a Retry button so a
// transient 500 (rolling deploy, DB blip) doesn't require a
// full page reload to recover.
<div
role="alert"
className="text-destructive bg-destructive/10 border-destructive/30 mt-4 flex flex-col items-start gap-2 rounded-md border px-3 py-2 text-sm sm:flex-row sm:items-center sm:justify-between"
>
<span>{t("admin.systemStatusLoadError")}</span>
<Button
type="button"
size="sm"
variant="outline"
disabled={isFetching}
onClick={() => void refetch()}
className="min-h-9"
data-testid="system-status-retry"
>
{isFetching ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
) : (
<RotateCw className="mr-1 h-3.5 w-3.5" />
{status.worker.lastReminderCheck && (
<StatusItem
icon={Bell}
label={t("admin.lastReminderCheck")}
value={formatDateTime(status.worker.lastReminderCheck)}
/>
)}
{t("admin.systemStatusRetry")}
</Button>
</div>
) : (
<div className="mt-4 flex items-center gap-2">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-sm">
{t("admin.loadingStatus")}
</span>
</div>
)}
{status.integrations.umami && (
<StatusItem
icon={Activity}
label="Umami"
value={
status.integrations.umami.enabled
? t("common.active")
: t("common.disabled")
}
className={
status.integrations.umami.enabled
? "text-dracula-green"
: "text-destructive"
}
/>
)}
{status.integrations.glitchtip && (
<StatusItem
icon={AlertTriangle}
label="GlitchTip"
value={
status.integrations.glitchtip.enabled
? t("common.active")
: t("common.disabled")
}
className={
status.integrations.glitchtip.enabled
? "text-dracula-green"
: "text-destructive"
}
/>
)}
{status.integrations.webPush && (
<StatusItem
icon={BellRing}
label={t("admin.integrationWebPush")}
value={t("admin.configured")}
className="text-dracula-green"
/>
)}
{status.integrations.bugReport && (
<StatusItem
icon={Bug}
label={t("admin.integrationBugReport")}
value={t("admin.configured")}
className="text-dracula-green"
/>
)}
</div>
) : isError ? (
// P19: surface load failures inline instead of leaving the
// section spinning forever. The hook throws on non-OK
// responses, so isError is true on 500s, network errors, etc.
// v1.4.16 Wave-C MED — pair the alert with a Retry button so a
// transient 500 (rolling deploy, DB blip) doesn't require a
// full page reload to recover.
<div
role="alert"
className="text-destructive bg-destructive/10 border-destructive/30 mt-4 flex flex-col items-start gap-2 rounded-md border px-3 py-2 text-sm sm:flex-row sm:items-center sm:justify-between"
>
<span>{t("admin.systemStatusLoadError")}</span>
<Button
type="button"
size="sm"
variant="outline"
disabled={isFetching}
onClick={() => void refetch()}
className="min-h-9"
data-testid="system-status-retry"
>
{isFetching ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
) : (
<RotateCw className="mr-1 h-3.5 w-3.5" />
)}
{t("admin.systemStatusRetry")}
</Button>
</div>
) : (
<div className="mt-4 flex items-center gap-2">
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-sm">
{t("admin.loadingStatus")}
</span>
</div>
)}
</div>
</div>
);
@@ -24,9 +24,7 @@ describe("<ChartEmptyState>", () => {
/>,
);
expect(html).toContain("Not enough mood data");
expect(html).toContain(
"Log at least 3 entries to unlock the trend line.",
);
expect(html).toContain("Log at least 3 entries to unlock the trend line.");
});
it("uses a custom height when supplied", () => {
@@ -64,9 +64,7 @@ describe("<ChartOverlayControls>", () => {
const start: ChartOverlayPrefs = DEFAULT_CHART_OVERLAY_PREFS;
// Render once — this binds the callback closure.
renderToStaticMarkup(
withProvider(
<ChartOverlayControls prefs={start} onChange={onChange} />,
),
withProvider(<ChartOverlayControls prefs={start} onChange={onChange} />),
);
// Simulate the merge the component performs: setKey("showTargetRange", true)
@@ -38,10 +38,7 @@ vi.mock("@/hooks/use-auth", () => ({
import { I18nProvider } from "@/lib/i18n/context";
import { HealthChart } from "../health-chart";
import {
resolveMiniRangePoints,
type DataWindow,
} from "../mini-window";
import { resolveMiniRangePoints, type DataWindow } from "../mini-window";
/**
* v1.4.16 phase B5c — chart mini-mode + windowOverride.
@@ -85,9 +82,7 @@ describe("resolveMiniRangePoints()", () => {
describe("<HealthChart mini>", () => {
it("does not render the range tabs / mode-switch row in mini mode", () => {
const html = render(
<HealthChart types={["WEIGHT"]} title="Weight" mini />,
);
const html = render(<HealthChart types={["WEIGHT"]} title="Weight" mini />);
// No "7d" / "30d" / "90d" range tabs.
expect(html).not.toMatch(/data-slot="chart-range-tab"/);
// No "7-day trend" toggle.
@@ -141,7 +141,7 @@ describe("<HealthChart> v1.4.18 clean-line revert", () => {
</I18nProvider>,
);
expect(html).not.toContain("data-slot=\"chart-linear-gradient\"");
expect(html).not.toContain('data-slot="chart-linear-gradient"');
expect(html).not.toContain("chart-gradient-BLOOD_PRESSURE_SYS");
expect(html).not.toContain("linearGradient");
});
@@ -36,9 +36,8 @@ vi.mock("@/hooks/use-auth", () => ({
describe("<MedicationComplianceChart> v1.4.18 clean-line revert", () => {
it("does NOT paint a gradient fill under the line", async () => {
const { I18nProvider } = await import("@/lib/i18n/context");
const { MedicationComplianceChart } = await import(
"../medication-compliance-chart"
);
const { MedicationComplianceChart } =
await import("../medication-compliance-chart");
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
@@ -46,7 +45,7 @@ describe("<MedicationComplianceChart> v1.4.18 clean-line revert", () => {
</I18nProvider>,
);
expect(html).not.toContain("data-slot=\"chart-linear-gradient\"");
expect(html).not.toContain('data-slot="chart-linear-gradient"');
expect(html).not.toContain("chart-gradient-medication");
expect(html).not.toContain("linearGradient");
});
@@ -60,9 +60,8 @@ vi.mock("@/hooks/use-chart-overlay-prefs", () => ({
async function renderChart(): Promise<string> {
// Re-import to pick up the freshly-set mock value.
const { MedicationComplianceChart } = await import(
"../medication-compliance-chart"
);
const { MedicationComplianceChart } =
await import("../medication-compliance-chart");
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<MedicationComplianceChart />
@@ -61,7 +61,7 @@ describe("<MoodChart> v1.4.18 clean-line revert", () => {
</I18nProvider>,
);
expect(html).not.toContain("data-slot=\"chart-linear-gradient\"");
expect(html).not.toContain('data-slot="chart-linear-gradient"');
expect(html).not.toContain("chart-gradient-mood");
expect(html).not.toContain("linearGradient");
});
@@ -78,10 +78,7 @@ export function ChartOverlayControls({
const trendArrowId = useId();
const targetRangeId = useId();
const setKey = (
key: keyof ChartOverlayPrefs,
value: boolean,
): void => {
const setKey = (key: keyof ChartOverlayPrefs, value: boolean): void => {
onChange({ ...prefs, [key]: value });
};
@@ -163,4 +160,3 @@ export function ChartOverlayControls({
</DropdownMenu>
);
}
+2 -9
View File
@@ -31,10 +31,7 @@ import {
computeWindowTrend,
SPLIT_HALF_THRESHOLD_DAYS,
} from "@/lib/analytics/window-trend";
import {
resolveMiniRangePoints,
type DataWindow,
} from "./mini-window";
import { resolveMiniRangePoints, type DataWindow } from "./mini-window";
import { shiftDailySeriesForward } from "@/lib/charts/comparison-shift";
import type {
ChartOverlayKey,
@@ -1229,11 +1226,7 @@ export function HealthChart({
}
if (rows.length === 0) return null;
return (
<RichChartTooltip
active
label={dateLabel}
rows={rows}
/>
<RichChartTooltip active label={dateLabel} rows={rows} />
);
}}
/>
+1 -5
View File
@@ -16,11 +16,7 @@
* pin the contract without depending on Recharts internals.
*/
export type DataWindow =
| "last7days"
| "last30days"
| "last90days"
| "allTime";
export type DataWindow = "last7days" | "last30days" | "last90days" | "allTime";
const POINTS_BY_WINDOW: Record<DataWindow, number> = {
last7days: 7,
+4 -8
View File
@@ -745,9 +745,9 @@ export function MoodChart({
delta = t("charts.deltaUnchanged");
} else {
const sign = diff > 0 ? "+" : "−";
const formatted = `${sign}${(
Math.abs(diff)
).toFixed(1)}`;
const formatted = `${sign}${Math.abs(diff).toFixed(
1,
)}`;
delta = t("charts.deltaVsBaseline").replace(
"{delta}",
formatted,
@@ -776,11 +776,7 @@ export function MoodChart({
}
if (rows.length === 0) return null;
return (
<RichChartTooltip
active
label={dateLabel}
rows={rows}
/>
<RichChartTooltip active label={dateLabel} rows={rows} />
);
}}
/>
@@ -216,9 +216,7 @@ describe("<InsightAdvisorCard> — summary typography polish (B1b)", () => {
// The summary's inline-chart wrapper must carry `data-mini="true"`
// so the chart shrinks to sparkline form. Per-finding charts use
// the default detail mode.
expect(html).toMatch(
/data-slot="insight-summary"[\s\S]*?data-mini="true"/,
);
expect(html).toMatch(/data-slot="insight-summary"[\s\S]*?data-mini="true"/);
});
});
@@ -64,9 +64,7 @@ function rec(
};
}
function buildInsight(
recommendations: InsightRecommendation[],
): InsightResult {
function buildInsight(recommendations: InsightRecommendation[]): InsightResult {
return {
insightType: "general",
summary: "Test summary",
@@ -80,7 +80,9 @@ describe("<InsightsPageHero>", () => {
const html = render(
<InsightsPageHero onRegenerate={() => {}} regenerating />,
);
expect(html).toMatch(/data-slot="insights-page-hero-regenerate"[^>]*disabled/);
expect(html).toMatch(
/data-slot="insights-page-hero-regenerate"[^>]*disabled/,
);
});
it("German locale — heading + subtitle translate", () => {
@@ -130,8 +130,7 @@ describe("end-to-end — generateInsight() confidence override flows into the ca
// Model still claims 99 — wrapper still discards.
confidence: 99,
metricSource: {
...strictPayloadWithModelConfidence.recommendations[0]
.metricSource,
...strictPayloadWithModelConfidence.recommendations[0].metricSource,
n: 1,
},
},
@@ -145,7 +144,9 @@ describe("end-to-end — generateInsight() confidence override flows into the ca
userPrompt: "u",
});
expect(outcome.parsed.recommendations[0].confidence).toBeLessThanOrEqual(15);
expect(outcome.parsed.recommendations[0].confidence).toBeLessThanOrEqual(
15,
);
const insight: InsightResult = {
insightType: "general",
@@ -80,10 +80,7 @@ function render(node: React.ReactNode, locale: "en" | "de" = "en") {
describe("<RecommendationCard> — confidence slot wiring", () => {
it("renders ConfidenceMeter inside rec-confidence-slot when confidence is set", () => {
const html = render(
<RecommendationCard
rec={{ ...recBase, confidence: 85 }}
index={0}
/>,
<RecommendationCard rec={{ ...recBase, confidence: 85 }} index={0} />,
);
// Slot wraps the meter
expect(html).toMatch(
@@ -230,7 +230,7 @@ describe("<RecommendationCard>", () => {
const html = render(
<RecommendationCard rec={recNoId} index={0} initiallyExpanded />,
);
expect(html).not.toContain('data-feedback-thumb=');
expect(html).not.toContain("data-feedback-thumb=");
});
it("does NOT render feedback thumbs when the metricSource.timeRange is out of vocabulary", () => {
@@ -241,6 +241,6 @@ describe("<RecommendationCard>", () => {
const html = render(
<RecommendationCard rec={recBadWindow} index={0} initiallyExpanded />,
);
expect(html).not.toContain('data-feedback-thumb=');
expect(html).not.toContain("data-feedback-thumb=");
});
});
@@ -40,14 +40,14 @@ function render(node: React.ReactNode, locale: "en" | "de" = "en") {
describe("<RecommendationFeedback>", () => {
it("renders thumbs-up + thumbs-down buttons", () => {
const html = render(<RecommendationFeedback {...baseProps} />);
expect(html).toContain("data-feedback-thumb=\"up\"");
expect(html).toContain("data-feedback-thumb=\"down\"");
expect(html).toContain('data-feedback-thumb="up"');
expect(html).toContain('data-feedback-thumb="down"');
});
it("attaches accessible aria-label keys", () => {
const html = render(<RecommendationFeedback {...baseProps} />);
expect(html).toContain("aria-label=\"Helpful");
expect(html).toContain("aria-label=\"Not helpful");
expect(html).toContain('aria-label="Helpful');
expect(html).toContain('aria-label="Not helpful');
});
it("translates the labels to German when locale=de", () => {
@@ -63,16 +63,19 @@ describe("<RecommendationFeedback>", () => {
// Confirmation text replaces both buttons.
expect(html).toContain("Thanks");
// Highlighted thumb-up still rendered for context.
expect(html).toContain("data-feedback-confirmed=\"up\"");
expect(html).toContain('data-feedback-confirmed="up"');
// The opposite button should NOT be re-clickable.
expect(html).not.toContain('data-feedback-thumb="down"');
});
it("renders the 'already rated' state with the prior verdict highlighted", () => {
const html = render(
<RecommendationFeedback {...baseProps} initialState="already-rated-down" />,
<RecommendationFeedback
{...baseProps}
initialState="already-rated-down"
/>,
);
expect(html).toContain("data-feedback-already-rated=\"down\"");
expect(html).toContain('data-feedback-already-rated="down"');
// Buttons are not interactive in this state.
expect(html).not.toContain('data-feedback-thumb="up"');
expect(html).not.toContain('data-feedback-thumb="down"');
@@ -81,13 +84,11 @@ describe("<RecommendationFeedback>", () => {
it("buttons carry type=button so they don't accidentally submit a parent form", () => {
const html = render(<RecommendationFeedback {...baseProps} />);
// Both up + down buttons must declare type=button.
const upMatch = html.match(
/<button[^>]*data-feedback-thumb="up"[^>]*>/,
);
const upMatch = html.match(/<button[^>]*data-feedback-thumb="up"[^>]*>/);
const downMatch = html.match(
/<button[^>]*data-feedback-thumb="down"[^>]*>/,
);
expect(upMatch?.[0]).toContain("type=\"button\"");
expect(downMatch?.[0]).toContain("type=\"button\"");
expect(upMatch?.[0]).toContain('type="button"');
expect(downMatch?.[0]).toContain('type="button"');
});
});
@@ -85,9 +85,7 @@ function rec(
};
}
function buildInsight(
recommendations: InsightRecommendation[],
): InsightResult {
function buildInsight(recommendations: InsightRecommendation[]): InsightResult {
return {
insightType: "general",
summary: "Test summary",
@@ -145,10 +143,7 @@ describe("InsightAdvisorCard + RecommendationsGrid full flow (B1b)", () => {
];
const html = renderToStaticMarkup(
<I18nProvider initialLocale="en">
<InsightAdvisorCard
title="draft"
insight={buildInsight(recs)}
/>
<InsightAdvisorCard title="draft" insight={buildInsight(recs)} />
</I18nProvider>,
);
@@ -120,10 +120,7 @@ describe("sortRecommendationsBySeverity", () => {
describe("<RecommendationsGrid>", () => {
it("renders cards in a 2-col responsive grid", () => {
const recs = [
rec("a", "info", "alpha"),
rec("b", "urgent", "bravo"),
];
const recs = [rec("a", "info", "alpha"), rec("b", "urgent", "bravo")];
const html = render(<RecommendationsGrid recs={recs} />);
expect(html).toMatch(/data-slot="rec-grid"/);
expect(html).toMatch(/grid-cols-1/);
@@ -131,10 +128,7 @@ describe("<RecommendationsGrid>", () => {
});
it("renders cards in severity-ordered sequence (urgent first)", () => {
const recs = [
rec("a", "info", "alpha"),
rec("b", "urgent", "bravo"),
];
const recs = [rec("a", "info", "alpha"), rec("b", "urgent", "bravo")];
const html = render(<RecommendationsGrid recs={recs} />);
// bravo (urgent) must appear before alpha (info) in the output.
expect(html.indexOf("bravo")).toBeLessThan(html.indexOf("alpha"));
+8 -6
View File
@@ -47,12 +47,14 @@ function litBarsFor(value: number): number {
return lit;
}
const BAR_LIT_COLOUR_BY_BAND: Record<Exclude<ConfidenceBand, "draft">, string> =
{
high: "bg-dracula-green",
medium: "bg-dracula-yellow",
low: "bg-dracula-orange",
};
const BAR_LIT_COLOUR_BY_BAND: Record<
Exclude<ConfidenceBand, "draft">,
string
> = {
high: "bg-dracula-green",
medium: "bg-dracula-yellow",
low: "bg-dracula-orange",
};
const RING_STROKE_COLOUR_BY_BAND: Record<
Exclude<ConfidenceBand, "draft">,
@@ -178,7 +178,9 @@ function RationaleCard({
feedbackProps,
}: {
rationale: InsightRecommendationRationale;
metricSource: { type: string; timeRange: string; summary: string } | undefined;
metricSource:
| { type: string; timeRange: string; summary: string }
| undefined;
referenceId: string | undefined;
confidence: number | undefined;
locale: Locale;
@@ -295,10 +297,7 @@ export function RecommendationCard({
// empty so a thumbs-click can never produce a 422.
const feedbackTimeRange = asFeedbackTimeRange(norm.metricSource?.timeRange);
const feedbackProps =
norm.id &&
norm.severity &&
norm.metricSource?.type &&
feedbackTimeRange
norm.id && norm.severity && norm.metricSource?.type && feedbackTimeRange
? {
recId: norm.id,
recText: norm.text,
@@ -113,10 +113,7 @@ function writeLocalState(
): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(
localCacheKey(userId, recId, recText),
verdict,
);
window.localStorage.setItem(localCacheKey(userId, recId, recText), verdict);
} catch {
// Best-effort — server dedup is the source of truth.
}
@@ -259,10 +256,7 @@ export function RecommendationFeedback({
const disabled = submittingUp || submittingDown;
return (
<div
className="flex items-center gap-1.5"
data-slot="rec-feedback-buttons"
>
<div className="flex items-center gap-1.5" data-slot="rec-feedback-buttons">
<button
type="button"
data-feedback-thumb="up"
@@ -96,7 +96,9 @@ export function RecommendationsGrid({ recs }: RecommendationsGridProps) {
SEVERITY_BORDER_FALLBACK;
return (
<div
key={typeof rec === "string" ? `${index}-${rec}` : (rec.id ?? index)}
key={
typeof rec === "string" ? `${index}-${rec}` : (rec.id ?? index)
}
data-stagger-index={index}
role="listitem"
className={`animate-insight-in transition-all md:hover:-translate-y-0.5 md:hover:shadow-lg ${borderClass} rounded-lg border-l-2`}
@@ -64,12 +64,10 @@ describe("<BottomNav> 5+More layout", () => {
const html = render();
// Attribute order isn't guaranteed by React's renderer, so match
// both possible directions instead of pinning the order.
const hasLabelFirst = /aria-label="More"[^>]*data-testid="bottom-nav-more"/.test(
html,
);
const hasTestidFirst = /data-testid="bottom-nav-more"[^>]*aria-label="More"/.test(
html,
);
const hasLabelFirst =
/aria-label="More"[^>]*data-testid="bottom-nav-more"/.test(html);
const hasTestidFirst =
/data-testid="bottom-nav-more"[^>]*aria-label="More"/.test(html);
expect(hasLabelFirst || hasTestidFirst).toBe(true);
});
});
@@ -13,10 +13,7 @@
* correctness.
*/
import { describe, expect, it } from "vitest";
import {
tourReferrerKey,
tourSessionDismissedKey,
} from "../tour-launcher";
import { tourReferrerKey, tourSessionDismissedKey } from "../tour-launcher";
describe("tour-launcher sessionStorage key scoping", () => {
it("scopes the dismiss key by user id", () => {
@@ -28,8 +28,18 @@ function lookup(bag: Bag, path: string[]): string | null {
describe("settings sections — i18n parity (B6)", () => {
for (const slug of SETTINGS_SECTION_SLUGS) {
it(`every locale resolves settings.sections.${slug}.title`, () => {
const enValue = lookup(en as Bag, ["settings", "sections", slug, "title"]);
const deValue = lookup(de as Bag, ["settings", "sections", slug, "title"]);
const enValue = lookup(en as Bag, [
"settings",
"sections",
slug,
"title",
]);
const deValue = lookup(de as Bag, [
"settings",
"sections",
slug,
"title",
]);
expect(enValue, `EN missing title for ${slug}`).toBeTypeOf("string");
expect(enValue?.length ?? 0).toBeGreaterThan(0);
expect(deValue, `DE missing title for ${slug}`).toBeTypeOf("string");
@@ -49,9 +59,13 @@ describe("settings sections — i18n parity (B6)", () => {
slug,
"description",
]);
expect(enValue, `EN missing description for ${slug}`).toBeTypeOf("string");
expect(enValue, `EN missing description for ${slug}`).toBeTypeOf(
"string",
);
expect(enValue?.length ?? 0).toBeGreaterThan(0);
expect(deValue, `DE missing description for ${slug}`).toBeTypeOf("string");
expect(deValue, `DE missing description for ${slug}`).toBeTypeOf(
"string",
);
expect(deValue?.length ?? 0).toBeGreaterThan(0);
});
}
+3 -13
View File
@@ -105,11 +105,7 @@ const DEFAULT_CHAIN: readonly ChainEntry[] = [
* fine-tunes (e.g. `gpt-5`, which is admitted via the env override on
* the Codex backend but not yet a documented OpenAI model id).
*/
const OPENAI_MODEL_PRESETS = [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
] as const;
const OPENAI_MODEL_PRESETS = ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"] as const;
const ANTHROPIC_MODEL_PRESETS = [
"claude-sonnet-4-6",
"claude-opus-4-7",
@@ -268,10 +264,7 @@ function AiInsightsCard({ isAuthenticated }: { isAuthenticated: boolean }) {
{t("settings.kiInsightsDescription")}
</p>
<ActiveProviderSelect
value={selectedProvider}
onChange={pickProvider}
/>
<ActiveProviderSelect value={selectedProvider} onChange={pickProvider} />
<ProviderConfigCard
provider={selectedProvider}
@@ -1096,10 +1089,7 @@ function LocalProviderForm({
if (userProvider?.provider === "LOCAL") {
setBaseUrl(userProvider.baseUrl ?? "");
const saved = userProvider.model ?? "";
if (
saved &&
(LOCAL_MODEL_PRESETS as readonly string[]).includes(saved)
) {
if (saved && (LOCAL_MODEL_PRESETS as readonly string[]).includes(saved)) {
setModelChoice(saved);
setCustomModel("");
} else if (saved) {
+1 -3
View File
@@ -54,9 +54,7 @@ export function useChartOverlayPrefs(
const prefs = useMemo<ChartOverlayPrefs>(() => {
if (!chartKey) return DEFAULT_CHART_OVERLAY_PREFS;
return (
layout?.chartOverlayPrefs?.[chartKey] ?? DEFAULT_CHART_OVERLAY_PREFS
);
return layout?.chartOverlayPrefs?.[chartKey] ?? DEFAULT_CHART_OVERLAY_PREFS;
}, [layout, chartKey]);
const mutation = useMutation({
@@ -1,8 +1,5 @@
import { describe, it, expect } from "vitest";
import {
aiInsightResponseSchema,
aiRecommendationSchema,
} from "../schema";
import { aiInsightResponseSchema, aiRecommendationSchema } from "../schema";
/**
* v1.4.16 phase B5d — `recommendation.confidence` schema field.
@@ -75,7 +72,8 @@ describe("aiRecommendationSchema — confidence", () => {
it("rejects non-integer confidence", () => {
expect(
aiRecommendationSchema.safeParse({ ...baseRec, confidence: 67.5 }).success,
aiRecommendationSchema.safeParse({ ...baseRec, confidence: 67.5 })
.success,
).toBe(false);
});
@@ -102,8 +102,7 @@ describe("generateInsight() — confidence override", () => {
{
...validResponseWithModelConfidence.recommendations[0],
metricSource: {
...validResponseWithModelConfidence.recommendations[0]
.metricSource,
...validResponseWithModelConfidence.recommendations[0].metricSource,
n: 14,
},
},
@@ -127,8 +126,7 @@ describe("generateInsight() — confidence override", () => {
{
...validResponseWithModelConfidence.recommendations[0],
metricSource: {
...validResponseWithModelConfidence.recommendations[0]
.metricSource,
...validResponseWithModelConfidence.recommendations[0].metricSource,
n: 1,
},
},
@@ -182,8 +180,7 @@ describe("generateInsight() — confidence override", () => {
id: "rec-b",
confidence: 1,
metricSource: {
...validResponseWithModelConfidence.recommendations[0]
.metricSource,
...validResponseWithModelConfidence.recommendations[0].metricSource,
n: 5,
},
},
@@ -46,10 +46,9 @@ describe("MEDICAL_REFERENCES bundle", () => {
expect(ref.publishedYear, `year missing on ${ref.id}`).toBeGreaterThan(
1990,
);
expect(
Array.isArray(ref.scope),
`scope must be array on ${ref.id}`,
).toBe(true);
expect(Array.isArray(ref.scope), `scope must be array on ${ref.id}`).toBe(
true,
);
expect(
ref.scope.length,
`scope cannot be empty on ${ref.id}`,
@@ -67,7 +66,10 @@ describe("MEDICAL_REFERENCES bundle", () => {
it("every URL parses as a well-formed https:// URL", () => {
for (const ref of MEDICAL_REFERENCES) {
expect(() => new URL(ref.url), `URL malformed on ${ref.id}`).not.toThrow();
expect(
() => new URL(ref.url),
`URL malformed on ${ref.id}`,
).not.toThrow();
const parsed = new URL(ref.url);
expect(parsed.protocol, `non-https URL on ${ref.id}`).toBe("https:");
expect(parsed.hostname, `empty hostname on ${ref.id}`).toBeTruthy();
+1 -2
View File
@@ -101,8 +101,7 @@ describe("PROVIDER_CHAIN_DEFAULT", () => {
});
it("includes admin OpenAI as final fallback so misconfigured users still see insights", () => {
const last =
PROVIDER_CHAIN_DEFAULT[PROVIDER_CHAIN_DEFAULT.length - 1];
const last = PROVIDER_CHAIN_DEFAULT[PROVIDER_CHAIN_DEFAULT.length - 1];
expect(last.providerType).toBe("admin-openai");
});
+8 -12
View File
@@ -113,9 +113,7 @@ describe("runWithFallback — happy path", () => {
});
const result = await runWithFallback({
userId: "u1",
providers: [
{ providerType: "codex", instance: codex },
],
providers: [{ providerType: "codex", instance: codex }],
params: { systemPrompt: "s", userPrompt: "u" },
});
expect(result.parsed.summary).toBe("ok");
@@ -131,9 +129,7 @@ describe("runWithFallback — happy path", () => {
});
const result = await runWithFallback({
userId: "u1",
providers: [
{ providerType: "openai", instance: openai },
],
providers: [{ providerType: "openai", instance: openai }],
params: { systemPrompt: "s", userPrompt: "u" },
});
expect(result.workingProvider.providerType).toBe("openai");
@@ -197,10 +193,7 @@ describe("runWithFallback — all-fail cascade", () => {
const e = caught as AllProvidersFailedError;
expect(e.httpStatus).toBe(503);
expect(e.attempts).toHaveLength(2);
expect(e.attempts.map((a) => a.providerType)).toEqual([
"codex",
"openai",
]);
expect(e.attempts.map((a) => a.providerType)).toEqual(["codex", "openai"]);
});
});
@@ -215,7 +208,10 @@ describe("runWithFallback — non-hard errors bubble unchanged", () => {
// Both wrapper attempts return invalid JSON → wrapper raises
// InsightSchemaError(422). The runner does not walk to provider
// 2 because that error is not a "hard provider failure".
script: [{ ok: true, content: "not-json" }, { ok: true, content: "still-not-json" }],
script: [
{ ok: true, content: "not-json" },
{ ok: true, content: "still-not-json" },
],
});
const openai = new ScriptedProvider({
type: "admin-key",
@@ -348,7 +344,7 @@ describe("runRawCompletionWithFallback — legacy route shim", () => {
});
const openai = new ScriptedProvider({
type: "admin-key",
script: [{ ok: true, content: "{ \"legacy\": true }" }],
script: [{ ok: true, content: '{ "legacy": true }' }],
});
const result = await runRawCompletionWithFallback({
userId: "u-raw",
@@ -70,9 +70,9 @@ const baseValid: AIInsightResponse = {
describe("aiRecommendationRationaleSchema", () => {
it("accepts a well-formed rationale", () => {
expect(aiRecommendationRationaleSchema.safeParse(baseRationale).success).toBe(
true,
);
expect(
aiRecommendationRationaleSchema.safeParse(baseRationale).success,
).toBe(true);
});
it.each(["last7days", "last30days", "last90days", "allTime"] as const)(
@@ -185,10 +185,7 @@ describe("aiInsightResponseSchema — rationale integration", () => {
void _r;
const result = aiInsightResponseSchema.safeParse({
...baseValid,
recommendations: [
baseRec,
{ ...recWithoutRationale, id: "rec-2" },
],
recommendations: [baseRec, { ...recWithoutRationale, id: "rec-2" }],
});
expect(result.success).toBe(false);
});
+38 -41
View File
@@ -69,21 +69,20 @@ describe("resolveProviderChain", () => {
});
it("respects user-defined chain priority order", async () => {
vi.mocked(prisma.user.findUnique)
.mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: "enc-anth",
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
// user puts openai first, anthropic second; admin disabled
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: true },
{ providerType: "anthropic", priority: 2, enabled: true },
{ providerType: "admin-openai", priority: 3, enabled: false },
],
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: "enc-anth",
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
// user puts openai first, anthropic second; admin disabled
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: true },
{ providerType: "anthropic", priority: 2, enabled: true },
{ providerType: "admin-openai", priority: 3, enabled: false },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: "enc-admin",
adminAiModel: "gpt-4o-mini",
@@ -96,19 +95,18 @@ describe("resolveProviderChain", () => {
});
it("filters out chain entries the user has not credentialed", async () => {
vi.mocked(prisma.user.findUnique)
.mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null, // no anthropic key
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "anthropic", priority: 1, enabled: true },
{ providerType: "openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null, // no anthropic key
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "anthropic", priority: 1, enabled: true },
{ providerType: "openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: null,
adminAiModel: "gpt-4o-mini",
@@ -121,19 +119,18 @@ describe("resolveProviderChain", () => {
});
it("skips disabled entries even if the credential is present", async () => {
vi.mocked(prisma.user.findUnique)
.mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null,
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: false }, // disabled
{ providerType: "admin-openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
aiProvider: null,
aiModel: null,
aiBaseUrl: null,
aiAnthropicKeyEncrypted: null,
aiLocalKeyEncrypted: null,
aiOpenaiKeyEncrypted: "enc-openai",
aiProviderChain: [
{ providerType: "openai", priority: 1, enabled: false }, // disabled
{ providerType: "admin-openai", priority: 2, enabled: true },
],
} as never);
vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({
adminAiKeyEncrypted: "enc-admin",
adminAiModel: "gpt-4o-mini",
+2 -1
View File
@@ -38,7 +38,8 @@ function parseAuditDetails(raw: string | null): AuditDetailsShape {
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === "object") return parsed as AuditDetailsShape;
if (parsed && typeof parsed === "object")
return parsed as AuditDetailsShape;
} catch {
// Treat unparseable details as absent — the fallback applies.
}
+5 -11
View File
@@ -28,13 +28,7 @@
* model emits an id, the UI renders a labelled link.
*/
export type MedicalReferenceOrg =
| "AHA"
| "ESH"
| "ESC"
| "WHO"
| "DGE"
| "ADA";
export type MedicalReferenceOrg = "AHA" | "ESH" | "ESC" | "WHO" | "DGE" | "ADA";
export type MedicalReferenceMetric =
| "bp"
@@ -107,7 +101,8 @@ export const MEDICAL_REFERENCES: MedicalReference[] = [
{
id: "who-2021-hypertension",
org: "WHO",
title: "WHO Guideline for the Pharmacological Treatment of Hypertension in Adults",
title:
"WHO Guideline for the Pharmacological Treatment of Hypertension in Adults",
titleDe:
"WHO-Leitlinie zur pharmakologischen Behandlung der Hypertonie bei Erwachsenen",
url: "https://www.who.int/publications/i/item/9789240033986",
@@ -153,9 +148,8 @@ export const MEDICAL_REFERENCE_IDS: readonly string[] = MEDICAL_REFERENCES.map(
);
/** Stable list of all distinct issuing bodies. */
export const MEDICAL_REFERENCE_ORGS: readonly MedicalReferenceOrg[] = Array.from(
new Set(MEDICAL_REFERENCES.map((r) => r.org)),
);
export const MEDICAL_REFERENCE_ORGS: readonly MedicalReferenceOrg[] =
Array.from(new Set(MEDICAL_REFERENCES.map((r) => r.org)));
/**
* Look up a reference by id. Returns `undefined` for unknown ids — the
+1 -3
View File
@@ -93,9 +93,7 @@ export function parseProviderChain(
};
const providerType = candidate.providerType;
if (typeof providerType !== "string") continue;
if (
!PROVIDER_CHAIN_TYPES.includes(providerType as ProviderChainType)
) {
if (!PROVIDER_CHAIN_TYPES.includes(providerType as ProviderChainType)) {
continue;
}
if (seen.has(providerType as ProviderChainType)) continue;
+1 -5
View File
@@ -1,8 +1,4 @@
import type {
AIProvider,
CompletionParams,
CompletionResult,
} from "./types";
import type { AIProvider, CompletionParams, CompletionResult } from "./types";
import {
generateInsight,
type GenerateInsightOutcome,
+1 -4
View File
@@ -11,10 +11,7 @@ import {
decryptCodexCreds,
} from "./codex-oauth";
import { isPublicUrl } from "@/lib/validations/notifications";
import {
parseProviderChain,
type ProviderChainType,
} from "./provider-chain";
import { parseProviderChain, type ProviderChainType } from "./provider-chain";
import type { ProviderChainResolved } from "./provider-runner";
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
+1 -3
View File
@@ -65,9 +65,7 @@ export const insightRecommendationSchema = z.union([
n: z.number().optional(),
})
.optional(),
severity: z
.enum(["info", "suggestion", "important", "urgent"])
.optional(),
severity: z.enum(["info", "suggestion", "important", "urgent"]).optional(),
id: z.string().optional(),
/**
* v1.4.16 phase B5d — deterministic confidence score (0-100).
+2 -2
View File
@@ -93,12 +93,12 @@ export async function applyProfileUpdate(
// touch other fields.
const priorLocale =
data.locale !== undefined
? (
? ((
await prisma.user.findUnique({
where: { id: userId },
select: { locale: true },
})
)?.locale ?? null
)?.locale ?? null)
: null;
const updatedUser = await prisma.user.update({
+3 -1
View File
@@ -60,7 +60,9 @@ export function shiftDailySeriesForward<T extends { timestamp: number }>(
* average and the prior-period average so the difference is what the
* user sees in the chart, not a single-day cherry-pick.
*/
export function averageValue(values: Array<number | null | undefined>): number | null {
export function averageValue(
values: Array<number | null | undefined>,
): number | null {
let sum = 0;
let n = 0;
for (const v of values) {
+4 -1
View File
@@ -12,7 +12,10 @@
* vitest "node" environment.
*/
export function prefersReducedMotion(): boolean {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
) {
return false;
}
try {
+3 -1
View File
@@ -130,7 +130,9 @@ export const DEFAULT_CHART_OVERLAY_PREFS: ChartOverlayPrefs = {
showTargetRange: false,
};
export type ChartOverlayPrefsMap = Partial<Record<ChartOverlayKey, ChartOverlayPrefs>>;
export type ChartOverlayPrefsMap = Partial<
Record<ChartOverlayKey, ChartOverlayPrefs>
>;
function isChartOverlayKey(value: string): value is ChartOverlayKey {
return (CHART_OVERLAY_KEYS as readonly string[]).includes(value);
@@ -169,7 +169,10 @@ describe("applyDiscoveryFilter", () => {
});
it("keeps engagement / security achievements unconditionally", () => {
const items = [progressFor("login-streak-7"), progressFor("passkey-created-1")];
const items = [
progressFor("login-streak-7"),
progressFor("passkey-created-1"),
];
const filtered = applyDiscoveryFilter(items, NONE_EARNABLE);
expect(filtered).toHaveLength(2);
});
@@ -150,8 +150,16 @@ describe("getEngagementMetrics", () => {
{ type: "WEIGHT", measuredAt: new Date("2026-04-01T08:00:00Z") },
],
moodEntries: [
{ date: "2026-04-02", score: 3, moodLoggedAt: new Date("2026-04-02T08:00:00Z") },
{ date: "2026-04-03", score: 3, moodLoggedAt: new Date("2026-04-03T08:00:00Z") },
{
date: "2026-04-02",
score: 3,
moodLoggedAt: new Date("2026-04-02T08:00:00Z"),
},
{
date: "2026-04-03",
score: 3,
moodLoggedAt: new Date("2026-04-03T08:00:00Z"),
},
],
intakeEvents: [
{
+1 -3
View File
@@ -88,9 +88,7 @@ export interface EarnabilityFlags {
hasPulse: boolean;
}
function categoryForMetric(
metric: AchievementMetricKey,
): AchievementCategory {
function categoryForMetric(metric: AchievementMetricKey): AchievementCategory {
switch (metric) {
case "totalTakenIntakes":
case "overIntakeCount":
@@ -38,14 +38,14 @@ const monthlySnapshot: ComparisonSnapshot = {
describe("buildUserPrompt() — comparison block (B8)", () => {
it("appends NO comparison block when comparison is undefined", () => {
const prompt = buildUserPrompt("{\"weight\":[]}", "agg", "en");
const prompt = buildUserPrompt('{"weight":[]}', "agg", "en");
expect(prompt).not.toMatch(/COMPARISON MODE/i);
expect(prompt).not.toMatch(/VERGLEICHSMODUS/i);
});
it("appends an English comparison block when comparison is supplied with locale=en", () => {
const prompt = buildUserPrompt(
"{\"weight\":[]}",
'{"weight":[]}',
"agg",
"en",
monthlySnapshot,
@@ -60,7 +60,7 @@ describe("buildUserPrompt() — comparison block (B8)", () => {
it("appends a German comparison block when locale=de", () => {
const prompt = buildUserPrompt(
"{\"gewicht\":[]}",
'{"gewicht":[]}',
"agg",
"de",
monthlySnapshot,
+4 -4
View File
@@ -120,7 +120,9 @@ export function buildUserPrompt(
locale: Locale,
comparison?: ComparisonSnapshot,
): string {
const comparisonBlock = comparison ? buildComparisonBlock(locale, comparison) : "";
const comparisonBlock = comparison
? buildComparisonBlock(locale, comparison)
: "";
if (locale === "en") {
const modeLabel =
@@ -172,9 +174,7 @@ export function buildComparisonBlock(
? "den gleichen Zeitraum 30 Tage zuvor"
: "den gleichen Zeitraum 365 Tage zuvor";
const renderRow = (
metric: ComparisonSnapshot["metrics"][number],
): string => {
const renderRow = (metric: ComparisonSnapshot["metrics"][number]): string => {
if (metric.delta === null || metric.baselineAvg === null) {
return `- ${metric.type}: no prior-period data available`;
}
@@ -100,11 +100,7 @@ describe("buildFeedbackBuckets", () => {
},
];
const buckets = buildFeedbackBuckets(rows);
expect(buckets.map((b) => b.severity)).toEqual([
"info",
"info",
"urgent",
]);
expect(buckets.map((b) => b.severity)).toEqual(["info", "info", "urgent"]);
// Within "info" the metricSourceType is the secondary key.
expect(buckets[0].metricSourceType).toBe("bloodPressure");
expect(buckets[1].metricSourceType).toBe("mood");
@@ -131,17 +127,17 @@ describe("aggregateRecommendationFeedback", () => {
await aggregateRecommendationFeedback(prisma, { now });
expect(prisma.recommendationFeedback.findMany).toHaveBeenCalledTimes(1);
const call = (prisma.recommendationFeedback.findMany as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const call = (
prisma.recommendationFeedback.findMany as ReturnType<typeof vi.fn>
).mock.calls[0][0];
const expectedSince = new Date(
now.getTime() - DEFAULT_FEEDBACK_AGGREGATION_WINDOW_DAYS * 86_400_000,
);
expect(call.where.createdAt.gte).toEqual(expectedSince);
expect(prisma.appSettings.upsert).toHaveBeenCalledTimes(1);
const upsertCall = (
prisma.appSettings.upsert as ReturnType<typeof vi.fn>
).mock.calls[0][0];
const upsertCall = (prisma.appSettings.upsert as ReturnType<typeof vi.fn>)
.mock.calls[0][0];
expect(upsertCall.where).toEqual({ id: "singleton" });
const summary = upsertCall.update.adminAiInsightsFeedbackSummary;
expect(summary.windowDays).toBe(DEFAULT_FEEDBACK_AGGREGATION_WINDOW_DAYS);
@@ -156,18 +152,18 @@ describe("aggregateRecommendationFeedback", () => {
await aggregateRecommendationFeedback(prisma, { now, windowDays: 7 });
const call = (prisma.recommendationFeedback.findMany as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const call = (
prisma.recommendationFeedback.findMany as ReturnType<typeof vi.fn>
).mock.calls[0][0];
const expectedSince = new Date(now.getTime() - 7 * 86_400_000);
expect(call.where.createdAt.gte).toEqual(expectedSince);
const upsertCall = (
prisma.appSettings.upsert as ReturnType<typeof vi.fn>
).mock.calls[0][0];
const upsertCall = (prisma.appSettings.upsert as ReturnType<typeof vi.fn>)
.mock.calls[0][0];
expect(upsertCall.update.adminAiInsightsFeedbackSummary.windowDays).toBe(7);
// Empty buckets when no rows.
expect(
upsertCall.update.adminAiInsightsFeedbackSummary.buckets,
).toEqual([]);
expect(upsertCall.update.adminAiInsightsFeedbackSummary.buckets).toEqual(
[],
);
});
});
+1 -3
View File
@@ -88,9 +88,7 @@ async function seedNonAdminSession() {
async function callRoute(query = "") {
const { GET } = await import("@/app/api/admin/host-metrics/route");
const url = `http://localhost/api/admin/host-metrics${query ? `?${query}` : ""}`;
return GET(
new Request(url) as unknown as Parameters<typeof GET>[0],
);
return GET(new Request(url) as unknown as Parameters<typeof GET>[0]);
}
describe("GET /api/admin/host-metrics", () => {
+2 -6
View File
@@ -292,9 +292,7 @@ describe("BP-in-target % — windowed (7-day + 30-day) — production data shape
];
for (const r of seed) {
const at = new Date(
now.getTime() - r.daysAgo * 24 * 60 * 60 * 1000,
);
const at = new Date(now.getTime() - r.daysAgo * 24 * 60 * 60 * 1000);
await prisma.measurement.create({
data: {
userId: user.id,
@@ -360,9 +358,7 @@ describe("BP-in-target % — windowed (7-day + 30-day) — production data shape
});
const now = new Date();
const fourteenDaysAgo = new Date(
now.getTime() - 14 * 24 * 60 * 60 * 1000,
);
const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
await prisma.measurement.create({
data: {
+9 -12
View File
@@ -76,12 +76,10 @@ async function seedUser(): Promise<{ userId: string }> {
describe("PUT /api/dashboard/chart-overlay-prefs — integration", () => {
it("persists per-chart prefs and the follow-up GET /api/dashboard/widgets surfaces them", async () => {
const { userId } = await seedUser();
const { PUT } = await import(
"@/app/api/dashboard/chart-overlay-prefs/route"
);
const { GET: getWidgets } = await import(
"@/app/api/dashboard/widgets/route"
);
const { PUT } =
await import("@/app/api/dashboard/chart-overlay-prefs/route");
const { GET: getWidgets } =
await import("@/app/api/dashboard/widgets/route");
const putRes = await (PUT as (req: Request) => Promise<Response>)(
new Request("http://localhost/api/dashboard/chart-overlay-prefs", {
@@ -130,9 +128,9 @@ describe("PUT /api/dashboard/chart-overlay-prefs — integration", () => {
}),
);
const finalRes = await (
getWidgets as (req: Request) => Promise<Response>
)(new Request("http://localhost/api/dashboard/widgets"));
const finalRes = await (getWidgets as (req: Request) => Promise<Response>)(
new Request("http://localhost/api/dashboard/widgets"),
);
const finalBody = (await finalRes.json()) as {
data: { chartOverlayPrefs?: Record<string, unknown> };
};
@@ -159,9 +157,8 @@ describe("PUT /api/dashboard/chart-overlay-prefs — integration", () => {
it("rejects an unknown chart key with 422", async () => {
await seedUser();
const { PUT } = await import(
"@/app/api/dashboard/chart-overlay-prefs/route"
);
const { PUT } =
await import("@/app/api/dashboard/chart-overlay-prefs/route");
const res = await (PUT as (req: Request) => Promise<Response>)(
new Request("http://localhost/api/dashboard/chart-overlay-prefs", {
@@ -84,9 +84,7 @@ interface PutEnvelope {
describe("PUT /api/insights/provider-chain — integration", () => {
it("persists a reordered chain and the follow-up GET surfaces it", async () => {
const { userId } = await seedUser();
const { PUT } = await import(
"@/app/api/insights/provider-chain/route"
);
const { PUT } = await import("@/app/api/insights/provider-chain/route");
const res = await (PUT as (req: Request) => Promise<Response>)(
new Request("http://localhost/api/insights/provider-chain", {
@@ -118,9 +116,7 @@ describe("PUT /api/insights/provider-chain — integration", () => {
it("rejects an empty chain with 422 and leaves the column untouched", async () => {
const { userId } = await seedUser();
const { PUT } = await import(
"@/app/api/insights/provider-chain/route"
);
const { PUT } = await import("@/app/api/insights/provider-chain/route");
const res = await (PUT as (req: Request) => Promise<Response>)(
new Request("http://localhost/api/insights/provider-chain", {
@@ -74,9 +74,8 @@ beforeEach(async () => {
headerJar.clear();
// Wipe the in-process last-working cache so test order doesn't
// leak the fallback decision from one case into the next.
const { clearLastWorkingProviderCache } = await import(
"@/lib/ai/provider-runner"
);
const { clearLastWorkingProviderCache } =
await import("@/lib/ai/provider-runner");
clearLastWorkingProviderCache();
});
@@ -187,8 +186,9 @@ describe("POST /api/insights/generate — chain fallback", () => {
it("falls back from broken OpenAI primary to Anthropic secondary when the primary fails", async () => {
await seedUserWithKey();
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL) => {
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input: RequestInfo | URL) => {
if (isOpenAICall(input)) {
return new Response(
JSON.stringify({ error: { code: "invalid_api_key" } }),
@@ -199,8 +199,7 @@ describe("POST /api/insights/generate — chain fallback", () => {
return anthropicSuccess();
}
throw new Error(`unexpected URL: ${String(input)}`);
},
);
});
const { POST } = await import("@/app/api/insights/generate/route");
const res = await POST(jsonRequest() as never);
@@ -241,8 +240,9 @@ describe("POST /api/insights/generate — chain fallback", () => {
it("caches the working provider so the next call skips the broken primary", async () => {
await seedUserWithKey();
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL) => {
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input: RequestInfo | URL) => {
if (isOpenAICall(input)) {
return new Response("nope", { status: 401 });
}
@@ -250,8 +250,7 @@ describe("POST /api/insights/generate — chain fallback", () => {
return anthropicSuccess();
}
throw new Error(`unexpected URL: ${String(input)}`);
},
);
});
const { POST } = await import("@/app/api/insights/generate/route");