mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
merge: Settings and Admin information-architecture redesign
Bring the shared SettingsSectionFrame and the reorganised settings and admin navigation onto the v1.18.6 line.
This commit is contained in:
commit
325f4f6b6b
42 files changed
+822
-802
No files matched your search
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Konto",
|
||||
"description": "Profil, Passwort, Passkeys und deine Onboarding-Tour."
|
||||
"description": "Profil, Passwort, Passkeys und deine Onboarding-Tour.",
|
||||
"subtitle": "Dein Profil, deine Identität und deine Anmeldedaten."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrationen",
|
||||
"description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort."
|
||||
"description": "Verbundene Dienste, Zustellkanäle und die Quellen-Priorität an einem Ort.",
|
||||
"subtitle": "Datenquellen und Geräte verbinden und verwalten."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"description": "Alle Erinnerungen an einem Ort — was wann erinnert wird. Die Zustellkanäle liegen unter Integrationen.",
|
||||
"channelsHint": "Telegram, ntfy, Web-Push, Webhook und E-Mail richtest du jetzt unter",
|
||||
"channelsHintLink": "Integrationen → Kanäle",
|
||||
"inboxLink": "Posteingang öffnen"
|
||||
"inboxLink": "Posteingang öffnen",
|
||||
"subtitle": "Wähle, welche Erinnerungen du erhältst."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Layout",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Insights",
|
||||
"description": "Reihenfolge der Übersicht und der Navigations-Pills."
|
||||
}
|
||||
},
|
||||
"subtitle": "Passe an, wie Dashboard und Insights angeordnet sind."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Kachel-Layout und Reihenfolge."
|
||||
"description": "Kachel-Layout und Reihenfolge.",
|
||||
"subtitle": "Ordne die Kacheln auf deinem Dashboard an."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Zielwerte",
|
||||
"description": "Pro Metrik dein eigener Zielbereich für gesunde Werte."
|
||||
"description": "Pro Metrik dein eigener Zielbereich für gesunde Werte.",
|
||||
"subtitle": "Lege deine persönlichen Zielbereiche je Messgröße fest."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Quellen-Priorität",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Waage",
|
||||
"other": "Sonstige",
|
||||
"unknown": "Unbekannt"
|
||||
}
|
||||
},
|
||||
"subtitle": "Lege fest, welche Quelle gewinnt, wenn zwei dieselbe Messgröße liefern."
|
||||
},
|
||||
"ai": {
|
||||
"title": "KI-Auswertungen",
|
||||
"description": "Provider, Modell und API-Key für die Auswertungen — dazu dein „Über mich“-Kontext."
|
||||
"title": "KI-Anbieter",
|
||||
"description": "Provider, Modell und API-Key für die Auswertungen — dazu dein „Über mich“-Kontext.",
|
||||
"subtitle": "Wähle deinen KI-Anbieter und hinterlege deinen eigenen Schlüssel."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Bearer-Tokens für eigene Skripte und Apps — erfasse Messwerte oder Einnahmen von überall."
|
||||
"description": "Bearer-Tokens für eigene Skripte und Apps — erfasse Messwerte oder Einnahmen von überall.",
|
||||
"subtitle": "API-Endpunkte und Zugriffstoken verwalten."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweitert",
|
||||
"description": "Opt-in-Schalter für den Forschungsmodus sowie der Notausgang zum unwiderruflichen Löschen aller Gesundheitsdaten."
|
||||
"description": "Opt-in-Schalter für den Forschungsmodus sowie der Notausgang zum unwiderruflichen Löschen aller Gesundheitsdaten.",
|
||||
"subtitle": "Research-Modus, Daten zurücksetzen und Konto löschen."
|
||||
},
|
||||
"export": {
|
||||
"title": "Export & Import",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "{inserted} neu importiert und {updated} aktualisiert, {skipped} übersprungen.",
|
||||
"rowError": "Zeile {line}: {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Exportiere deine Daten oder importiere aus einer Sicherung."
|
||||
},
|
||||
"about": {
|
||||
"title": "Über diese App",
|
||||
"description": "Version, Lizenz, Links."
|
||||
"description": "Version, Lizenz, Links.",
|
||||
"subtitle": "Version, Lizenz und Projektlinks."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Freigabe",
|
||||
"description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen."
|
||||
"description": "Erstelle einen schreibgeschützten Link, um deine Gesundheitsakte mit einer Ärztin oder einem Arzt zu teilen.",
|
||||
"subtitle": "Teile schreibgeschützten Zugriff auf deine Daten."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Insights",
|
||||
"description": "Reihenfolge der Übersicht und der Navigations-Pills."
|
||||
"description": "Reihenfolge der Übersicht und der Navigations-Pills.",
|
||||
"subtitle": "Ordne die Übersichts-Bereiche und die Reihenfolge der Pills an."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Medikamente",
|
||||
"description": "Listenansicht, Reihenfolge und Injektionsstellen deiner Medikamente."
|
||||
"description": "Listenansicht, Reihenfolge und Injektionsstellen deiner Medikamente.",
|
||||
"subtitle": "Verwalte Ansicht und Reihenfolge deiner Medikamentenliste."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Stimmungs-Tags",
|
||||
"description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl."
|
||||
"description": "Gruppen, Tags und archivierte Tags der Stimmungs-Auswahl.",
|
||||
"subtitle": "Organisiere deine Stimmungs-Tags, Gruppen und Auswahlreihenfolge."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Module",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "In {section} verwalten",
|
||||
"operatorDisabled": "Serverweit deaktiviert"
|
||||
"operatorDisabled": "Serverweit deaktiviert",
|
||||
"subtitle": "Lege fest, welche Tracking-Bereiche HealthLog überall anzeigt."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Gesundheitsakte",
|
||||
"description": "Vollständige Gesundheitsakte als PDF, FHIR R4 oder kombiniertes ZIP-Paket exportieren."
|
||||
"description": "Vollständige Gesundheitsakte als PDF, FHIR R4 oder kombiniertes ZIP-Paket exportieren.",
|
||||
"subtitle": "Exportiere eine vollständige, arzttaugliche Gesundheitsakte."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Coach aktivieren, Ton und Umfang, gespeicherte Erinnerungen."
|
||||
"description": "Coach aktivieren, Ton und Umfang, gespeicherte Erinnerungen.",
|
||||
"subtitle": "Stelle den Coach, seine proaktiven Hinweise und sein Gedächtnis ein."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Kanäle",
|
||||
"description": "Zustellung über Telegram, ntfy, Web-Push, Webhook und E-Mail."
|
||||
"description": "Zustellung über Telegram, ntfy, Web-Push, Webhook und E-Mail.",
|
||||
"subtitle": "Wohin Erinnerungen und Hinweise zugestellt werden."
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Destruktive systemweite Operationen."
|
||||
},
|
||||
"about": {
|
||||
"title": "Über"
|
||||
"title": "Über",
|
||||
"subtitle": "Version, Lizenz und Update-Status."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Modulverfügbarkeit",
|
||||
"subtitle": "Module für jedes Konto auf diesem Server aktivieren oder deaktivieren."
|
||||
}
|
||||
},
|
||||
"carrier": "Anbieter",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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."
|
||||
|
||||
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"description": "Profile, password, passkeys, and your onboarding tour."
|
||||
"description": "Profile, password, passkeys, and your onboarding tour.",
|
||||
"subtitle": "Your profile, identity, and sign-in details."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrations",
|
||||
"description": "Connected services, delivery channels, and source priority in one place."
|
||||
"description": "Connected services, delivery channels, and source priority in one place.",
|
||||
"subtitle": "Connect and manage your data sources and devices."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Every reminder in one place — what gets reminded and when. The delivery channels live under Integrations.",
|
||||
"channelsHint": "Telegram, ntfy, Web Push, Webhook, and Email are now set up under",
|
||||
"channelsHintLink": "Integrations → Channels",
|
||||
"inboxLink": "Open the inbox"
|
||||
"inboxLink": "Open the inbox",
|
||||
"subtitle": "Choose which reminders you receive."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Layout",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Insights",
|
||||
"description": "Overview sections and navigation-pill order."
|
||||
}
|
||||
},
|
||||
"subtitle": "Personalize how your dashboard and insights are arranged."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Tile layout and order."
|
||||
"description": "Tile layout and order.",
|
||||
"subtitle": "Arrange the tiles on your dashboard."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Targets",
|
||||
"description": "Per metric: your own target range for healthy values."
|
||||
"description": "Per metric: your own target range for healthy values.",
|
||||
"subtitle": "Set your personal target ranges per metric."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Source priority",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Scale",
|
||||
"other": "Other",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
},
|
||||
"subtitle": "Decide which source wins when two report the same metric."
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI Insights",
|
||||
"description": "Provider, model, and API key for the insights — plus your “about me” context."
|
||||
"title": "AI provider",
|
||||
"description": "Provider, model, and API key for the insights — plus your “about me” context.",
|
||||
"subtitle": "Choose your AI provider and bring your own key."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Bearer tokens for your own scripts and apps — log measurements or medication intake from anywhere."
|
||||
"description": "Bearer tokens for your own scripts and apps — log measurements or medication intake from anywhere.",
|
||||
"subtitle": "Manage API endpoints and access tokens."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Opt-in Research Mode toggle plus the account-wide danger zone for wiping every health record."
|
||||
"description": "Opt-in Research Mode toggle plus the account-wide danger zone for wiping every health record.",
|
||||
"subtitle": "Research mode, data reset, and account deletion."
|
||||
},
|
||||
"export": {
|
||||
"title": "Export & Import",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "Imported {inserted} new and updated {updated}, {skipped} skipped.",
|
||||
"rowError": "Line {line}: {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Export your data or import from a backup."
|
||||
},
|
||||
"about": {
|
||||
"title": "About this app",
|
||||
"description": "Version, license, links."
|
||||
"description": "Version, license, links.",
|
||||
"subtitle": "Version, license, and project links."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Sharing",
|
||||
"description": "Create a read-only link to share your health record with a clinician."
|
||||
"description": "Create a read-only link to share your health record with a clinician.",
|
||||
"subtitle": "Share read-only access to your data."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Insights",
|
||||
"description": "Overview and navigation-pill order."
|
||||
"description": "Overview and navigation-pill order.",
|
||||
"subtitle": "Arrange your overview sections and pill order."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Medications",
|
||||
"description": "List view, order, and injection sites for your medications."
|
||||
"description": "List view, order, and injection sites for your medications.",
|
||||
"subtitle": "Manage your medication list view and order."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Mood tags",
|
||||
"description": "Groups, tags, and archived tags for the mood picker."
|
||||
"description": "Groups, tags, and archived tags for the mood picker.",
|
||||
"subtitle": "Organize your mood tags, groups, and picker order."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Modules",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "Manage in {section}",
|
||||
"operatorDisabled": "Disabled server-wide"
|
||||
"operatorDisabled": "Disabled server-wide",
|
||||
"subtitle": "Choose which tracking areas HealthLog shows across the app."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Health record",
|
||||
"description": "Export your full health record as PDF, FHIR R4, or a combined ZIP package."
|
||||
"description": "Export your full health record as PDF, FHIR R4, or a combined ZIP package.",
|
||||
"subtitle": "Export a complete, clinician-ready health record."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Enable the Coach, tone and scope, stored memories."
|
||||
"description": "Enable the Coach, tone and scope, stored memories.",
|
||||
"subtitle": "Tune the Coach, its proactive nudges, and its memory."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Channels",
|
||||
"description": "Telegram, ntfy, Web Push, Webhook, and Email delivery."
|
||||
"description": "Telegram, ntfy, Web Push, Webhook, and Email delivery.",
|
||||
"subtitle": "Where reminders and alerts are delivered."
|
||||
}
|
||||
},
|
||||
"profile": "Profile",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Destructive system-wide operations."
|
||||
},
|
||||
"about": {
|
||||
"title": "About"
|
||||
"title": "About",
|
||||
"subtitle": "Version, license, and update status."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Module availability",
|
||||
"subtitle": "Turn modules on or off for every account on this server."
|
||||
}
|
||||
},
|
||||
"carrier": "Carrier",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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."
|
||||
|
||||
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Cuenta",
|
||||
"description": "Perfil, contraseña, claves de acceso y tu recorrido de incorporación."
|
||||
"description": "Perfil, contraseña, claves de acceso y tu recorrido de incorporación.",
|
||||
"subtitle": "Tu perfil, identidad y datos de acceso."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integraciones",
|
||||
"description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar."
|
||||
"description": "Servicios conectados, canales de entrega y prioridad de fuentes en un solo lugar.",
|
||||
"subtitle": "Conecta y gestiona tus fuentes de datos y dispositivos."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo. Los canales de entrega están en Integraciones.",
|
||||
"channelsHint": "Telegram, ntfy, Web Push, Webhook y correo se configuran ahora en",
|
||||
"channelsHintLink": "Integraciones → Canales",
|
||||
"inboxLink": "Abrir la bandeja de entrada"
|
||||
"inboxLink": "Abrir la bandeja de entrada",
|
||||
"subtitle": "Elige qué recordatorios recibes."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Diseño",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Análisis",
|
||||
"description": "Orden de la vista general y de las pestañas de navegación."
|
||||
}
|
||||
},
|
||||
"subtitle": "Personaliza cómo se organizan tu panel e insights."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel",
|
||||
"description": "Diseño y orden de los mosaicos."
|
||||
"description": "Diseño y orden de los mosaicos.",
|
||||
"subtitle": "Organiza los mosaicos de tu panel."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Objetivos",
|
||||
"description": "Por métrica: tu propio rango objetivo de valores saludables."
|
||||
"description": "Por métrica: tu propio rango objetivo de valores saludables.",
|
||||
"subtitle": "Define tus rangos objetivo personales por métrica."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Prioridad de fuentes",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Báscula",
|
||||
"other": "Otro",
|
||||
"unknown": "Desconocido"
|
||||
}
|
||||
},
|
||||
"subtitle": "Decide qué fuente prevalece cuando dos informan la misma métrica."
|
||||
},
|
||||
"ai": {
|
||||
"title": "Análisis con IA",
|
||||
"description": "Proveedor, modelo y clave API para los análisis, además de tu contexto «sobre mí»."
|
||||
"title": "Proveedor de IA",
|
||||
"description": "Proveedor, modelo y clave API para los análisis, además de tu contexto «sobre mí».",
|
||||
"subtitle": "Elige tu proveedor de IA y usa tu propia clave."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Tokens Bearer para tus propios scripts y apps — registra mediciones o tomas de medicación desde cualquier lugar."
|
||||
"description": "Tokens Bearer para tus propios scripts y apps — registra mediciones o tomas de medicación desde cualquier lugar.",
|
||||
"subtitle": "Gestiona los endpoints de la API y los tokens de acceso."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avanzado",
|
||||
"description": "Activador opt-in del modo Investigación y zona de peligro para borrar irrevocablemente todos los registros de salud."
|
||||
"description": "Activador opt-in del modo Investigación y zona de peligro para borrar irrevocablemente todos los registros de salud.",
|
||||
"subtitle": "Modo investigación, restablecer datos y eliminar cuenta."
|
||||
},
|
||||
"export": {
|
||||
"title": "Exportar e importar",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "{inserted} nuevas importadas y {updated} actualizadas, {skipped} omitidas.",
|
||||
"rowError": "Línea {line}: {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Exporta tus datos o importa desde una copia de seguridad."
|
||||
},
|
||||
"about": {
|
||||
"title": "Acerca de esta app",
|
||||
"description": "Versión, licencia, enlaces."
|
||||
"description": "Versión, licencia, enlaces.",
|
||||
"subtitle": "Versión, licencia y enlaces del proyecto."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Compartir",
|
||||
"description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico."
|
||||
"description": "Crea un enlace de solo lectura para compartir tu historial de salud con un profesional médico.",
|
||||
"subtitle": "Comparte acceso de solo lectura a tus datos."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Análisis",
|
||||
"description": "Orden de la vista general y de las pestañas de navegación."
|
||||
"description": "Orden de la vista general y de las pestañas de navegación.",
|
||||
"subtitle": "Organiza las secciones de tu resumen y el orden de las píldoras."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Medicamentos",
|
||||
"description": "Vista de lista, orden y zonas de inyección de tus medicamentos."
|
||||
"description": "Vista de lista, orden y zonas de inyección de tus medicamentos.",
|
||||
"subtitle": "Gestiona la vista y el orden de tu lista de medicamentos."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Etiquetas de ánimo",
|
||||
"description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo."
|
||||
"description": "Grupos, etiquetas y etiquetas archivadas del selector de ánimo.",
|
||||
"subtitle": "Organiza tus etiquetas de ánimo, grupos y orden del selector."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Módulos",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "Gestionar en {section}",
|
||||
"operatorDisabled": "Desactivado en todo el servidor"
|
||||
"operatorDisabled": "Desactivado en todo el servidor",
|
||||
"subtitle": "Elige qué áreas de seguimiento muestra HealthLog en la app."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Historial médico",
|
||||
"description": "Exporta tu historial médico completo como PDF, FHIR R4 o un paquete ZIP combinado."
|
||||
"description": "Exporta tu historial médico completo como PDF, FHIR R4 o un paquete ZIP combinado.",
|
||||
"subtitle": "Exporta un historial de salud completo, listo para el médico."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Activa el Coach, tono y alcance, recuerdos guardados."
|
||||
"description": "Activa el Coach, tono y alcance, recuerdos guardados.",
|
||||
"subtitle": "Ajusta el Coach, sus avisos proactivos y su memoria."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canales",
|
||||
"description": "Envío por Telegram, ntfy, Web Push, Webhook y correo electrónico."
|
||||
"description": "Envío por Telegram, ntfy, Web Push, Webhook y correo electrónico.",
|
||||
"subtitle": "Dónde se entregan los recordatorios y avisos."
|
||||
}
|
||||
},
|
||||
"profile": "Perfil",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Operaciones destructivas en todo el sistema."
|
||||
},
|
||||
"about": {
|
||||
"title": "Acerca de"
|
||||
"title": "Acerca de",
|
||||
"subtitle": "Versión, licencia y estado de actualizaciones."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Disponibilidad de módulos",
|
||||
"subtitle": "Activa o desactiva módulos para todas las cuentas de este servidor."
|
||||
}
|
||||
},
|
||||
"carrier": "Operador",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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."
|
||||
|
||||
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Compte",
|
||||
"description": "Profil, mot de passe, clés d’accès et votre visite guidée."
|
||||
"description": "Profil, mot de passe, clés d’accès et votre visite guidée.",
|
||||
"subtitle": "Votre profil, votre identité et vos identifiants."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Intégrations",
|
||||
"description": "Services connectés, canaux de diffusion et priorité des sources au même endroit."
|
||||
"description": "Services connectés, canaux de diffusion et priorité des sources au même endroit.",
|
||||
"subtitle": "Connectez et gérez vos sources de données et appareils."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Tous les rappels au même endroit — quoi et quand. Les canaux de diffusion se trouvent dans Intégrations.",
|
||||
"channelsHint": "Telegram, ntfy, Web Push, Webhook et e-mail se configurent désormais dans",
|
||||
"channelsHintLink": "Intégrations → Canaux",
|
||||
"inboxLink": "Ouvrir la boîte de réception"
|
||||
"inboxLink": "Ouvrir la boîte de réception",
|
||||
"subtitle": "Choisissez les rappels que vous recevez."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Disposition",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Analyses",
|
||||
"description": "Ordre de la vue d'ensemble et des onglets de navigation."
|
||||
}
|
||||
},
|
||||
"subtitle": "Personnalisez l'agencement de votre tableau de bord et de vos insights."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord",
|
||||
"description": "Disposition et ordre des tuiles."
|
||||
"description": "Disposition et ordre des tuiles.",
|
||||
"subtitle": "Organisez les tuiles de votre tableau de bord."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Objectifs",
|
||||
"description": "Par métrique : votre propre plage cible pour des valeurs saines."
|
||||
"description": "Par métrique : votre propre plage cible pour des valeurs saines.",
|
||||
"subtitle": "Définissez vos plages cibles personnelles par mesure."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Priorité des sources",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Balance",
|
||||
"other": "Autre",
|
||||
"unknown": "Inconnu"
|
||||
}
|
||||
},
|
||||
"subtitle": "Choisissez la source prioritaire en cas de doublon de mesure."
|
||||
},
|
||||
"ai": {
|
||||
"title": "Analyses IA",
|
||||
"description": "Fournisseur, modèle et clé API pour les analyses, ainsi que votre contexte « à propos de moi »."
|
||||
"title": "Fournisseur d'IA",
|
||||
"description": "Fournisseur, modèle et clé API pour les analyses, ainsi que votre contexte « à propos de moi ».",
|
||||
"subtitle": "Choisissez votre fournisseur d'IA et utilisez votre propre clé."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Jetons Bearer pour vos propres scripts et applis — enregistrez des mesures ou des prises de médicaments depuis n’importe où."
|
||||
"description": "Jetons Bearer pour vos propres scripts et applis — enregistrez des mesures ou des prises de médicaments depuis n’importe où.",
|
||||
"subtitle": "Gérez les points d'accès API et les jetons d'accès."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avancé",
|
||||
"description": "Bouton opt-in du mode Recherche et zone dangereuse pour effacer irrévocablement toutes les données de santé."
|
||||
"description": "Bouton opt-in du mode Recherche et zone dangereuse pour effacer irrévocablement toutes les données de santé.",
|
||||
"subtitle": "Mode recherche, réinitialisation des données et suppression du compte."
|
||||
},
|
||||
"export": {
|
||||
"title": "Export et import",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "{inserted} nouvelles importées et {updated} mises à jour, {skipped} ignorées.",
|
||||
"rowError": "Ligne {line} : {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Exportez vos données ou importez depuis une sauvegarde."
|
||||
},
|
||||
"about": {
|
||||
"title": "À propos de cette app",
|
||||
"description": "Version, licence, liens."
|
||||
"description": "Version, licence, liens.",
|
||||
"subtitle": "Version, licence et liens du projet."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Partage",
|
||||
"description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé."
|
||||
"description": "Créez un lien en lecture seule pour partager votre dossier de santé avec un professionnel de santé.",
|
||||
"subtitle": "Partagez un accès en lecture seule à vos données."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Analyses",
|
||||
"description": "Ordre de la vue d'ensemble et des onglets de navigation."
|
||||
"description": "Ordre de la vue d'ensemble et des onglets de navigation.",
|
||||
"subtitle": "Organisez les sections d'aperçu et l'ordre des pastilles."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Médicaments",
|
||||
"description": "Vue liste, ordre et sites d’injection de vos médicaments."
|
||||
"description": "Vue liste, ordre et sites d’injection de vos médicaments.",
|
||||
"subtitle": "Gérez l'affichage et l'ordre de votre liste de médicaments."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Étiquettes d’humeur",
|
||||
"description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur."
|
||||
"description": "Groupes, étiquettes et étiquettes archivées du sélecteur d’humeur.",
|
||||
"subtitle": "Organisez vos tags d'humeur, groupes et ordre du sélecteur."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Modules",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "Gérer dans {section}",
|
||||
"operatorDisabled": "Désactivé sur tout le serveur"
|
||||
"operatorDisabled": "Désactivé sur tout le serveur",
|
||||
"subtitle": "Choisissez les domaines de suivi affichés par HealthLog."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Dossier médical",
|
||||
"description": "Exportez votre dossier médical complet au format PDF, FHIR R4 ou en paquet ZIP combiné."
|
||||
"description": "Exportez votre dossier médical complet au format PDF, FHIR R4 ou en paquet ZIP combiné.",
|
||||
"subtitle": "Exportez un dossier de santé complet, prêt pour le médecin."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Activez le Coach, le ton et la portée, les souvenirs enregistrés."
|
||||
"description": "Activez le Coach, le ton et la portée, les souvenirs enregistrés.",
|
||||
"subtitle": "Réglez le Coach, ses suggestions proactives et sa mémoire."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canaux",
|
||||
"description": "Distribution via Telegram, ntfy, Web Push, Webhook et e-mail."
|
||||
"description": "Distribution via Telegram, ntfy, Web Push, Webhook et e-mail.",
|
||||
"subtitle": "Où sont envoyés les rappels et alertes."
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Opérations destructrices à l'échelle du système."
|
||||
},
|
||||
"about": {
|
||||
"title": "À propos"
|
||||
"title": "À propos",
|
||||
"subtitle": "Version, licence et état des mises à jour."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Disponibilité des modules",
|
||||
"subtitle": "Activez ou désactivez les modules pour tous les comptes de ce serveur."
|
||||
}
|
||||
},
|
||||
"carrier": "Opérateur",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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."
|
||||
|
||||
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Account",
|
||||
"description": "Profilo, password, passkey e il tour di benvenuto."
|
||||
"description": "Profilo, password, passkey e il tour di benvenuto.",
|
||||
"subtitle": "Il tuo profilo, la tua identità e i dati di accesso."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integrazioni",
|
||||
"description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto."
|
||||
"description": "Servizi collegati, canali di consegna e priorità delle fonti in un unico posto.",
|
||||
"subtitle": "Collega e gestisci le tue fonti di dati e i dispositivi."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifiche",
|
||||
"description": "Tutti i promemoria in un unico posto — cosa e quando. I canali di recapito si trovano in Integrazioni.",
|
||||
"channelsHint": "Telegram, ntfy, Web Push, Webhook ed e-mail ora si configurano in",
|
||||
"channelsHintLink": "Integrazioni → Canali",
|
||||
"inboxLink": "Apri la posta in arrivo"
|
||||
"inboxLink": "Apri la posta in arrivo",
|
||||
"subtitle": "Scegli quali promemoria ricevere."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Layout",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Approfondimenti",
|
||||
"description": "Ordine della panoramica e delle schede di navigazione."
|
||||
}
|
||||
},
|
||||
"subtitle": "Personalizza la disposizione di dashboard e insights."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Cruscotto",
|
||||
"description": "Layout e ordine dei riquadri."
|
||||
"description": "Layout e ordine dei riquadri.",
|
||||
"subtitle": "Disponi i riquadri della tua dashboard."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Obiettivi",
|
||||
"description": "Per metrica: il tuo intervallo target per valori sani."
|
||||
"description": "Per metrica: il tuo intervallo target per valori sani.",
|
||||
"subtitle": "Imposta i tuoi intervalli target personali per metrica."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Priorità delle fonti",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Bilancia",
|
||||
"other": "Altro",
|
||||
"unknown": "Sconosciuto"
|
||||
}
|
||||
},
|
||||
"subtitle": "Decidi quale fonte prevale quando due riportano la stessa metrica."
|
||||
},
|
||||
"ai": {
|
||||
"title": "Analisi IA",
|
||||
"description": "Provider, modello e chiave API per gli approfondimenti, oltre al tuo contesto «su di me»."
|
||||
"title": "Provider IA",
|
||||
"description": "Provider, modello e chiave API per gli approfondimenti, oltre al tuo contesto «su di me».",
|
||||
"subtitle": "Scegli il tuo provider IA e usa la tua chiave."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Token Bearer per i tuoi script e le tue app — registra misurazioni o assunzioni di farmaci da ovunque."
|
||||
"description": "Token Bearer per i tuoi script e le tue app — registra misurazioni o assunzioni di farmaci da ovunque.",
|
||||
"subtitle": "Gestisci gli endpoint API e i token di accesso."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avanzate",
|
||||
"description": "Interruttore opt-in della modalità Ricerca e zona pericolosa per cancellare in modo irrevocabile tutti i dati di salute."
|
||||
"description": "Interruttore opt-in della modalità Ricerca e zona pericolosa per cancellare in modo irrevocabile tutti i dati di salute.",
|
||||
"subtitle": "Modalità ricerca, ripristino dati ed eliminazione account."
|
||||
},
|
||||
"export": {
|
||||
"title": "Esporta e importa",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "{inserted} nuove importate e {updated} aggiornate, {skipped} saltate.",
|
||||
"rowError": "Riga {line}: {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Esporta i tuoi dati o importa da un backup."
|
||||
},
|
||||
"about": {
|
||||
"title": "Informazioni su questa app",
|
||||
"description": "Versione, licenza, link."
|
||||
"description": "Versione, licenza, link.",
|
||||
"subtitle": "Versione, licenza e link al progetto."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Condivisione",
|
||||
"description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico."
|
||||
"description": "Crea un link di sola lettura per condividere la tua cartella clinica con un medico.",
|
||||
"subtitle": "Condividi l'accesso in sola lettura ai tuoi dati."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Approfondimenti",
|
||||
"description": "Ordine della panoramica e delle schede di navigazione."
|
||||
"description": "Ordine della panoramica e delle schede di navigazione.",
|
||||
"subtitle": "Disponi le sezioni della panoramica e l'ordine dei pill."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Farmaci",
|
||||
"description": "Vista elenco, ordine e siti di iniezione dei tuoi farmaci."
|
||||
"description": "Vista elenco, ordine e siti di iniezione dei tuoi farmaci.",
|
||||
"subtitle": "Gestisci vista e ordine del tuo elenco di farmaci."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Tag dell’umore",
|
||||
"description": "Gruppi, tag e tag archiviati del selettore dell’umore."
|
||||
"description": "Gruppi, tag e tag archiviati del selettore dell’umore.",
|
||||
"subtitle": "Organizza i tag dell'umore, i gruppi e l'ordine del selettore."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Moduli",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "Gestisci in {section}",
|
||||
"operatorDisabled": "Disattivato a livello di server"
|
||||
"operatorDisabled": "Disattivato a livello di server",
|
||||
"subtitle": "Scegli quali aree di monitoraggio HealthLog mostra nell'app."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Cartella clinica",
|
||||
"description": "Esporta la tua cartella clinica completa in PDF, FHIR R4 o in un pacchetto ZIP combinato."
|
||||
"description": "Esporta la tua cartella clinica completa in PDF, FHIR R4 o in un pacchetto ZIP combinato.",
|
||||
"subtitle": "Esporta una cartella clinica completa, pronta per il medico."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Attiva il Coach, tono e ambito, ricordi salvati."
|
||||
"description": "Attiva il Coach, tono e ambito, ricordi salvati.",
|
||||
"subtitle": "Regola il Coach, i suoi suggerimenti proattivi e la sua memoria."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canali",
|
||||
"description": "Recapito tramite Telegram, ntfy, Web Push, Webhook ed e-mail."
|
||||
"description": "Recapito tramite Telegram, ntfy, Web Push, Webhook ed e-mail.",
|
||||
"subtitle": "Dove vengono recapitati promemoria e avvisi."
|
||||
}
|
||||
},
|
||||
"profile": "Profilo",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Operazioni distruttive a livello di sistema."
|
||||
},
|
||||
"about": {
|
||||
"title": "Informazioni"
|
||||
"title": "Informazioni",
|
||||
"subtitle": "Versione, licenza e stato degli aggiornamenti."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Disponibilità dei moduli",
|
||||
"subtitle": "Attiva o disattiva i moduli per tutti gli account su questo server."
|
||||
}
|
||||
},
|
||||
"carrier": "Operatore",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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."
|
||||
|
||||
+47
-40
@@ -3778,18 +3778,21 @@
|
||||
"sections": {
|
||||
"account": {
|
||||
"title": "Konto",
|
||||
"description": "Profil, hasło, klucze dostępu i przewodnik wprowadzający."
|
||||
"description": "Profil, hasło, klucze dostępu i przewodnik wprowadzający.",
|
||||
"subtitle": "Twój profil, tożsamość i dane logowania."
|
||||
},
|
||||
"integrations": {
|
||||
"title": "Integracje",
|
||||
"description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu."
|
||||
"description": "Połączone usługi, kanały dostarczania i priorytet źródeł w jednym miejscu.",
|
||||
"subtitle": "Połącz i zarządzaj źródłami danych oraz urządzeniami."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Powiadomienia",
|
||||
"description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy. Kanały dostarczania znajdują się w Integracjach.",
|
||||
"channelsHint": "Telegram, ntfy, Web Push, Webhook i e-mail konfigurujesz teraz w",
|
||||
"channelsHintLink": "Integracje → Kanały",
|
||||
"inboxLink": "Otwórz skrzynkę odbiorczą"
|
||||
"inboxLink": "Otwórz skrzynkę odbiorczą",
|
||||
"subtitle": "Wybierz, które przypomnienia otrzymujesz."
|
||||
},
|
||||
"layout": {
|
||||
"title": "Układ",
|
||||
@@ -3802,15 +3805,18 @@
|
||||
"insights": {
|
||||
"title": "Analizy",
|
||||
"description": "Kolejność przeglądu i pigułek nawigacyjnych."
|
||||
}
|
||||
},
|
||||
"subtitle": "Dostosuj układ pulpitu i analiz."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Pulpit",
|
||||
"description": "Układ i kolejność kafelków."
|
||||
"description": "Układ i kolejność kafelków.",
|
||||
"subtitle": "Rozmieść kafelki na pulpicie."
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Cele",
|
||||
"description": "Dla każdej metryki: własny zakres docelowy zdrowych wartości."
|
||||
"description": "Dla każdej metryki: własny zakres docelowy zdrowych wartości.",
|
||||
"subtitle": "Ustaw osobiste zakresy docelowe dla każdej metryki."
|
||||
},
|
||||
"sources": {
|
||||
"title": "Priorytet źródeł",
|
||||
@@ -3869,19 +3875,23 @@
|
||||
"scale": "Waga",
|
||||
"other": "Inne",
|
||||
"unknown": "Nieznane"
|
||||
}
|
||||
},
|
||||
"subtitle": "Zdecyduj, które źródło wygrywa, gdy dwa podają tę samą metrykę."
|
||||
},
|
||||
"ai": {
|
||||
"title": "Analizy AI",
|
||||
"description": "Dostawca, model i klucz API do analiz, a także Twój kontekst „o mnie”."
|
||||
"title": "Dostawca AI",
|
||||
"description": "Dostawca, model i klucz API do analiz, a także Twój kontekst „o mnie”.",
|
||||
"subtitle": "Wybierz dostawcę AI i użyj własnego klucza."
|
||||
},
|
||||
"api": {
|
||||
"title": "API & Tokens",
|
||||
"description": "Tokeny Bearer dla własnych skryptów i aplikacji — zapisuj pomiary lub przyjęcia leków z dowolnego miejsca."
|
||||
"description": "Tokeny Bearer dla własnych skryptów i aplikacji — zapisuj pomiary lub przyjęcia leków z dowolnego miejsca.",
|
||||
"subtitle": "Zarządzaj punktami końcowymi API i tokenami dostępu."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Zaawansowane",
|
||||
"description": "Opt-in przełącznika trybu Badawczego oraz strefa zagrożenia umożliwiająca nieodwracalne wymazanie wszystkich danych zdrowotnych."
|
||||
"description": "Opt-in przełącznika trybu Badawczego oraz strefa zagrożenia umożliwiająca nieodwracalne wymazanie wszystkich danych zdrowotnych.",
|
||||
"subtitle": "Tryb badawczy, reset danych i usunięcie konta."
|
||||
},
|
||||
"export": {
|
||||
"title": "Eksport i import",
|
||||
@@ -3982,27 +3992,33 @@
|
||||
"resultSummary": "Zaimportowano {inserted} nowych i zaktualizowano {updated}, pominięto {skipped}.",
|
||||
"rowError": "Wiersz {line}: {reason}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtitle": "Wyeksportuj dane lub zaimportuj z kopii zapasowej."
|
||||
},
|
||||
"about": {
|
||||
"title": "O tej aplikacji",
|
||||
"description": "Wersja, licencja, linki."
|
||||
"description": "Wersja, licencja, linki.",
|
||||
"subtitle": "Wersja, licencja i linki do projektu."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Udostępnianie",
|
||||
"description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi."
|
||||
"description": "Utwórz link tylko do odczytu, aby udostępnić swoją dokumentację zdrowotną lekarzowi.",
|
||||
"subtitle": "Udostępnij dostęp tylko do odczytu do swoich danych."
|
||||
},
|
||||
"insights": {
|
||||
"title": "Analizy",
|
||||
"description": "Kolejność przeglądu i pigułek nawigacyjnych."
|
||||
"description": "Kolejność przeglądu i pigułek nawigacyjnych.",
|
||||
"subtitle": "Rozmieść sekcje przeglądu i kolejność etykiet."
|
||||
},
|
||||
"medications": {
|
||||
"title": "Leki",
|
||||
"description": "Widok listy, kolejność i miejsca wstrzyknięć Twoich leków."
|
||||
"description": "Widok listy, kolejność i miejsca wstrzyknięć Twoich leków.",
|
||||
"subtitle": "Zarządzaj widokiem i kolejnością listy leków."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Tagi nastroju",
|
||||
"description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju."
|
||||
"description": "Grupy, tagi i zarchiwizowane tagi w wyborze nastroju.",
|
||||
"subtitle": "Uporządkuj tagi nastroju, grupy i kolejność wyboru."
|
||||
},
|
||||
"modules": {
|
||||
"title": "Moduły",
|
||||
@@ -4012,26 +4028,24 @@
|
||||
"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.",
|
||||
"manageIn": "Zarządzaj w {section}",
|
||||
"operatorDisabled": "Wyłączone na całym serwerze"
|
||||
"operatorDisabled": "Wyłączone na całym serwerze",
|
||||
"subtitle": "Wybierz, które obszary śledzenia pokazuje HealthLog w aplikacji."
|
||||
},
|
||||
"gesundheitsakte": {
|
||||
"title": "Dokumentacja zdrowotna",
|
||||
"description": "Wyeksportuj pełną dokumentację zdrowotną jako PDF, FHIR R4 lub łączony pakiet ZIP."
|
||||
"description": "Wyeksportuj pełną dokumentację zdrowotną jako PDF, FHIR R4 lub łączony pakiet ZIP.",
|
||||
"subtitle": "Eksportuj kompletną dokumentację zdrowotną gotową dla lekarza."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach",
|
||||
"description": "Włącz Coacha, ton i zakres, zapisane wspomnienia."
|
||||
"description": "Włącz Coacha, ton i zakres, zapisane wspomnienia.",
|
||||
"subtitle": "Dostosuj Coacha, jego proaktywne podpowiedzi i pamięć."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Kanały",
|
||||
"description": "Dostarczanie przez Telegram, ntfy, Web Push, Webhook i e-mail."
|
||||
"description": "Dostarczanie przez Telegram, ntfy, Web Push, Webhook i e-mail.",
|
||||
"subtitle": "Gdzie dostarczane są przypomnienia i powiadomienia."
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -5291,7 +5305,12 @@
|
||||
"subtitle": "Destrukcyjne operacje obejmujące cały system."
|
||||
},
|
||||
"about": {
|
||||
"title": "O programie"
|
||||
"title": "O programie",
|
||||
"subtitle": "Wersja, licencja i stan aktualizacji."
|
||||
},
|
||||
"module-availability": {
|
||||
"title": "Dostępność modułów",
|
||||
"subtitle": "Włącz lub wyłącz moduły dla wszystkich kont na tym serwerze."
|
||||
}
|
||||
},
|
||||
"carrier": "Operator",
|
||||
@@ -6588,18 +6607,6 @@
|
||||
"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ść."
|
||||
|
||||
@@ -94,18 +94,32 @@ export function AdminSectionRenderer({
|
||||
// v1.18.1 — the former ai-quality, assistant, and coach-feedback
|
||||
// sections fold into one Coach area: surface toggles + operator
|
||||
// config first, then the two feedback-quality tables.
|
||||
// v1.18.6 (W9) — server-wide module availability moved out to its own
|
||||
// `module-availability` section: it gates EVERY module, not just the
|
||||
// coach, so it no longer belongs stacked under Coach.
|
||||
return (
|
||||
<SectionFrame
|
||||
title={t("admin.section.coach.title")}
|
||||
subtitle={t("admin.section.coach.subtitle")}
|
||||
>
|
||||
<AssistantSection />
|
||||
<ModuleAvailabilitySection />
|
||||
<AiServerKeySection />
|
||||
<CoachFeedbackSection />
|
||||
<AiQualitySection />
|
||||
</SectionFrame>
|
||||
);
|
||||
case "module-availability":
|
||||
// v1.18.6 (W9) — operator-side server-wide module on/off, its own
|
||||
// admin section (was stacked under Coach). Reconciled name: the
|
||||
// user-facing "Module" settings pick from what is available here.
|
||||
return (
|
||||
<SectionFrame
|
||||
title={t("admin.section.module-availability.title")}
|
||||
subtitle={t("admin.section.module-availability.subtitle")}
|
||||
>
|
||||
<ModuleAvailabilitySection />
|
||||
</SectionFrame>
|
||||
);
|
||||
case "feedback":
|
||||
return (
|
||||
<SectionFrame
|
||||
@@ -191,12 +205,18 @@ export function AdminSectionRenderer({
|
||||
</SectionFrame>
|
||||
);
|
||||
case "about":
|
||||
// v1.4.36 W4e — About section reused as-is from the settings
|
||||
// surface. The component owns its own heading + cards layout
|
||||
// so no SectionFrame wrapper.
|
||||
// v1.18.6 — the About surface no longer carries the tour-replay card
|
||||
// (consolidated onto Settings → Advanced), so there's nothing to hide.
|
||||
return <AboutSection />;
|
||||
// v1.4.36 W4e — About section reused as-is from the settings surface.
|
||||
// v1.18.6 (W9) — route it through `SectionFrame` like every other admin
|
||||
// section so it gets the same visible heading + subtitle (the one admin
|
||||
// page that was bypassing the frame and showing no heading).
|
||||
return (
|
||||
<SectionFrame
|
||||
title={t("admin.section.about.title")}
|
||||
subtitle={t("admin.section.about.subtitle")}
|
||||
>
|
||||
<AboutSection />
|
||||
</SectionFrame>
|
||||
);
|
||||
default:
|
||||
slug satisfies never;
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { JSX } from "react";
|
||||
import type { JSX, ReactNode } from "react";
|
||||
|
||||
import { AccountSection } from "@/components/settings/account-section";
|
||||
import { AboutSection } from "@/components/settings/about-section";
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
type SettingsSectionSlug,
|
||||
} from "@/components/settings/section-slugs";
|
||||
import { SettingsShell } from "@/components/settings/settings-shell";
|
||||
import { SettingsSectionFrame } from "@/components/settings/settings-section-frame";
|
||||
import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
/**
|
||||
* Dynamic settings section route. Each of the `SETTINGS_SECTION_SLUGS`
|
||||
@@ -71,6 +74,50 @@ const SECTION_COMPONENTS: Record<
|
||||
advanced: AdvancedSection,
|
||||
};
|
||||
|
||||
// v1.18.6 (W9) — the two Layout-hub child editors render a "← back to hub"
|
||||
// link above their heading. The shared `<SettingsSectionFrame>` exposes a
|
||||
// `topSlot` for exactly this; the link lives here so the section bodies stay
|
||||
// pure card content.
|
||||
const HUB_CHILD_SLUGS: ReadonlySet<SettingsSectionSlug> = new Set([
|
||||
"dashboard",
|
||||
"insights",
|
||||
]);
|
||||
|
||||
/** Optional content rendered above the canonical heading, per slug. */
|
||||
function frameTopSlot(slug: SettingsSectionSlug): ReactNode {
|
||||
if (HUB_CHILD_SLUGS.has(slug)) {
|
||||
return (
|
||||
<SettingsHubBackLink
|
||||
href="/settings/layout"
|
||||
labelKey="settings.sections.layout.backToHub"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Optional inline content rendered at the top-right of the heading row. */
|
||||
function frameHeadingAccessory(slug: SettingsSectionSlug): ReactNode {
|
||||
// v1.18.6 (W9) — the module guided-tour re-entry triggers used to live in
|
||||
// a `justify-end` header inside the section body; they move to the frame's
|
||||
// heading row so the heading itself is the spotlight anchor.
|
||||
if (slug === "gesundheitsakte") {
|
||||
return (
|
||||
<span data-tour-id="export-hero">
|
||||
<ModuleTourTrigger stopId="export" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (slug === "integrations") {
|
||||
return (
|
||||
<span data-tour-id="integrations-hero">
|
||||
<ModuleTourTrigger stopId="integrations" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
// Next.js 16 made route `params` an async Promise. We `await` it before use.
|
||||
params: Promise<{ section: string }>;
|
||||
@@ -91,8 +138,17 @@ export default async function SettingsSectionPage({ params }: PageProps) {
|
||||
// guards the slug, but a future slug added to `SETTINGS_SECTION_SLUGS`
|
||||
// without a wired component would otherwise crash silently — placeholder
|
||||
// surfaces the gap visually instead.
|
||||
// v1.18.6 (W9) — every section now renders inside the shared
|
||||
// `<SettingsSectionFrame>`, which emits the one visible heading + subtitle
|
||||
// each page must show (the section bodies no longer carry their own).
|
||||
const body = SectionComponent ? (
|
||||
<SectionComponent />
|
||||
<SettingsSectionFrame
|
||||
slug={section}
|
||||
topSlot={frameTopSlot(section)}
|
||||
headingAccessory={frameHeadingAccessory(section)}
|
||||
>
|
||||
<SectionComponent />
|
||||
</SettingsSectionFrame>
|
||||
) : (
|
||||
<SectionPlaceholder slug={section} />
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Bell,
|
||||
Blocks,
|
||||
Database,
|
||||
FileText,
|
||||
Info,
|
||||
@@ -39,6 +40,7 @@ import { cn } from "@/lib/utils";
|
||||
import { scrollBehaviorForUser } from "@/lib/motion";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SectionNavHeadingSpacer } from "@/components/settings/section-nav-heading-spacer";
|
||||
import { isAdminSectionSlug, type AdminSectionSlug } from "./section-slugs";
|
||||
|
||||
interface AdminSection {
|
||||
@@ -82,6 +84,13 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [
|
||||
titleKey: "admin.section.coach.title",
|
||||
icon: Sparkles,
|
||||
},
|
||||
// v1.18.6 (W9) — server-wide module availability split out of Coach into
|
||||
// its own entry, sitting next to it.
|
||||
{
|
||||
slug: "module-availability",
|
||||
titleKey: "admin.section.module-availability.title",
|
||||
icon: Blocks,
|
||||
},
|
||||
{
|
||||
slug: "feedback",
|
||||
titleKey: "admin.section.feedback.title",
|
||||
@@ -276,6 +285,9 @@ export function AdminShell({ active, children }: AdminShellProps) {
|
||||
className="hidden md:block"
|
||||
>
|
||||
<div className="sticky top-20">
|
||||
{/* v1.18.6 (W9) — align the first nav item with the top of the
|
||||
first card, matching the settings shell. */}
|
||||
<SectionNavHeadingSpacer />
|
||||
<ul className="space-y-1">
|
||||
<li>
|
||||
<Link
|
||||
|
||||
@@ -16,11 +16,15 @@ export const ADMIN_SECTION_SLUGS = [
|
||||
"integrations",
|
||||
// v1.18.1 — the former `ai-quality`, `assistant`, and `coach-feedback`
|
||||
// sections collapse into one "Coach" area. It holds the assistant
|
||||
// surface toggles, the server-key + module-availability operator
|
||||
// config, and both feedback-quality tables (Coach + Insights). Three
|
||||
// navigation entries for one coherent coach-quality concern read as
|
||||
// clutter; one card opens the full picture.
|
||||
// surface toggles, the server-key operator config, and both
|
||||
// feedback-quality tables (Coach + Insights). Three navigation entries
|
||||
// for one coherent coach-quality concern read as clutter; one card opens
|
||||
// the full picture.
|
||||
"coach",
|
||||
// v1.18.6 (W9) — server-wide module availability is its own section now
|
||||
// (was stacked under Coach). It gates every module, not just the coach,
|
||||
// so it reads better next to Coach than buried inside it.
|
||||
"module-availability",
|
||||
"feedback",
|
||||
"reminders",
|
||||
"users",
|
||||
|
||||
@@ -31,12 +31,17 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { CoachSection } from "../coach-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en") {
|
||||
// v1.18.6 (W9) — the visible heading lives in the shared frame the route
|
||||
// wraps the section in; render through it so the heading id is present.
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<CoachSection />
|
||||
<SettingsSectionFrame slug="coach">
|
||||
<CoachSection />
|
||||
</SettingsSectionFrame>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
@@ -51,6 +56,9 @@ describe("<CoachSection> — SSR smoke", () => {
|
||||
// v1.18.1 (D7) — polarity flipped to activate/default-on.
|
||||
expect(html).toContain("Activate Coach");
|
||||
expect(html).toContain("Show the Coach button and drawer. On by default");
|
||||
// v1.18.6 (W9) — the proactive Coach nudge card moved here from
|
||||
// Benachrichtigungen; its anchor renders under the enabled-coach gate.
|
||||
expect(html).toContain('id="coach-nudge"');
|
||||
// Raw key never leaks past i18n.
|
||||
expect(html).not.toContain("settings.sections.coach.");
|
||||
expect(html).not.toContain("settings.coach.activate.");
|
||||
|
||||
@@ -44,6 +44,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { ExportSection } from "../export-section";
|
||||
|
||||
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
@@ -54,7 +55,13 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
|
||||
describe("<ExportSection> — SSR smoke", () => {
|
||||
it("renders the section heading + description", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// v1.18.6 (W9) — the page heading comes from the shared frame the route
|
||||
// wraps the section in.
|
||||
const html = render(
|
||||
<SettingsSectionFrame slug="export">
|
||||
<ExportSection />
|
||||
</SettingsSectionFrame>,
|
||||
);
|
||||
// v1.15.7 — the section was relabelled "Export & Import" (issue #281).
|
||||
expect(html).toContain("Export & Import");
|
||||
// Raw key never leaks past i18n.
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { GesundheitsakteSection } from "../gesundheitsakte-section";
|
||||
|
||||
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
@@ -38,8 +39,14 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
}
|
||||
|
||||
describe("<GesundheitsakteSection> — SSR smoke", () => {
|
||||
it("renders the section description and the health-record panel", () => {
|
||||
const html = render(<GesundheitsakteSection />);
|
||||
it("renders the section heading and the health-record panel", () => {
|
||||
// v1.18.6 (W9) — the visible heading (with the historic id) comes from
|
||||
// the shared frame the route wraps the section in.
|
||||
const html = render(
|
||||
<SettingsSectionFrame slug="gesundheitsakte">
|
||||
<GesundheitsakteSection />
|
||||
</SettingsSectionFrame>,
|
||||
);
|
||||
expect(html).toContain('id="settings-section-gesundheitsakte-title"');
|
||||
expect(html).toContain('data-testid="health-record-export-panel"');
|
||||
// Raw key never leaks past i18n.
|
||||
|
||||
@@ -65,12 +65,17 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { MedicationsSection } from "../medications-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en"): string {
|
||||
// v1.18.6 (W9) — the visible heading (with the historic id) comes from the
|
||||
// shared frame the route wraps the section in.
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<MedicationsSection />
|
||||
<SettingsSectionFrame slug="medications">
|
||||
<MedicationsSection />
|
||||
</SettingsSectionFrame>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { AuthUser } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
MODULE_KEYS,
|
||||
CORE_DOMAIN_KEYS,
|
||||
moduleDelegatesTo,
|
||||
} from "@/lib/modules/registry";
|
||||
import { MODULE_KEYS, moduleDelegatesTo } from "@/lib/modules/registry";
|
||||
|
||||
// Capture the latest useMutation config + a shared mutate spy so we can
|
||||
// invoke `mutationFn` / `onSuccess` by hand (the SSR pass can't fire a
|
||||
@@ -46,9 +42,9 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const authSpy = vi.fn<() => { user: AuthUser | null; isAuthenticated: boolean }>(
|
||||
() => ({ user: buildUser({}), isAuthenticated: true }),
|
||||
);
|
||||
const authSpy = vi.fn<
|
||||
() => { user: AuthUser | null; isAuthenticated: boolean }
|
||||
>(() => ({ user: buildUser({}), isAuthenticated: true }));
|
||||
vi.mock("@/hooks/use-auth", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@/hooks/use-auth")>(
|
||||
@@ -109,7 +105,10 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("<ModulesSection>", () => {
|
||||
it("renders a Switch for every non-delegated toggleable module, a managed deep-link for delegated ones, and locked switches for core", () => {
|
||||
it("renders a Switch for every non-delegated toggleable module and a managed deep-link for delegated ones", () => {
|
||||
// v1.18.6 (W9) — the "Always on" core-domains card was removed (a domain
|
||||
// that can't be turned off doesn't need listing), so this section now
|
||||
// only renders the toggleable modules.
|
||||
const html = render();
|
||||
for (const key of MODULE_KEYS) {
|
||||
if (moduleDelegatesTo(key) !== undefined) {
|
||||
@@ -126,8 +125,16 @@ describe("<ModulesSection>", () => {
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const key of CORE_DOMAIN_KEYS) {
|
||||
expect(html, `core switch ${key}`).toContain(`id="module-toggle-${key}"`);
|
||||
});
|
||||
|
||||
it("no longer renders the 'Always on' core-domains card", () => {
|
||||
// v1.18.6 (W9) — the read-only core card (weight / BP / pulse) was
|
||||
// removed; the core domains are always active regardless.
|
||||
const html = render();
|
||||
for (const key of ["weight", "bloodPressure", "pulse"]) {
|
||||
expect(html, `core ${key} not rendered`).not.toContain(
|
||||
`id="module-toggle-${key}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,18 +159,6 @@ describe("<ModulesSection>", () => {
|
||||
expect(moodTag).toMatch(/data-state="checked"/);
|
||||
});
|
||||
|
||||
it("renders the core domains locked (checked + disabled)", () => {
|
||||
const html = render();
|
||||
for (const key of CORE_DOMAIN_KEYS) {
|
||||
const tag = html.match(
|
||||
new RegExp(`<button[^>]*id="module-toggle-${key}"[^>]*>`),
|
||||
)?.[0];
|
||||
expect(tag, `core ${key}`).toBeDefined();
|
||||
expect(tag).toMatch(/data-state="checked"/);
|
||||
expect(tag).toMatch(/disabled/);
|
||||
}
|
||||
});
|
||||
|
||||
it("PATCHes only the flipped key as a disabled-allowlist entry", async () => {
|
||||
render();
|
||||
expect(lastMutation?.mutationFn).toBeTypeOf("function");
|
||||
|
||||
@@ -131,12 +131,17 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import { MoodSection } from "../mood-section";
|
||||
|
||||
function render(locale: "en" | "de" = "en"): string {
|
||||
// v1.18.6 (W9) — the visible heading (with the historic id) comes from the
|
||||
// shared frame the route wraps the section in.
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<MoodSection />
|
||||
<SettingsSectionFrame slug="mood">
|
||||
<MoodSection />
|
||||
</SettingsSectionFrame>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
* v1.18.0 (S4) — module-gated per-type visibility on the consolidated
|
||||
* "Benachrichtigungen" reminder-types home.
|
||||
*
|
||||
* The mood reminder shows only when the `mood` module is enabled; the Coach
|
||||
* nudge only when `coach` is enabled. Low-stock maps to the medications CORE
|
||||
* domain (always shown). The gate fails OPEN, so a `/me` payload without a
|
||||
* module map keeps every card visible.
|
||||
* The mood reminder shows only when the `mood` module is enabled. Low-stock
|
||||
* maps to the medications domain (shown unless explicitly disabled). The gate
|
||||
* fails OPEN, so a `/me` payload without a module map keeps every card
|
||||
* visible.
|
||||
*
|
||||
* v1.18.6 (W9) — the proactive Coach nudge card moved to Settings → Coach, so
|
||||
* it no longer appears on this screen.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
@@ -47,30 +50,23 @@ function render(modules?: Record<string, boolean>) {
|
||||
}
|
||||
|
||||
describe("<NotificationsSection> module-gated reminder types", () => {
|
||||
it("shows mood + coach cards when no module map (fails open)", () => {
|
||||
it("shows mood + low-stock cards when no module map (fails open); no coach nudge", () => {
|
||||
const html = render(undefined);
|
||||
expect(html).toContain('id="mood-reminder"');
|
||||
expect(html).toContain('id="coach-nudge"');
|
||||
expect(html).toContain('id="low-stock"');
|
||||
// v1.18.6 (W9) — the coach nudge moved to Settings → Coach.
|
||||
expect(html).not.toContain('id="coach-nudge"');
|
||||
});
|
||||
|
||||
it("hides the mood card when the mood module is disabled", () => {
|
||||
const html = render({ mood: false });
|
||||
expect(html).not.toContain('id="mood-reminder"');
|
||||
// Coach + low-stock are unaffected.
|
||||
expect(html).toContain('id="coach-nudge"');
|
||||
// Low-stock is unaffected.
|
||||
expect(html).toContain('id="low-stock"');
|
||||
});
|
||||
|
||||
it("hides the coach card when the coach module is disabled", () => {
|
||||
const html = render({ coach: false });
|
||||
expect(html).not.toContain('id="coach-nudge"');
|
||||
expect(html).toContain('id="mood-reminder"');
|
||||
expect(html).toContain('id="low-stock"');
|
||||
});
|
||||
|
||||
it("always shows low-stock (medications is a CORE domain)", () => {
|
||||
const html = render({ mood: false, coach: false });
|
||||
it("always shows low-stock unless medications is disabled", () => {
|
||||
const html = render({ mood: false });
|
||||
expect(html).toContain('id="low-stock"');
|
||||
expect(html).not.toContain('id="mood-reminder"');
|
||||
expect(html).not.toContain('id="coach-nudge"');
|
||||
@@ -82,9 +78,9 @@ describe("<NotificationsSection> module-gated reminder types", () => {
|
||||
const html = render({});
|
||||
expect(html).not.toContain('data-slot="notifications-channels-cross-link"');
|
||||
expect(html).not.toContain('data-slot="notifications-inbox-cross-link"');
|
||||
// The three reminder-type cards remain.
|
||||
// The remaining reminder-type cards (no coach nudge here anymore).
|
||||
expect(html).toContain('id="mood-reminder"');
|
||||
expect(html).toContain('id="low-stock"');
|
||||
expect(html).toContain('id="coach-nudge"');
|
||||
expect(html).not.toContain('id="coach-nudge"');
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,11 @@
|
||||
* v1.4.16 phase B6 — settings i18n parity.
|
||||
*
|
||||
* Every slug declared in `SETTINGS_SECTION_SLUGS` MUST have a
|
||||
* `settings.sections.<slug>.title` AND a `settings.sections.<slug>.description`
|
||||
* resolved in both English and German. The audit doc
|
||||
* (`docs/audit/v1416-settings-audit.md`) calls this out as the canonical
|
||||
* shape — the sidebar, the page `<h1>`, and the page subtitle all read
|
||||
* from these two keys, so a missing key paints a raw key in production.
|
||||
* `settings.sections.<slug>.title` AND a `settings.sections.<slug>.subtitle`
|
||||
* resolved in both English and German. The sidebar reads `.title`; the shared
|
||||
* `<SettingsSectionFrame>` reads `.title` + `.subtitle` for the visible page
|
||||
* heading + subheading (v1.18.6 W9), so a missing key paints a raw key in
|
||||
* production.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -46,43 +46,39 @@ describe("settings sections — i18n parity (B6)", () => {
|
||||
expect(deValue?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it(`every locale resolves settings.sections.${slug}.description`, () => {
|
||||
it(`every locale resolves settings.sections.${slug}.subtitle`, () => {
|
||||
const enValue = lookup(en as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
"subtitle",
|
||||
]);
|
||||
const deValue = lookup(de as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
"subtitle",
|
||||
]);
|
||||
expect(enValue, `EN missing description for ${slug}`).toBeTypeOf(
|
||||
"string",
|
||||
);
|
||||
expect(enValue, `EN missing subtitle for ${slug}`).toBeTypeOf("string");
|
||||
expect(enValue?.length ?? 0).toBeGreaterThan(0);
|
||||
expect(deValue, `DE missing description for ${slug}`).toBeTypeOf(
|
||||
"string",
|
||||
);
|
||||
expect(deValue, `DE missing subtitle for ${slug}`).toBeTypeOf("string");
|
||||
expect(deValue?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
it("EN and DE description for every slug are different (catches accidental copy-paste)", () => {
|
||||
it("EN and DE subtitle for every slug are different (catches accidental copy-paste)", () => {
|
||||
for (const slug of SETTINGS_SECTION_SLUGS) {
|
||||
const enValue = lookup(en as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
"subtitle",
|
||||
]);
|
||||
const deValue = lookup(de as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
"subtitle",
|
||||
]);
|
||||
// It's legitimate for some titles to coincide between locales when
|
||||
// the proper noun is the same (e.g. "Dashboard", "API & Tokens").
|
||||
@@ -90,7 +86,7 @@ describe("settings sections — i18n parity (B6)", () => {
|
||||
// EN value into the DE bag is a bug.
|
||||
expect(
|
||||
deValue,
|
||||
`DE description for ${slug} is identical to EN — needs translation`,
|
||||
`DE subtitle for ${slug} is identical to EN — needs translation`,
|
||||
).not.toBe(enValue);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,6 +87,8 @@ vi.mock("@/components/settings/thresholds-editor-section", () => ({
|
||||
ThresholdsEditorSection: () => <div data-testid="thresholds-editor" />,
|
||||
}));
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { SettingsSectionFrame } from "../settings-section-frame";
|
||||
import type { SettingsSectionSlug } from "../section-slugs";
|
||||
import { AboutSection } from "../about-section";
|
||||
import { AccountSection } from "../account-section";
|
||||
import { AdvancedSection } from "../advanced-section";
|
||||
@@ -108,10 +110,25 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
);
|
||||
}
|
||||
|
||||
// v1.18.6 (W9) — the visible page heading + subtitle moved out of the section
|
||||
// bodies into the shared `<SettingsSectionFrame>` that the route wraps every
|
||||
// section in. The heading-level smoke checks render the body THROUGH the frame
|
||||
// (mirroring production) so they assert what the user actually sees.
|
||||
function renderFramed(
|
||||
slug: SettingsSectionSlug,
|
||||
node: React.ReactElement,
|
||||
locale: "en" | "de" = "en",
|
||||
) {
|
||||
return render(
|
||||
<SettingsSectionFrame slug={slug}>{node}</SettingsSectionFrame>,
|
||||
locale,
|
||||
);
|
||||
}
|
||||
|
||||
describe("settings sections — SSR smoke", () => {
|
||||
it("<AccountSection> renders the Account heading and Profile card", () => {
|
||||
const html = render(<AccountSection />);
|
||||
// Section title resolves via the i18n provider.
|
||||
const html = renderFramed("account", <AccountSection />);
|
||||
// Section title resolves via the frame + i18n provider.
|
||||
expect(html).toContain("Account");
|
||||
// Profile card heading is also painted.
|
||||
expect(html).toContain("Profile");
|
||||
@@ -136,7 +153,7 @@ describe("settings sections — SSR smoke", () => {
|
||||
});
|
||||
|
||||
it("<AboutSection> renders version + license + sources/docs cards (no Updates panel)", () => {
|
||||
const html = render(<AboutSection />);
|
||||
const html = renderFramed("about", <AboutSection />);
|
||||
expect(html).toContain("About");
|
||||
// v1.4.36 W4f — the dedicated "Updates" card with its manual
|
||||
// "Check now" button is gone. The 24 h auto-check still runs and
|
||||
@@ -158,34 +175,33 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).not.toContain("Replay the tour");
|
||||
});
|
||||
|
||||
it("<AiSection> renders the AI Insights heading", () => {
|
||||
const html = render(<AiSection />);
|
||||
// v1.8.7.1 — section renamed to "AI Insights" so the AI nature of the
|
||||
// surface is explicit (was the bare "Insights"). The section H1 carries
|
||||
// the only heading.
|
||||
expect(html).toContain("AI Insights");
|
||||
it("<AiSection> renders the AI provider heading", () => {
|
||||
const html = renderFramed("ai", <AiSection />);
|
||||
// v1.18.6 (W9) — section renamed to "AI provider"; the visible heading
|
||||
// now comes from the shared frame, which keeps the historic id.
|
||||
expect(html).toContain("AI provider");
|
||||
expect(html).toContain("settings-section-ai-title");
|
||||
});
|
||||
|
||||
it("<IntegrationsSection> renders Withings card title", () => {
|
||||
const html = render(<IntegrationsSection />);
|
||||
const html = renderFramed("integrations", <IntegrationsSection />);
|
||||
expect(html).toContain("Integrations");
|
||||
expect(html).toContain("Withings");
|
||||
});
|
||||
|
||||
it("<NotificationsSection> renders the single reminder-types home", () => {
|
||||
const html = render(<NotificationsSection />);
|
||||
// v1.18.1 (D0/D5) — the screen is lean: sr-only h1 only, no section blurb,
|
||||
// no channels/inbox cross-links, and no embedded Vorsorge editor.
|
||||
const html = renderFramed("notifications", <NotificationsSection />);
|
||||
// v1.18.6 (W9) — the visible heading comes from the frame; the proactive
|
||||
// Coach nudge moved to Settings → Coach, so it no longer renders here.
|
||||
expect(html).toContain("settings-section-notifications-title");
|
||||
expect(html).not.toContain("Notification channels");
|
||||
expect(html).not.toContain('data-slot="notifications-channels-cross-link"');
|
||||
expect(html).not.toContain('data-slot="notifications-inbox-cross-link"');
|
||||
// The mood + coach cards fail OPEN (mock user carries no module map),
|
||||
// so all three reminder-type cards render.
|
||||
// The mood card fails OPEN (mock user carries no module map), so both
|
||||
// remaining reminder-type cards render. The coach-nudge card is gone.
|
||||
expect(html).toContain('id="mood-reminder"');
|
||||
expect(html).toContain('id="coach-nudge"');
|
||||
expect(html).toContain('id="low-stock"');
|
||||
expect(html).not.toContain('id="coach-nudge"');
|
||||
// No raw i18n key leaks past the provider.
|
||||
expect(html).not.toContain("settings.sections.notifications.");
|
||||
});
|
||||
@@ -194,32 +210,31 @@ describe("settings sections — SSR smoke", () => {
|
||||
// v1.4.3 split: the per-metric thresholds (Persönliche Zielwerte) moved
|
||||
// out into their own settings section under `/settings/thresholds`,
|
||||
// so the dashboard panel is now layout-only. Verifying the heading
|
||||
// says "Dashboard" (not "Übersicht" — the v1.4.2 placeholder) and the
|
||||
// layout slot still mounts.
|
||||
const html = render(<DashboardSection />);
|
||||
// (supplied by the frame) says "Dashboard" and the layout slot mounts.
|
||||
const html = renderFramed("dashboard", <DashboardSection />);
|
||||
expect(html).toContain("Dashboard");
|
||||
expect(html).toContain('data-testid="dashboard-layout"');
|
||||
expect(html).not.toContain('data-testid="thresholds"');
|
||||
});
|
||||
|
||||
it("<ApiSection> renders endpoints + tokens cards", () => {
|
||||
const html = render(<ApiSection />);
|
||||
const html = renderFramed("api", <ApiSection />);
|
||||
expect(html).toContain("API & Tokens");
|
||||
});
|
||||
|
||||
it("<AdvancedSection> renders the danger-zone card only (export moved to /settings/export)", () => {
|
||||
const html = render(<AdvancedSection />);
|
||||
const html = renderFramed("advanced", <AdvancedSection />);
|
||||
// v1.4.16 phase B7: every export path moved out into the dedicated
|
||||
// `<ExportSection>` so what remains is the irreversible delete-all
|
||||
// path. The section heading + dangerZone surface are the only
|
||||
// primary surfaces here now.
|
||||
// path. The heading (supplied by the frame) + dangerZone surface are the
|
||||
// only primary surfaces here now.
|
||||
expect(html).toContain("Advanced");
|
||||
expect(html).not.toContain("settings.export");
|
||||
expect(html).not.toContain("doctorReport");
|
||||
});
|
||||
|
||||
it("<AboutSection> resolves about.* keys in German", () => {
|
||||
const html = render(<AboutSection />, "de");
|
||||
const html = renderFramed("about", <AboutSection />, "de");
|
||||
// v1.4.36 W4f — "Updates" panel + "Jetzt prüfen" button removed;
|
||||
// the surviving German copy is the section title + the Sources
|
||||
// & docs heading + the version line.
|
||||
@@ -235,7 +250,7 @@ describe("settings sections — SSR smoke", () => {
|
||||
// `<ThresholdsEditorSection>` (was `<ThresholdsSection>`).
|
||||
// v1.8.7.1 — Sources split back onto its own page, so this wrapper
|
||||
// renders the target-range editor only.
|
||||
const html = render(<ThresholdsSection />);
|
||||
const html = renderFramed("thresholds", <ThresholdsSection />);
|
||||
// v1.8.7.1: section renamed to "Targets".
|
||||
expect(html).toContain("Targets");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
@@ -244,7 +259,7 @@ describe("settings sections — SSR smoke", () => {
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> resolves the German title", () => {
|
||||
const html = render(<ThresholdsSection />, "de");
|
||||
const html = renderFramed("thresholds", <ThresholdsSection />, "de");
|
||||
// v1.8.7.1 — German title is "Zielwerte" (Targets only).
|
||||
expect(html).toContain("Zielwerte");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
|
||||
@@ -319,9 +319,9 @@ describe("<SettingsShell>", () => {
|
||||
expect(html).toContain('href="/settings/mood"');
|
||||
// Targets keeps its German nav entry ("Zielwerte").
|
||||
expect(html).toContain("Zielwerte");
|
||||
// v1.8.7.1 — the AI Insights section is named "KI-Auswertungen" in
|
||||
// German (the "KI" prefix makes the AI nature explicit).
|
||||
expect(html).toContain("KI-Auswertungen");
|
||||
// v1.18.6 (W9) — the AI section is named "KI-Anbieter" in German: the
|
||||
// page is about the provider / BYOK, not an "Auswertung".
|
||||
expect(html).toContain("KI-Anbieter");
|
||||
// API & Tokens is identical in both locales (proper noun + ampersand)
|
||||
expect(html).toContain("API & Tokens");
|
||||
expect(html).toContain("Erweitert");
|
||||
|
||||
@@ -181,17 +181,12 @@ export function AboutSection() {
|
||||
}, [version?.version]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the section
|
||||
// frame that wraps this component: `<SettingsSectionFrame>` on the settings
|
||||
// route and the admin renderer's `SectionFrame` on `/admin/about`. The body
|
||||
// is the version / license / sources cards.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-about-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-about-title" className="sr-only">
|
||||
{t("settings.sections.about.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Identity card — version + license inline. The v1.4.2 layout
|
||||
stacked the license under the version inside its own boxed Badge,
|
||||
which read as a separate field even though the two values belong
|
||||
@@ -321,6 +316,6 @@ export function AboutSection() {
|
||||
button were collectively never the point of the surface —
|
||||
the only signal users ever cared about was "is a newer
|
||||
release out?", and the badge answers that with no clicks. */}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -367,20 +367,10 @@ export function AccountSection() {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the profile cards.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-account-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — the explanatory section blurb was dropped so every
|
||||
settings section starts at the same vertical height; the sr-only
|
||||
heading stays for the accessibility tree. */}
|
||||
<header>
|
||||
<h1 id="settings-section-account-title" className="sr-only">
|
||||
{t("settings.sections.account.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Profile card */}
|
||||
{/* Profile photo card */}
|
||||
<AvatarSection />
|
||||
@@ -760,7 +750,7 @@ export function AccountSection() {
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,26 +51,16 @@ import { apiFetchRaw } from "@/lib/api/api-fetch";
|
||||
* dialog.
|
||||
*/
|
||||
export function AdvancedSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the advanced cards.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-advanced-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<h1 id="settings-section-advanced-title" className="sr-only">
|
||||
{t("settings.sections.advanced.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* v1.18.1 (D1) — "Tour neu starten" relocated here from Account. */}
|
||||
<TourReplayCard />
|
||||
<ResearchModeCard />
|
||||
<DataResetCard />
|
||||
<AccountDeleteCard />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
import { AiInsightsCard } from "@/components/settings/ai/ai-insights-card";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMounted } from "@/hooks/use-mounted";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export function AiSection() {
|
||||
const { t } = useTranslations();
|
||||
const { isAuthenticated } = useAuth();
|
||||
// v1.16.6 — the auth query can resolve before this boundary
|
||||
// hydrates; every `disabled={!isAuthenticated}` binding in the child
|
||||
@@ -35,19 +33,8 @@ export function AiSection() {
|
||||
const mounted = useMounted();
|
||||
const authed = mounted && isAuthenticated;
|
||||
|
||||
return (
|
||||
<section aria-labelledby="settings-section-ai-title" className="space-y-6">
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<h1 id="settings-section-ai-title" className="sr-only">
|
||||
{t("settings.sections.ai.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<AiInsightsCard isAuthenticated={authed} />
|
||||
|
||||
{/* v1.18.1 (D8) — the "About me" context moved to Settings → Account
|
||||
(under Profil, before Zyklus-Tracking). */}
|
||||
</section>
|
||||
);
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the single provider
|
||||
// card. (The "About me" context moved to Settings → Account in v1.18.1 D8.)
|
||||
return <AiInsightsCard isAuthenticated={authed} />;
|
||||
}
|
||||
@@ -37,19 +37,13 @@ interface ApiTokenInfo {
|
||||
}
|
||||
|
||||
export function ApiSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the API cards.
|
||||
return (
|
||||
<section aria-labelledby="settings-section-api-title" className="space-y-6">
|
||||
<header>
|
||||
<h1 id="settings-section-api-title" className="sr-only">
|
||||
{t("settings.sections.api.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
<ApiEndpointsCard />
|
||||
<ApiTokensCard />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,30 +8,13 @@
|
||||
* delivery provider — the same family as a connected device — and now reads as
|
||||
* a first-class settings home rather than a tab hidden behind Integrationen.
|
||||
*
|
||||
* The content is the existing `<NotificationChannelsPanel>`; this wrapper only
|
||||
* owns the section frame (sr-only heading scaffold matching every other
|
||||
* settings section).
|
||||
* The content is the existing `<NotificationChannelsPanel>`. v1.18.6 (W9) —
|
||||
* the visible heading + subtitle now come from the shared
|
||||
* `<SettingsSectionFrame>` in the route; this wrapper is the panel only.
|
||||
*/
|
||||
|
||||
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export function ChannelsSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-channels-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<h1 id="settings-section-channels-title" className="sr-only">
|
||||
{t("settings.sections.channels.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<NotificationChannelsPanel />
|
||||
</section>
|
||||
);
|
||||
return <NotificationChannelsPanel />;
|
||||
}
|
||||
@@ -10,25 +10,31 @@
|
||||
* 1. Disable Coach — the per-user opt-out (kept first so a user who
|
||||
* wants the surface gone finds it immediately).
|
||||
* 2. Coach preferences — tone, verbosity, data scope (`useCoachPrefs`).
|
||||
* 3. Coach memory — durable-fact review + forget controls.
|
||||
* 3. Proactive nudge — the daily-nudge opt-out + per-group frequency
|
||||
* (moved here from Settings → Benachrichtigungen in
|
||||
* v1.18.6: it is a Coach setting, not a generic
|
||||
* notification).
|
||||
* 4. Coach memory — durable-fact review + forget controls.
|
||||
*
|
||||
* The nav entry is module-gated on `coach`. The preference + memory cards
|
||||
* keep their own content gate on `!user.disableCoach` (mirrored from the
|
||||
* The nav entry is module-gated on `coach`. The preference + nudge + memory
|
||||
* cards keep their own content gate on `!user.disableCoach` (mirrored from the
|
||||
* old AI section): hiding the Coach hides its tuning + memory controls too.
|
||||
*
|
||||
* The "about me" context lives in the AI section, not here — the daily
|
||||
* briefing reads it too, so it is not a Coach-only setting.
|
||||
*
|
||||
* v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
* `<SettingsSectionFrame>` in the route; this body is the Coach cards only.
|
||||
*/
|
||||
|
||||
import { DisableCoachCard } from "@/components/settings/ai/disable-coach-card";
|
||||
import { CoachMemorySection } from "@/components/settings/coach-memory-section";
|
||||
import { CoachNudgeCard } from "@/components/settings/coach-nudge-card";
|
||||
import { CoachPrefsSection } from "@/components/settings/coach-prefs-section";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMounted } from "@/hooks/use-mounted";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export function CoachSection() {
|
||||
const { t } = useTranslations();
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
// Mirror the AI section's hydration-safe gating: the auth query can
|
||||
// resolve before this boundary hydrates, so gate the child props on
|
||||
@@ -42,22 +48,20 @@ export function CoachSection() {
|
||||
const coachEnabled = !mounted || !user?.disableCoach;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-coach-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<h1 id="settings-section-coach-title" className="sr-only">
|
||||
{t("settings.sections.coach.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
<DisableCoachCard isAuthenticated={authed} />
|
||||
|
||||
{coachEnabled && <CoachPrefsSection isAuthenticated={authed} />}
|
||||
|
||||
{/* Proactive Coach nudge — moved here from Benachrichtigungen (v1.18.6).
|
||||
Keeps the `#coach-nudge` anchor so existing deep links resolve. */}
|
||||
{coachEnabled && (
|
||||
<div id="coach-nudge" className="scroll-mt-28">
|
||||
<CoachNudgeCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{coachEnabled && <CoachMemorySection isAuthenticated={authed} />}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { DashboardLayoutSection } from "@/components/settings/dashboard-layout-section";
|
||||
import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
* `<SettingsSectionFrame>`, and the "← back to hub" link rides the frame's
|
||||
* `topSlot`. The body is the layout customizer only.
|
||||
*/
|
||||
export function DashboardSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-dashboard-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header className="space-y-2">
|
||||
<SettingsHubBackLink
|
||||
href="/settings/layout"
|
||||
labelKey="settings.sections.layout.backToHub"
|
||||
/>
|
||||
<h1 id="settings-section-dashboard-title" className="sr-only">
|
||||
{t("settings.sections.dashboard.title")}
|
||||
</h1>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
</header>
|
||||
|
||||
<DashboardLayoutSection id="dashboard-layout" />
|
||||
</section>
|
||||
);
|
||||
return <DashboardLayoutSection id="dashboard-layout" />;
|
||||
}
|
||||
@@ -70,17 +70,11 @@ async function downloadFromUrl(url: string, filename: string): Promise<void> {
|
||||
|
||||
export function ExportSection() {
|
||||
const { t } = useTranslations();
|
||||
// v1.18.6 (W9) — the visible page heading + subtitle now come from the
|
||||
// shared `<SettingsSectionFrame>` in the route; the inner `<h2>` keeps its
|
||||
// own "other options" subsection label.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-export-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-export-title" className="sr-only">
|
||||
{t("settings.sections.export.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* v1.18.0 (S5) — the full health-record export moved to its own
|
||||
top-level "Gesundheitsakte" section. This page keeps the generic
|
||||
CSV/JSON data-out paths and the import surface. */}
|
||||
@@ -111,7 +105,7 @@ export function ExportSection() {
|
||||
`export.zip` and the generic JSON paths. The export routes had
|
||||
backends with no UI until now. */}
|
||||
<ImportPanel />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,32 +18,10 @@
|
||||
*/
|
||||
|
||||
import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export function GesundheitsakteSection() {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-gesundheitsakte-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header className="flex items-center justify-end">
|
||||
<h1
|
||||
id="settings-section-gesundheitsakte-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.gesundheitsakte.title")}
|
||||
</h1>
|
||||
{/* v1.18.6 — guided-tour re-entry for the doctor-report module + a
|
||||
real spotlight anchor (the section h1 is sr-only). */}
|
||||
<span data-tour-id="export-hero">
|
||||
<ModuleTourTrigger stopId="export" />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<HealthRecordExportPanel />
|
||||
</section>
|
||||
);
|
||||
// v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay
|
||||
// trigger now live in the shared `<SettingsSectionFrame>` in the route; the
|
||||
// body is the health-record export panel only.
|
||||
return <HealthRecordExportPanel />;
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { InsightsOverviewArrangeSection } from "@/components/settings/insights-overview-arrange-section";
|
||||
import { InsightsPillOrderSection } from "@/components/settings/insights-pill-order-section";
|
||||
import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.15.18 — the "Insights" settings section (analogous to the Dashboard
|
||||
@@ -15,28 +13,16 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
*
|
||||
* Both write through the same `/api/insights/layout` contract the tab strip
|
||||
* and the overview share, so a save here repaints both surfaces in lockstep.
|
||||
*
|
||||
* v1.18.6 (W9) — the visible "Insights" heading + subtitle now come from the
|
||||
* shared `<SettingsSectionFrame>`, and the "← back to hub" link rides the
|
||||
* frame's `topSlot`. The body is the two arrange cards only.
|
||||
*/
|
||||
export function InsightsSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-insights-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header className="space-y-2">
|
||||
<SettingsHubBackLink
|
||||
href="/settings/layout"
|
||||
labelKey="settings.sections.layout.backToHub"
|
||||
/>
|
||||
<h1 id="settings-section-insights-title" className="sr-only">
|
||||
{t("settings.sections.insights.title")}
|
||||
</h1>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
<InsightsOverviewArrangeSection id="insights-overview-arrange" />
|
||||
<InsightsPillOrderSection id="insights-pill-order" />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
*/
|
||||
|
||||
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export {
|
||||
parseOAuthOutcome,
|
||||
@@ -25,27 +23,8 @@ export {
|
||||
} from "@/components/settings/integrations/connections-panel";
|
||||
|
||||
export function IntegrationsSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-integrations-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header className="flex items-center justify-end">
|
||||
<h1 id="settings-section-integrations-title" className="sr-only">
|
||||
{t("settings.sections.integrations.title")}
|
||||
</h1>
|
||||
{/* v1.18.6 — guided-tour re-entry for the integrations module. The
|
||||
visible affordance also gives the spotlight a real anchor (the
|
||||
section h1 is sr-only). */}
|
||||
<span data-tour-id="integrations-hero">
|
||||
<ModuleTourTrigger stopId="integrations" />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ConnectionsPanel />
|
||||
</section>
|
||||
);
|
||||
// v1.18.6 (W9) — the visible heading + subtitle and the module tour-replay
|
||||
// trigger now live in the shared `<SettingsSectionFrame>` in the route; the
|
||||
// body is the connections panel only.
|
||||
return <ConnectionsPanel />;
|
||||
}
|
||||
@@ -53,47 +53,38 @@ const LAYOUT_LINKS: ReadonlyArray<LayoutLink> = [
|
||||
export function LayoutSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the hub link list.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-layout-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-layout-title" className="sr-only">
|
||||
{t("settings.sections.layout.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<ul className="space-y-3">
|
||||
{LAYOUT_LINKS.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"bg-card border-border hover:bg-accent/40 group flex items-center gap-4 rounded-xl border p-4 transition-colors sm:p-5",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="text-muted-foreground h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-sm font-semibold">{t(link.titleKey)}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(link.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className="text-muted-foreground/60 group-hover:text-foreground h-4 w-4 shrink-0 transition-colors"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
<ul className="space-y-3">
|
||||
{LAYOUT_LINKS.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className={cn(
|
||||
"bg-card border-border hover:bg-accent/40 group flex items-center gap-4 rounded-xl border p-4 transition-colors sm:p-5",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="text-muted-foreground h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<p className="text-sm font-semibold">{t(link.titleKey)}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t(link.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className="text-muted-foreground/60 group-hover:text-foreground h-4 w-4 shrink-0 transition-colors"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -73,17 +73,10 @@ export function MedicationsSection() {
|
||||
),
|
||||
].map((m) => ({ id: m.id, name: m.name, dose: m.dose, active: m.active }));
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the medication cards.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-medications-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-medications-title" className="sr-only">
|
||||
{t("settings.sections.medications.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* View preference — cards vs table. The shared header toggle
|
||||
writes optimistically, so there is no Save button here. */}
|
||||
<div
|
||||
@@ -130,6 +123,6 @@ export function MedicationsSection() {
|
||||
they moved here from the account profile so all medication-specific
|
||||
preferences live in one place. */}
|
||||
<InjectionSitesCard isAuthenticated={isAuthenticated} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,12 +32,9 @@ import {
|
||||
Droplet,
|
||||
Dumbbell,
|
||||
FileText,
|
||||
HeartPulse,
|
||||
Lock,
|
||||
MessageCircleHeart,
|
||||
Moon,
|
||||
Pill,
|
||||
Scale,
|
||||
Smile,
|
||||
Sparkles,
|
||||
TestTube,
|
||||
@@ -52,10 +49,8 @@ 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,
|
||||
moduleDelegatesTo,
|
||||
type CoreDomainKey,
|
||||
type ModuleKey,
|
||||
} from "@/lib/modules/registry";
|
||||
|
||||
@@ -77,13 +72,6 @@ const MODULE_ICONS: Record<ModuleKey, LucideIcon> = {
|
||||
doctorReport: FileText,
|
||||
};
|
||||
|
||||
/** Neutral Lucide glyph per core (always-on) domain. */
|
||||
const CORE_ICONS: Record<CoreDomainKey, LucideIcon> = {
|
||||
weight: Scale,
|
||||
bloodPressure: HeartPulse,
|
||||
pulse: Activity,
|
||||
};
|
||||
|
||||
export function ModulesSection() {
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
@@ -113,103 +101,65 @@ export function ModulesSection() {
|
||||
},
|
||||
});
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` (the former muted-`<p>` intro folded into the
|
||||
// frame's standard subtitle). The "Module immer aktiv" core-domains card
|
||||
// was removed: a domain that can't be turned off doesn't need to be listed.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-modules-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.6 — restore a short intro line so a new user understands
|
||||
that this page drives the nav, tiles and assessments they see.
|
||||
(Dropped in v1.18.1 D0 for top alignment; the orientation cost
|
||||
outweighed the alignment.) */}
|
||||
<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 leading-relaxed">
|
||||
{t("settings.sections.modules.description")}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCardHeader
|
||||
icon={Blocks}
|
||||
title={t("settings.sections.modules.toggleable.title")}
|
||||
className="mb-2"
|
||||
/>
|
||||
{/* v1.18.6 — restore the one-line "what this section does" hint. */}
|
||||
<p className="text-muted-foreground mb-3 pl-7 text-sm leading-relaxed">
|
||||
{t("settings.sections.modules.toggleable.description")}
|
||||
</p>
|
||||
<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;
|
||||
// Delegated modules (cycle/coach) are owned elsewhere — never a
|
||||
// live toggle here; deep-link to their real control instead.
|
||||
const delegated = moduleDelegatesTo(key) !== undefined;
|
||||
// Operator turned this module off server-wide: no per-user
|
||||
// toggle can re-enable it, so show a read-only note. Delegated
|
||||
// rows already deep-link, so this only matters for owned keys.
|
||||
const operatorDisabled =
|
||||
!delegated && moduleAvailability[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}
|
||||
managedAt={
|
||||
delegated && def.managedAt
|
||||
? {
|
||||
href: def.managedAt.href,
|
||||
label: t(def.managedAt.labelKey),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
manageLinkLabel={
|
||||
delegated && def.managedAt
|
||||
? t("settings.sections.modules.manageIn", {
|
||||
section: t(def.managedAt.labelKey),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
operatorDisabled={operatorDisabled}
|
||||
operatorNote={t("settings.sections.modules.operatorDisabled")}
|
||||
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) => (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCardHeader
|
||||
icon={Blocks}
|
||||
title={t("settings.sections.modules.toggleable.title")}
|
||||
className="mb-2"
|
||||
/>
|
||||
{/* v1.18.6 — restore the one-line "what this section does" hint. */}
|
||||
<p className="text-muted-foreground mb-3 pl-7 text-sm leading-relaxed">
|
||||
{t("settings.sections.modules.toggleable.description")}
|
||||
</p>
|
||||
<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;
|
||||
// Delegated modules (cycle/coach) are owned elsewhere — never a
|
||||
// live toggle here; deep-link to their real control instead.
|
||||
const delegated = moduleDelegatesTo(key) !== undefined;
|
||||
// Operator turned this module off server-wide: no per-user
|
||||
// toggle can re-enable it, so show a read-only note. Delegated
|
||||
// rows already deep-link, so this only matters for owned keys.
|
||||
const operatorDisabled =
|
||||
!delegated && moduleAvailability[key] === false;
|
||||
return (
|
||||
<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")}
|
||||
icon={MODULE_ICONS[key]}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
enabled={enabled}
|
||||
pending={toggle.isPending}
|
||||
managedAt={
|
||||
delegated && def.managedAt
|
||||
? {
|
||||
href: def.managedAt.href,
|
||||
label: t(def.managedAt.labelKey),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
manageLinkLabel={
|
||||
delegated && def.managedAt
|
||||
? t("settings.sections.modules.manageIn", {
|
||||
section: t(def.managedAt.labelKey),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
operatorDisabled={operatorDisabled}
|
||||
operatorNote={t("settings.sections.modules.operatorDisabled")}
|
||||
onToggle={(next) => toggle.mutate({ key, enabled: next })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,17 +40,10 @@ export function MoodSection() {
|
||||
</div>
|
||||
);
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` in the route; this body is the mood-tag cards.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-mood-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-mood-title" className="sr-only">
|
||||
{t("settings.sections.mood.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Groups — picker group order + own groups. */}
|
||||
<div
|
||||
id="mood-groups"
|
||||
@@ -87,6 +80,6 @@ export function MoodSection() {
|
||||
<ArchivedTagsCard catalog={catalog} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { CoachNudgeCard } from "@/components/settings/coach-nudge-card";
|
||||
import { LowStockCard } from "@/components/settings/low-stock-card";
|
||||
import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
|
||||
|
||||
@@ -17,44 +15,31 @@ import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
|
||||
*
|
||||
* Each reminder-type card is shown ONLY when its module is enabled, read from
|
||||
* the resolved `useAuth().user.modules` map (the same `/auth/me` map nav and
|
||||
* Insights pills gate off). Mood → `mood`, Coach nudge → `coach`, low-stock
|
||||
* runway → `medications` (a toggleable fail-open module since D3, so a missing
|
||||
* key reads as enabled and a stale `/me` payload never blanks a card).
|
||||
* Insights pills gate off). Mood → `mood`, low-stock runway → `medications`
|
||||
* (a toggleable fail-open module since D3, so a missing key reads as enabled
|
||||
* and a stale `/me` payload never blanks a card).
|
||||
*
|
||||
* v1.18.1 (D5) — the page is intentionally lean: the section blurb, the
|
||||
* channels / inbox cross-links, and the embedded Vorsorge (preventive-care)
|
||||
* editor were all removed. Vorsorge has its own `/vorsorge` page, so embedding
|
||||
* it here was a duplicate. Delivery CHANNELS (Telegram / ntfy / Web Push /
|
||||
* Webhook / Email) live under Settings → Integrations → Channels.
|
||||
*
|
||||
* v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
* `<SettingsSectionFrame>`. The proactive-Coach nudge card moved to Settings →
|
||||
* Coach (it is a Coach setting, not a generic notification), so this screen
|
||||
* carries the mood + low-stock reminder cards only.
|
||||
*/
|
||||
export function NotificationsSection() {
|
||||
const { t } = useTranslations();
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
|
||||
// v1.18.0 (S4) — module-gated per-type visibility. Fail OPEN (`!== false`)
|
||||
// so a stale `/me` payload without the module map keeps every card visible.
|
||||
const moodEnabled = user?.modules?.mood !== false;
|
||||
const coachEnabled = user?.modules?.coach !== false;
|
||||
const medsEnabled = user?.modules?.medications !== false;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-notifications-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0/D5) — the section blurb and the channels / inbox cross-
|
||||
links were dropped: the header now starts at the same height as every
|
||||
other section and the page carries only the reminder-type cards. */}
|
||||
<header>
|
||||
<h1 id="settings-section-notifications-title" className="sr-only">
|
||||
{t("settings.sections.notifications.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* v1.18.1 (D5) — the Vorsorge (preventive-care) block was removed; it
|
||||
has its own /vorsorge page, so embedding the editor here was a
|
||||
duplicate. This screen keeps the three reminder-type cards below. */}
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Mood check-in reminder — only when the mood module is enabled. */}
|
||||
{moodEnabled ? (
|
||||
<div id="mood-reminder" className="scroll-mt-28">
|
||||
@@ -69,13 +54,6 @@ export function NotificationsSection() {
|
||||
<LowStockCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Proactive Coach nudge — only when the coach module is enabled. */}
|
||||
{coachEnabled ? (
|
||||
<div id="coach-nudge" className="scroll-mt-28">
|
||||
<CoachNudgeCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* `<SectionNavHeadingSpacer>` — invisible spacer that reserves exactly the
|
||||
* height of a section's heading block (h1 + subtitle) plus the `space-y-6`
|
||||
* gap that separates the heading from the first card.
|
||||
*
|
||||
* v1.18.6 (W9) — Marc's symmetry ask: the left settings / admin menu's first
|
||||
* item must line up with the TOP OF THE FIRST CARD, not with the page
|
||||
* heading. The page heading lives in the content column above the first card,
|
||||
* so the sidebar needs to start one heading-block lower. Replicating the
|
||||
* exact heading markup (same `text-2xl` h1 + `text-sm` subtitle) as a hidden,
|
||||
* `aria-hidden` block keeps the offset pixel-accurate across locales and
|
||||
* line-wrapping — a hard-coded `pt-[…]` would drift the moment a title or
|
||||
* subtitle wraps to a second line.
|
||||
*
|
||||
* It is only rendered on `md+` (the desktop sticky sidebar); the mobile chip
|
||||
* strip has no heading-to-align-with.
|
||||
*/
|
||||
export function SectionNavHeadingSpacer() {
|
||||
return (
|
||||
<div aria-hidden="true" className="invisible mb-6 select-none">
|
||||
<h1 className="text-2xl font-bold tracking-tight"> </h1>
|
||||
<p className="text-muted-foreground text-sm"> </p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* `<SettingsSectionFrame>` — the canonical page-frame for every Settings
|
||||
* section. The settings twin of the admin renderer's `SectionFrame`
|
||||
* (`src/app/admin/[section]/renderer.tsx`): it emits the one visible
|
||||
* heading + subheading every page must show, in the reference style used by
|
||||
* the Admin Konsole → Übersicht landing.
|
||||
*
|
||||
* Before this primitive every settings section hand-rolled its own
|
||||
* `sr-only` `<h1>`, so no settings page showed a visible heading and the
|
||||
* left-menu-to-first-card alignment drifted page to page. Routing all 19
|
||||
* sections through one frame fixes both: a constant-height heading block
|
||||
* above the first card, and a real visible `h1 + subtitle`.
|
||||
*
|
||||
* <SettingsSectionFrame slug="account">
|
||||
* {cards}
|
||||
* </SettingsSectionFrame>
|
||||
*
|
||||
* Title comes from `settings.sections.<slug>.title`; subtitle from
|
||||
* `settings.sections.<slug>.subtitle`. The `<h1>` keeps the historic
|
||||
* `settings-section-<slug>-title` id so the section's own `aria-labelledby`
|
||||
* linkage — and the spotlight-tour anchors that reference it — stay intact.
|
||||
*
|
||||
* `topSlot` renders ABOVE the heading (used by the Layout-hub child
|
||||
* editors for their "← back to hub" link, which must sit above the title).
|
||||
* `headingAccessory` renders inline at the top-right of the heading row
|
||||
* (used by Gesundheitsakte for its guided-tour re-entry trigger).
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { SettingsSectionSlug } from "./section-slugs";
|
||||
|
||||
export interface SettingsSectionFrameProps {
|
||||
slug: SettingsSectionSlug;
|
||||
/** Optional content rendered above the heading (e.g. a back-to-hub link). */
|
||||
topSlot?: React.ReactNode;
|
||||
/** Optional content rendered inline at the top-right of the heading row
|
||||
* (e.g. a guided-tour re-entry trigger). */
|
||||
headingAccessory?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SettingsSectionFrame({
|
||||
slug,
|
||||
topSlot,
|
||||
headingAccessory,
|
||||
children,
|
||||
}: SettingsSectionFrameProps) {
|
||||
const { t } = useTranslations();
|
||||
const titleId = `settings-section-${slug}-title`;
|
||||
|
||||
return (
|
||||
<section aria-labelledby={titleId} className="space-y-6">
|
||||
{topSlot ? <div>{topSlot}</div> : null}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h1 id={titleId} className="text-2xl font-bold tracking-tight">
|
||||
{t(`settings.sections.${slug}.title`)}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(`settings.sections.${slug}.subtitle`)}
|
||||
</p>
|
||||
</div>
|
||||
{headingAccessory ? (
|
||||
<div className="shrink-0">{headingAccessory}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
isSettingsSectionSlug,
|
||||
type SettingsSectionSlug,
|
||||
} from "./section-slugs";
|
||||
import { SectionNavHeadingSpacer } from "./section-nav-heading-spacer";
|
||||
|
||||
// Re-export so existing call-sites importing from this module keep working.
|
||||
// New server-side call-sites (e.g. `generateStaticParams()`) should import
|
||||
@@ -259,8 +260,7 @@ export function SettingsShell({ active, children }: SettingsShellProps) {
|
||||
// no `moduleGate` (global / CORE) are always shown.
|
||||
const modules = user?.modules;
|
||||
const visibleSections = SETTINGS_SECTIONS.filter(
|
||||
(section) =>
|
||||
!section.moduleGate || modules?.[section.moduleGate] !== false,
|
||||
(section) => !section.moduleGate || modules?.[section.moduleGate] !== false,
|
||||
);
|
||||
|
||||
// v1.4.33 IW4 — keep the active chip in view inside the horizontal
|
||||
@@ -358,6 +358,10 @@ export function SettingsShell({ active, children }: SettingsShellProps) {
|
||||
className="hidden md:block"
|
||||
>
|
||||
<div className="sticky top-20">
|
||||
{/* v1.18.6 (W9) — push the first nav item down to the top of the
|
||||
first card (level with the page heading's following gap), so
|
||||
the menu reads symmetric with the content column. */}
|
||||
<SectionNavHeadingSpacer />
|
||||
<ul className="space-y-1">
|
||||
{visibleSections.map((section) => {
|
||||
const isActive = section.slug === activeSlug;
|
||||
|
||||
@@ -278,31 +278,13 @@ export function SourcesSection() {
|
||||
return Object.keys(priority.deviceTypePriority).length;
|
||||
}, [priority]);
|
||||
|
||||
// v1.18.6 (W9) — the visible heading + subtitle now come from the shared
|
||||
// `<SettingsSectionFrame>` (the off-style muted intro folded into the
|
||||
// standard subtitle). The quiet cross-link back to Integrationen — where
|
||||
// the sources this ladder ranks are added / removed — stays inside the
|
||||
// card, below the ladder.
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-sources-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0/D4) — standalone left-side entry again. The section
|
||||
blurb is dropped for consistent top alignment; the cross-link back
|
||||
to Integrationen (where the sources this ladder ranks are added /
|
||||
removed) stays as a quiet pointer. */}
|
||||
<header>
|
||||
<h1 id="settings-section-sources-title" className="sr-only">
|
||||
{t("settings.sections.sources.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.sections.sources.integrationsHint")}{" "}
|
||||
<Link
|
||||
href="/settings/integrations"
|
||||
className="text-primary underline underline-offset-2"
|
||||
data-slot="sources-integrations-cross-link"
|
||||
>
|
||||
{t("settings.sections.sources.integrationsHintLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-card border-border space-y-4 rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCardHeader
|
||||
icon={Layers}
|
||||
@@ -578,8 +560,22 @@ export function SourcesSection() {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* v1.18.6 (W9) — quiet pointer back to Integrationen, where the
|
||||
sources this ladder ranks are connected / removed. Folded into
|
||||
the card so the page top stays the standard heading + subtitle. */}
|
||||
<p className="text-muted-foreground border-border border-t pt-3 text-xs">
|
||||
{t("settings.sections.sources.integrationsHint")}{" "}
|
||||
<Link
|
||||
href="/settings/integrations"
|
||||
className="text-primary underline underline-offset-2"
|
||||
data-slot="sources-integrations-cross-link"
|
||||
>
|
||||
{t("settings.sections.sources.integrationsHintLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ThresholdsEditorSection } from "@/components/settings/thresholds-editor-section";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* `<ThresholdsSection>` — route-level wrapper for `/settings/thresholds`
|
||||
@@ -16,7 +15,8 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
* flows, so each concern is self-contained on its own page.
|
||||
*
|
||||
* Visual structure on this page:
|
||||
* 1. Section header (title + description).
|
||||
* 1. Section heading + subtitle — supplied by the shared
|
||||
* `<SettingsSectionFrame>` in the route (v1.18.6 W9).
|
||||
* 2. `<ThresholdsEditorSection>` — per-metric range inputs. The editor
|
||||
* card keeps its own affordance row (icon, reset-all button).
|
||||
*
|
||||
@@ -31,21 +31,6 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
* the names stop clashing.
|
||||
*/
|
||||
export function ThresholdsSection() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="settings-section-thresholds-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
<header>
|
||||
<h1 id="settings-section-thresholds-title" className="sr-only">
|
||||
{t("settings.sections.thresholds.title")}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* Per-metric threshold ranges. */}
|
||||
<ThresholdsEditorSection id="thresholds" />
|
||||
</section>
|
||||
);
|
||||
// Per-metric threshold ranges; the heading is supplied by the route frame.
|
||||
return <ThresholdsEditorSection id="thresholds" />;
|
||||
}
|
||||
Reference in new issue
Block a user