diff --git a/messages/de.json b/messages/de.json index bb1026aa7..3acdc22e7 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Stimmungs-Tags", "description": "Gruppen, Tags und Reihenfolge der Tag-Auswahl." + }, + "reminders": { + "title": "Erinnerungen & Benachrichtigungen", + "description": "Ein Ort für jede Erinnerung — woran und wann erinnert wird — und die Kanäle, die sie zustellen.", + "categoriesGroup": "Erinnerungen", + "channelsGroup": "Zustellkanäle", + "medication": { + "title": "Medikamenten-Erinnerungen", + "description": "Einnahmezeiten und Zeitfenster, je Medikament eingestellt." + }, + "vorsorge": { + "title": "Vorsorge", + "description": "Wiederkehrende Untersuchungen und Messungen, inklusive Ort." + }, + "mood": { + "title": "Stimmungs-Check-in", + "description": "Eine tägliche Erinnerung, dein Befinden zu erfassen." + }, + "lowStock": { + "title": "Reichweite bei Restbestand", + "description": "Werde gewarnt, bevor ein Medikament zur Neige geht." + }, + "coach": { + "title": "Coach-Hinweis", + "description": "Proaktive Impulse vom KI-Coach." + }, + "channels": { + "title": "Benachrichtigungskanäle", + "description": "APNs, Telegram, ntfy, Web Push, Webhook und E-Mail." + } } }, "profile": "Profil", @@ -4773,7 +4803,8 @@ "token": "Der Token-Austausch mit Oura ist fehlgeschlagen. Bitte starte die Verbindung erneut.", "rate_limited": "Zu viele Verbindungsversuche. Warte einen Moment und versuche es erneut.", "generic": "Unbekannter Fehler bei der Verbindung. Bitte starte die Verbindung erneut." - } + }, + "integrationSetupGuide": "Einrichtungsanleitung" }, "admin": { "title": "Administration", diff --git a/messages/en.json b/messages/en.json index 63210fddf..8339967fc 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Mood tags", "description": "Groups, tags, and order of the mood picker." + }, + "reminders": { + "title": "Reminders & Notifications", + "description": "One home for every reminder — what gets reminded and when — plus the channels that deliver them.", + "categoriesGroup": "Reminders", + "channelsGroup": "Delivery channels", + "medication": { + "title": "Medication reminders", + "description": "Dose times and windows, configured on each medication." + }, + "vorsorge": { + "title": "Preventive care", + "description": "Recurring check-ups and measurements, with where to go." + }, + "mood": { + "title": "Mood check-in", + "description": "A daily nudge to log how you feel." + }, + "lowStock": { + "title": "Low-stock runway", + "description": "Get warned before a medication runs out." + }, + "coach": { + "title": "Coach nudge", + "description": "Proactive prompts from the AI coach." + }, + "channels": { + "title": "Notification channels", + "description": "APNs, Telegram, ntfy, Web Push, Webhook, and Email." + } } }, "profile": "Profile", @@ -4773,7 +4803,8 @@ "token": "The token exchange with Oura failed. Please start the connection again.", "rate_limited": "Too many connection attempts. Wait a moment and try again.", "generic": "Unknown error while connecting. Please start the connection again." - } + }, + "integrationSetupGuide": "Setup guide" }, "admin": { "title": "Administration", diff --git a/messages/es.json b/messages/es.json index 2da94f8f0..f5c8d5fcb 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Etiquetas de ánimo", "description": "Grupos, etiquetas y orden del selector de ánimo." + }, + "reminders": { + "title": "Recordatorios y notificaciones", + "description": "Un único lugar para cada recordatorio — qué se recuerda y cuándo — y los canales que lo entregan.", + "categoriesGroup": "Recordatorios", + "channelsGroup": "Canales de entrega", + "medication": { + "title": "Recordatorios de medicación", + "description": "Horas y ventanas de dosis, configuradas en cada medicamento." + }, + "vorsorge": { + "title": "Cuidado preventivo", + "description": "Revisiones y mediciones periódicas, con el lugar al que acudir." + }, + "mood": { + "title": "Registro de ánimo", + "description": "Un aviso diario para registrar cómo te sientes." + }, + "lowStock": { + "title": "Margen por bajo stock", + "description": "Recibe un aviso antes de que se acabe un medicamento." + }, + "coach": { + "title": "Sugerencia del coach", + "description": "Indicaciones proactivas del coach de IA." + }, + "channels": { + "title": "Canales de notificación", + "description": "APNs, Telegram, ntfy, Web Push, Webhook y correo." + } } }, "profile": "Perfil", @@ -4773,7 +4803,8 @@ "token": "El intercambio de token con Oura falló. Inicia la conexión de nuevo.", "rate_limited": "Demasiados intentos de conexión. Espera un momento e inténtalo de nuevo.", "generic": "Error desconocido al conectar. Inicia la conexión de nuevo." - } + }, + "integrationSetupGuide": "Guía de configuración" }, "admin": { "title": "Administration", diff --git a/messages/fr.json b/messages/fr.json index 21461fbc4..7506b034d 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Étiquettes d’humeur", "description": "Groupes, étiquettes et ordre du sélecteur d’humeur." + }, + "reminders": { + "title": "Rappels et notifications", + "description": "Un seul endroit pour chaque rappel — quoi et quand — ainsi que les canaux qui les délivrent.", + "categoriesGroup": "Rappels", + "channelsGroup": "Canaux de diffusion", + "medication": { + "title": "Rappels de médication", + "description": "Heures et fenêtres de prise, configurées sur chaque médicament." + }, + "vorsorge": { + "title": "Prévention", + "description": "Bilans et mesures récurrents, avec le lieu où se rendre." + }, + "mood": { + "title": "Suivi de l'humeur", + "description": "Un rappel quotidien pour noter votre ressenti." + }, + "lowStock": { + "title": "Marge de stock faible", + "description": "Soyez averti avant qu'un médicament ne s'épuise." + }, + "coach": { + "title": "Suggestion du coach", + "description": "Invitations proactives du coach IA." + }, + "channels": { + "title": "Canaux de notification", + "description": "APNs, Telegram, ntfy, Web Push, Webhook et e-mail." + } } }, "profile": "Profil", @@ -4773,7 +4803,8 @@ "token": "L'échange de jeton avec Oura a échoué. Veuillez relancer la connexion.", "rate_limited": "Trop de tentatives de connexion. Patientez un instant et réessayez.", "generic": "Erreur inconnue lors de la connexion. Veuillez relancer la connexion." - } + }, + "integrationSetupGuide": "Guide de configuration" }, "admin": { "title": "Administration", diff --git a/messages/it.json b/messages/it.json index 265885255..0719bf886 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Tag dell’umore", "description": "Gruppi, tag e ordine del selettore dell’umore." + }, + "reminders": { + "title": "Promemoria e notifiche", + "description": "Un'unica sede per ogni promemoria — cosa ricordare e quando — e i canali che li recapitano.", + "categoriesGroup": "Promemoria", + "channelsGroup": "Canali di consegna", + "medication": { + "title": "Promemoria dei farmaci", + "description": "Orari e finestre delle dosi, impostati su ogni farmaco." + }, + "vorsorge": { + "title": "Prevenzione", + "description": "Controlli e misurazioni ricorrenti, con il luogo dove recarsi." + }, + "mood": { + "title": "Check-in dell'umore", + "description": "Un promemoria quotidiano per registrare come ti senti." + }, + "lowStock": { + "title": "Autonomia scorte", + "description": "Ricevi un avviso prima che un farmaco finisca." + }, + "coach": { + "title": "Suggerimento del coach", + "description": "Stimoli proattivi dal coach IA." + }, + "channels": { + "title": "Canali di notifica", + "description": "APNs, Telegram, ntfy, Web Push, Webhook ed e-mail." + } } }, "profile": "Profilo", @@ -4773,7 +4803,8 @@ "token": "Lo scambio del token con Oura è fallito. Riavvia la connessione.", "rate_limited": "Troppi tentativi di connessione. Attendi un momento e riprova.", "generic": "Errore sconosciuto durante la connessione. Riavvia la connessione." - } + }, + "integrationSetupGuide": "Guida alla configurazione" }, "admin": { "title": "Administration", diff --git a/messages/pl.json b/messages/pl.json index a969c66f2..ae04caf95 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4087,6 +4087,36 @@ "mood": { "title": "Tagi nastroju", "description": "Grupy, tagi i kolejność w wyborze nastroju." + }, + "reminders": { + "title": "Przypomnienia i powiadomienia", + "description": "Jedno miejsce dla każdego przypomnienia — o czym i kiedy — oraz kanały, które je dostarczają.", + "categoriesGroup": "Przypomnienia", + "channelsGroup": "Kanały dostarczania", + "medication": { + "title": "Przypomnienia o lekach", + "description": "Godziny i okna dawek, ustawiane przy każdym leku." + }, + "vorsorge": { + "title": "Profilaktyka", + "description": "Cykliczne badania i pomiary wraz z miejscem, gdzie się udać." + }, + "mood": { + "title": "Zapis nastroju", + "description": "Codzienne przypomnienie, by zapisać samopoczucie." + }, + "lowStock": { + "title": "Zapas na wyczerpaniu", + "description": "Otrzymaj ostrzeżenie, zanim lek się skończy." + }, + "coach": { + "title": "Podpowiedź trenera", + "description": "Proaktywne wskazówki od trenera AI." + }, + "channels": { + "title": "Kanały powiadomień", + "description": "APNs, Telegram, ntfy, Web Push, Webhook i e-mail." + } } }, "profile": "Profil", @@ -4773,7 +4803,8 @@ "token": "Wymiana tokena z Oura nie powiodła się. Rozpocznij połączenie ponownie.", "rate_limited": "Zbyt wiele prób połączenia. Poczekaj chwilę i spróbuj ponownie.", "generic": "Nieznany błąd podczas łączenia. Rozpocznij połączenie ponownie." - } + }, + "integrationSetupGuide": "Przewodnik konfiguracji" }, "admin": { "title": "Administracja", diff --git a/src/components/settings/__tests__/integrations-section.test.tsx b/src/components/settings/__tests__/integrations-section.test.tsx index 8381da09e..69b49d1fb 100644 --- a/src/components/settings/__tests__/integrations-section.test.tsx +++ b/src/components/settings/__tests__/integrations-section.test.tsx @@ -303,4 +303,43 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => { // Fitbit / Google Health). The moodLog integration was removed. expect(count(html, 'data-testid="integration-card-divider"')).toBe(3); }); + + // v1.17.1 — every integration card carries the same discreet "Setup guide" + // doc-link pointing at its runbook under the single shared docs base. The + // affordance is one family across all six providers. + it("every integration card carries the shared Setup-guide doc-link", () => { + setIntegrationStatus({ + threshold: 3, + integrations: [ + { + integration: "withings", + state: "connected", + lastSuccessAt: "2026-05-09T18:00:00.000Z", + lastAttemptAt: "2026-05-09T18:00:00.000Z", + lastError: null, + connected: true, + configured: true, + legacyLastSyncedAt: "2026-05-09T18:00:00.000Z", + hasActivityScope: true, + }, + ], + }); + + const html = render(); + // One setup-guide link per card → six providers. + expect(count(html, 'data-slot="integration-setup-guide"')).toBe(6); + for (const provider of [ + "withings", + "whoop", + "fitbit", + "polar", + "oura", + "nightscout", + ]) { + expect(html).toContain(`data-testid="${provider}-setup-guide"`); + expect(html).toContain( + `href="https://docs.healthlog.dev/integrations/${provider}"`, + ); + } + }); }); diff --git a/src/components/settings/integrations/fitbit-card.tsx b/src/components/settings/integrations/fitbit-card.tsx index 5b394c615..f2b1c3b04 100644 --- a/src/components/settings/integrations/fitbit-card.tsx +++ b/src/components/settings/integrations/fitbit-card.tsx @@ -51,6 +51,7 @@ import { pillStateFor, type IntegrationStatusViewModel, } from "./shared"; +import { IntegrationSetupGuideLink } from "./setup-guide-link"; export function FitbitCard({ viewModel, @@ -484,6 +485,8 @@ export function FitbitCard({ {t("settings.fitbitNoCredentials")} )} + + ); diff --git a/src/components/settings/integrations/nightscout-card.tsx b/src/components/settings/integrations/nightscout-card.tsx index 97edbc3e7..6dc028bb9 100644 --- a/src/components/settings/integrations/nightscout-card.tsx +++ b/src/components/settings/integrations/nightscout-card.tsx @@ -39,6 +39,7 @@ import { useTranslations } from "@/lib/i18n/context"; import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys"; import { IntegrationErrorMessage } from "./shared"; +import { IntegrationSetupGuideLink } from "./setup-guide-link"; interface NightscoutStatus { connected: boolean; @@ -366,6 +367,8 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) { )} + + ); diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx index 77b2e97fd..d7642ecab 100644 --- a/src/components/settings/integrations/oauth-provider-card.tsx +++ b/src/components/settings/integrations/oauth-provider-card.tsx @@ -37,6 +37,7 @@ import { TestConnectionButton } from "@/components/settings/test-connection-butt import { apiFetchRaw, apiGet, apiPost } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys"; +import { IntegrationSetupGuideLink } from "./setup-guide-link"; export interface OAuthProviderStatus { connected: boolean; @@ -398,6 +399,8 @@ export function OAuthProviderCard({ {msg}

)} + + ); diff --git a/src/components/settings/integrations/setup-guide-link.tsx b/src/components/settings/integrations/setup-guide-link.tsx new file mode 100644 index 000000000..a6cf6a652 --- /dev/null +++ b/src/components/settings/integrations/setup-guide-link.tsx @@ -0,0 +1,57 @@ +"use client"; + +/** + * v1.17.1 — shared "Setup guide" doc-link for every Settings → Integrations + * card. Each card paints the same affordance: one discreet, external link to + * the provider's setup runbook ("was eingeben, wo klicken"). Single-sourced + * here so all six cards (WHOOP / Withings / Fitbit / Polar / Oura / Nightscout) + * read as one family and the docs host lives in exactly one place. + * + * The runbooks themselves are authored separately; this link wires the + * destination now so a user who is mid-setup always knows where to go. + */ + +import { ExternalLink } from "lucide-react"; + +import { useTranslations } from "@/lib/i18n/context"; + +/** + * Base for every provider setup runbook. The provider key is appended as a + * path segment, e.g. `https://docs.healthlog.dev/integrations/whoop`. Kept as + * a single constant so the host never drifts across cards. + */ +export const INTEGRATION_DOCS_BASE = + "https://docs.healthlog.dev/integrations"; + +export type IntegrationDocsProvider = + | "whoop" + | "withings" + | "fitbit" + | "polar" + | "oura" + | "nightscout"; + +export function integrationDocsHref(provider: IntegrationDocsProvider): string { + return `${INTEGRATION_DOCS_BASE}/${provider}`; +} + +export function IntegrationSetupGuideLink({ + provider, +}: { + provider: IntegrationDocsProvider; +}) { + const { t } = useTranslations(); + return ( + + {t("settings.integrationSetupGuide")} + + ); +} diff --git a/src/components/settings/integrations/whoop-card.tsx b/src/components/settings/integrations/whoop-card.tsx index a3acc2077..23b875f4e 100644 --- a/src/components/settings/integrations/whoop-card.tsx +++ b/src/components/settings/integrations/whoop-card.tsx @@ -43,6 +43,7 @@ import { pillStateFor, type IntegrationStatusViewModel, } from "./shared"; +import { IntegrationSetupGuideLink } from "./setup-guide-link"; export function WhoopCard({ viewModel, @@ -456,6 +457,8 @@ export function WhoopCard({ {t("settings.whoopNoCredentials")} )} + + ); diff --git a/src/components/settings/integrations/withings-card.tsx b/src/components/settings/integrations/withings-card.tsx index d9613697a..009566d7d 100644 --- a/src/components/settings/integrations/withings-card.tsx +++ b/src/components/settings/integrations/withings-card.tsx @@ -36,6 +36,7 @@ import { pillStateFor, type IntegrationStatusViewModel, } from "./shared"; +import { IntegrationSetupGuideLink } from "./setup-guide-link"; export function WithingsCard({ viewModel, @@ -491,6 +492,8 @@ export function WithingsCard({ {t("settings.withingsNoCredentials")} )} + + );