mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
refactor(insights): mother page hosts overview only; metric depth moves to sub-pages
After W4a scaffolded the routed sub-pages and lifted the sticky tab
strip into src/app/insights/layout.tsx, the mother page becomes the
overview-only surface it always wanted to be: Hero + DailyBriefing +
CorrelationRow + TrendsRow + InsightAdvisorCard + CoachDrawer.
What's gone from the page (now lives on the matching sub-page):
- Six per-section <HealthChart> / <MoodChart> mounts (bp/weight/
pulse/mood/meds/bmi) with their target-zone + value-band wiring.
- Per-metric status-card queries (blood-pressure-status,
weight-status, pulse-status, bmi-status, mood-status,
medication-compliance-status).
- All the per-section <InsightStatusCard> + scatter-correlation
cards (BP × continuity, weight × BP, mood × BP, mood × weight).
- The bulky helper functions (getBloodPressureSectionStatus,
getWeightSectionStatus, …, classifyRangeValue) and their imports
(getBpTargets, buildWeightBandsFromHeight, pulse-targets, etc.).
- The inline <InsightsSectionNav> mount — superseded by the strip
that <InsightsLayoutShell> now mounts.
What stays here:
- HeroStrip (greeting, weekly-report banner, Health Score panel).
- DailyBriefing (the AI morning paragraph).
- CorrelationRow (cross-metric hypotheses).
- TrendsRow (BP / weight / mood mini-charts above each annotation).
- InsightAdvisorCard (the rich recommendations grid).
- CoachDrawer — mother-page-only, per Marc's directive.
The two insights-polish guard tests are re-pointed at the new homes
of their assertions: the tab-strip refresh affordance now lives on
the layout shell (src/components/insights/insights-layout-shell.tsx),
and the comparisonBaseline plumbing migrated into the shared
useInsightsLayoutPrefs() hook.
trends-row.test.tsx wraps every render in a QueryClientProvider —
the v1.4.25 W7 timezone change added a useAuth() call inside
<TrendsRow> for chart-side userTimezone plumbing, which needs a
TanStack-Query client at mount time.
This commit is contained in:
1 parent
cb825ec67a
commit
84b54d9f11
9 files changed
+280
-1493
No files matched your search
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* v1.4.25 W7b — date-key contract for MoodEntry rows.
|
||||
*
|
||||
* Pin both halves of Decision A:
|
||||
*
|
||||
* - Legacy rows (tz = null) are read as Europe/Berlin → continue to
|
||||
* produce the same day-key the v1.4.24 write path would have.
|
||||
*
|
||||
* - New rows with explicit non-Berlin tz produce a different
|
||||
* day-key in line with the user's own clock — the regression we
|
||||
* are fixing for non-Berlin users.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { moodDateKey, effectiveMoodTz, DEFAULT_TIMEZONE } from "../date-key";
|
||||
|
||||
describe("moodDateKey", () => {
|
||||
it("formats a UTC instant as a Europe/Berlin YYYY-MM-DD by default", () => {
|
||||
// 2026-05-14T21:30:00Z = 23:30 in Berlin (CEST, UTC+2)
|
||||
expect(moodDateKey(new Date("2026-05-14T21:30:00Z"), "Europe/Berlin")).toBe(
|
||||
"2026-05-14",
|
||||
);
|
||||
});
|
||||
|
||||
it("buckets a 23:50 Pacific/Auckland reading to the Auckland day", () => {
|
||||
// 2026-05-14T11:50:00Z = 23:50 Auckland (NZST, UTC+12) ; still
|
||||
// 13:50 Berlin (CEST, UTC+2). The Auckland bucket and the Berlin
|
||||
// bucket agree here — pick a UTC instant where they diverge.
|
||||
expect(
|
||||
moodDateKey(new Date("2026-05-14T11:50:00Z"), "Pacific/Auckland"),
|
||||
).toBe("2026-05-14");
|
||||
expect(moodDateKey(new Date("2026-05-14T11:50:00Z"), "Europe/Berlin")).toBe(
|
||||
"2026-05-14",
|
||||
);
|
||||
|
||||
// 2026-05-14T13:00:00Z = 01:00 next day Auckland, still
|
||||
// 15:00 same day Berlin. Pin the divergence.
|
||||
expect(
|
||||
moodDateKey(new Date("2026-05-14T13:00:00Z"), "Pacific/Auckland"),
|
||||
).toBe("2026-05-15");
|
||||
expect(moodDateKey(new Date("2026-05-14T13:00:00Z"), "Europe/Berlin")).toBe(
|
||||
"2026-05-14",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to Europe/Berlin for empty or undefined tz", () => {
|
||||
expect(moodDateKey(new Date("2026-05-14T22:00:00Z"), "")).toBe(
|
||||
"2026-05-15",
|
||||
);
|
||||
expect(
|
||||
moodDateKey(new Date("2026-05-14T22:00:00Z"), undefined as unknown as string),
|
||||
).toBe("2026-05-15");
|
||||
});
|
||||
});
|
||||
|
||||
describe("effectiveMoodTz", () => {
|
||||
it("returns the row's explicit tz when set", () => {
|
||||
expect(effectiveMoodTz({ tz: "Pacific/Auckland" })).toBe(
|
||||
"Pacific/Auckland",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns Europe/Berlin for legacy rows (tz = null)", () => {
|
||||
expect(effectiveMoodTz({ tz: null })).toBe("Europe/Berlin");
|
||||
expect(effectiveMoodTz({ tz: undefined })).toBe("Europe/Berlin");
|
||||
expect(effectiveMoodTz({ tz: "" })).toBe("Europe/Berlin");
|
||||
});
|
||||
|
||||
it("exposes the same fallback constant the writer uses", () => {
|
||||
expect(DEFAULT_TIMEZONE).toBe("Europe/Berlin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("read-path interpretation of legacy vs new rows", () => {
|
||||
// Build a row's day-key the way the route writes it: write the
|
||||
// instant `moodLoggedAt`, anchored to the row's resolved tz.
|
||||
function bucketFor(row: {
|
||||
moodLoggedAt: Date;
|
||||
tz: string | null;
|
||||
}): string {
|
||||
return moodDateKey(row.moodLoggedAt, effectiveMoodTz(row));
|
||||
}
|
||||
|
||||
it("legacy null-tz row buckets the same as a Europe/Berlin row at the same instant", () => {
|
||||
const instant = new Date("2026-05-14T22:00:00Z");
|
||||
const legacy = bucketFor({ moodLoggedAt: instant, tz: null });
|
||||
const explicitBerlin = bucketFor({
|
||||
moodLoggedAt: instant,
|
||||
tz: "Europe/Berlin",
|
||||
});
|
||||
expect(legacy).toBe(explicitBerlin);
|
||||
expect(legacy).toBe("2026-05-15");
|
||||
});
|
||||
|
||||
it("new Pacific/Auckland row buckets differently from a legacy row at the same instant", () => {
|
||||
const instant = new Date("2026-05-14T13:00:00Z");
|
||||
const legacy = bucketFor({ moodLoggedAt: instant, tz: null });
|
||||
const akl = bucketFor({ moodLoggedAt: instant, tz: "Pacific/Auckland" });
|
||||
expect(legacy).toBe("2026-05-14");
|
||||
expect(akl).toBe("2026-05-15");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* v1.4.25 W7b — per-row timezone attribution for MoodEntry rows.
|
||||
*
|
||||
* Background. Up to v1.4.24 the mood-entries POST handler computed
|
||||
* `MoodEntry.date` (YYYY-MM-DD) by formatting `moodLoggedAt` in
|
||||
* Europe/Berlin regardless of where the user actually was. That is
|
||||
* correct for users in DE, wrong for everyone else: a 23:45 NZST
|
||||
* reading would store `date` as the Berlin day, one day earlier than
|
||||
* the user's own "today".
|
||||
*
|
||||
* Decision A (proposal §7): forward-only fix.
|
||||
*
|
||||
* - Schema. Add nullable `MoodEntry.tz` column (migration 0044).
|
||||
*
|
||||
* - Writes. Every new row captures the user's current
|
||||
* `displayTimezone`, stores it on the row's `tz` column, and
|
||||
* computes `date` using THAT zone.
|
||||
*
|
||||
* - Reads. Rows with `tz === null` (legacy) are interpreted as if
|
||||
* `tz === "Europe/Berlin"`, matching the implicit write-time
|
||||
* assumption. Rows with explicit `tz` are interpreted in their
|
||||
* stored zone. No backfill is required because the read-side
|
||||
* interpretation rebuilds the historical bucketing without
|
||||
* touching the data.
|
||||
*
|
||||
* The helpers below are the single source of truth for both the write
|
||||
* path (`moodDateKey`) and any read path that needs to re-interpret a
|
||||
* legacy date string (`effectiveMoodTz`).
|
||||
*/
|
||||
|
||||
export const DEFAULT_TIMEZONE = "Europe/Berlin";
|
||||
|
||||
/**
|
||||
* Build the `YYYY-MM-DD` day-key for a mood entry given the user's
|
||||
* timezone at write time. Used by `POST /api/mood-entries` and the
|
||||
* `PUT /api/mood-entries/:id` route when `moodLoggedAt` changes.
|
||||
*
|
||||
* The Swedish locale ("sv-SE") is the historical choice for ISO 8601
|
||||
* day output via `Intl.DateTimeFormat`; we keep it so the new path
|
||||
* produces byte-identical strings to the legacy `toBerlinDate()` for
|
||||
* Europe/Berlin users (the only path tested under the v1.4.24 suite).
|
||||
*/
|
||||
export function moodDateKey(date: Date, tz: string): string {
|
||||
const safe = tz && tz.length > 0 ? tz : DEFAULT_TIMEZONE;
|
||||
return new Intl.DateTimeFormat("sv-SE", { timeZone: safe }).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective timezone for a MoodEntry row. Read-path
|
||||
* helpers (e.g. analytics aggregation by mood-day, export) call this
|
||||
* to know which zone the row's `date` string is anchored to. A
|
||||
* non-null `tz` column takes precedence; legacy rows fall back to
|
||||
* Europe/Berlin.
|
||||
*/
|
||||
export function effectiveMoodTz(row: { tz: string | null | undefined }): string {
|
||||
return row.tz && row.tz.length > 0 ? row.tz : DEFAULT_TIMEZONE;
|
||||
}
|
||||
Reference in new issue
Block a user