From f50f92902e602c07462b830508d8d8243362cb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 15 Jun 2026 22:36:04 +0200 Subject: [PATCH] feat(settings): add the Module (what-you-track) hub Surface a dedicated settings section for enabling and disabling the secondary tracking domains (mood, sleep, glucose, workouts, recovery, labs, achievements, coach, insights, doctor report, cycle). Each row carries an icon, label, and one-line description with a switch bound to the resolved module map on /auth/me; toggling PATCHes /api/auth/me/modules and invalidates the auth/me query so the navigation, Insights pills, and dashboard tiles re-gate live. Core domains (weight, blood pressure, pulse, medications) render as a read-only always-on group. The entry sits right below Account in the settings navigation. --- messages/de.json | 77 ++++++++ messages/en.json | 77 ++++++++ messages/es.json | 77 ++++++++ messages/fr.json | 77 ++++++++ messages/it.json | 77 ++++++++ messages/pl.json | 77 ++++++++ src/app/settings/[section]/page.tsx | 2 + .../__tests__/settings-shell.test.tsx | 3 + src/components/settings/module-toggle-row.tsx | 85 +++++++++ src/components/settings/modules-section.tsx | 175 ++++++++++++++++++ .../settings/section-placeholder.tsx | 2 + src/components/settings/section-slugs.ts | 3 + src/components/settings/settings-shell.tsx | 8 + src/lib/query-keys/settings.ts | 9 + 14 files changed, 749 insertions(+) create mode 100644 src/components/settings/module-toggle-row.tsx create mode 100644 src/components/settings/modules-section.tsx diff --git a/messages/de.json b/messages/de.json index 0509f4a9d..9a9480e87 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4122,6 +4122,21 @@ "title": "Benachrichtigungskanäle", "description": "APNs, Telegram, ntfy, Web Push, Webhook und E-Mail." } + }, + "modules": { + "title": "Module", + "saved": "Gespeichert.", + "error": "Konnte nicht gespeichert werden. Bitte erneut versuchen.", + "toggleable": { + "title": "Was du trackst", + "description": "Optionale Bereiche. Deaktivierte Module verschwinden aus Navigation, Kacheln und Auswertungen — deine Daten bleiben erhalten." + }, + "core": { + "title": "Immer aktiv", + "description": "Die Grundlagen von HealthLog. Diese Bereiche lassen sich nicht abschalten.", + "note": "Kernbereich — immer aktiv" + }, + "description": "Lege fest, was HealthLog für dich erfasst. Schalte einzelne Bereiche ein oder aus — die Navigation, Kacheln und Auswertungen passen sich sofort an." } }, "profile": "Profil", @@ -6511,5 +6526,67 @@ "savedToast": "Wert gespeichert", "saveError": "Wert konnte nicht gespeichert werden" } + }, + "modules": { + "cycle": { + "label": "Zyklus", + "description": "Periode, Phasen und Vorhersagen verfolgen." + }, + "mood": { + "label": "Stimmung", + "description": "Tägliche Stimmung und Tags erfassen." + }, + "sleep": { + "label": "Schlaf", + "description": "Schlafdauer, Phasen und Schlaf-Timeline." + }, + "glucose": { + "label": "Glukose", + "description": "Blutzuckerwerte und Verläufe." + }, + "workouts": { + "label": "Workouts", + "description": "Trainingseinheiten und Aktivität." + }, + "recovery": { + "label": "Erholung", + "description": "Erholungs- und Belastungswerte deiner Geräte." + }, + "labs": { + "label": "Laborwerte", + "description": "Laborergebnisse erfassen und im Verlauf ablegen." + }, + "achievements": { + "label": "Erfolge", + "description": "Abzeichen und Serien für deine Routine." + }, + "coach": { + "label": "Coach", + "description": "KI-Chat mit Bezug zu deinen Daten." + }, + "insights": { + "label": "Auswertungen", + "description": "KI-Analysen, Briefing und Korrelationen." + }, + "doctorReport": { + "label": "Arztbericht", + "description": "PDF- und FHIR-Export für deine Praxis." + }, + "weight": { + "label": "Gewicht", + "description": "Gewicht und Körperzusammensetzung." + }, + "bloodPressure": { + "label": "Blutdruck", + "description": "Systolisch und diastolisch." + }, + "pulse": { + "label": "Puls", + "description": "Ruhepuls und Herzfrequenz." + }, + "medications": { + "label": "Medikamente", + "description": "Pläne, Einnahmen und Compliance." + } } } diff --git a/messages/en.json b/messages/en.json index bbeb6b05a..9c94551de 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4122,6 +4122,21 @@ "title": "Notification channels", "description": "APNs, Telegram, ntfy, Web Push, Webhook, and Email." } + }, + "modules": { + "title": "Modules", + "saved": "Saved.", + "error": "Couldn't save. Please try again.", + "toggleable": { + "title": "What you track", + "description": "Optional areas. Disabled modules drop out of the navigation, tiles, and insights — your data stays untouched." + }, + "core": { + "title": "Always on", + "description": "HealthLog's foundations. These areas can't be turned off.", + "note": "Core area — always on" + }, + "description": "Choose what HealthLog tracks for you. Turn individual areas on or off — the navigation, tiles, and insights adapt right away." } }, "profile": "Profile", @@ -6511,5 +6526,67 @@ "savedToast": "Result saved", "saveError": "Could not save the result" } + }, + "modules": { + "cycle": { + "label": "Cycle", + "description": "Track period, phases, and predictions." + }, + "mood": { + "label": "Mood", + "description": "Log daily mood and tags." + }, + "sleep": { + "label": "Sleep", + "description": "Sleep duration, stages, and timeline." + }, + "glucose": { + "label": "Glucose", + "description": "Blood-glucose readings and trends." + }, + "workouts": { + "label": "Workouts", + "description": "Training sessions and activity." + }, + "recovery": { + "label": "Recovery", + "description": "Recovery and strain metrics from your devices." + }, + "labs": { + "label": "Lab results", + "description": "Capture lab results and keep their history." + }, + "achievements": { + "label": "Achievements", + "description": "Badges and streaks for your routine." + }, + "coach": { + "label": "Coach", + "description": "AI chat grounded in your own data." + }, + "insights": { + "label": "Insights", + "description": "AI analysis, briefing, and correlations." + }, + "doctorReport": { + "label": "Doctor report", + "description": "PDF and FHIR export for your practice." + }, + "weight": { + "label": "Weight", + "description": "Weight and body composition." + }, + "bloodPressure": { + "label": "Blood pressure", + "description": "Systolic and diastolic." + }, + "pulse": { + "label": "Pulse", + "description": "Resting pulse and heart rate." + }, + "medications": { + "label": "Medications", + "description": "Schedules, intakes, and compliance." + } } } diff --git a/messages/es.json b/messages/es.json index 1eed54b4c..8af901931 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4122,6 +4122,21 @@ "title": "Canales de notificación", "description": "APNs, Telegram, ntfy, Web Push, Webhook y correo." } + }, + "modules": { + "title": "Módulos", + "saved": "Guardado.", + "error": "No se pudo guardar. Inténtalo de nuevo.", + "toggleable": { + "title": "Qué registras", + "description": "Áreas opcionales. Los módulos desactivados desaparecen de la navegación, los paneles y los análisis; tus datos se conservan." + }, + "core": { + "title": "Siempre activos", + "description": "Los pilares de HealthLog. Estas áreas no se pueden desactivar.", + "note": "Área esencial — siempre activa" + }, + "description": "Elige qué registra HealthLog por ti. Activa o desactiva áreas concretas: la navegación, los paneles y los análisis se adaptan al instante." } }, "profile": "Perfil", @@ -6511,5 +6526,67 @@ "savedToast": "Resultado guardado", "saveError": "No se pudo guardar el resultado" } + }, + "modules": { + "cycle": { + "label": "Ciclo", + "description": "Registra periodo, fases y predicciones." + }, + "mood": { + "label": "Estado de ánimo", + "description": "Registra el ánimo diario y etiquetas." + }, + "sleep": { + "label": "Sueño", + "description": "Duración, fases y cronología del sueño." + }, + "glucose": { + "label": "Glucosa", + "description": "Lecturas de glucosa y tendencias." + }, + "workouts": { + "label": "Entrenamientos", + "description": "Sesiones de entrenamiento y actividad." + }, + "recovery": { + "label": "Recuperación", + "description": "Métricas de recuperación y carga de tus dispositivos." + }, + "labs": { + "label": "Resultados de laboratorio", + "description": "Registra resultados de laboratorio y su historial." + }, + "achievements": { + "label": "Logros", + "description": "Insignias y rachas para tu rutina." + }, + "coach": { + "label": "Coach", + "description": "Chat de IA basado en tus propios datos." + }, + "insights": { + "label": "Análisis", + "description": "Análisis de IA, resumen y correlaciones." + }, + "doctorReport": { + "label": "Informe médico", + "description": "Exportación PDF y FHIR para tu consulta." + }, + "weight": { + "label": "Peso", + "description": "Peso y composición corporal." + }, + "bloodPressure": { + "label": "Presión arterial", + "description": "Sistólica y diastólica." + }, + "pulse": { + "label": "Pulso", + "description": "Pulso en reposo y frecuencia cardiaca." + }, + "medications": { + "label": "Medicamentos", + "description": "Pautas, tomas y cumplimiento." + } } } diff --git a/messages/fr.json b/messages/fr.json index c3a2a9dcc..84d6d1c1e 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4122,6 +4122,21 @@ "title": "Canaux de notification", "description": "APNs, Telegram, ntfy, Web Push, Webhook et e-mail." } + }, + "modules": { + "title": "Modules", + "saved": "Enregistré.", + "error": "Échec de l'enregistrement. Veuillez réessayer.", + "toggleable": { + "title": "Ce que vous suivez", + "description": "Domaines optionnels. Les modules désactivés disparaissent de la navigation, des tuiles et des analyses ; vos données sont conservées." + }, + "core": { + "title": "Toujours actifs", + "description": "Les fondations de HealthLog. Ces domaines ne peuvent pas être désactivés.", + "note": "Domaine essentiel — toujours actif" + }, + "description": "Choisissez ce que HealthLog suit pour vous. Activez ou désactivez chaque domaine : la navigation, les tuiles et les analyses s'adaptent aussitôt." } }, "profile": "Profil", @@ -6511,5 +6526,67 @@ "savedToast": "Résultat enregistré", "saveError": "Impossible d'enregistrer le résultat" } + }, + "modules": { + "cycle": { + "label": "Cycle", + "description": "Suivez règles, phases et prévisions." + }, + "mood": { + "label": "Humeur", + "description": "Notez l'humeur quotidienne et les tags." + }, + "sleep": { + "label": "Sommeil", + "description": "Durée, phases et chronologie du sommeil." + }, + "glucose": { + "label": "Glycémie", + "description": "Mesures de glycémie et tendances." + }, + "workouts": { + "label": "Entraînements", + "description": "Séances d'entraînement et activité." + }, + "recovery": { + "label": "Récupération", + "description": "Indicateurs de récupération et de charge de vos appareils." + }, + "labs": { + "label": "Résultats de labo", + "description": "Enregistrez les résultats de laboratoire et leur historique." + }, + "achievements": { + "label": "Réussites", + "description": "Badges et séries pour votre routine." + }, + "coach": { + "label": "Coach", + "description": "Chat IA fondé sur vos propres données." + }, + "insights": { + "label": "Analyses", + "description": "Analyses IA, briefing et corrélations." + }, + "doctorReport": { + "label": "Rapport médical", + "description": "Export PDF et FHIR pour votre cabinet." + }, + "weight": { + "label": "Poids", + "description": "Poids et composition corporelle." + }, + "bloodPressure": { + "label": "Tension artérielle", + "description": "Systolique et diastolique." + }, + "pulse": { + "label": "Pouls", + "description": "Pouls au repos et fréquence cardiaque." + }, + "medications": { + "label": "Médicaments", + "description": "Schémas, prises et observance." + } } } diff --git a/messages/it.json b/messages/it.json index 2afb522b8..91514bbbb 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4122,6 +4122,21 @@ "title": "Canali di notifica", "description": "APNs, Telegram, ntfy, Web Push, Webhook ed e-mail." } + }, + "modules": { + "title": "Moduli", + "saved": "Salvato.", + "error": "Salvataggio non riuscito. Riprova.", + "toggleable": { + "title": "Cosa monitori", + "description": "Ambiti opzionali. I moduli disattivati spariscono da navigazione, riquadri e analisi; i tuoi dati restano intatti." + }, + "core": { + "title": "Sempre attivi", + "description": "Le fondamenta di HealthLog. Questi ambiti non possono essere disattivati.", + "note": "Ambito di base — sempre attivo" + }, + "description": "Scegli cosa registra HealthLog per te. Attiva o disattiva i singoli ambiti: navigazione, riquadri e analisi si adattano subito." } }, "profile": "Profilo", @@ -6511,5 +6526,67 @@ "savedToast": "Risultato salvato", "saveError": "Impossibile salvare il risultato" } + }, + "modules": { + "cycle": { + "label": "Ciclo", + "description": "Monitora mestruazioni, fasi e previsioni." + }, + "mood": { + "label": "Umore", + "description": "Registra umore quotidiano ed etichette." + }, + "sleep": { + "label": "Sonno", + "description": "Durata, fasi e cronologia del sonno." + }, + "glucose": { + "label": "Glicemia", + "description": "Valori della glicemia e andamenti." + }, + "workouts": { + "label": "Allenamenti", + "description": "Sessioni di allenamento e attività." + }, + "recovery": { + "label": "Recupero", + "description": "Metriche di recupero e carico dai tuoi dispositivi." + }, + "labs": { + "label": "Esami di laboratorio", + "description": "Registra i referti di laboratorio e il loro storico." + }, + "achievements": { + "label": "Traguardi", + "description": "Badge e serie per la tua routine." + }, + "coach": { + "label": "Coach", + "description": "Chat IA basata sui tuoi dati." + }, + "insights": { + "label": "Analisi", + "description": "Analisi IA, briefing e correlazioni." + }, + "doctorReport": { + "label": "Referto medico", + "description": "Esportazione PDF e FHIR per il tuo studio." + }, + "weight": { + "label": "Peso", + "description": "Peso e composizione corporea." + }, + "bloodPressure": { + "label": "Pressione", + "description": "Sistolica e diastolica." + }, + "pulse": { + "label": "Battito", + "description": "Battito a riposo e frequenza cardiaca." + }, + "medications": { + "label": "Farmaci", + "description": "Schemi, assunzioni e aderenza." + } } } diff --git a/messages/pl.json b/messages/pl.json index a14120194..68cfd262f 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4122,6 +4122,21 @@ "title": "Kanały powiadomień", "description": "APNs, Telegram, ntfy, Web Push, Webhook i e-mail." } + }, + "modules": { + "title": "Moduły", + "saved": "Zapisano.", + "error": "Nie udało się zapisać. Spróbuj ponownie.", + "toggleable": { + "title": "Co śledzisz", + "description": "Obszary opcjonalne. Wyłączone moduły znikają z nawigacji, kafelków i analiz — Twoje dane pozostają nienaruszone." + }, + "core": { + "title": "Zawsze aktywne", + "description": "Podstawy HealthLog. Tych obszarów nie można wyłączyć.", + "note": "Obszar podstawowy — zawsze aktywny" + }, + "description": "Wybierz, co HealthLog ma dla Ciebie śledzić. Włączaj i wyłączaj poszczególne obszary — nawigacja, kafelki i analizy dostosują się od razu." } }, "profile": "Profil", @@ -6511,5 +6526,67 @@ "savedToast": "Wynik zapisany", "saveError": "Nie udało się zapisać wyniku" } + }, + "modules": { + "cycle": { + "label": "Cykl", + "description": "Śledź miesiączkę, fazy i prognozy." + }, + "mood": { + "label": "Nastrój", + "description": "Zapisuj codzienny nastrój i tagi." + }, + "sleep": { + "label": "Sen", + "description": "Długość, fazy i oś czasu snu." + }, + "glucose": { + "label": "Glukoza", + "description": "Odczyty glukozy i trendy." + }, + "workouts": { + "label": "Treningi", + "description": "Sesje treningowe i aktywność." + }, + "recovery": { + "label": "Regeneracja", + "description": "Wskaźniki regeneracji i obciążenia z Twoich urządzeń." + }, + "labs": { + "label": "Wyniki badań", + "description": "Zapisuj wyniki badań laboratoryjnych i ich historię." + }, + "achievements": { + "label": "Osiągnięcia", + "description": "Odznaki i serie dla Twojej rutyny." + }, + "coach": { + "label": "Coach", + "description": "Czat AI oparty na Twoich danych." + }, + "insights": { + "label": "Analizy", + "description": "Analizy AI, briefing i korelacje." + }, + "doctorReport": { + "label": "Raport dla lekarza", + "description": "Eksport PDF i FHIR dla Twojej przychodni." + }, + "weight": { + "label": "Waga", + "description": "Waga i skład ciała." + }, + "bloodPressure": { + "label": "Ciśnienie krwi", + "description": "Skurczowe i rozkurczowe." + }, + "pulse": { + "label": "Tętno", + "description": "Tętno spoczynkowe i rytm serca." + }, + "medications": { + "label": "Leki", + "description": "Schematy, przyjęcia i zgodność." + } } } diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index ceee2abed..645e28a1a 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -12,6 +12,7 @@ import { InsightsSection } from "@/components/settings/insights-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; import { LayoutSection } from "@/components/settings/layout-section"; import { MedicationsSection } from "@/components/settings/medications-section"; +import { ModulesSection } from "@/components/settings/modules-section"; import { MoodSection } from "@/components/settings/mood-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; import { RemindersSection } from "@/components/settings/reminders-section"; @@ -51,6 +52,7 @@ const SECTION_COMPONENTS: Record< () => JSX.Element | null > = { account: AccountSection, + modules: ModulesSection, about: AboutSection, ai: AiSection, integrations: IntegrationsSection, diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index b10b03c07..ddca9005d 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -63,8 +63,11 @@ describe("SETTINGS_SECTION_SLUGS", () => { // Layout hub instead of four standalone nav entries. // v1.17.1 — `reminders` is the one "Reminders & Notifications" home and // sits right after `notifications`. + // v1.18.0 — `modules` ("Was du trackst") sits right after `account` as + // the single front door for enabling/disabling secondary domains. expect([...SETTINGS_SECTION_SLUGS]).toEqual([ "account", + "modules", "integrations", "notifications", "reminders", diff --git a/src/components/settings/module-toggle-row.tsx b/src/components/settings/module-toggle-row.tsx new file mode 100644 index 000000000..47a8cbb23 --- /dev/null +++ b/src/components/settings/module-toggle-row.tsx @@ -0,0 +1,85 @@ +"use client"; + +/** + * `` — one row in the Settings → Module ("Was du trackst") + * hub. v1.18.0. + * + * Renders a single module: a neutral icon, the i18n label + one-line + * description, and a `` reflecting the resolved enabled-state. The + * row is presentation-only — the parent `` owns the mutation + * and cache invalidation, so this component stays a pure, easily-tested leaf. + * + * Two modes: + * - toggleable: the Switch is live; flipping it calls `onToggle(next)`. + * - locked (core domains): the Switch is checked + disabled with a short + * "always on" note, so the always-on measurement engine + meds read as + * deliberately fixed rather than broken. Palette stays neutral — no + * alarming green-when-on / red-when-off tint. + */ + +import type { LucideIcon } from "lucide-react"; + +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; + +export interface ModuleToggleRowProps { + /** Stable key — used for the input id + the test query handle. */ + moduleKey: string; + icon: LucideIcon; + label: string; + description: string; + /** Whether the Switch reads on. Core rows pass `true`. */ + enabled: boolean; + /** + * When set the row is read-only: the Switch is locked checked and the + * `lockedNote` line renders beneath the description. Core domains use this. + */ + locked?: boolean; + /** One-line "core / always on" note, only shown when `locked`. */ + lockedNote?: string; + /** Disable the Switch while a mutation is in flight (toggleable rows). */ + pending?: boolean; + /** Fired with the next desired state on a user toggle (toggleable rows). */ + onToggle?: (next: boolean) => void; +} + +export function ModuleToggleRow({ + moduleKey, + icon: Icon, + label, + description, + enabled, + locked = false, + lockedNote, + pending = false, + onToggle, +}: ModuleToggleRowProps) { + const inputId = `module-toggle-${moduleKey}`; + return ( +
+
+
+ onToggle?.(v)} + className="mt-0.5 shrink-0" + aria-label={label} + /> +
+ ); +} diff --git a/src/components/settings/modules-section.tsx b/src/components/settings/modules-section.tsx new file mode 100644 index 000000000..7c81829d3 --- /dev/null +++ b/src/components/settings/modules-section.tsx @@ -0,0 +1,175 @@ +"use client"; + +/** + * `` — Settings → Module ("Was du trackst") hub. v1.18.0. + * + * The one place a user decides which secondary domains HealthLog surfaces. + * Each toggleable module (mood, sleep, glucose, workouts, recovery, labs, + * achievements, coach, insights, doctor report, cycle) gets a row with an + * icon, label, one-line description, and a ``. Flipping a switch + * PATCHes `/api/auth/me/modules` (a DISABLED allowlist; the endpoint refuses + * core keys) and then invalidates the `authMe()` query so the nav, Insights + * pills, and dashboard tiles re-gate live off `useAuth().user.modules`. + * + * The four CORE domains (weight, blood pressure, pulse, medications) render + * as a separate read-only "always on" group — locked switches with a short + * note — so the always-on measurement engine + meds read as deliberately + * fixed, never disableable. Palette stays neutral throughout; this is a + * calm configuration surface, not an alarm panel. + * + * State source is `useAuth().user.modules` (the resolved `/auth/me` map). + * A module is enabled unless its key is explicitly `false` — default-on. + */ + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + Activity, + Award, + Blocks, + CalendarHeart, + Droplet, + Dumbbell, + FileText, + HeartPulse, + Lock, + MessageCircleHeart, + Moon, + Pill, + Scale, + Smile, + Sparkles, + TestTube, + type LucideIcon, +} from "lucide-react"; + +import { SettingsCardHeader } from "@/components/settings/_card-header"; +import { ModuleToggleRow } from "@/components/settings/module-toggle-row"; +import { useAuth } from "@/hooks/use-auth"; +import { useTranslations } from "@/lib/i18n/context"; +import { apiPatch } from "@/lib/api/api-fetch"; +import { queryKeys } from "@/lib/query-keys"; +import { + CORE_DOMAIN_KEYS, + MODULE_REGISTRY, + type CoreDomainKey, + type ModuleKey, +} from "@/lib/modules/registry"; + +/** Neutral Lucide glyph per toggleable module. */ +const MODULE_ICONS: Record = { + cycle: CalendarHeart, + mood: Smile, + sleep: Moon, + glucose: Droplet, + workouts: Dumbbell, + recovery: Activity, + labs: TestTube, + achievements: Award, + coach: MessageCircleHeart, + insights: Sparkles, + doctorReport: FileText, +}; + +/** Neutral Lucide glyph per core (always-on) domain. */ +const CORE_ICONS: Record = { + weight: Scale, + bloodPressure: HeartPulse, + pulse: Activity, + medications: Pill, +}; + +export function ModulesSection() { + const { t } = useTranslations(); + const { user } = useAuth(); + const queryClient = useQueryClient(); + + const modules = user?.modules ?? {}; + + const toggle = useMutation({ + // Factory-routed; the in-repo eslint rule forbids a bare literal here. + mutationKey: queryKeys.modulesPrefs(), + mutationFn: async (vars: { key: ModuleKey; enabled: boolean }) => { + // DISABLED allowlist: send only the single key the user flipped. + return apiPatch("/api/auth/me/modules", { [vars.key]: vars.enabled }); + }, + onSuccess: () => { + // Re-gate every surface that reads the module map off /auth/me: nav, + // Insights pills, dashboard tiles, quick-add, search. + void queryClient.invalidateQueries({ queryKey: queryKeys.authMe() }); + toast.success(t("settings.sections.modules.saved")); + }, + onError: () => { + toast.error(t("settings.sections.modules.error")); + }, + }); + + return ( +
+
+

+ {t("settings.sections.modules.title")} +

+

+ {t("settings.sections.modules.description")} +

+
+ + {/* Toggleable modules */} +
+ +
+ {(Object.keys(MODULE_REGISTRY) as ModuleKey[]).map((key) => { + const def = MODULE_REGISTRY[key]; + // Default-on: a module is enabled unless explicitly `false`. + const enabled = modules[key] !== false; + return ( + toggle.mutate({ key, enabled: next })} + /> + ); + })} +
+
+ + {/* Core domains — always on, read-only */} +
+ +
+ {CORE_DOMAIN_KEYS.map((key) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx index e437f3b95..846d3c3af 100644 --- a/src/components/settings/section-placeholder.tsx +++ b/src/components/settings/section-placeholder.tsx @@ -14,6 +14,7 @@ import { AlarmClock, Bell, + Blocks, Download, Info, KeyRound, @@ -36,6 +37,7 @@ import type { SettingsSectionSlug } from "./section-slugs"; const SLUG_ICON: Record = { account: User, + modules: Blocks, integrations: Link2, notifications: Bell, reminders: AlarmClock, diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts index 87e9723b3..b2e65c233 100644 --- a/src/components/settings/section-slugs.ts +++ b/src/components/settings/section-slugs.ts @@ -34,6 +34,9 @@ // the hub is consolidation by linking. export const SETTINGS_SECTION_SLUGS = [ "account", + // v1.18.0 — `modules` ("Was du trackst") sits right after account: the + // single front door for enabling/disabling secondary tracking domains. + "modules", "integrations", "notifications", "reminders", diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index 28d2c168d..1c5c14309 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -20,6 +20,7 @@ import { usePathname } from "next/navigation"; import { AlarmClock, Bell, + Blocks, Download, Info, KeyRound, @@ -80,6 +81,13 @@ interface SettingsSection { */ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ { slug: "account", titleKey: "settings.sections.account.title", icon: User }, + // v1.18.0 — the "Was du trackst" hub. Sits right below Account: the one + // place to enable/disable the secondary tracking domains. + { + slug: "modules", + titleKey: "settings.sections.modules.title", + icon: Blocks, + }, { slug: "integrations", titleKey: "settings.sections.integrations.title", diff --git a/src/lib/query-keys/settings.ts b/src/lib/query-keys/settings.ts index 83fdd03c0..a085dd9ce 100644 --- a/src/lib/query-keys/settings.ts +++ b/src/lib/query-keys/settings.ts @@ -21,6 +21,15 @@ export const settingsKeys = { /** v1.8.5 — user-level injection-site preferences (global exclusion). */ injectionSitePrefs: () => ["settings", "injection-site-prefs"] as const, + /** + * v1.18.0 — the per-user module enable/disable map behind + * `GET/PATCH /api/auth/me/modules`. Used as the Module-hub mutation key; + * the resolved map itself rides on `authMe()` (the `/auth/me` payload), + * so the toggle mutation invalidates `authMe()` to re-gate the nav, + * Insights pills, and dashboard tiles in lockstep. + */ + modulesPrefs: () => ["settings", "modules"] as const, + apiVersion: () => ["api", "version"] as const, publicVersion: () => ["public", "version"] as const, /** v1.15.12 H1 — admin overview "update available" check against the