mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
chore(cleanup): retire orphan exports and the stale legacy insights prompt module
Drop fourteen exported helpers and constants with zero call sites: countMessages, buildGeneratePrompts, _resetSafetyContractsCacheForTests, forEachLocaleAndRule, getPulseRange, trendLinePoints, CHART_MINI_HEIGHT_PX, formatDateWithWeekday, resolveGeneralStatusLocale, isSubPageSlug, recordHeartbeat, EVENT_TYPE_LABELS, cleanupExpiredRateLimits, and sleepStageEnum. Also retire the v1.4.20 legacy insights prompt scaffold — getInsightsSystemPrompt, InsightsOutput, and the two dead INSIGHTS_SYSTEM_PROMPT_EN/DE constants — plus the matching vi.mock key in the generate route test that no longer maps to a live export. Drop the empty __testables block in glp1-snapshot.ts, the legacy helper reference in the insight-generator JSDoc, and the two stale v1.4.16 InsightsPageHero comments in hero-strip + its test file. Consume the MedicationComplianceChart compareBaseline prop via an explicit void so the destructure documents the intentional accept-and- defer pattern instead of looking like a missed wire-up. Every symbol verified zero-caller via grep across src/, scripts/, and __tests__/ before drop.
This commit is contained in:
1 parent
0baea139a3
commit
267868c013
20 files changed
+15
-264
No files matched your search
@@ -77,7 +77,6 @@ vi.mock("@/lib/insights/features", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/insights/prompt", () => ({
|
||||
getInsightsSystemPrompt: vi.fn(() => "system"),
|
||||
buildUserPrompt: vi.fn(() => "user"),
|
||||
}));
|
||||
|
||||
|
||||
@@ -191,7 +191,15 @@ interface MedicationComplianceChartProps {
|
||||
export function MedicationComplianceChart({
|
||||
title,
|
||||
userTimezone = "Europe/Berlin",
|
||||
compareBaseline,
|
||||
}: MedicationComplianceChartProps) {
|
||||
// v1.4.27 R4 RC3 — the prop is accepted so the dashboard can pass
|
||||
// `compareBaseline={compareBaseline}` uniformly across every chart.
|
||||
// The comparison overlay itself is deferred: medication compliance is
|
||||
// a percentage-of-doses metric whose prior-period overlay needs a
|
||||
// second window of intake events. Discard explicitly so the
|
||||
// destructure is intentional rather than a missed wire-up.
|
||||
void compareBaseline;
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
|
||||
@@ -5,11 +5,9 @@ import { HeroStrip } from "../hero-strip";
|
||||
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B1 — Insights hero strip.
|
||||
*
|
||||
* Replaces v1.4.16 `<InsightsPageHero>`. Pinning the slots so future
|
||||
* polish can't silently drop the greeting / subtitle / action row /
|
||||
* suggested-prompt strip.
|
||||
* Insights hero strip — pins the slots so future polish can't
|
||||
* silently drop the greeting / subtitle / action row / suggested-
|
||||
* prompt strip.
|
||||
*/
|
||||
|
||||
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B1 — Insights redesign hero strip.
|
||||
* Insights redesign hero strip.
|
||||
*
|
||||
* Replaces the v1.4.16 `<InsightsPageHero>` with the wider band from
|
||||
* the design handoff (`prototype/artboard-fullpage.jsx → BriefingHero`):
|
||||
* The wider band lifted from the design handoff
|
||||
* (`prototype/artboard-fullpage.jsx → BriefingHero`):
|
||||
*
|
||||
* - locale-aware time-of-day greeting line ("Good morning, …" /
|
||||
* "Guten Morgen, …") + a narrative subtitle (the briefing
|
||||
|
||||
@@ -410,10 +410,3 @@ export async function buildGlp1SnapshotBlock(
|
||||
};
|
||||
}
|
||||
|
||||
/** Exported for tests. */
|
||||
export const __testables = {
|
||||
deriveDrugNames,
|
||||
parseTagList,
|
||||
parseDaysOfWeek,
|
||||
predictNextInjection,
|
||||
};
|
||||
@@ -317,10 +317,3 @@ export async function deleteConversation(
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count assistant + user turns in one conversation. Used by the
|
||||
* 20-turn cap before a summarise-and-restart pass.
|
||||
*/
|
||||
export async function countMessages(conversationId: string): Promise<number> {
|
||||
return prisma.coachMessage.count({ where: { conversationId } });
|
||||
}
|
||||
@@ -72,41 +72,3 @@ Fokussiere auf das Zusammenspiel der verschiedenen Parameter und identifiziere d
|
||||
${snapshotJson}`;
|
||||
}
|
||||
|
||||
// Backward-compatible export for insights/generate route (Task 8 uses this)
|
||||
export function buildGeneratePrompts(
|
||||
featuresJson: string,
|
||||
privacyMode: string,
|
||||
locale: Locale,
|
||||
): { systemPrompt: string; userPrompt: string } {
|
||||
if (locale === "en") {
|
||||
const modeLabel =
|
||||
privacyMode === "raw"
|
||||
? "Aggregated data + raw values (entire available period, anonymised)"
|
||||
: "Aggregated data only (no exact timestamps or raw values)";
|
||||
|
||||
return {
|
||||
systemPrompt: getGeneralStatusSystemPrompt(locale),
|
||||
userPrompt: `Analyse the following health data.
|
||||
Data mode: ${modeLabel}
|
||||
Use each metric's coverage object for measurement frequency and time spans.
|
||||
Use correlations for cross-metric analysis and historicalComparison for temporal context.
|
||||
|
||||
${featuresJson}`,
|
||||
};
|
||||
}
|
||||
|
||||
const modeLabel =
|
||||
privacyMode === "raw"
|
||||
? "Aggregierte Daten + Rohdaten (gesamter verfügbarer Zeitraum, anonymisiert)"
|
||||
: "Nur aggregierte Daten (keine exakten Zeitstempel oder Rohwerte)";
|
||||
|
||||
return {
|
||||
systemPrompt: getGeneralStatusSystemPrompt(locale),
|
||||
userPrompt: `Analysiere die folgenden Gesundheitsdaten.
|
||||
Datenmodus: ${modeLabel}
|
||||
Beachte die coverage-Objekte jeder Metrik für Informationen zu Messhäufigkeit und Zeiträumen.
|
||||
Nutze die correlations-Daten für Kreuzanalysen und historicalComparison für temporale Einordnung.
|
||||
|
||||
${featuresJson}`,
|
||||
};
|
||||
}
|
||||
@@ -714,8 +714,7 @@ const INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK: Record<
|
||||
|
||||
/**
|
||||
* Returns the active scope-hardened system prompt for a given locale.
|
||||
* Use this in place of the legacy `getInsightsSystemPrompt` once the
|
||||
* route migrates to `generateInsight()` (planned v1.4.16).
|
||||
* The route consumes this via `generateInsight()`.
|
||||
*/
|
||||
export function getStrictInsightsSystemPrompt(locale: Locale): string {
|
||||
if (locale === "de") return SYSTEM_PROMPT_DE;
|
||||
|
||||
@@ -187,11 +187,6 @@ export function loadSafetyContracts(locale: Locale): SafetyContractMatrix {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Clear the in-process cache. Used in tests that swap matrices. */
|
||||
export function _resetSafetyContractsCacheForTests(): void {
|
||||
cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locale-specific body of a ground rule. For EN the matrix
|
||||
* stores it under `.en`; for every other locale it lives under
|
||||
@@ -213,20 +208,5 @@ export function getGroundRuleBody(
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate every (locale, ground-rule-key) pair. Used by the parity +
|
||||
* refusal-probe tests so they cover the full 14×6 = 84 grid without
|
||||
* hard-coding the locale list.
|
||||
*/
|
||||
export function forEachLocaleAndRule(
|
||||
fn: (locale: Locale, key: GroundRuleKey) => void,
|
||||
): void {
|
||||
for (const locale of ALL_LOCALES) {
|
||||
for (const key of GROUND_RULE_KEYS) {
|
||||
fn(locale, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordered locale list — exported so tests can iterate identically. */
|
||||
export const SAFETY_CONTRACT_LOCALES = ALL_LOCALES;
|
||||
@@ -305,13 +305,6 @@ export function getWeightRange(heightCm: number): { min: number; max: number } {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal resting pulse range (AHA).
|
||||
*/
|
||||
export function getPulseRange(): { min: number; max: number } {
|
||||
return { min: 60, max: 100 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommended nightly sleep duration for adults (AASM/SRS).
|
||||
*/
|
||||
|
||||
@@ -104,57 +104,6 @@ export function trendSlope(
|
||||
};
|
||||
}
|
||||
|
||||
// ── Trend Line Points ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate two data points representing the linear trend line endpoints.
|
||||
* Used by chart components to overlay a regression line.
|
||||
*/
|
||||
export function trendLinePoints(
|
||||
data: DataPoint[],
|
||||
windowDays: number,
|
||||
): { start: DataPoint; end: DataPoint } | null {
|
||||
if (data.length < 2) return null;
|
||||
|
||||
const sorted = [...data].sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
const cutoff =
|
||||
sorted[sorted.length - 1].date.getTime() - windowDays * 24 * 60 * 60 * 1000;
|
||||
const window = sorted.filter((p) => p.date.getTime() >= cutoff);
|
||||
|
||||
if (window.length < 2) return null;
|
||||
|
||||
const startTime = window[0].date.getTime();
|
||||
const points = window.map((p) => ({
|
||||
x: (p.date.getTime() - startTime) / (24 * 60 * 60 * 1000),
|
||||
y: p.value,
|
||||
}));
|
||||
|
||||
const n = points.length;
|
||||
const sumX = points.reduce((s, p) => s + p.x, 0);
|
||||
const sumY = points.reduce((s, p) => s + p.y, 0);
|
||||
const sumXY = points.reduce((s, p) => s + p.x * p.y, 0);
|
||||
const sumXX = points.reduce((s, p) => s + p.x * p.x, 0);
|
||||
|
||||
const denominator = n * sumXX - sumX * sumX;
|
||||
if (denominator === 0) return null;
|
||||
|
||||
const slope = (n * sumXY - sumX * sumY) / denominator;
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
|
||||
const endDays = points[points.length - 1].x;
|
||||
|
||||
return {
|
||||
start: {
|
||||
date: window[0].date,
|
||||
value: Math.round(intercept * 100) / 100,
|
||||
},
|
||||
end: {
|
||||
date: window[window.length - 1].date,
|
||||
value: Math.round((intercept + slope * endDays) * 100) / 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── Anomaly Detection ────────────────────────────────────
|
||||
|
||||
export interface Anomaly {
|
||||
|
||||
@@ -16,13 +16,6 @@
|
||||
/** Full-size trend-card body height (non-mini chart cards on the dashboard). */
|
||||
export const CHART_HEIGHT_PX = 240;
|
||||
|
||||
/**
|
||||
* Compact / mini variant used by inline previews (rationale cards,
|
||||
* dashboard tile carve-outs). Smaller so the chart fits inside a host
|
||||
* card without doubling its vertical footprint.
|
||||
*/
|
||||
export const CHART_MINI_HEIGHT_PX = 160;
|
||||
|
||||
/**
|
||||
* Range-preset options for charts that expose a 7d / 30d / 90d / All
|
||||
* range selector. Each entry carries the i18n keys for the visible
|
||||
|
||||
@@ -61,7 +61,3 @@ export function formatTime(date: Date | string): string {
|
||||
return formatters().time(date);
|
||||
}
|
||||
|
||||
/** Locale-aware short weekday + date. */
|
||||
export function formatDateWithWeekday(date: Date | string): string {
|
||||
return formatters().dateWithWeekday(date);
|
||||
}
|
||||
@@ -403,8 +403,3 @@ export async function generateGeneralStatusForUser(
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGeneralStatusLocale(
|
||||
locale: string | null | undefined,
|
||||
): SupportedLocale {
|
||||
return normalizeLocale(locale);
|
||||
}
|
||||
@@ -40,77 +40,6 @@ export interface ComparisonSnapshot {
|
||||
}>;
|
||||
}
|
||||
|
||||
const INSIGHTS_SYSTEM_PROMPT_DE = `Du bist ein Gesundheitsdaten-Insights-Assistent. Du analysierst aggregierte Gesundheitsdaten und gibst klare, datenbasierte Hinweise.
|
||||
|
||||
WICHTIGE REGELN:
|
||||
- Erkläre immer, welche Datenpunkte zu deinen Schlussfolgerungen geführt haben.
|
||||
- Gib ein Konfidenzniveau an (niedrig/mittel/hoch) basierend auf der Datenmenge UND Datendichte.
|
||||
- Antworte auf Deutsch. Das Feld "confidence" muss exakt einer der englischen Enum-Schlüssel ("niedrig"|"mittel"|"hoch") sein — nicht übersetzen.
|
||||
|
||||
DATENABDECKUNG UND MESSZEITRÄUME:
|
||||
- Jede Metrik enthält ein "coverage"-Objekt mit: count (Anzahl Messungen), spanDays (Zeitspanne in Tagen), avgDaysBetween (durchschnittlicher Abstand zwischen Messungen), oldestDaysAgo und newestDaysAgo.
|
||||
- Berücksichtige diese Informationen bei deiner Analyse:
|
||||
* Weniger als 5 Messpunkte pro Metrik: Sage explizit, dass noch nicht genügend Daten für eine fundierte Aussage vorliegen.
|
||||
* Große Lücken (avgDaysBetween > 7): Weise darauf hin, dass die Daten spärlich sind und Trends nur eingeschränkt belastbar sind.
|
||||
* Daten über einen langen Zeitraum (spanDays > 60) aber wenig Messpunkte: Versuche trotzdem eine grobe Entwicklung abzuleiten, weise aber auf die eingeschränkte Aussagekraft hin.
|
||||
* Wenn die neueste Messung länger als 7 Tage zurückliegt (newestDaysAgo > 7): Erwähne, dass die Daten nicht aktuell sind.
|
||||
- context.dataSpanDays zeigt den Gesamtzeitraum aller Messungen, context.oldestMeasurementDaysAgo und context.newestMeasurementDaysAgo den ältesten und neuesten Messpunkt.
|
||||
|
||||
AUSGABEFORMAT (JSON):
|
||||
{
|
||||
"changed": "Was hat sich verändert? (Bezug auf Zeiträume und Datenbasis)",
|
||||
"stable": "Was ist stabil geblieben?",
|
||||
"drivers": "Mögliche Zusammenhänge und Hypothesen (mit Vorsicht formuliert)",
|
||||
"nextSteps": "Nächste kleine Schritte",
|
||||
"confidence": "niedrig|mittel|hoch",
|
||||
"limitations": "Einschränkungen dieser Analyse (inkl. Datenlücken und Messhäufigkeit)"
|
||||
}
|
||||
|
||||
Antworte NUR mit validem JSON im obigen Format. Alle natürlichsprachigen Felder auf Deutsch.`;
|
||||
|
||||
const INSIGHTS_SYSTEM_PROMPT_EN = `You are a health-data insights assistant. You analyse aggregated health data and provide clear, data-backed observations.
|
||||
|
||||
KEY RULES:
|
||||
- Always explain which data points drove each conclusion.
|
||||
- Provide a confidence level (niedrig/mittel/hoch) based on data volume AND density.
|
||||
- Reply in English. The "confidence" field must contain exactly one of the stable enum keys ("niedrig"|"mittel"|"hoch") — do not translate.
|
||||
|
||||
DATA COVERAGE AND MEASUREMENT WINDOWS:
|
||||
- Each metric carries a "coverage" object with: count (number of measurements), spanDays (timespan in days), avgDaysBetween (mean spacing between measurements), oldestDaysAgo and newestDaysAgo.
|
||||
- Factor these into the analysis:
|
||||
* Fewer than 5 points per metric: state explicitly that there is not yet enough data for a reliable statement.
|
||||
* Large gaps (avgDaysBetween > 7): note that the data is sparse and trends are only weakly supported.
|
||||
* Long span (spanDays > 60) but few points: still try to derive a coarse direction, but flag the limited reliability.
|
||||
* If the newest measurement is more than 7 days old (newestDaysAgo > 7): mention that the data is not current.
|
||||
- context.dataSpanDays shows the overall span of all measurements; context.oldestMeasurementDaysAgo and context.newestMeasurementDaysAgo show the oldest and newest points.
|
||||
|
||||
OUTPUT FORMAT (JSON):
|
||||
{
|
||||
"changed": "What changed? (Reference time spans and data basis.)",
|
||||
"stable": "What stayed stable?",
|
||||
"drivers": "Possible associations and hypotheses (worded cautiously).",
|
||||
"nextSteps": "Next small steps.",
|
||||
"confidence": "niedrig|mittel|hoch",
|
||||
"limitations": "Limitations of this analysis (incl. data gaps and measurement frequency)."
|
||||
}
|
||||
|
||||
Reply with VALID JSON ONLY in the format above. All natural-language fields in English.`;
|
||||
|
||||
export function getInsightsSystemPrompt(locale: Locale): string {
|
||||
return locale === "en"
|
||||
? INSIGHTS_SYSTEM_PROMPT_EN
|
||||
: INSIGHTS_SYSTEM_PROMPT_DE;
|
||||
}
|
||||
|
||||
export interface InsightsOutput {
|
||||
changed: string;
|
||||
stable: string;
|
||||
drivers: string;
|
||||
nextSteps: string;
|
||||
confidence: "niedrig" | "mittel" | "hoch";
|
||||
limitations: string;
|
||||
}
|
||||
|
||||
export function buildUserPrompt(
|
||||
featuresJson: string,
|
||||
privacyMode: string,
|
||||
|
||||
@@ -46,6 +46,3 @@ export const SUB_PAGE_SLUGS = Object.keys(SUB_PAGE_METRIC) as SubPageSlug[];
|
||||
*/
|
||||
export const INSIGHTS_OVERVIEW_PATH = "/insights" as const;
|
||||
|
||||
export function isSubPageSlug(value: string): value is SubPageSlug {
|
||||
return (SUB_PAGE_SLUGS as readonly string[]).includes(value);
|
||||
}
|
||||
@@ -44,11 +44,6 @@ export function markWorkerStarted() {
|
||||
status.lastHeartbeat = new Date().toISOString();
|
||||
}
|
||||
|
||||
export function recordHeartbeat() {
|
||||
const status = getStatus();
|
||||
status.lastHeartbeat = new Date().toISOString();
|
||||
}
|
||||
|
||||
export function recordReminderCheck() {
|
||||
const status = getStatus();
|
||||
status.lastReminderCheck = new Date().toISOString();
|
||||
|
||||
@@ -20,15 +20,6 @@ export const EVENT_TYPES = [
|
||||
] as const;
|
||||
export type EventType = (typeof EVENT_TYPES)[number];
|
||||
|
||||
export const EVENT_TYPE_LABELS: Record<EventType, string> = {
|
||||
MEDICATION_REMINDER: "Medication reminder",
|
||||
MEASUREMENT_ANOMALY: "Anomalous measurements",
|
||||
COMPLIANCE_LOW: "Low compliance",
|
||||
WITHINGS_SYNC_FAILED: "Withings sync failed",
|
||||
SYSTEM_ALERT: "System alerts",
|
||||
PERSONAL_RECORD: "Personal records",
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-event default policy when no `NotificationPreference` row
|
||||
* exists. The dispatcher reads this to decide whether to fire on a
|
||||
|
||||
@@ -51,16 +51,6 @@ export async function checkRateLimit(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired rate limit entries. Called periodically by pg-boss.
|
||||
*/
|
||||
export async function cleanupExpiredRateLimits(): Promise<number> {
|
||||
const result = await prisma.$executeRaw`
|
||||
DELETE FROM rate_limits WHERE reset_at < NOW()
|
||||
`;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit response headers.
|
||||
*/
|
||||
|
||||
@@ -41,15 +41,6 @@ export const glucoseContextEnum = z.enum([
|
||||
"BEDTIME",
|
||||
]);
|
||||
|
||||
export const sleepStageEnum = z.enum([
|
||||
"IN_BED",
|
||||
"AWAKE",
|
||||
"ASLEEP",
|
||||
"REM",
|
||||
"CORE",
|
||||
"DEEP",
|
||||
]);
|
||||
|
||||
export const measurementSourceEnum = z.enum([
|
||||
"MANUAL",
|
||||
"WITHINGS",
|
||||
|
||||
Reference in new issue
Block a user