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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 22:36:04 +02:00
1 parent dceac65ab8
commit f50f92902e
14 files changed
+749

No files matched your search

+77
View File
@@ -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."
}
}
}
+77
View File
@@ -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."
}
}
}
+77
View File
@@ -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."
}
}
}
+77
View File
@@ -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."
}
}
}
+77
View File
@@ -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."
}
}
}
+77
View File
@@ -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ść."
}
}
}
+2
View File
@@ -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,
@@ -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",
@@ -0,0 +1,85 @@
"use client";
/**
* `<ModuleToggleRow>` — 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 `<Switch>` reflecting the resolved enabled-state. The
* row is presentation-only — the parent `<ModulesSection>` 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 (
<div className="flex items-start justify-between gap-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<Icon
className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0"
aria-hidden="true"
/>
<div className="min-w-0 space-y-0.5">
<Label htmlFor={inputId} className="text-sm font-medium">
{label}
</Label>
<p className="text-muted-foreground text-xs">{description}</p>
{locked && lockedNote ? (
<p className="text-muted-foreground/80 text-xs">{lockedNote}</p>
) : null}
</div>
</div>
<Switch
id={inputId}
checked={locked ? true : enabled}
disabled={locked || pending}
onCheckedChange={locked ? undefined : (v) => onToggle?.(v)}
className="mt-0.5 shrink-0"
aria-label={label}
/>
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
"use client";
/**
* `<ModulesSection>` — 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 `<Switch>`. 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<ModuleKey, LucideIcon> = {
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<CoreDomainKey, LucideIcon> = {
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 (
<section
aria-labelledby="settings-section-modules-title"
className="space-y-6"
>
<header className="space-y-1">
<h1 id="settings-section-modules-title" className="sr-only">
{t("settings.sections.modules.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("settings.sections.modules.description")}
</p>
</header>
{/* Toggleable modules */}
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCardHeader
icon={Blocks}
title={t("settings.sections.modules.toggleable.title")}
description={t("settings.sections.modules.toggleable.description")}
className="mb-2"
/>
<div className="divide-border divide-y pl-7">
{(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 (
<ModuleToggleRow
key={key}
moduleKey={key}
icon={MODULE_ICONS[key]}
label={t(def.labelKey)}
description={t(def.descriptionKey)}
enabled={enabled}
pending={toggle.isPending}
onToggle={(next) => toggle.mutate({ key, enabled: next })}
/>
);
})}
</div>
</div>
{/* Core domains — always on, read-only */}
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
<SettingsCardHeader
icon={Lock}
title={t("settings.sections.modules.core.title")}
description={t("settings.sections.modules.core.description")}
className="mb-2"
/>
<div className="divide-border divide-y pl-7">
{CORE_DOMAIN_KEYS.map((key) => (
<ModuleToggleRow
key={key}
moduleKey={key}
icon={CORE_ICONS[key]}
label={t(`modules.${key}.label`)}
description={t(`modules.${key}.description`)}
enabled
locked
lockedNote={t("settings.sections.modules.core.note")}
/>
))}
</div>
</div>
</section>
);
}
@@ -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<SettingsSectionSlug, LucideIcon> = {
account: User,
modules: Blocks,
integrations: Link2,
notifications: Bell,
reminders: AlarmClock,
+3
View File
@@ -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",
@@ -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",
+9
View File
@@ -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