fix(ux): close medium UX-audit findings across flows, IA, and onboarding

Coach deep-links now carry launch context in the URL instead of dropping it
at the route boundary: /coach reads ?scope= (validated against the closed
CoachScopeSource enum) and ?ask= to seed the drawer's own prefill/scope
props, the Today check-in's "Adjust" action seeds ?ask= with the plan's own
words instead of opening a blank chat, and the workout detail page scopes
its coach button to workouts.

IA: give the document vault a "View N lab values" link on LAB_RESULT-shaped
documents (the auto-stage/OCR-commit linkage existed server-side with no UI
surface); disambiguate the two "Recovery" pages (RECOVERY_SCORE anatomy vs
the composite wearable page) with a distinct label plus a two-way
cross-link; move the opt-in nutrients sync card from Source priority to
Integrations, where every other sync surface lives; rename the user-menu
theme switcher off "Appearance" (collides with the Settings hub) and the
Environment settings section to "Location & weather"; cross-link mood,
mental-wellbeing check-ins, and mood insights, which were three
unconnected islands.

Onboarding: the getting-started checklist's data-source/notifications rows
now gate their fetch on the same checklistRelevant condition the other rows
use, instead of only-while-onboarding — they used to be stuck undone
forever for anyone who finished the wizard with under 5 measurements; the
tour wrap-up no longer promises a replay affordance that was removed;
the wizard's Withings card shows a connected badge once the OAuth
round-trip lands instead of leaving the button looking untouched in the
tab it was opened from; the done screen and Modules hub get a quiet
cross-link; the sleep empty-state CTA reads "Connect a source" instead of
being pinned to one vendor.

All copy changes land across every locale; de.json reviewed for register.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 05:06:24 +02:00
1 parent e9e186dca5
commit 8dd04e0170
23 files changed
+431 -79

No files matched your search

+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Abmelden",
"login": "Anmelden",
"notifications": "Benachrichtigungen",
"theme": "Darstellung",
"theme": "Design",
"themeSystem": "System",
"themeDark": "Dunkel",
"themeLight": "Hell",
@@ -605,6 +605,7 @@
"mood": {
"title": "Stimmung",
"subtitle": "Stimmungsverlauf und Einträge",
"mentalWellbeingLink": "Check-in zum seelischen Wohlbefinden machen",
"addEntry": "Hinzufügen",
"editEntry": "Eintrag bearbeiten",
"moodLevel": "Stimmung",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "Noch keine Schlafwerte",
"description": "Sobald Schlafdaten vorliegen (manuell oder via Withings / Apple Health), erscheint hier der nächtliche Stadien-Verlauf.",
"cta": "Withings verbinden"
"cta": "Quelle verbinden"
}
},
"navWorkouts": "Workouts",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Gehgeschwindigkeit",
"mood": {
"logLink": "Stimmung eintragen",
"mentalWellbeingLink": "Check-in zum seelischen Wohlbefinden machen",
"heatmapTitle": "Stimmungskalender",
"heatmapNoEntry": "Kein Eintrag",
"legendGreat": "Super",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Zusammengesetzte Werte für Erholung, Stress und Belastung aus deinen verbundenen Geräten",
"loadError": "Deine Wellness-Werte konnten gerade nicht geladen werden.",
"loadingLabel": "Deine Gesundheitswerte werden geladen",
"recovery": "Erholung",
"recovery": "Erholungs-Score",
"recoveryCrossLink": "Erholungs- und Belastungsdaten deines Wearables ansehen",
"stress": "Stress",
"strain": "Belastung"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Erholung",
"description": "Wie gut sich dein Körper erholt hat – die Erholungs-, Belastungs- und Load-Signale, die dein Wearable über Nacht aufzeichnet.",
"scoreCrossLink": "Den zusammengesetzten Erholungs-Score ansehen",
"emptyTitle": "Noch keine Erholungsdaten",
"empty": "Noch keine Erholungsdaten. Verbinde ein WHOOP-, Polar- oder Oura-Konto, dann füllen sich diese Werte, sobald dein Gerät synchronisiert.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "Das war die Tour",
"body": "Du kannst jedes Modul jederzeit einzeln erneut ansehen („Diese Tour zeigen“ auf jeder Seite) oder die komplette Tour unter Einstellungen → Erweitert neu starten."
"body": "Diese Führung läuft einmalig – sie öffnet sich nicht von selbst erneut. Alles Gezeigte bleibt erhalten, und du kannst Funktionen jederzeit unter Einstellungen → Module ein- oder ausschalten."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Manche Einblicke – Glukose, Schlafrhythmus, Blutdrucktrends, Zyklus – schärfen sich in den ersten ein bis zwei Wochen, sobald sich deine Daten ansammeln. Leere Kacheln heißen jetzt nur, dass wir deine Basislinie noch lernen.",
"connectCta": "Gerät verbinden",
"logCta": "Ersten Wert eintragen",
"importCta": "Bestehende Daten importieren"
"importCta": "Bestehende Daten importieren",
"modulesCta": "Module auswählen"
},
"welcomeBack": {
"title": "Willkommen zurück",
@@ -4549,7 +4555,7 @@
"description": "Strukturierte Datensätze zu Allergien und Familienanamnese."
},
"environment": {
"title": "Umwelt",
"title": "Standort & Wetter",
"subtitle": "Wetter, Tageslicht und Standort-Kontext.",
"disabledHint": "Aktiviere das Umwelt-Modul, um einen Heimatort festzulegen und tägliches Wetter und Tageslicht abzurufen.",
"searchPlaceholder": "Stadt suchen",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Seelisches Wohlbefinden",
"pageDescription": "Vier validierte, freiwillige Selbsttests — PHQ-9 (Stimmung), GAD-7 (Sorgen), WHO-5 (Wohlbefinden) und SCI (Schlaf) — ergänzend zu deinem Stimmungstagebuch. Aussagekräftig ist der Verlauf über die Zeit, nicht ein einzelner Wert.",
"moodLink": "Deinen Stimmungsverlauf ansehen",
"attributionLabel": "Quelle",
"attributionShow": "Quelle und Lizenz für {instrument}",
"choosePrompt": "Check-in auswählen",
@@ -8404,7 +8411,8 @@
"noConditions": "Mit keiner Erkrankung verknüpft",
"previewUnavailable": "Keine Inline-Vorschau für diesen Dateityp",
"uploadedMeta": "Hinzugefügt am {date} · {size}",
"share": "Teilen"
"share": "Teilen",
"labValuesLink": "{count} Laborwerte ansehen"
},
"dropOverlay": {
"title": "Zum Hochladen ablegen",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "Zeit, einen Plan anzuschauen",
"body": "Es ist etwa eine Woche her, seit du diesen Plan gesetzt hast — behalte ihn, passe ihn an oder lass ihn los. Ganz ohne Druck.",
"bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los — ganz ohne Druck."
"bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los — ganz ohne Druck.",
"adjustPrompt": "Ich möchte meinen Check-in-Plan anpassen.",
"adjustPromptPlan": "Ich möchte diesen Plan anpassen: „{plan}“"
},
"tensionWindow": {
"title": "Vielleicht eine angespannte Phase",
+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Sign out",
"login": "Sign in",
"notifications": "Notifications",
"theme": "Appearance",
"theme": "Theme",
"themeSystem": "System",
"themeDark": "Dark",
"themeLight": "Light",
@@ -605,6 +605,7 @@
"mood": {
"title": "Mood",
"subtitle": "Mood tracker and entries",
"mentalWellbeingLink": "Take a mental-wellbeing check-in",
"addEntry": "Add",
"editEntry": "Edit entry",
"moodLevel": "Mood",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "No sleep entries yet",
"description": "Once sleep data lands (manual or via Withings / Apple Health), the per-night stage breakdown will appear here.",
"cta": "Connect Withings"
"cta": "Connect a source"
}
},
"navWorkouts": "Workouts",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Walking Speed",
"mood": {
"logLink": "Log a mood entry",
"mentalWellbeingLink": "Take a mental-wellbeing check-in",
"heatmapTitle": "Mood calendar",
"heatmapNoEntry": "No entry",
"legendGreat": "Great",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Composite recovery, stress and strain read from your connected devices",
"loadError": "Could not load your wellness scores right now.",
"loadingLabel": "Loading your health scores",
"recovery": "Recovery",
"recovery": "Recovery score",
"recoveryCrossLink": "See your wearable's recovery & strain data",
"stress": "Stress",
"strain": "Strain"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Recovery",
"description": "How well your body has bounced back — the recovery, strain and load signals your wearable records overnight.",
"scoreCrossLink": "See the composite recovery score",
"emptyTitle": "No recovery data yet",
"empty": "No recovery data yet. Connect a WHOOP, Polar or Oura account and these scores fill in once your device syncs.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "That’s the tour",
"body": "You can revisit any module on its own at any time (“Show this tour” on every page) or restart the whole tour under Settings → Advanced."
"body": "This walkthrough runs once — it won't reopen on its own. Everything it covered stays put, and you can turn features on or off any time under Settings → Modules."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Some insights — glucose, sleep rhythm, blood-pressure trends, cycle — sharpen over the first week or two as your data builds up. Empty tiles now just mean we're still learning your baseline.",
"connectCta": "Connect a device",
"logCta": "Log your first measurement",
"importCta": "Import existing history"
"importCta": "Import existing history",
"modulesCta": "Choose your modules"
},
"welcomeBack": {
"title": "Welcome back",
@@ -4549,7 +4555,7 @@
"description": "Structured allergies and family history records."
},
"environment": {
"title": "Environment",
"title": "Location & weather",
"subtitle": "Weather, daylight, and location context.",
"disabledHint": "Turn on the Environment module to set a home location and pull in daily weather and daylight.",
"searchPlaceholder": "Search for a city",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Mental wellbeing",
"pageDescription": "Validated, opt-in self-checks — PHQ-9 (mood), GAD-7 (worry), WHO-5 (well-being) and SCI (sleep) — that sit beside your mood tracking. The value is the trend over time, not any single number.",
"moodLink": "View your mood history",
"attributionLabel": "Attribution",
"attributionShow": "Source and licence for {instrument}",
"choosePrompt": "Choose a check-in",
@@ -8404,7 +8411,8 @@
"noConditions": "Not linked to a condition",
"previewUnavailable": "No inline preview for this file type",
"uploadedMeta": "Added {date} · {size}",
"share": "Share"
"share": "Share",
"labValuesLink": "View {count} lab values"
},
"dropOverlay": {
"title": "Drop files to upload",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "Time to check in on a plan",
"body": "It's been about a week since you set this plan — keep it, adjust it, or let it go. No pressure either way.",
"bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go — no pressure either way."
"bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go — no pressure either way.",
"adjustPrompt": "I'd like to adjust my check-in plan.",
"adjustPromptPlan": "I'd like to adjust this plan: “{plan}”"
},
"tensionWindow": {
"title": "Possible tension today",
+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Cerrar sesión",
"login": "Iniciar sesión",
"notifications": "Notificaciones",
"theme": "Apariencia",
"theme": "Tema",
"themeSystem": "Sistema",
"themeDark": "Oscuro",
"themeLight": "Claro",
@@ -605,6 +605,7 @@
"mood": {
"title": "Estado de ánimo",
"subtitle": "Tendencia y registros de estado de ánimo",
"mentalWellbeingLink": "Hacer un chequeo de bienestar mental",
"addEntry": "Añadir",
"editEntry": "Editar entrada",
"moodLevel": "Estado de ánimo",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "Aún no hay registros de sueño",
"description": "Una vez disponibles los datos de sueño (manual o vía Withings / Apple Health), aquí aparecerá el desglose por fases.",
"cta": "Conectar Withings"
"cta": "Conectar una fuente"
}
},
"navWorkouts": "Entrenamientos",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Velocidad al andar",
"mood": {
"logLink": "Registrar un estado de ánimo",
"mentalWellbeingLink": "Hacer un chequeo de bienestar mental",
"heatmapTitle": "Calendario de ánimo",
"heatmapNoEntry": "Sin registro",
"legendGreat": "Genial",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Valores combinados de recuperación, estrés y esfuerzo leídos de tus dispositivos conectados",
"loadError": "No se pudieron cargar tus puntuaciones de bienestar en este momento.",
"loadingLabel": "Cargando tus indicadores de salud",
"recovery": "Recuperación",
"recovery": "Puntuación de recuperación",
"recoveryCrossLink": "Ver los datos de recuperación y esfuerzo de tu wearable",
"stress": "Estrés",
"strain": "Esfuerzo"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Recuperación",
"description": "Cómo se ha recuperado tu cuerpo: las señales de recuperación, esfuerzo y carga que tu wearable registra durante la noche.",
"scoreCrossLink": "Ver la puntuación de recuperación combinada",
"emptyTitle": "Aún no hay datos de recuperación",
"empty": "Aún no hay datos de recuperación. Conecta una cuenta de WHOOP, Polar u Oura y estas puntuaciones se completarán cuando tu dispositivo sincronice.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "Esto es la visita",
"body": "Puedes volver a ver cualquier módulo por separado cuando quieras («Mostrar esta visita» en cada página) o reiniciar la visita completa en Ajustes → Avanzado."
"body": "Esta visita guiada se muestra una sola vez, no volverá a abrirse por sí sola. Todo lo que has visto sigue disponible, y puedes activar o desactivar funciones cuando quieras en Ajustes → Módulos."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Algunas conclusiones —glucosa, ritmo de sueño, tendencias de presión arterial, ciclo— se afinan durante la primera semana o dos a medida que se acumulan tus datos. Las casillas vacías ahora solo significan que aún estamos aprendiendo tu línea base.",
"connectCta": "Conectar un dispositivo",
"logCta": "Registra tu primera medición",
"importCta": "Importar historial existente"
"importCta": "Importar historial existente",
"modulesCta": "Elige tus módulos"
},
"welcomeBack": {
"title": "Bienvenido de nuevo",
@@ -4549,7 +4555,7 @@
"description": "Registros estructurados de alergias y antecedentes familiares."
},
"environment": {
"title": "Entorno",
"title": "Ubicación y clima",
"subtitle": "Contexto de tiempo, luz diurna y ubicación.",
"disabledHint": "Activa el módulo Entorno para definir una ubicación principal e incorporar el tiempo y las horas de luz diarias.",
"searchPlaceholder": "Buscar una ciudad",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Bienestar mental",
"pageDescription": "Cuatro autoevaluaciones validadas y opcionales — PHQ-9 (estado de ánimo), GAD-7 (preocupación), WHO-5 (bienestar) y SCI (sueño) — junto a tu registro de ánimo. Lo valioso es la tendencia en el tiempo, no un único número.",
"moodLink": "Ver tu historial de estado de ánimo",
"attributionLabel": "Fuente",
"attributionShow": "Fuente y licencia de {instrument}",
"choosePrompt": "Elige una revisión",
@@ -8404,7 +8411,8 @@
"noConditions": "Sin vincular a una afección",
"previewUnavailable": "Sin vista previa integrada para este tipo de archivo",
"uploadedMeta": "Añadido el {date} · {size}",
"share": "Compartir"
"share": "Compartir",
"labValuesLink": "Ver {count} valores de laboratorio"
},
"dropOverlay": {
"title": "Suelta los archivos para subirlos",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "Es buen momento para revisar un plan",
"body": "Ha pasado alrededor de una semana desde que definiste este plan: consérvalo, ajústalo o déjalo ir. Sin presión.",
"bodyPlan": "Hace aproximadamente una semana te propusiste: «{plan}». Consérvalo, ajústalo o déjalo ir, sin presión."
"bodyPlan": "Hace aproximadamente una semana te propusiste: «{plan}». Consérvalo, ajústalo o déjalo ir, sin presión.",
"adjustPrompt": "Me gustaría ajustar mi plan de seguimiento.",
"adjustPromptPlan": "Me gustaría ajustar este plan: «{plan}»"
},
"tensionWindow": {
"title": "Quizá algo de tensión hoy",
+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Se déconnecter",
"login": "Se connecter",
"notifications": "Notifications",
"theme": "Apparence",
"theme": "Thème",
"themeSystem": "Système",
"themeDark": "Sombre",
"themeLight": "Clair",
@@ -605,6 +605,7 @@
"mood": {
"title": "Humeur",
"subtitle": "Tendance et entrées d’humeur",
"mentalWellbeingLink": "Faire un bilan de bien-être mental",
"addEntry": "Ajouter",
"editEntry": "Modifier l’entrée",
"moodLevel": "Humeur",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "Aucune donnée de sommeil",
"description": "Dès que des données de sommeil sont disponibles (manuel ou via Withings / Apple Health), la répartition par stade apparaîtra ici.",
"cta": "Connecter Withings"
"cta": "Connecter une source"
}
},
"navWorkouts": "Entraînements",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Vitesse de marche",
"mood": {
"logLink": "Enregistrer une humeur",
"mentalWellbeingLink": "Faire un bilan de bien-être mental",
"heatmapTitle": "Calendrier d'humeur",
"heatmapNoEntry": "Aucune entrée",
"legendGreat": "Excellent",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Valeurs combinées de récupération, de stress et d'effort issues de tes appareils connectés",
"loadError": "Impossible de charger vos scores de bien-être pour le moment.",
"loadingLabel": "Chargement de tes indicateurs de santé",
"recovery": "Récupération",
"recovery": "Score de récupération",
"recoveryCrossLink": "Voir les données de récupération et d'effort de votre wearable",
"stress": "Stress",
"strain": "Effort"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Récupération",
"description": "À quel point votre corps a récupéré : les signaux de récupération, de charge et d'effort que votre wearable enregistre la nuit.",
"scoreCrossLink": "Voir le score de récupération composite",
"emptyTitle": "Pas encore de données de récupération",
"empty": "Pas encore de données de récupération. Connectez un compte WHOOP, Polar ou Oura et ces scores se rempliront dès que votre appareil se synchronise.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "C’est la fin de la visite",
"body": "Vous pouvez revoir chaque module séparément à tout moment (« Afficher cette visite » sur chaque page) ou relancer la visite complète dans Réglages → Avancé."
"body": "Cette visite guidée ne s'affiche qu'une fois — elle ne se rouvrira pas d'elle-même. Tout ce qu'elle a montré reste en place, et vous pouvez activer ou désactiver des fonctionnalités à tout moment dans Réglages → Modules."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Certaines analyses — glycémie, rythme de sommeil, tendances de tension, cycle — s'affinent au cours de la première semaine ou deux à mesure que vos données s'accumulent. Des tuiles vides signifient simplement que nous apprenons encore votre référence.",
"connectCta": "Connecter un appareil",
"logCta": "Enregistrez votre première mesure",
"importCta": "Importer un historique existant"
"importCta": "Importer un historique existant",
"modulesCta": "Choisir vos modules"
},
"welcomeBack": {
"title": "Bon retour",
@@ -4549,7 +4555,7 @@
"description": "Dossiers structurés des allergies et des antécédents familiaux."
},
"environment": {
"title": "Environnement",
"title": "Localisation et météo",
"subtitle": "Contexte météo, lumière du jour et lieu.",
"disabledHint": "Activez le module Environnement pour définir un lieu de référence et intégrer la météo et la durée du jour quotidiennes.",
"searchPlaceholder": "Rechercher une ville",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Bien-être mental",
"pageDescription": "Quatre auto-évaluations validées et facultatives — PHQ-9 (humeur), GAD-7 (inquiétude), WHO-5 (bien-être) et SCI (sommeil) — en complément de ton suivi de l'humeur. Ce qui compte, c'est la tendance dans le temps, pas un chiffre isolé.",
"moodLink": "Voir ton historique d'humeur",
"attributionLabel": "Source",
"attributionShow": "Source et licence pour {instrument}",
"choosePrompt": "Choisis un bilan",
@@ -8404,7 +8411,8 @@
"noConditions": "Lié à aucune affection",
"previewUnavailable": "Pas d'aperçu intégré pour ce type de fichier",
"uploadedMeta": "Ajouté le {date} · {size}",
"share": "Partager"
"share": "Partager",
"labValuesLink": "Voir {count} valeurs de laboratoire"
},
"dropOverlay": {
"title": "Déposez les fichiers pour les téléverser",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "Il est temps de revoir un plan",
"body": "Cela fait environ une semaine que vous avez défini ce plan : gardez-le, ajustez-le ou laissez-le. Sans pression.",
"bodyPlan": "Il y a environ une semaine, vous vous étiez fixé : « {plan} ». Gardez-le, ajustez-le ou laissez-le, sans pression."
"bodyPlan": "Il y a environ une semaine, vous vous étiez fixé : « {plan} ». Gardez-le, ajustez-le ou laissez-le, sans pression.",
"adjustPrompt": "J'aimerais ajuster mon plan de suivi.",
"adjustPromptPlan": "J'aimerais ajuster ce plan : « {plan} »"
},
"tensionWindow": {
"title": "Peut-être un peu de tension",
+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Esci",
"login": "Accedi",
"notifications": "Notifiche",
"theme": "Aspetto",
"theme": "Tema",
"themeSystem": "Sistema",
"themeDark": "Scuro",
"themeLight": "Chiaro",
@@ -605,6 +605,7 @@
"mood": {
"title": "Umore",
"subtitle": "Andamento e voci dell’umore",
"mentalWellbeingLink": "Fai un check-in sul benessere mentale",
"addEntry": "Aggiungi",
"editEntry": "Modifica voce",
"moodLevel": "Umore",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "Nessuna voce di sonno",
"description": "Quando saranno disponibili dati sul sonno (manuale o tramite Withings / Apple Health), qui apparirà la suddivisione per fase.",
"cta": "Collega Withings"
"cta": "Collega una fonte"
}
},
"navWorkouts": "Allenamenti",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Velocità di cammino",
"mood": {
"logLink": "Registra un umore",
"mentalWellbeingLink": "Fai un check-in sul benessere mentale",
"heatmapTitle": "Calendario dell'umore",
"heatmapNoEntry": "Nessun dato",
"legendGreat": "Ottimo",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Valori combinati di recupero, stress e sforzo letti dai tuoi dispositivi connessi",
"loadError": "Al momento non è stato possibile caricare i tuoi punteggi di benessere.",
"loadingLabel": "Caricamento dei tuoi indicatori di salute",
"recovery": "Recupero",
"recovery": "Punteggio di recupero",
"recoveryCrossLink": "Vedi i dati di recupero e sforzo del tuo wearable",
"stress": "Stress",
"strain": "Sforzo"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Recupero",
"description": "Quanto bene il tuo corpo si è ripreso: i segnali di recupero, sforzo e carico che il tuo wearable registra durante la notte.",
"scoreCrossLink": "Vedi il punteggio di recupero composito",
"emptyTitle": "Ancora nessun dato di recupero",
"empty": "Ancora nessun dato di recupero. Collega un account WHOOP, Polar o Oura e questi punteggi si popoleranno appena il dispositivo si sincronizza.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "Questa è la visita guidata",
"body": "Puoi rivedere ogni modulo singolarmente in qualsiasi momento («Mostra questa visita» su ogni pagina) o riavviare l’intera visita da Impostazioni → Avanzate."
"body": "Questa visita guidata viene mostrata una sola volta: non si riaprirà da sola. Tutto ciò che hai visto resta al suo posto e puoi attivare o disattivare le funzioni quando vuoi da Impostazioni → Moduli."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Alcune analisi — glicemia, ritmo del sonno, tendenze della pressione, ciclo — si affinano nella prima settimana o due man mano che i tuoi dati si accumulano. Le caselle vuote ora indicano solo che stiamo ancora imparando il tuo riferimento.",
"connectCta": "Collega un dispositivo",
"logCta": "Registra la tua prima misurazione",
"importCta": "Importa la cronologia esistente"
"importCta": "Importa la cronologia esistente",
"modulesCta": "Scegli i tuoi moduli"
},
"welcomeBack": {
"title": "Bentornato",
@@ -4549,7 +4555,7 @@
"description": "Record strutturati di allergie e anamnesi familiare."
},
"environment": {
"title": "Ambiente",
"title": "Posizione e meteo",
"subtitle": "Contesto meteo, luce del giorno e posizione.",
"disabledHint": "Attiva il modulo Ambiente per impostare una posizione principale e integrare il meteo e le ore di luce giornaliere.",
"searchPlaceholder": "Cerca una città",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Benessere mentale",
"pageDescription": "Quattro autovalutazioni validate e facoltative — PHQ-9 (umore), GAD-7 (preoccupazione), WHO-5 (benessere) e SCI (sonno) — accanto al tuo diario dell'umore. Ciò che conta è l'andamento nel tempo, non un singolo numero.",
"moodLink": "Vedi il tuo andamento dell'umore",
"attributionLabel": "Fonte",
"attributionShow": "Fonte e licenza di {instrument}",
"choosePrompt": "Scegli un check-in",
@@ -8404,7 +8411,8 @@
"noConditions": "Non collegato a una patologia",
"previewUnavailable": "Nessuna anteprima integrata per questo tipo di file",
"uploadedMeta": "Aggiunto il {date} · {size}",
"share": "Condividi"
"share": "Condividi",
"labValuesLink": "Vedi {count} valori di laboratorio"
},
"dropOverlay": {
"title": "Rilascia i file per caricarli",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "È il momento di rivedere un piano",
"body": "È passata circa una settimana da quando hai definito questo piano: mantienilo, modificalo o lascialo andare. Senza pressioni.",
"bodyPlan": "Circa una settimana fa ti eri proposto: «{plan}». Mantienilo, modificalo o lascialo andare, senza pressioni."
"bodyPlan": "Circa una settimana fa ti eri proposto: «{plan}». Mantienilo, modificalo o lascialo andare, senza pressioni.",
"adjustPrompt": "Vorrei modificare il mio piano di controllo.",
"adjustPromptPlan": "Vorrei modificare questo piano: «{plan}»"
},
"tensionWindow": {
"title": "Forse un po' di tensione oggi",
+18 -8
View File
@@ -228,7 +228,7 @@
"logout": "Wyloguj",
"login": "Zaloguj",
"notifications": "Powiadomienia",
"theme": "Wygląd",
"theme": "Motyw",
"themeSystem": "Systemowy",
"themeDark": "Ciemny",
"themeLight": "Jasny",
@@ -605,6 +605,7 @@
"mood": {
"title": "Nastrój",
"subtitle": "Trend i wpisy nastroju",
"mentalWellbeingLink": "Wykonaj sprawdzian dobrostanu psychicznego",
"addEntry": "Dodaj",
"editEntry": "Edytuj wpis",
"moodLevel": "Nastrój",
@@ -2894,7 +2895,7 @@
"sleep": {
"title": "Brak danych o śnie",
"description": "Gdy pojawią się dane o śnie (ręcznie lub przez Withings / Apple Health), wyświetli się tu rozkład faz na noc.",
"cta": "Połącz Withings"
"cta": "Połącz źródło"
}
},
"navWorkouts": "Treningi",
@@ -3262,6 +3263,8 @@
},
"navWalkingSpeed": "Prędkość chodu",
"mood": {
"logLink": "Zapisz nastrój",
"mentalWellbeingLink": "Wykonaj sprawdzian dobrostanu psychicznego",
"heatmapTitle": "Kalendarz nastroju",
"heatmapNoEntry": "Brak wpisu",
"legendGreat": "Świetnie",
@@ -3550,7 +3553,8 @@
"sectionSubtitle": "Złożone wartości regeneracji, stresu i obciążenia odczytane z Twoich połączonych urządzeń",
"loadError": "Nie udało się teraz wczytać Twoich wyników dobrostanu.",
"loadingLabel": "Wczytywanie Twoich wskaźników zdrowia",
"recovery": "Regeneracja",
"recovery": "Wynik regeneracji",
"recoveryCrossLink": "Zobacz dane regeneracji i obciążenia z Twojego urządzenia",
"stress": "Stres",
"strain": "Obciążenie"
},
@@ -3721,6 +3725,7 @@
"recovery": {
"title": "Regeneracja",
"description": "Jak dobrze Twój organizm się zregenerował – sygnały regeneracji, obciążenia i load, które Twoje urządzenie rejestruje w nocy.",
"scoreCrossLink": "Zobacz łączny wynik regeneracji",
"emptyTitle": "Brak danych o regeneracji",
"empty": "Brak danych o regeneracji. Połącz konto WHOOP, Polar lub Oura, a te wyniki uzupełnią się po synchronizacji urządzenia.",
"block": {
@@ -4009,7 +4014,7 @@
},
"wrapUp": {
"title": "To koniec przewodnika",
"body": "Każdy moduł możesz obejrzeć osobno w dowolnej chwili („Pokaż ten przewodnik” na każdej stronie) lub uruchomić cały przewodnik ponownie w Ustawienia → Zaawansowane."
"body": "Ten przewodnik pokazuje się tylko raz — nie otworzy się ponownie sam. Wszystko, co pokazał, zostaje na miejscu, a funkcje możesz włączać i wyłączać w dowolnym momencie w Ustawienia → Moduły."
}
}
},
@@ -4084,7 +4089,8 @@
"learning": "Niektóre wnioski — glukoza, rytm snu, trendy ciśnienia, cykl — wyostrzają się w ciągu pierwszego tygodnia lub dwóch, w miarę gromadzenia danych. Puste kafelki oznaczają teraz tylko, że wciąż poznajemy Twój punkt odniesienia.",
"connectCta": "Połącz urządzenie",
"logCta": "Zapisz pierwszy pomiar",
"importCta": "Importuj istniejącą historię"
"importCta": "Importuj istniejącą historię",
"modulesCta": "Wybierz swoje moduły"
},
"welcomeBack": {
"title": "Witaj ponownie",
@@ -4549,7 +4555,7 @@
"description": "Ustrukturyzowane rekordy alergii i wywiadu rodzinnego."
},
"environment": {
"title": "Środowisko",
"title": "Lokalizacja i pogoda",
"subtitle": "Kontekst pogody, światła dziennego i lokalizacji.",
"disabledHint": "Włącz moduł Środowisko, aby ustawić lokalizację domową i pobierać dzienną pogodę oraz długość dnia.",
"searchPlaceholder": "Szukaj miasta",
@@ -7959,6 +7965,7 @@
"mentalHealth": {
"pageTitle": "Dobrostan psychiczny",
"pageDescription": "Cztery zwalidowane, dobrowolne testy — PHQ-9 (nastrój), GAD-7 (lęk), WHO-5 (samopoczucie) i SCI (sen) — obok Twojego dziennika nastroju. Liczy się trend w czasie, a nie pojedynczy wynik.",
"moodLink": "Zobacz historię swojego nastroju",
"attributionLabel": "Źródło",
"attributionShow": "Źródło i licencja: {instrument}",
"choosePrompt": "Wybierz sprawdzenie",
@@ -8404,7 +8411,8 @@
"noConditions": "Niepowiązany ze schorzeniem",
"previewUnavailable": "Brak podglądu dla tego typu pliku",
"uploadedMeta": "Dodano {date} · {size}",
"share": "Udostępnij"
"share": "Udostępnij",
"labValuesLink": "Zobacz {count} wyników laboratoryjnych"
},
"dropOverlay": {
"title": "Upuść pliki, aby je przesłać",
@@ -8666,7 +8674,9 @@
"coachCheckin": {
"title": "Czas przyjrzeć się planowi",
"body": "Minął mniej więcej tydzień, odkąd ustaliłeś ten plan — zachowaj go, dostosuj lub odpuść. Bez presji.",
"bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść — bez presji."
"bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść — bez presji.",
"adjustPrompt": "Chciałbym dostosować mój plan kontrolny.",
"adjustPromptPlan": "Chciałbym dostosować ten plan: „{plan}”"
},
"tensionWindow": {
"title": "Może chwila napięcia dzisiaj",
+42 -2
View File
@@ -6,11 +6,13 @@ import { useQuery } from "@tanstack/react-query";
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
import type { CoachNudgeStatus } from "@/components/insights/layout-coach-fab";
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
import { useDisableCoach } from "@/hooks/use-disable-coach";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
import { coachScopeSourceSchema } from "@/lib/ai/coach/types";
/**
* v1.12.0 (Coach v2 #6) — full-page Coach conversation.
@@ -57,6 +59,23 @@ import { apiGet } from "@/lib/api/api-fetch";
* most-recent thread so the user lands on what the Coach said.
* The search-param read sits in a Suspense child so the client-bailout
* never de-opts the route.
*
* 2026-07-17 UX-flows audit — the route understood only `?c=` / `?doc=`, so
* every other cross-surface hand-off (a metric card's "ask about this", the
* Today check-in's "Adjust", a workout detail's coach button) dropped its
* context at the URL boundary and landed on a blank composer (F1-2 / F4-1 /
* F6-1). Two additive params close that gap, both seeding the SAME props
* `<CoachConversation>` already exposes for the drawer's suggested-prompt
* chips — no new plumbing inside the conversation surface itself:
* - `?scope=<CoachScopeSource>` — narrows the snapshot the FIRST turn of a
* fresh conversation reads (validated against the closed enum; an
* unrecognised value is silently dropped rather than reaching the chat
* route with a free-form string).
* - `?ask=<text>` — seeds the composer prefill (mirrors `prefill` on the
* in-app `askCoach()` launch call). The user still reviews/sends it —
* this is a prefill, not an auto-send.
* Both are ignored once `?c=` or `?doc=` pin an existing/scoped thread — scope
* only ever applies to a fresh conversation's first turn.
*/
function CoachPageBody() {
const searchParams = useSearchParams();
@@ -71,13 +90,30 @@ function CoachPageBody() {
const rawDoc = searchParams.get("doc");
const seedDocumentId = deepLinkedId === null && rawDoc ? rawDoc : null;
// A fresh conversation only — an existing thread (`?c=`) or a doc-scoped
// chat (`?doc=`) keeps its own established scope.
const freshChat = deepLinkedId === null && seedDocumentId === null;
const rawScope = freshChat ? searchParams.get("scope") : null;
const scopeResult = rawScope
? coachScopeSourceSchema.safeParse(rawScope)
: null;
const launchScope: CoachLaunchScope | null = scopeResult?.success
? { metric: scopeResult.data }
: null;
const seedPrefill = freshChat ? searchParams.get("ask") : null;
// C1 exception — an unread coach-initiated message opens the most-recent
// conversation (which holds that proactive turn). Only consulted when the
// entry did not pin a specific thread or ask for a fresh chat.
// A `?doc=` open is an explicit fresh doc-scoped chat — never override it by
// resuming the most-recent thread on an unread nudge.
// resuming the most-recent thread on an unread nudge. A `?scope=`/`?ask=`
// hand-off is likewise an explicit fresh-chat request.
const exceptionEligible =
deepLinkedId === null && rawC !== "new" && seedDocumentId === null;
deepLinkedId === null &&
rawC !== "new" &&
seedDocumentId === null &&
launchScope === null &&
!seedPrefill;
const { data: nudge } = useQuery({
queryKey: queryKeys.coachNudgeStatus(),
queryFn: async (): Promise<CoachNudgeStatus> =>
@@ -95,6 +131,10 @@ function CoachPageBody() {
// v1.28.51 — seed the document scope for a `?doc=<id>` open so the first
// turn is created + sent through the hardened fenced document endpoint.
initialDocumentId={seedDocumentId}
// 2026-07-17 UX-flows audit F1-2/F4-1/F6-1 — seed the scope/prefill a
// cross-surface hand-off carried in the URL.
launchScope={launchScope}
prefill={seedPrefill}
// Default is the new-chat hero; only resume most-recent for the
// unread coach-initiated exception.
autoOpenMostRecent={hasUnreadCoachMessage}
+28 -2
View File
@@ -3,7 +3,7 @@
import { useQuery } from "@tanstack/react-query";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Smile } from "lucide-react";
import { ArrowRight, Smile } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { queryKeys } from "@/lib/query-keys";
@@ -41,7 +41,7 @@ interface ComprehensiveMoodData {
}
export default function InsightsStimmungPage() {
const { isAuthenticated } = useAuth();
const { user, isAuthenticated } = useAuth();
const { t } = useTranslations();
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
@@ -101,6 +101,32 @@ export default function InsightsStimmungPage() {
The heatmap and the assessment are lifted out of `MoodInsightsSections`
into their own regions so they land in this exact order; all three
regions share one `moodInsights` query (TanStack dedups the fetch). */}
{/* 2026-07-17 UX/IA audit M9 — mood, the mental-wellbeing screeners, and
this insights page form one mental-health domain; this cross-links
into the other two so the trend a user reads here can lead
somewhere. */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
<Link
href="/mood"
data-slot="insights-mood-log-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("insights.mood.logLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
{user?.modules?.mentalHealth === true ? (
<Link
href="/mental-wellbeing"
data-slot="insights-mood-mental-wellbeing-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("insights.mood.mentalWellbeingLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
) : null}
</div>
<MoodInsightsSections region="heatmap" />
{/* No `<MetricRangeControls>` here: mood is event-driven, not a
+16 -1
View File
@@ -1,6 +1,7 @@
"use client";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { ArrowRight, Loader2 } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { useModulePageGuard } from "@/hooks/use-module-page-guard";
@@ -46,6 +47,20 @@ export default function InsightsRecoveryPage() {
explainerMetric="recoveryPage"
coachLaunch
>
{/* 2026-07-17 UX/IA audit M4 — reverse direction of the cross-link added
to the score-anatomy page: this composite wearable page shares the
"Recovery" nav label with the RECOVERY_SCORE anatomy view at
`/insights/scores/recovery`, so a user who arrived here looking for
the score ring / contributor breakdown gets a pointer instead of a
dead end. */}
<Link
href="/insights/scores/recovery"
data-slot="recovery-anatomy-cross-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("insights.recovery.scoreCrossLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
<RecoverySection />
</SubPageShell>
);
+18
View File
@@ -1,6 +1,8 @@
"use client";
import { useParams } from "next/navigation";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { BackLink } from "@/components/ui/back-link";
@@ -94,6 +96,22 @@ export default function CompositeScorePage() {
}
coachLaunch
>
{/* 2026-07-17 UX/IA audit M4 — this anatomy view and the `/insights/recovery`
composite-wearable page both used to render as plain "Recovery", a
same-name/different-content trap: the score ring here promised one
thing, the nav pill led somewhere else with no pointer back. The
label above is now "Recovery score" (matches `measurements.typeRecoveryScore`);
this line closes the loop with an explicit cross-link. */}
{metric === "RECOVERY_SCORE" ? (
<Link
href="/insights/recovery"
data-slot="recovery-score-cross-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("insights.derived.scores.recoveryCrossLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
) : null}
<CompositeScoreAnatomy metric={metric} />
</SubPageShell>
);
+5 -1
View File
@@ -78,7 +78,11 @@ export default function InsightsWorkoutDetailPage({
<WorkoutDetailStats workout={data} />
<WorkoutDetailRoute workout={data} />
<WorkoutDetailHRChart workout={data} />
<CoachLaunchButton />
{/* 2026-07-17 UX-flows audit F6-1 — the button used to open an
unscoped chat; the user had to restate which session they
meant. `workouts` narrows the snapshot the first turn reads
(pairs with the coach-scope param added for F4-1). */}
<CoachLaunchButton scope={{ metric: "workouts" }} />
</>
)}
</SubPageShell>
+17 -1
View File
@@ -13,7 +13,7 @@ import { PageHeader } from "@/components/ui/page-header";
import { PageAuthGate } from "@/components/ui/page-auth-gate";
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Plus, Wrench } from "lucide-react";
import { ArrowRight, Plus, Wrench } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useTranslations } from "@/lib/i18n/context";
@@ -120,6 +120,22 @@ export default function MoodPage() {
</ResponsiveSheet>
<MoodList onAddFirst={() => setDialogOpen(true)} />
{/* 2026-07-17 UX/IA audit M9 — mood tracking, the mental-wellbeing
screeners, and the mood insights page form one mental-health
domain but used to be three unconnected islands. A quiet pointer
here (module-gated — no nav change) rather than a merge: capture
vs. screener stay two defensible surfaces, just cross-linked. */}
{user?.modules?.mentalHealth === true ? (
<Link
href="/mental-wellbeing"
data-slot="mood-mental-wellbeing-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("mood.mentalWellbeingLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
) : null}
</div>
);
}
@@ -18,8 +18,10 @@
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
ArrowRight,
Check,
Download,
FlaskConical,
Pencil,
Plus,
Share2,
@@ -32,6 +34,7 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useIllnessEpisodes } from "@/components/illness/use-illness";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { DateField } from "@/components/ui/date-field";
import { Input } from "@/components/ui/input";
@@ -230,6 +233,21 @@ export function DocumentDetailSheet({
});
const doc = detail.data;
// 2026-07-17 UX/IA audit M5 / F5-5 — the document → lab-values direction
// had no UI link at all: an auto-staged (PENDING) or OCR-confirmed
// (APPROVED, see `linkOcrLabsToVaultDocument`) OBSERVATION fact already
// ties this document to specific lab rows, but nothing on the sheet said
// so. There is no per-value route yet (`/labs` has no `?analyte=`
// deep-link — see the labs page's own "no per-value relation" comment),
// so this links to the labs list rather than a single biomarker; still a
// real jump where there was none before.
const labFactCount =
doc?.facts.filter(
(fact) => fact.factType === "OBSERVATION" && fact.status !== "REJECTED",
).length ?? 0;
const { user } = useAuth();
const labsModuleEnabled = user?.modules?.labs !== false;
const episodes = useIllnessEpisodes(true);
// AI layer — capability drives the vision/text transport; the mutations run
@@ -792,6 +810,18 @@ export function DocumentDetailSheet({
</div>
</div>
{labsModuleEnabled && labFactCount > 0 ? (
<Link
href="/labs"
data-slot="document-detail-lab-values-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 self-start rounded-md text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
>
<FlaskConical className="size-4" aria-hidden="true" />
{t("documents.detail.labValuesLink", { count: labFactCount })}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
) : null}
<p className="text-muted-foreground text-xs">
{t("documents.detail.uploadedMeta", {
date: format.date(doc.createdAt),
@@ -5,6 +5,7 @@ import { Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
import { useFeatureFlags } from "@/hooks/use-feature-flags";
import { useDisableCoach } from "@/hooks/use-disable-coach";
@@ -31,6 +32,13 @@ export interface CoachLaunchButtonProps {
label?: string;
/** Optional prefill seed for the next Coach turn. */
prefill?: string;
/**
* 2026-07-17 UX-flows audit F6-1 — optional scope so the conversation this
* button opens narrows to the relevant source(s) instead of starting from
* a blank, unscoped chat. Passed straight through to `askCoach`'s second
* argument (mirrors every other ambient-scope call site).
*/
scope?: CoachLaunchScope;
/** Optional className passthrough for inline overrides. */
className?: string;
/**
@@ -49,6 +57,7 @@ export interface CoachLaunchButtonProps {
export function CoachLaunchButton({
label,
prefill,
scope,
className,
variant = "inline",
}: CoachLaunchButtonProps) {
@@ -83,7 +92,7 @@ export function CoachLaunchButton({
data-slot="coach-launch-icon"
aria-label={accessibleLabel}
title={accessibleLabel}
onClick={() => launch.askCoach(prefill ?? null)}
onClick={() => launch.askCoach(prefill ?? null, scope)}
className={cn("text-muted-foreground hover:text-foreground", className)}
>
<Sparkles className="size-4" aria-hidden="true" />
@@ -101,7 +110,7 @@ export function CoachLaunchButton({
variant="outline"
size="sm"
data-slot="coach-launch-inline"
onClick={() => launch.askCoach(prefill ?? null)}
onClick={() => launch.askCoach(prefill ?? null, scope)}
className={cn("inline-flex h-10 gap-2 self-end", className)}
>
<Sparkles className="size-4" aria-hidden="true" />
@@ -28,7 +28,9 @@
* the page someone arrives at to take stock.
*/
import { useMemo, useState } from "react";
import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowRight } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
@@ -143,6 +145,19 @@ export function MentalWellbeing() {
</ul>
</section>
{/* 2026-07-17 UX/IA audit M9 — mood tracking, this screener surface,
and the mood insights page form one mental-health domain but used
to be three unconnected islands. A quiet pointer to mood history,
mirroring the one added on `/mood` pointing back here. */}
<Link
href="/mood"
data-slot="mental-wellbeing-mood-link"
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 self-start text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
>
{t("mentalHealth.moodLink")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
{/* Per-instrument detail, opened from a card body: last result,
trend chart, dated history, Start — the Verlauf lives here,
not on the landing. */}
@@ -15,7 +15,11 @@ const integrationStatusState = vi.hoisted(() => ({
data: undefined as
| {
threshold: number;
integrations: Array<{ integration: string; configured?: boolean }>;
integrations: Array<{
integration: string;
configured?: boolean;
connected?: boolean;
}>;
}
| undefined,
}));
@@ -88,4 +92,23 @@ describe("<SourceCardGrid> Withings card", () => {
expect(html).not.toContain('data-testid="source-card-withings-setup"');
expect(html).toContain("Connect Withings");
});
// 2026-07-17 UX-onboarding audit M6 — the connect CTA opens OAuth in a
// new tab; the callback flips the connection in THAT tab while this
// wizard tab used to render the same unchanged button forever. Once
// `useIntegrationStatuses` (refetches on window focus) reports
// `connected: true`, the card must swap to a badge instead of a dead
// "Connect Withings" button that would just reopen a redundant flow.
it("shows a connected badge instead of the connect button once Withings is linked", () => {
integrationStatusState.data = {
threshold: 3,
integrations: [
{ integration: "withings", configured: true, connected: true },
],
};
const html = render();
expect(html).toContain('data-testid="source-card-withings-connected"');
expect(html).not.toContain('href="/api/withings/connect"');
expect(html).not.toContain("Connect Withings");
});
});
+31 -8
View File
@@ -3,7 +3,14 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { CheckCircle2, FileUp, PlusCircle, Plug, Sparkles } from "lucide-react";
import {
CheckCircle2,
FileUp,
PlusCircle,
Plug,
Settings2,
Sparkles,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
@@ -220,13 +227,29 @@ export function DoneScreen() {
</Button>
</div>
<Link
href="/settings/export"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
>
<FileUp className="size-3.5" />
{t("onboarding.done.importCta")}
</Link>
{/* 2026-07-17 UX-onboarding audit M5 — the module system (default-on
set + opt-ins like nutrients / mental health / documents) is
otherwise undiscoverable during first-run: the wizard never
mentions it, and the only pointer used to be one tour stop.
A quiet link here, beside the existing import link, gives every
new account one honest pointer to Settings → Modules before they
leave the wizard. */}
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5">
<Link
href="/settings/export"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
>
<FileUp className="size-3.5" />
{t("onboarding.done.importCta")}
</Link>
<Link
href="/settings/modules"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
>
<Settings2 className="size-3.5" />
{t("onboarding.done.modulesCta")}
</Link>
</div>
</section>
);
}
@@ -249,13 +249,19 @@ export function GettingStartedChecklist() {
enabled: checklistRelevant,
});
// v1.5 perf audit: skip these two fetches once the user is past
// onboarding. The checklist hides itself anyway via shouldShowChecklist
// when onboardingCompletedAt != null AND measurementCount >= 5;
// v1.5 perf audit: skip these two fetches once the card can't render at
// all (`checklistRelevant`, same gate the meds/AI queries above use).
// withings/status and notifications/preferences are unique to this
// component and were burning ~950 ms of network on every dashboard load
// for established users. See docs/audit/v15-performance.md.
const onboardingPending = !!user && user.onboardingCompletedAt == null;
//
// 2026-07-17 UX audit M1 — this used to read a separate `onboardingPending`
// (`onboardingCompletedAt == null`) instead of `checklistRelevant`, so a
// user who finished the wizard but still had under 5 measurements kept
// seeing the card (`shouldShowChecklist` allows that) while these two
// queries stayed disabled forever: "Connect a data source" and
// "Configure notifications" could never flip done even after the user
// actually connected Withings or a push channel post-wizard.
// v1.17.0 — any connected data source satisfies the step, so we read
// the consolidated integrations envelope (Withings / WHOOP / Fitbit /
@@ -265,7 +271,7 @@ export function GettingStartedChecklist() {
queryFn: async () => {
return apiGet("/api/integrations/status");
},
enabled: onboardingPending,
enabled: checklistRelevant,
});
const { data: notificationsData } = useQuery<NotificationsPreferences>({
@@ -273,7 +279,7 @@ export function GettingStartedChecklist() {
queryFn: async () => {
return apiGet("/api/notifications/preferences");
},
enabled: onboardingPending,
enabled: checklistRelevant,
});
const medicationCount = medsData?.length ?? 0;
+31 -1
View File
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import {
CheckCircle2,
ClipboardCheck,
FileUp,
Plug,
@@ -119,6 +120,16 @@ export function SourceCardGrid() {
const { data: integrationStatus } = useIntegrationStatuses(isAuthenticated);
const withingsConfigured =
pickStatus(integrationStatus, "withings")?.configured ?? false;
// 2026-07-17 UX-onboarding audit M6 — the connect CTA opens the OAuth
// handshake in a new tab; the callback lands THAT tab on
// Settings → Integrations while this wizard tab never learned the
// connection succeeded. `useIntegrationStatuses` already refetches on
// window focus, so returning to this tab after the OAuth round trip
// picks up the fresh `connected` flag for free — the only missing piece
// was rendering it as a badge instead of leaving the button looking
// untouched.
const withingsConnected =
pickStatus(integrationStatus, "withings")?.connected ?? false;
async function advance() {
if (advancing) return;
@@ -164,6 +175,8 @@ export function SourceCardGrid() {
}
withingsCtaLabel={t("onboarding.source.withings.cta")}
withingsConfigured={withingsConfigured}
withingsConnected={withingsConnected}
withingsConnectedLabel={t("settings.integrationPill.connected")}
withingsSetupCtaLabel={t("onboarding.source.withings.setupCta")}
withingsSetupHint={t("onboarding.source.withings.setupHint")}
/>
@@ -260,6 +273,13 @@ interface SourceCardItemProps {
* credentials saved — i.e. whether `/api/withings/connect` can
* actually succeed right now. */
withingsConfigured: boolean;
/** Whether Withings is already connected (OAuth completed in the tab the
* connect CTA opened). Replaces the connect button with a badge so
* returning to this wizard tab shows the outcome instead of an
* unchanged card. */
withingsConnected: boolean;
/** Localised "Connected" label for the badge above. */
withingsConnectedLabel: string;
/** CTA label shown instead of "Connect Withings" when unconfigured. */
withingsSetupCtaLabel: string;
/** One-sentence explanation of the BYO credential prerequisite. */
@@ -275,6 +295,8 @@ function SourceCardItem({
recommendedLabel,
withingsCtaLabel,
withingsConfigured,
withingsConnected,
withingsConnectedLabel,
withingsSetupCtaLabel,
withingsSetupHint,
}: SourceCardItemProps) {
@@ -325,7 +347,15 @@ function SourceCardItem({
>
{headerRow}
{body}
{withingsConfigured ? (
{withingsConnected ? (
<div
className="text-primary flex items-center gap-1.5 pt-1 text-sm font-medium"
data-testid="source-card-withings-connected"
>
<CheckCircle2 className="size-3.5" aria-hidden="true" />
{withingsConnectedLabel}
</div>
) : withingsConfigured ? (
<div className="flex items-center gap-2 pt-1">
<Button asChild variant="outline" size="sm">
<a
@@ -19,6 +19,13 @@
*
* `parseOAuthOutcome` / `oauthReasonKey` are re-exported from
* `connections-panel.tsx` so existing unit-test imports keep resolving.
*
* 2026-07-17 UX/IA audit M7 — `<NutrientIntakeCard>` (v1.28) moved here from
* Settings → Sources. It is a read-only sync inventory (what the Apple
* Health nutrients opt-in has stored), not a which-connection-wins ranking —
* "Source priority" describes the latter only. Matches the standing
* providers→Integrations rule; the card still renders nothing while the
* `nutrients` module is off.
*/
import Link from "next/link";
@@ -28,6 +35,7 @@ import { useTranslations } from "@/lib/i18n/context";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
import { Button } from "@/components/ui/button";
export {
@@ -55,6 +63,15 @@ export function IntegrationsSection() {
)}
/>
<ConnectionsPanel />
{/* 2026-07-17 UX/IA audit M7 — the opt-in nutrients sync inventory
(moved from Settings → Sources, where "source priority" only
ever meant which-connection-wins ranking, not a read-only sync
list). The card carries its own header + renders nothing while
the `nutrients` module is off, so it slots in as a sibling card
under the same Connections group rather than a new labelled
section that could paint an orphaned heading over nothing. */}
<NutrientIntakeCard />
</section>
{/* Delivery channels ("where it's delivered"). The existing channels
+5 -4
View File
@@ -45,7 +45,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import {
@@ -585,9 +584,11 @@ export function SourcesSection() {
</p>
</SettingsCard>
{/* v1.28 — read-only synced-nutrient list for the opt-in `nutrients`
module (renders nothing while the module is off). */}
<NutrientIntakeCard />
{/* v1.28 shipped `<NutrientIntakeCard>` here; the 2026-07-17 UX/IA
audit (M7) moved it to Settings → Integrations — it is a read-only
Apple-Health sync inventory, not a which-connection-wins ranking,
so it never belonged on the "source priority" ladder. See
`<IntegrationsSection>` for the card's new home. */}
</div>
);
}
+8 -1
View File
@@ -335,7 +335,14 @@ describe("buildDailyDigest — coach check-in (S3)", () => {
expect(item?.body).toContain("every morning → weigh in");
// Keep / let-go carry the plan id; adjust is a plain navigation href.
expect(item?.actions[0].intent).toBe(`${COACH_CHECKIN_KEEP_INTENT}:p1`);
expect(item?.actions[1].href).toBe("/coach");
// 2026-07-17 UX-flows audit F1-2 — adjust used to be a bare `/coach` link
// dropping all plan context; it now carries `?ask=` seeding the coach
// composer with the plan's own words, so the target is the coach route
// with the plan text echoed in the query, not a blank chat.
expect(item?.actions[1].href).toContain("/coach?ask=");
expect(item?.actions[1].href).toContain(
encodeURIComponent("every morning → weigh in"),
);
expect(item?.actions[2].intent).toBe(`${COACH_CHECKIN_LETGO_INTENT}:p1`);
});
+13 -1
View File
@@ -451,6 +451,18 @@ function buildCoachCheckinItem(
? t("daily.item.coachCheckin.bodyPlan", { plan: plan.planText })
: t("daily.item.coachCheckin.body");
// 2026-07-17 UX-flows audit F1-2 — "Adjust" used to be a bare `/coach`
// link carrying no plan context: keep/let-go both thread the plan id
// into their intent, but adjust dropped the user into an empty new chat
// with no memory of which plan prompted the tap. The coach route's
// full-page surface now reads `?ask=` and seeds it as the composer
// prefill for a fresh conversation (`CoachConversation`'s `prefill` prop,
// same mechanism the drawer's suggested-prompt chips use) — the user
// lands with the plan already named instead of a blank composer.
const adjustPrompt = plan.planText
? t("daily.item.coachCheckin.adjustPromptPlan", { plan: plan.planText })
: t("daily.item.coachCheckin.adjustPrompt");
return {
kind: "coach_checkin",
title: t("daily.item.coachCheckin.title"),
@@ -464,7 +476,7 @@ function buildCoachCheckinItem(
{
labelKey: "daily.action.checkinAdjust",
intent: COACH_CHECKIN_ADJUST_INTENT,
href: "/coach",
href: `/coach?ask=${encodeURIComponent(adjustPrompt)}`,
},
{
labelKey: "daily.action.checkinLetGo",