mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(insights): let the trends row fill a switched-off module's slot from the next metric
selectTrendCharts takes the record's module map and skips every slot whose module is off, on the briefing path and on the fallback row, through the surface map. Sleep was never covered before, and the fallback row shrank to two charts with mood off; it now reaches for pulse. The row reads the module map itself.
This commit is contained in:
1 parent
74154460fd
commit
81cb6aa63d
3 files changed
+85
-21
No files matched your search
@@ -4,7 +4,7 @@ import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, TrendingUp } from "lucide-react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useAccountOnceMounted, useAuth } from "@/hooks/use-auth";
|
||||
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
|
||||
import { SectionHeading } from "@/components/ui/section-heading";
|
||||
import { HealthChartDynamicMini } from "@/components/charts/health-chart-dynamic";
|
||||
@@ -67,8 +67,8 @@ const MoodChart = dynamic(
|
||||
|
||||
interface TrendsRowProps {
|
||||
/**
|
||||
* Metrics to leave out because their module is switched off (for
|
||||
* example `mood`). Passed straight to `selectTrendCharts`.
|
||||
* Further metrics to leave out. Switched-off modules need no entry here:
|
||||
* the row reads the record's module map itself.
|
||||
*/
|
||||
hiddenMetrics?: SelectTrendChartsOptions["hiddenMetrics"];
|
||||
/**
|
||||
@@ -145,7 +145,15 @@ export function TrendsRow({
|
||||
// v1.8.5 — derive the chart set from the briefing. No new fetch: the
|
||||
// briefing payload is already on the page (advisor cache), so this is
|
||||
// a pure read that respects the v1.8.3 anti-freeze contract.
|
||||
const charts = selectTrendCharts(briefing, { hiddenMetrics });
|
||||
//
|
||||
// A slot whose module is off is left out and the next eligible metric fills
|
||||
// it (`trend:*` in the surface map). The mount-pinned account keeps the
|
||||
// hydration render identical to the server's, which had no account.
|
||||
const account = useAccountOnceMounted();
|
||||
const charts = selectTrendCharts(briefing, {
|
||||
modules: account?.modules,
|
||||
hiddenMetrics,
|
||||
});
|
||||
|
||||
// v1.4.36 W2 T3 — derive the tri-state status per metric from the
|
||||
// advisor's loading flag + the annotation presence. Pending wins
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import type { DailyBriefing, DailyBriefingKeyFinding } from "@/lib/ai/schema";
|
||||
import { isSurfaceVisible, surfaceModule } from "@/lib/modules/surface";
|
||||
import {
|
||||
selectTrendCharts,
|
||||
DEFAULT_TREND_CHART_CAP,
|
||||
@@ -158,23 +159,58 @@ describe("selectTrendCharts", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectTrendCharts — hidden metrics", () => {
|
||||
it("drops mood from the fallback triple when the mood module is off", () => {
|
||||
const charts = selectTrendCharts(null, { hiddenMetrics: ["mood"] });
|
||||
expect(charts.map((c) => c.metric)).not.toContain("mood");
|
||||
expect(charts).toHaveLength(2);
|
||||
describe("selectTrendCharts — switched-off modules", () => {
|
||||
it("fills the fallback row from the next metric when mood is off", () => {
|
||||
const charts = selectTrendCharts(null, { modules: { mood: false } });
|
||||
expect(charts.map((c) => c.metric)).toEqual(["bp", "weight", "pulse"]);
|
||||
});
|
||||
|
||||
it("skips a hidden metric the briefing flags", () => {
|
||||
it("keeps the familiar blood pressure, weight, mood row with every module on", () => {
|
||||
const charts = selectTrendCharts(null, { modules: {} });
|
||||
expect(charts.map((c) => c.metric)).toEqual(["bp", "weight", "mood"]);
|
||||
});
|
||||
|
||||
it("skips a flagged metric whose module is off and lets the next finding take the slot", () => {
|
||||
const charts = selectTrendCharts(briefing(["sleep", "mood", "weight"]), {
|
||||
modules: { sleep: false, mood: false },
|
||||
});
|
||||
expect(charts.map((c) => c.metric)).toEqual(["weight"]);
|
||||
});
|
||||
|
||||
it("still honours an explicit hide list", () => {
|
||||
const charts = selectTrendCharts(briefing(["mood", "weight"]), {
|
||||
hiddenMetrics: ["mood"],
|
||||
});
|
||||
expect(charts.map((c) => c.metric)).not.toContain("mood");
|
||||
expect(charts).toHaveLength(1);
|
||||
expect(charts.map((c) => c.metric)).toEqual(["weight"]);
|
||||
});
|
||||
|
||||
it("keeps mood when nothing is hidden", () => {
|
||||
const charts = selectTrendCharts(null, { hiddenMetrics: [] });
|
||||
expect(charts.map((c) => c.metric)).toContain("mood");
|
||||
it("never returns a slot whose module is off, for any module map and briefing", () => {
|
||||
// Exhaustive over every on/off combination of the owning modules, with
|
||||
// every finding a briefing can carry, in both directions.
|
||||
const owners = [
|
||||
...new Set(
|
||||
Object.keys(TREND_CHART_CONFIG)
|
||||
.map((m) => surfaceModule(`trend:${m}`))
|
||||
.filter((k): k is NonNullable<typeof k> => k !== undefined),
|
||||
),
|
||||
];
|
||||
expect(owners.length).toBeGreaterThan(0);
|
||||
const all = Object.keys(TREND_CHART_CONFIG) as Array<
|
||||
keyof typeof TREND_CHART_CONFIG
|
||||
>;
|
||||
const briefings = [null, briefing(all), briefing([...all].reverse())];
|
||||
for (let mask = 0; mask < 1 << owners.length; mask += 1) {
|
||||
const modules = Object.fromEntries(
|
||||
owners.map((key, i) => [key, (mask & (1 << i)) === 0]),
|
||||
);
|
||||
for (const b of briefings) {
|
||||
for (const chart of selectTrendCharts(b, { modules })) {
|
||||
expect(
|
||||
isSurfaceVisible(`trend:${chart.metric}`, modules),
|
||||
`${chart.metric} under ${JSON.stringify(modules)}`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DailyBriefing, DailyBriefingKeyFinding } from "@/lib/ai/schema";
|
||||
import { isSurfaceVisible, type SurfaceModuleMap } from "@/lib/modules/surface";
|
||||
|
||||
/**
|
||||
* v1.8.5 — derive the Insights overview "Trends" chart set from the
|
||||
@@ -261,6 +262,15 @@ export const DEFAULT_TREND_METRICS: ReadonlyArray<
|
||||
DailyBriefingKeyFinding["sourceMetric"]
|
||||
> = ["bp", "weight", "mood"];
|
||||
|
||||
/**
|
||||
* Metrics the fallback row reaches for, in order, when a default slot is left
|
||||
* out because its module is off. Pulse is a core vital, so a row without mood
|
||||
* still paints three charts rather than two.
|
||||
*/
|
||||
const FALLBACK_FILL_METRICS: ReadonlyArray<
|
||||
DailyBriefingKeyFinding["sourceMetric"]
|
||||
> = ["pulse"];
|
||||
|
||||
/** Max charts the row renders. Three keeps the equal-height 3-up grid. */
|
||||
export const DEFAULT_TREND_CHART_CAP = 3;
|
||||
|
||||
@@ -268,10 +278,13 @@ export interface SelectTrendChartsOptions {
|
||||
/** Max number of charts to return. Defaults to {@link DEFAULT_TREND_CHART_CAP}. */
|
||||
cap?: number;
|
||||
/**
|
||||
* Metrics whose module is switched off. They are skipped on both the
|
||||
* briefing path and the fallback triple, so a disabled module never
|
||||
* charts on the overview.
|
||||
* The record's resolved module map (`GET /api/auth/me` `modules`). A slot
|
||||
* whose module is off (`trend:<metric>` in the surface map) is skipped on
|
||||
* both the briefing path and the fallback row, and the next eligible
|
||||
* metric takes its place. Absent or partial: every slot is eligible.
|
||||
*/
|
||||
modules?: SurfaceModuleMap | null;
|
||||
/** Further metrics to leave out, on top of the module map. */
|
||||
hiddenMetrics?: ReadonlyArray<DailyBriefingKeyFinding["sourceMetric"]>;
|
||||
}
|
||||
|
||||
@@ -279,12 +292,14 @@ function configsFor(
|
||||
metrics: ReadonlyArray<DailyBriefingKeyFinding["sourceMetric"]>,
|
||||
cap: number,
|
||||
hidden: ReadonlySet<string>,
|
||||
modules: SurfaceModuleMap | null | undefined,
|
||||
): TrendChartConfig[] {
|
||||
const seen = new Set<string>();
|
||||
const out: TrendChartConfig[] = [];
|
||||
for (const metric of metrics) {
|
||||
if (out.length >= cap) break;
|
||||
if (hidden.has(metric)) continue; // module switched off
|
||||
if (hidden.has(metric)) continue;
|
||||
if (!isSurfaceVisible(`trend:${metric}`, modules)) continue; // module off
|
||||
const config = TREND_CHART_CONFIG[metric];
|
||||
if (!config) continue; // metric has no standalone trend chart
|
||||
if (seen.has(config.metric)) continue; // dedupe
|
||||
@@ -313,11 +328,16 @@ export function selectTrendCharts(
|
||||
|
||||
const findingMetrics =
|
||||
briefing?.keyFindings?.map((f) => f.sourceMetric) ?? [];
|
||||
const fromBriefing = configsFor(findingMetrics, cap, hidden);
|
||||
const fromBriefing = configsFor(findingMetrics, cap, hidden, options.modules);
|
||||
|
||||
if (fromBriefing.length > 0) {
|
||||
return fromBriefing;
|
||||
}
|
||||
|
||||
return configsFor(DEFAULT_TREND_METRICS, cap, hidden);
|
||||
return configsFor(
|
||||
[...DEFAULT_TREND_METRICS, ...FALLBACK_FILL_METRICS],
|
||||
cap,
|
||||
hidden,
|
||||
options.modules,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user