diff --git a/src/app/api/insights/generate/__tests__/route.test.ts b/src/app/api/insights/generate/__tests__/route.test.ts index beb2ff51b..938d27bbb 100644 --- a/src/app/api/insights/generate/__tests__/route.test.ts +++ b/src/app/api/insights/generate/__tests__/route.test.ts @@ -77,7 +77,6 @@ vi.mock("@/lib/insights/features", () => ({ })); vi.mock("@/lib/insights/prompt", () => ({ - getInsightsSystemPrompt: vi.fn(() => "system"), buildUserPrompt: vi.fn(() => "user"), })); diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx index 674aa1089..081495c57 100644 --- a/src/components/charts/medication-compliance-chart.tsx +++ b/src/components/charts/medication-compliance-chart.tsx @@ -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(); diff --git a/src/components/insights/__tests__/hero-strip.test.tsx b/src/components/insights/__tests__/hero-strip.test.tsx index e6d3a7ea5..35f748a20 100644 --- a/src/components/insights/__tests__/hero-strip.test.tsx +++ b/src/components/insights/__tests__/hero-strip.test.tsx @@ -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 ``. 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") { diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx index 413f72a5a..2f3d84e95 100644 --- a/src/components/insights/hero-strip.tsx +++ b/src/components/insights/hero-strip.tsx @@ -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 `` 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 diff --git a/src/lib/ai/coach/glp1-snapshot.ts b/src/lib/ai/coach/glp1-snapshot.ts index 3fb131689..bda039927 100644 --- a/src/lib/ai/coach/glp1-snapshot.ts +++ b/src/lib/ai/coach/glp1-snapshot.ts @@ -410,10 +410,3 @@ export async function buildGlp1SnapshotBlock( }; } -/** Exported for tests. */ -export const __testables = { - deriveDrugNames, - parseTagList, - parseDaysOfWeek, - predictNextInjection, -}; diff --git a/src/lib/ai/coach/persistence.ts b/src/lib/ai/coach/persistence.ts index d4ae22537..22a6141e5 100644 --- a/src/lib/ai/coach/persistence.ts +++ b/src/lib/ai/coach/persistence.ts @@ -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 { - return prisma.coachMessage.count({ where: { conversationId } }); -} diff --git a/src/lib/ai/prompts/general-status.ts b/src/lib/ai/prompts/general-status.ts index 357be131d..fb4c5b604 100644 --- a/src/lib/ai/prompts/general-status.ts +++ b/src/lib/ai/prompts/general-status.ts @@ -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}`, - }; -} diff --git a/src/lib/ai/prompts/insight-generator.ts b/src/lib/ai/prompts/insight-generator.ts index 6924d71d7..095580d8a 100644 --- a/src/lib/ai/prompts/insight-generator.ts +++ b/src/lib/ai/prompts/insight-generator.ts @@ -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; diff --git a/src/lib/ai/prompts/safety-contracts.ts b/src/lib/ai/prompts/safety-contracts.ts index fa0e158a5..0dc12f407 100644 --- a/src/lib/ai/prompts/safety-contracts.ts +++ b/src/lib/ai/prompts/safety-contracts.ts @@ -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; diff --git a/src/lib/analytics/classifications.ts b/src/lib/analytics/classifications.ts index ebdae36d3..9eba5b3df 100644 --- a/src/lib/analytics/classifications.ts +++ b/src/lib/analytics/classifications.ts @@ -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). */ diff --git a/src/lib/analytics/trends.ts b/src/lib/analytics/trends.ts index e22127e0b..d8971b720 100644 --- a/src/lib/analytics/trends.ts +++ b/src/lib/analytics/trends.ts @@ -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 { diff --git a/src/lib/charts/constants.ts b/src/lib/charts/constants.ts index 74209c4ca..d6494a9c2 100644 --- a/src/lib/charts/constants.ts +++ b/src/lib/charts/constants.ts @@ -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 diff --git a/src/lib/format.ts b/src/lib/format.ts index 5a27a29e1..85e72959f 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -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); -} diff --git a/src/lib/insights/general-status.ts b/src/lib/insights/general-status.ts index b2073ad40..1c4ce1479 100644 --- a/src/lib/insights/general-status.ts +++ b/src/lib/insights/general-status.ts @@ -403,8 +403,3 @@ export async function generateGeneralStatusForUser( }; } -export function resolveGeneralStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); -} diff --git a/src/lib/insights/prompt.ts b/src/lib/insights/prompt.ts index ddff3689f..18fa44368 100644 --- a/src/lib/insights/prompt.ts +++ b/src/lib/insights/prompt.ts @@ -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, diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts index b66fe7060..4472d7c60 100644 --- a/src/lib/insights/sub-page-metric.ts +++ b/src/lib/insights/sub-page-metric.ts @@ -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); -} diff --git a/src/lib/jobs/worker-status.ts b/src/lib/jobs/worker-status.ts index 412739fc7..104a48854 100644 --- a/src/lib/jobs/worker-status.ts +++ b/src/lib/jobs/worker-status.ts @@ -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(); diff --git a/src/lib/notifications/types.ts b/src/lib/notifications/types.ts index 749a37d0e..241a6fac0 100644 --- a/src/lib/notifications/types.ts +++ b/src/lib/notifications/types.ts @@ -20,15 +20,6 @@ export const EVENT_TYPES = [ ] as const; export type EventType = (typeof EVENT_TYPES)[number]; -export const EVENT_TYPE_LABELS: Record = { - 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 diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts index ececfaa1d..e336099b0 100644 --- a/src/lib/rate-limit.ts +++ b/src/lib/rate-limit.ts @@ -51,16 +51,6 @@ export async function checkRateLimit( }; } -/** - * Delete expired rate limit entries. Called periodically by pg-boss. - */ -export async function cleanupExpiredRateLimits(): Promise { - const result = await prisma.$executeRaw` - DELETE FROM rate_limits WHERE reset_at < NOW() - `; - return result; -} - /** * Rate limit response headers. */ diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 7f7359d8e..45d37dc0c 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -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",