refactor(v1.4.18): apply simplify-review safe suggestions

Seven mechanical cleanups, no behaviour changes:

- Drop the duplicate ChartOverlayPrefs type + DEFAULT in
  chart-overlay-controls; re-export the canonical pair from
  dashboard-layout so a future fourth toggle can't drift between
  the UI and persistence layers.
- useChartOverlayPrefs accepts null/undefined chartKey and short-
  circuits the dashboard-layout fetch when none is supplied. Drops
  the chartKey ?? "bp" fallback and the three discard-the-result
  ternaries in health-chart; mini-mode renders no longer subscribe
  to the BP chart's persisted prefs.
- /api/analytics no longer pair-and-counts BP twice — the windowed
  helper already produces last30Days, so the headline reads it back
  instead of running computeBpInTargetPct on the same input.
- Drop the redundant moodEntries.map shape in the achievements
  route; the prisma select already returns the matching shape.
- Merge the five hidden-metric cases into the no-precondition block
  of isEarnable; the defensive comment admitted they couldn't fire.
- Remove the deprecated getAchievementCategory wrapper (zero callers).
- Strip six "v1.4.18 gradient removed" what-comments scattered
  across the chart wrappers; the polish tests pin the absence.

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:01:01 +02:00
1 parent 545f44c074
commit 720e6c89b9
9 files changed
+47 -79

No files matched your search

+6 -19
View File
@@ -4,10 +4,7 @@ import { annotate } from "@/lib/logging/context";
import { apiSuccess } from "@/lib/api-response";
import { summarize, type DataPoint } from "@/lib/analytics/trends";
import { getBpTargets } from "@/lib/analytics/bp-targets";
import {
computeBpInTargetPct,
computeBpInTargetWindows,
} from "@/lib/analytics/bp-in-target";
import { computeBpInTargetWindows } from "@/lib/analytics/bp-in-target";
import type { MeasurementType } from "@/generated/prisma/client";
import { measurementTypeEnum } from "@/lib/validations/measurement";
@@ -82,27 +79,17 @@ export const GET = apiHandler(async () => {
}),
]);
// Pure pair-and-count helper (see lib/analytics/bp-in-target.ts).
// Replaces an earlier inline implementation that divided by
// `sysData.length` and ignored a same-Berlin-day fallback for
// imports rounded past the 5-minute window — which made the tile
// read 0 % even when readings clearly sat inside target.
const result = computeBpInTargetPct(sysData, diaData, bpTargets);
bpInTargetPct = result?.pct ?? null;
// v1.4.18 A1 — populate the dashboard tile's 7T / 30T sub-values.
// The headline (`bpInTargetPct`, also 30 days) is kept for backward
// compatibility with cached client bundles; the tile reads the
// explicit window fields below for the sub-row. `last30Days` here
// is the same calculation as the headline but kept independent so
// a future refactor (e.g. headline → "today's reading in target?")
// can move without breaking the sub-values.
// v1.4.18 A1 — windowed helper computes the 7d / 30d sub-values
// 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,
);
bpInTargetPct = windows.last30Days?.pct ?? null;
bpInTargetPct7d = windows.last7Days?.pct ?? null;
bpInTargetPct30d = windows.last30Days?.pct ?? null;
}
@@ -604,11 +604,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
const expansionValues = buildExpansionMetricValues({
measurements,
moodEntries: moodEntries.map((m) => ({
date: m.date,
score: m.score,
moodLoggedAt: m.moodLoggedAt,
})),
moodEntries,
intakeEvents,
auditEvents,
});
@@ -5,7 +5,7 @@ import { I18nProvider } from "@/lib/i18n/context";
import {
ChartOverlayControls,
DEFAULT_CHART_OVERLAY_PREFS,
type ChartOverlayPrefsValue,
type ChartOverlayPrefs,
} from "../chart-overlay-controls";
/**
@@ -61,7 +61,7 @@ describe("<ChartOverlayControls>", () => {
// mirrors what the radix-ui Switch will pass at runtime without
// needing a DOM event-loop.
const onChange = vi.fn();
const start: ChartOverlayPrefsValue = DEFAULT_CHART_OVERLAY_PREFS;
const start: ChartOverlayPrefs = DEFAULT_CHART_OVERLAY_PREFS;
// Render once — this binds the callback closure.
renderToStaticMarkup(
withProvider(
@@ -71,7 +71,7 @@ describe("<ChartOverlayControls>", () => {
// Simulate the merge the component performs: setKey("showTargetRange", true)
// would call onChange({ ...start, showTargetRange: true }).
const next: ChartOverlayPrefsValue = {
const next: ChartOverlayPrefs = {
...start,
showTargetRange: true,
};
@@ -14,6 +14,7 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
import { type ChartOverlayPrefs } from "@/lib/dashboard-layout";
/**
* v1.4.18 — per-chart overlay-controls popover.
@@ -46,18 +47,20 @@ import { useTranslations } from "@/lib/i18n/context";
* pure UI.
*/
export interface ChartOverlayPrefsValue {
/** "7-day trend" line + numeric delta chip in chart header. */
showTrendIndicator: boolean;
/** Linear-regression trend line + arrow direction + personal baseline. */
showTrendArrow: boolean;
/** Target-zone shading (BP / BMI / medication thresholds). */
showTargetRange: boolean;
}
/**
* Re-export the canonical type+default from `@/lib/dashboard-layout`
* so chart-side callers can keep importing them from this module.
* Single source of truth lives in dashboard-layout (the persistence
* layer); this module is just the UI.
*/
export {
DEFAULT_CHART_OVERLAY_PREFS,
type ChartOverlayPrefs,
} from "@/lib/dashboard-layout";
export interface ChartOverlayControlsProps {
prefs: ChartOverlayPrefsValue;
onChange: (next: ChartOverlayPrefsValue) => void;
prefs: ChartOverlayPrefs;
onChange: (next: ChartOverlayPrefs) => void;
/**
* Optional CSS class for the trigger button — letting the chart
* wrapper align the cog with its title row.
@@ -76,7 +79,7 @@ export function ChartOverlayControls({
const targetRangeId = useId();
const setKey = (
key: keyof ChartOverlayPrefsValue,
key: keyof ChartOverlayPrefs,
value: boolean,
): void => {
onChange({ ...prefs, [key]: value });
@@ -161,8 +164,3 @@ export function ChartOverlayControls({
);
}
export const DEFAULT_CHART_OVERLAY_PREFS: ChartOverlayPrefsValue = {
showTrendIndicator: false,
showTrendArrow: false,
showTargetRange: false,
};
+6 -11
View File
@@ -316,12 +316,12 @@ export function HealthChart({
// v1.4.18 — three overlay toggles (showTrendIndicator / showTrendArrow
// / showTargetRange) are persisted per chart via the new
// `useChartOverlayPrefs` hook. When the chart isn't bound to a
// persistent key (mini mode / ad-hoc render) the prefs fall through
// to the clean-line default (every flag false).
const overlayPrefs = useChartOverlayPrefs(chartKey ?? "bp");
const showMA = chartKey ? overlayPrefs.prefs.showTrendIndicator : false;
const showTrend = chartKey ? overlayPrefs.prefs.showTrendArrow : false;
const showBands = chartKey ? overlayPrefs.prefs.showTargetRange : false;
// persistent key (mini mode / ad-hoc render) the hook short-circuits
// to the clean-line default and skips its dashboard-layout fetch.
const overlayPrefs = useChartOverlayPrefs(chartKey);
const showMA = overlayPrefs.prefs.showTrendIndicator;
const showTrend = overlayPrefs.prefs.showTrendArrow;
const showBands = overlayPrefs.prefs.showTargetRange;
const bmiDivisor =
valueMode === "bmi" && user?.heightCm ? (user.heightCm / 100) ** 2 : null;
@@ -968,9 +968,6 @@ export function HealthChart({
</div>
) : null}
{/* v1.4.18 — gradient fill removed per Marc's feedback.
The clean line is the chart; no painted background under
it. */}
<div className="relative z-10 h-full touch-pan-y">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
@@ -1249,8 +1246,6 @@ export function HealthChart({
}}
/>
)}
{/* v1.4.18 — gradient Area removed; only the clean
line below paints the metric. */}
{types.map((type, i) => (
<Line
key={type}
@@ -307,7 +307,6 @@ export function MedicationComplianceChart({
</p>
)}
{/* v1.4.18 — gradient defs removed; clean line only. */}
{isLoading ? (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-6 w-6 animate-spin" />
@@ -422,7 +421,6 @@ export function MedicationComplianceChart({
/>
</>
) : null}
{/* v1.4.18 — gradient Area removed; clean line only. */}
<Line
type="monotone"
dataKey="rate"
-2
View File
@@ -572,7 +572,6 @@ export function MoodChart({
</div>
</CardHeader>
<CardContent>
{/* v1.4.18 — gradient defs removed; clean line only. */}
{isLoading ? (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-6 w-6 animate-spin" />
@@ -779,7 +778,6 @@ export function MoodChart({
);
}}
/>
{/* v1.4.18 — gradient Area removed; clean line only. */}
<Line
type="monotone"
dataKey="score"
+12 -1
View File
@@ -26,7 +26,9 @@ import {
* round-trip resolves. On error the cache is rolled back to its
* previous value.
*/
export function useChartOverlayPrefs(chartKey: ChartOverlayKey): {
export function useChartOverlayPrefs(
chartKey: ChartOverlayKey | null | undefined,
): {
prefs: ChartOverlayPrefs;
setPrefs: (next: ChartOverlayPrefs) => void;
isSaving: boolean;
@@ -44,9 +46,14 @@ export function useChartOverlayPrefs(chartKey: ChartOverlayKey): {
// Keep the cache warm — the dashboard page mounts a query against
// the same key on first paint, so this hook just piggy-backs.
staleTime: 60_000,
// Skip the fetch entirely when the caller doesn't pass a chartKey
// (mini-mode / ad-hoc chart usage). Saves a wasted GET on pages like
// /insights that mount charts without per-chart persistence.
enabled: Boolean(chartKey),
});
const prefs = useMemo<ChartOverlayPrefs>(() => {
if (!chartKey) return DEFAULT_CHART_OVERLAY_PREFS;
return (
layout?.chartOverlayPrefs?.[chartKey] ?? DEFAULT_CHART_OVERLAY_PREFS
);
@@ -54,6 +61,9 @@ export function useChartOverlayPrefs(chartKey: ChartOverlayKey): {
const mutation = useMutation({
mutationFn: async (next: ChartOverlayPrefs): Promise<void> => {
// No-op when no chartKey — the setter is wired up but the hook
// is in "ad-hoc render" mode where overlays don't persist.
if (!chartKey) return;
const res = await fetch("/api/dashboard/chart-overlay-prefs", {
method: "PUT",
headers: { "content-type": "application/json" },
@@ -64,6 +74,7 @@ export function useChartOverlayPrefs(chartKey: ChartOverlayKey): {
}
},
onMutate: async (next) => {
if (!chartKey) return { previous: undefined };
await queryClient.cancelQueries({ queryKey: ["dashboard-layout"] });
const previous = queryClient.getQueryData<DashboardLayout>([
"dashboard-layout",
+5 -20
View File
@@ -39,8 +39,7 @@ export type AchievementMetricKey =
* Achievement categories — used by the /achievements UI to visually group
* the badges. Pure presentation: the computation logic in this file does
* not branch on category, and the metric → category mapping is derived in
* `getAchievementCategory` below so a metric is always in exactly one
* group.
* `categoryForMetric` below so a metric is always in exactly one group.
*
* v1.4.18 adds two new buckets:
* - `mood` for the new mood-tracking achievements
@@ -129,18 +128,6 @@ function categoryForMetric(
}
}
/**
* @deprecated kept only for legacy callers; new code should read
* `AchievementDefinition.category` directly. v1.4.18 made category a
* stored field on the definition so hidden Easter-eggs (which share
* metrics with no other badge) don't need a special case.
*/
export function getAchievementCategory(
metric: AchievementMetricKey,
): AchievementCategory {
return categoryForMetric(metric);
}
/**
* Stable category render order on the /achievements page. Mood slots
* between vitals and security so the most-used categories cluster at
@@ -823,17 +810,15 @@ function isEarnable(
case "consistentMonthCount":
case "entryDayStreak":
case "weekendStreakCount":
// No metric-data precondition — the user has an account, so the
// achievement is always discoverable.
return true;
case "nightOwlCount":
case "earlyBirdCount":
case "leapDayCount":
case "doctorPdfCount":
case "localeFlipCount":
// Hidden achievements are filtered by category check above; this
// branch is only reached if a hidden definition's category got
// misconfigured. Stay defensive.
// No metric-data precondition — the user has an account, so the
// achievement is always discoverable. The five hidden-metric keys
// are filtered earlier in `applyDiscoveryFilter` by category, but
// we cover them here for exhaustiveness.
return true;
}
}