fix(medications): purple dose accent shared across all medication kinds

The generic medication card surfaces the upcoming schedule dose with
a `font-medium text-purple-400` accent so titration overrides on the
schedule visually pop out of the muted next-intake line. The GLP-1
card omitted the accent entirely.

Mirror the same accent on the GLP-1 card so the only difference
between Mounjaro and Ramipril detail surfaces is the data being
shown, not the styling treatment. The schedule.dose still rides
inside the existing next-injection paragraph (hidden below sm: like
the generic card so the narrow-viewport row stays one line).
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 14:29:45 +02:00
1 parent a1f61bb29b
commit 511d29ab29
3 files changed
+70 -6

No files matched your search

+13 -2
View File
@@ -58,8 +58,19 @@ describe("v1.4.20 B3 — /insights mounts CorrelationRow + TrendsRow", () => {
});
describe("v1.4.20 B3 — /api/analytics surfaces correlations", () => {
// v1.4.37 W2 — the inline correlation builder relocated to
// `src/lib/analytics/correlations-fast-path.ts`. The route now
// delegates through `computeCorrelationHypothesesFastPath`; the
// three Pearson runner imports live alongside the new dispatcher.
// These guards continue to pin the same load-bearing surface — we
// just look up the corresponding file.
const CORRELATIONS_FAST_PATH = join(
ROOT,
"src/lib/analytics/correlations-fast-path.ts",
);
it("imports the three correlation runners", () => {
const src = load(ANALYTICS_ROUTE_PATH);
const src = load(CORRELATIONS_FAST_PATH);
expect(src).toContain("correlateBpCompliance");
expect(src).toContain("correlateMoodPulse");
expect(src).toContain("correlateWeightWeekday");
@@ -72,7 +83,7 @@ describe("v1.4.20 B3 — /api/analytics surfaces correlations", () => {
});
it("annotates the wide event with per-hypothesis status", () => {
const src = load(ANALYTICS_ROUTE_PATH);
const src = load(CORRELATIONS_FAST_PATH);
expect(src).toMatch(
/annotate\(\{[\s\S]*correlations:[\s\S]*bpCompliance:[\s\S]*moodPulse:[\s\S]*weightWeekday:/,
);
+42 -3
View File
@@ -19,12 +19,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
measurement: { findMany: vi.fn() },
// v1.4.37 W2 — `ensureUserRollupsFresh` pokes `measurement.findFirst`
// for the newest-measurement watermark; mock both shapes.
measurement: {
findMany: vi.fn(),
findFirst: vi.fn().mockResolvedValue(null),
},
moodEntry: { findMany: vi.fn() },
medicationIntakeEvent: { findMany: vi.fn() },
medication: { findMany: vi.fn() },
// v1.4.33 C1 — slim summaries slice runs through `$queryRaw`.
$queryRaw: vi.fn(),
// v1.4.33 C1 — slim summaries slice runs through `$queryRaw`. The
// v1.4.36 per-type coverage probe and the v1.4.37 default-slice
// probe also ride `$queryRaw`. Default to an empty coverage map so
// the route falls back to the live aggregator branches and the
// assertions stay byte-shape stable.
$queryRaw: vi.fn().mockResolvedValue([]),
$queryRawUnsafe: vi.fn().mockResolvedValue([]),
// v1.4.35 — slim slice also reads DAY buckets; the freshness
// watermark inside `ensureUserRollupsFresh` pokes `findFirst`.
// Default both to empty so the parity check falls back to live
@@ -32,7 +42,13 @@ vi.mock("@/lib/db", () => ({
measurementRollup: {
findMany: vi.fn().mockResolvedValue([]),
findFirst: vi.fn().mockResolvedValue(null),
deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
upsert: vi.fn().mockResolvedValue({}),
},
$transaction: vi.fn().mockImplementation(async (queries: unknown[]) => {
if (Array.isArray(queries)) return Promise.all(queries);
return undefined;
}),
},
}));
@@ -119,6 +135,29 @@ beforeEach(() => {
vi.mocked(prisma.measurementRollup.findFirst).mockResolvedValue(
null as never,
);
vi.mocked(prisma.measurementRollup.deleteMany).mockResolvedValue(
{ count: 0 } as never,
);
vi.mocked(prisma.measurementRollup.upsert).mockResolvedValue(
{} as never,
);
// v1.4.37 W2 — `ensureUserRollupsFresh` reads `measurement.findFirst`;
// the per-type coverage probe + the rollup-recompute aggregator ride
// `$queryRaw` / `$queryRawUnsafe`. Default to empty so the route
// falls back to the live fast-path branches and the assertions stay
// byte-shape stable.
vi.mocked(prisma.measurement.findFirst).mockResolvedValue(null as never);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(prisma.$queryRaw as any).mockResolvedValue([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(prisma.$queryRawUnsafe as any).mockResolvedValue([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(prisma.$transaction as any).mockImplementation(
async (queries: unknown) => {
if (Array.isArray(queries)) return Promise.all(queries);
return undefined;
},
);
});
afterEach(() => {
@@ -397,7 +397,21 @@ export function Glp1MedicationCard({
})}
</p>
)}
<p className="text-foreground/85">{nextInjectionLabel()}</p>
<p className="text-foreground/85">
{nextInjectionLabel()}
{/* v1.4.37 W4b — purple dose accent on the upcoming
schedule dose, byte-equivalent with the generic
medication card. Schedule.dose can override the
medication-level dose during titration, so we surface
it here when set. Hidden below sm: to keep the narrow
viewport row tight, matching the generic card. */}
{schedule?.dose && (
<span className="hidden font-medium text-purple-400 sm:inline">
{" "}
— {schedule.dose}
</span>
)}
</p>
</div>
{/* Rotation hint — only when we have a last + recommended site