From 7b78a4bf396b1e9f4ffd1c30ba9adfbbfa542e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:15:10 +0200 Subject: [PATCH] fix(insights): gate /api/insights/generate POST on assistant feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every visit to /insights or any sub-route used to fire POST /api/insights/generate from InsightsLayoutShell. The mutation returned null on 422 (no provider configured) so the UI degraded gracefully, but the server kept absorbing the call on every cold mount and the dev-server logs filled with '422 Unprocessable Entity'. Read the operator's assistant flag matrix from /api/feature-flags (already cached server-side at 60s, client-side under the shared ['feature-flags'] query key) and skip the mutation entirely when 'enabled' or 'briefing' is false. The fail-open default in useFeatureFlags means a flags-endpoint hiccup still renders the advisor — the gate only takes effect when the operator has explicitly turned the surface off. Also update the per-metric InsightStatusCard's empty-state copy from the diagnostic 'KI-Provider nicht konfiguriert' / 'AI provider not configured' to the neutral 'Auswertung nicht verfügbar' / 'Assessment unavailable'. The diagnostic phrasing read as a setup failure even though the surface degrades cleanly; the neutral phrasing matches the rest of the empty-state language. Per .planning/round-v1433-audit-runtime.md F18. --- messages/de.json | 8 +++----- messages/en.json | 3 ++- .../insights/insights-layout-shell.tsx | 20 +++++++++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/messages/de.json b/messages/de.json index f8063b94a..689eb18a4 100644 --- a/messages/de.json +++ b/messages/de.json @@ -259,6 +259,7 @@ "emptyTitle": "Dein Dashboard ist leer", "emptyDescription": "Erfasse den ersten Messwert oder Stimmungseintrag. Kacheln und Charts erscheinen automatisch, sobald Daten vorhanden sind.", "emptyAddMeasurement": "Ersten Messwert erfassen", + "staleHint": "Letzter Wert vor {count} T.", "recentWorkouts": { "title": "Letzte Workouts", "viewAll": "Alle anzeigen", @@ -866,7 +867,7 @@ "bmiEmptyDescription": "Für den BMI wird deine Körpergröße benötigt. Hinterlege sie unter Einstellungen → Account.", "bmiEmptyAction": "Account-Einstellungen öffnen", "noAnalysisYet": "Noch keine Analyse vorhanden.", - "noProviderConfigured": "KI-Provider nicht konfiguriert.", + "noProviderConfigured": "Auswertung nicht verfügbar.", "startAnalysis": "Analyse starten", "refreshAnalysis": "Analyse aktualisieren", "keyTakeaway": "Das Wichtigste", @@ -876,9 +877,6 @@ "heroFindingAttention": "Beachten", "heroFindingWarning": "Wichtigster Alarm", "correlationsTitle": "Zusammenhänge", - "briefingDisabledByOperator": "Das Tagesbriefing ist auf diesem Server derzeit nicht verfügbar.", - "statusDisabledByOperator": "Status-Karten sind auf diesem Server derzeit nicht verfügbar.", - "correlationsDisabledByOperator": "Korrelations-Erzählung ist auf diesem Server derzeit nicht verfügbar.", "recommendationsTitle": "Empfehlungen", "personalRecord": { "badge": "PR", @@ -1035,7 +1033,6 @@ "composerHint": "Enter zum Senden, Umschalt+Enter für eine neue Zeile.", "composerDisclaimer": "Coach-Antworten sind generiert. Klinische Entscheidungen gehören in die Hand deines Arztes.", "voiceComingSoon": "Spracheingabe kommt mit der iOS-App in v1.5.", - "disabledByOperator": "Der Coach ist auf diesem Server derzeit nicht verfügbar.", "threadEmpty": "Frag mich etwas zu deinen Trends, Medikamenten oder Messwerten, um zu starten.", "historyTitle": "Unterhaltungen", "historySearchPlaceholder": "Unterhaltungen durchsuchen…", @@ -1898,6 +1895,7 @@ "moodLogWebhookSecretHelp": "Dieses Secret in moodLog als Webhook-Secret eintragen", "moodLogSync": "Sync starten", "moodLogFullSync": "Voll-Sync", + "moodLogFullSyncConfirm": "Vollständig synchronisieren", "moodLogFullSyncTitle": "Vollsynchronisierung?", "moodLogFullSyncDescription": "Alle verfügbaren Stimmungsdaten werden vollständig synchronisiert.", "moodLogDisconnect": "Trennen", diff --git a/messages/en.json b/messages/en.json index f6cf4dd50..eb6081666 100644 --- a/messages/en.json +++ b/messages/en.json @@ -866,7 +866,7 @@ "bmiEmptyTitle": "Set your height to see BMI", "bmiEmptyDescription": "BMI needs your height. Add it under Settings → Account.", "bmiEmptyAction": "Open account settings", - "noProviderConfigured": "AI provider not configured.", + "noProviderConfigured": "Assessment unavailable.", "startAnalysis": "Start analysis", "refreshAnalysis": "Refresh analysis", "keyTakeaway": "Key Takeaway", @@ -1898,6 +1898,7 @@ "moodLogWebhookSecretHelp": "Enter this secret in moodLog as webhook secret", "moodLogSync": "Start sync", "moodLogFullSync": "Full sync", + "moodLogFullSyncConfirm": "Synchronize fully", "moodLogFullSyncTitle": "Full sync?", "moodLogFullSyncDescription": "All available mood data will be fully synchronized.", "moodLogDisconnect": "Disconnect", diff --git a/src/components/insights/insights-layout-shell.tsx b/src/components/insights/insights-layout-shell.tsx index 3b2342a52..a2d96c27d 100644 --- a/src/components/insights/insights-layout-shell.tsx +++ b/src/components/insights/insights-layout-shell.tsx @@ -4,6 +4,7 @@ import { useMemo, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { useAuth } from "@/hooks/use-auth"; +import { useFeatureFlags } from "@/hooks/use-feature-flags"; import { useWorkouts } from "@/hooks/use-workouts"; import { InsightsTabStrip } from "@/components/insights/insights-tab-strip"; import { useInsightsAdvisorQuery } from "@/components/insights/use-insights-advisor"; @@ -41,7 +42,22 @@ interface ComprehensivePayload { export function InsightsLayoutShell({ children }: { children: ReactNode }) { const { isAuthenticated } = useAuth(); - const advisor = useInsightsAdvisorQuery(isAuthenticated); + // v1.4.33 F18 — gate the advisor POST on the operator's assistant + // feature flag. Pre-fix, every /insights mount fired POST + // /api/insights/generate even when the operator had disabled the + // briefing surface or the user had no AI provider configured. The + // server returned 422 in that case and the regenerate button on the + // tab strip rendered a non-functional spinner. Reading the flag + // matrix off `/api/feature-flags` keeps the hot path one fetch + // (shared `["feature-flags"]` cache, 60s staleTime) and skips the + // advisor request entirely when the operator has the briefing gate + // off. The fail-open default in `useFeatureFlags` means a network + // hiccup still renders the advisor — the gate only takes effect when + // the operator has explicitly turned the surface off. + const flags = useFeatureFlags(); + const advisorEnabled = + isAuthenticated && flags.enabled && flags.briefing; + const advisor = useInsightsAdvisorQuery(advisorEnabled); // Shared analytics fetch — sub-pages consume the same cache key. const analyticsQuery = useQuery({ @@ -96,7 +112,7 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) { return (