mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(integrations): add a Setup-guide doc-link to every integration card
Each integration card now carries the same discreet "Setup guide" link to its runbook under docs.healthlog.dev/integrations/<provider>, so a user who is mid-setup always knows where to go. The docs host lives in one shared constant and the affordance renders identically across all six providers (WHOOP, Withings, Fitbit, Polar, Oura, Nightscout).
This commit is contained in:
1 parent
45ccdcb305
commit
001c158203
13 files changed
+303
-6
No files matched your search
+32
-1
@@ -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",
|
||||
|
||||
+32
-1
@@ -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",
|
||||
|
||||
+32
-1
@@ -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",
|
||||
|
||||
+32
-1
@@ -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",
|
||||
|
||||
+32
-1
@@ -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",
|
||||
|
||||
+32
-1
@@ -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",
|
||||
|
||||
@@ -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}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="fitbit" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 }) {
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="nightscout" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider={provider} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<a
|
||||
href={integrationDocsHref(provider)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid={`${provider}-setup-guide`}
|
||||
data-slot="integration-setup-guide"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("settings.integrationSetupGuide")}
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -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")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="whoop" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="withings" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user