mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
refactor(types): consolidate AnalyticsData into src/types/analytics.ts
Three structurally-distinct `interface AnalyticsData {…}` declarations
shared a name across `src/app/page.tsx` (dashboard), `src/app/insights/
page.tsx` (insights mother), and `src/components/onboarding/
getting-started-checklist.tsx` (checklist). The collision made the
shapes un-discoverable from any one site and forced every iOS-contract
addition to land in three places.
Hoist all three into `src/types/analytics.ts` as named exports:
`DashboardAnalyticsData`, `InsightsAnalyticsData`, `ChecklistAnalytics
Data`. Each call site re-imports its shape under the original alias to
keep downstream code readable; the slim `SubPageAnalyticsData` that was
already there stays put. We deliberately keep the four shapes distinct
(rather than collapsing to one with everything optional) so call sites
can't cross-read fields they shouldn't reach for — closes v1.4.39 org-
audit recommendation #2.
This commit is contained in:
1 parent
a62b94983b
commit
dc66dfcfdc
4 files changed
+148
-98
No files matched your search
@@ -96,48 +96,10 @@ interface ComprehensiveData {
|
||||
totalMeasurements: number;
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
correlations?: {
|
||||
bpCompliance: CorrelationResult;
|
||||
moodPulse: CorrelationResult;
|
||||
weightWeekday: CorrelationResult;
|
||||
} | null;
|
||||
healthScore?: {
|
||||
score: number;
|
||||
band: "green" | "yellow" | "red";
|
||||
components: {
|
||||
// v1.4.25 W8e — the optional `source`/`asOf` slots feed the
|
||||
// provenance accordion. Older clients reading this payload
|
||||
// happily ignore the extras (additive contract).
|
||||
bp: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
weight: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
mood: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
compliance: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
};
|
||||
delta: number | null;
|
||||
} | null;
|
||||
}
|
||||
// v1.4.41 W-ORG — shared shape lives in `src/types/analytics.ts` as
|
||||
// `InsightsAnalyticsData`; aliased back to the local name to keep the
|
||||
// rest of this file readable.
|
||||
import type { InsightsAnalyticsData as AnalyticsData } from "@/types/analytics";
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
|
||||
+1
-40
@@ -25,6 +25,7 @@ import {
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import type { DataSummary as DataSummaryType } from "@/lib/analytics/trends";
|
||||
import type { DashboardAnalyticsData as AnalyticsData } from "@/types/analytics";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
@@ -91,46 +92,6 @@ import {
|
||||
getPersonalizedPulseTarget,
|
||||
} from "@/lib/analytics/pulse-targets";
|
||||
|
||||
interface AnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
bpInTargetPct: number | null;
|
||||
/**
|
||||
* v1.4.18 A1 — share of paired BP readings inside target over the
|
||||
* last 7 / 30 days. Drive the BD-Zielbereich tile's `7T:` / `30T:`
|
||||
* sub-values; render "—" when the field is null (no paired readings
|
||||
* in the window).
|
||||
*/
|
||||
bpInTargetPct7d?: number | null;
|
||||
bpInTargetPct30d?: number | null;
|
||||
/**
|
||||
* v1.4.22 A1 — long-arc all-time aggregate. After the headline
|
||||
* re-anchor to last-30-days the all-time number lives as a sub-value
|
||||
* on the BD-Zielbereich tile (alongside `7d` and `30d`).
|
||||
*/
|
||||
bpInTargetPctAllTime?: number | null;
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
|
||||
* pcts. The BD-Zielbereich tile's comparison-overlay caption picks
|
||||
* `priorMonth` for `comparisonBaseline === "lastMonth"` and
|
||||
* `priorYear` for `lastYear` so the rendered "Δ X% vs. last month"
|
||||
* stays honest. Null when the prior window has no paired readings.
|
||||
*/
|
||||
bpInTargetPctPriorMonth?: number | null;
|
||||
bpInTargetPctPriorYear?: number | null;
|
||||
glucoseByContext?: Record<string, DataSummaryType>;
|
||||
/**
|
||||
* v1.4.34 IW-B — per-type freshness map from `/api/analytics`. The
|
||||
* tile-strip helper reads `lastSeenByType[type]?.daysAgo` and forwards
|
||||
* it to each `<TrendCard staleDays>` so a metric the user hasn't
|
||||
* logged in a while keeps its tile visible (with an explicit
|
||||
* "Letzter Wert vor …" caption) instead of disappearing.
|
||||
*/
|
||||
lastSeenByType?: Record<
|
||||
string,
|
||||
{ lastSeenAt: string; daysAgo: number } | null
|
||||
>;
|
||||
}
|
||||
|
||||
interface RangeDisplayConfig {
|
||||
range: TrafficRange | null;
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ const ITEM_LABEL_KEYS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
interface AnalyticsData {
|
||||
summaries?: Record<string, { count?: number } | undefined>;
|
||||
}
|
||||
// v1.4.41 W-ORG — shared shape lives in `src/types/analytics.ts` as
|
||||
// `ChecklistAnalyticsData`; aliased back to the local name.
|
||||
import type { ChecklistAnalyticsData as AnalyticsData } from "@/types/analytics";
|
||||
|
||||
interface WithingsStatus {
|
||||
connected?: boolean;
|
||||
|
||||
+140
-13
@@ -1,23 +1,150 @@
|
||||
/**
|
||||
* v1.4.28 R3d (BK-F-M1) — shared shape for the trimmed `/api/analytics`
|
||||
* payload that every insights sub-page consumes.
|
||||
* Shared analytics DTOs.
|
||||
*
|
||||
* Pre-fix, each of the seven `/insights/<metric>/page.tsx` modules
|
||||
* declared its own `interface AnalyticsData { summaries: Record<...> }`
|
||||
* inline. They were structurally identical so the TanStack-Query cache
|
||||
* unified anyway, but maintaining seven copies meant every iOS-contract
|
||||
* addition (e.g. a new `MeasurementType`) showed up only after the
|
||||
* matching sub-page was touched. Hoisting the interface here is a
|
||||
* single-place change.
|
||||
* Pre-v1.4.41, three structurally-distinct `interface AnalyticsData {…}`
|
||||
* declarations lived inline in `src/app/page.tsx` (dashboard), in
|
||||
* `src/app/insights/page.tsx` (insights-mother), and in
|
||||
* `src/components/onboarding/getting-started-checklist.tsx` (checklist).
|
||||
* The names collided across files even though the shapes did not, which
|
||||
* made "where does the analytics payload live?" un-discoverable.
|
||||
*
|
||||
* NOTE: the dashboard's `AnalyticsData` (in `src/app/page.tsx`) is a
|
||||
* wider type — it carries `bpInTargetPct*` aggregates the sub-pages
|
||||
* don't use. The dashboard keeps its own declaration; we'd lose more
|
||||
* than we gain by trying to merge them.
|
||||
* v1.4.41 W-ORG (org-audit rec #2) hoists every shape into this module
|
||||
* under a name that says which surface it describes. The header below
|
||||
* documents the relationships so a future contract change has one
|
||||
* place to land.
|
||||
*
|
||||
* - `SubPageAnalyticsData` — slim shape consumed by every
|
||||
* `/insights/<metric>/page.tsx`; just `summaries`.
|
||||
* - `DashboardAnalyticsData` — dashboard root (`src/app/page.tsx`);
|
||||
* carries the `bpInTargetPct*` aggregates the sub-pages don't use
|
||||
* plus `glucoseByContext` and `lastSeenByType`.
|
||||
* - `InsightsAnalyticsData` — insights mother page
|
||||
* (`src/app/insights/page.tsx`); carries `correlations` +
|
||||
* `healthScore` blocks that the sub-pages also don't use.
|
||||
* - `ChecklistAnalyticsData` — onboarding checklist component;
|
||||
* ultra-loose shape that only checks per-type `count` for
|
||||
* "do you have any data of this kind yet?".
|
||||
*
|
||||
* NOTE: we keep four shapes (rather than collapsing into one with
|
||||
* everything optional) because the call-sites use TypeScript control
|
||||
* flow to *demand* the fields they need — the slim sub-pages should
|
||||
* not be able to type-pass an access to `bpInTargetPct`, the checklist
|
||||
* should not be able to read `correlations`, and the dashboard should
|
||||
* not be reading `healthScore` without going through the insights
|
||||
* surface. Three named shapes capture the contract; one swiss-army
|
||||
* shape loses it.
|
||||
*/
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
import type { DataSummary as DataSummaryType } from "@/lib/analytics/trends";
|
||||
import type { CorrelationResult } from "@/lib/insights/correlations";
|
||||
|
||||
/** Shape consumed by every `/insights/<metric>/page.tsx`. */
|
||||
export interface SubPageAnalyticsData {
|
||||
/** `Record<MeasurementType, DataSummary>` — sentinel value for "not loaded yet" is `undefined`. */
|
||||
summaries: Record<string, DataSummary>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape consumed by the dashboard root (`src/app/page.tsx`).
|
||||
*
|
||||
* `bpInTargetPct*` drive the BD-Zielbereich tile; `lastSeenByType`
|
||||
* keeps stale-but-still-relevant tiles visible with an explicit
|
||||
* "Letzter Wert vor …" caption.
|
||||
*/
|
||||
export interface DashboardAnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
bpInTargetPct: number | null;
|
||||
/**
|
||||
* v1.4.18 A1 — share of paired BP readings inside target over the
|
||||
* last 7 / 30 days. Drive the BD-Zielbereich tile's `7T:` / `30T:`
|
||||
* sub-values; render "—" when the field is null (no paired readings
|
||||
* in the window).
|
||||
*/
|
||||
bpInTargetPct7d?: number | null;
|
||||
bpInTargetPct30d?: number | null;
|
||||
/**
|
||||
* v1.4.22 A1 — long-arc all-time aggregate. After the headline
|
||||
* re-anchor to last-30-days the all-time number lives as a sub-value
|
||||
* on the BD-Zielbereich tile (alongside `7d` and `30d`).
|
||||
*/
|
||||
bpInTargetPctAllTime?: number | null;
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
|
||||
* pcts. The BD-Zielbereich tile's comparison-overlay caption picks
|
||||
* `priorMonth` for `comparisonBaseline === "lastMonth"` and
|
||||
* `priorYear` for `lastYear` so the rendered "Δ X% vs. last month"
|
||||
* stays honest. Null when the prior window has no paired readings.
|
||||
*/
|
||||
bpInTargetPctPriorMonth?: number | null;
|
||||
bpInTargetPctPriorYear?: number | null;
|
||||
glucoseByContext?: Record<string, DataSummaryType>;
|
||||
/**
|
||||
* v1.4.34 IW-B — per-type freshness map from `/api/analytics`. The
|
||||
* tile-strip helper reads `lastSeenByType[type]?.daysAgo` and forwards
|
||||
* it to each `<TrendCard staleDays>` so a metric the user hasn't
|
||||
* logged in a while keeps its tile visible (with an explicit
|
||||
* "Letzter Wert vor …" caption) instead of disappearing.
|
||||
*/
|
||||
lastSeenByType?: Record<
|
||||
string,
|
||||
{ lastSeenAt: string; daysAgo: number } | null
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape consumed by the insights mother page
|
||||
* (`src/app/insights/page.tsx`). Carries the correlation + health-score
|
||||
* blocks the sub-pages don't use.
|
||||
*/
|
||||
export interface InsightsAnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
correlations?: {
|
||||
bpCompliance: CorrelationResult;
|
||||
moodPulse: CorrelationResult;
|
||||
weightWeekday: CorrelationResult;
|
||||
} | null;
|
||||
healthScore?: {
|
||||
score: number;
|
||||
band: "green" | "yellow" | "red";
|
||||
components: {
|
||||
// v1.4.25 W8e — the optional `source`/`asOf` slots feed the
|
||||
// provenance accordion. Older clients reading this payload
|
||||
// happily ignore the extras (additive contract).
|
||||
bp: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
weight: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
mood: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
compliance: {
|
||||
value: number | null;
|
||||
weight: number;
|
||||
source?: "manual" | "withings" | "appleHealth" | "mixed" | "none";
|
||||
asOf?: string;
|
||||
};
|
||||
};
|
||||
delta: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape consumed by the onboarding checklist
|
||||
* (`src/components/onboarding/getting-started-checklist.tsx`).
|
||||
* Intentionally loose — the checklist only asks
|
||||
* "does any per-type bucket have a non-zero count?".
|
||||
*/
|
||||
export interface ChecklistAnalyticsData {
|
||||
summaries?: Record<string, { count?: number } | undefined>;
|
||||
}
|
||||
Reference in new issue
Block a user