diff --git a/messages/de.json b/messages/de.json index b7b6373a8..7bde66b09 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5279,7 +5279,7 @@ "integrationPill": { "connected": "Verbunden", "errorReconnect": "Fehler — neu verbinden", - "warningServerError": "Verbunden, aber Serverfehler", + "warningServerError": "Verbunden, Sync-Problem", "parkedReconnect": "Pausiert — manuell wieder verbinden", "notConnected": "Nicht verbunden", "justNow": "gerade eben", @@ -5289,8 +5289,26 @@ "ariaLabel": "Integrationsstatus", "resumeCta": "Wieder verbinden", "resumeSuccess": "Integration wieder aktiv", - "resumeError": "Wieder verbinden fehlgeschlagen" + "resumeError": "Wieder verbinden fehlgeschlagen", + "stale": "Verbunden, keine neuen Daten", + "stalled": "Sync gestoppt", + "pendingSetup": "Warte auf erste Daten" }, + "integrationFreshness": { + "title": "Empfangene Daten", + "summary": "{count} Datentypen · neuestes {relative}", + "quiet": "{count} still", + "workouts": "Workouts" + }, + "integrationFallback": { + "description": "Für dieses Konto eingerichtet, hat aber keine eigene Einstellungskarte. Diese Zeile sorgt dafür, dass eine Verbindung nicht unbemerkt ausfallen kann.", + "lastAttempt": "Letzter Versuch {relative}", + "noAttempt": "Kein Sync-Versuch aufgezeichnet" + }, + "withingsViewData": "Messwerte ansehen", + "whoopViewData": "Erholung ansehen", + "fitbitViewData": "Schlaf ansehen", + "googleHealthViewData": "Schritte ansehen", "apiTokens": "API-Tokens", "apiTokensDescription": "API-Tokens für die externe Medikamenten-Einnahme prüfen und widerrufen. Tokens werden pro Medikament über dessen API-Endpoint-Schalter ausgestellt.", "tokenMintMovedDescription": "Tokens werden pro Medikament ausgestellt. Öffne ein Medikament und aktiviere dessen API-Endpoint, um ein Token zu erhalten, das nur für dieses Medikament gilt.", @@ -5850,17 +5868,14 @@ "title": "Apple Health", "description": "Die Live-Synchronisierung erfolgt über die HealthLog-iOS-App – nicht über eine Web- oder OAuth-Verbindung.", "status": { - "checking": "Apple-Health-Daten werden geprüft…", - "dataReceived": "Apple-Health-Daten empfangen", - "setup": "In der iOS-App einrichten", "unavailable": "Apple-Health-Status nicht verfügbar" }, - "lastDataLabel": "Letzte Apple-Health-Daten:", "setupTitle": "Live-Synchronisierung aktivieren", "permissionStep": "Öffne in der HealthLog-iOS-App Einstellungen → Apple Health und erlaube die Gesundheitskategorien, die du teilen möchtest.", "backgroundStep": "Lass die Hintergrundaktualisierung aktiviert. Die App synchronisiert neue Daten automatisch im Vordergrund und wenn iOS Hintergrundzeit gewährt.", "importNote": "Du brauchst ältere Daten oder kannst die iOS-App nicht nutzen? Der Apple-Health-XML/ZIP-Upload ist ein einmaliger Ersatzweg und aktiviert keine Live-Synchronisierung.", - "importAction": "Einmaligen Import öffnen" + "importAction": "Einmaligen Import öffnen", + "staleNote": "Seit über einer Woche sind keine Apple-Health-Daten angekommen. Öffne die HealthLog-App auf deinem iPhone und prüfe die Apple-Health-Berechtigungen." }, "garminInfo": { "title": "Garmin", diff --git a/messages/en.json b/messages/en.json index eb739414e..e6b4d44d8 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5269,7 +5269,7 @@ "integrationPill": { "connected": "Connected", "errorReconnect": "Error — reconnect", - "warningServerError": "Connected, server error", + "warningServerError": "Connected, sync issue", "parkedReconnect": "Paused — reconnect manually", "notConnected": "Not connected", "justNow": "just now", @@ -5279,8 +5279,26 @@ "ariaLabel": "Integration status", "resumeCta": "Reconnect", "resumeSuccess": "Integration resumed", - "resumeError": "Reconnect failed" + "resumeError": "Reconnect failed", + "stale": "Connected, no recent data", + "stalled": "Sync stopped", + "pendingSetup": "Waiting for first data" }, + "integrationFreshness": { + "title": "Data received", + "summary": "{count} data types · newest {relative}", + "quiet": "{count} quiet", + "workouts": "Workouts" + }, + "integrationFallback": { + "description": "Configured on this account without a settings card of its own. This row exists so a connection cannot stop working unnoticed.", + "lastAttempt": "Last attempt {relative}", + "noAttempt": "No sync attempt recorded" + }, + "withingsViewData": "View your measurements", + "whoopViewData": "View your recovery", + "fitbitViewData": "View your sleep", + "googleHealthViewData": "View your steps", "apiTokens": "API Tokens", "apiTokensDescription": "Review and revoke API tokens for external medication intake. Tokens are issued per medication from the medication's API endpoint toggle.", "tokenMintMovedDescription": "Tokens are issued per medication. Open a medication and enable its API endpoint to get a token scoped to that medication alone.", @@ -5840,17 +5858,14 @@ "title": "Apple Health", "description": "Live sync comes from the HealthLog iOS app. It is not a web or OAuth connection.", "status": { - "checking": "Checking Apple Health data…", - "dataReceived": "Apple Health data received", - "setup": "Set up in iOS app", "unavailable": "Apple Health status unavailable" }, - "lastDataLabel": "Last Apple Health data:", "setupTitle": "Turn on live sync", "permissionStep": "In the HealthLog iOS app, open Settings → Apple Health and allow the health categories you want to share.", "backgroundStep": "Keep Background App Refresh enabled. The app syncs new data automatically in the foreground and when iOS grants background time.", "importNote": "Need older history or cannot use the iOS app? The Apple Health XML/ZIP upload is a one-shot fallback and does not enable live sync.", - "importAction": "Open one-shot import" + "importAction": "Open one-shot import", + "staleNote": "No Apple Health data has arrived for over a week. Open the HealthLog app on your iPhone and check the Apple Health permissions." }, "garminInfo": { "title": "Garmin", diff --git a/messages/es.json b/messages/es.json index 5799a20cf..75b5da539 100644 --- a/messages/es.json +++ b/messages/es.json @@ -5281,7 +5281,7 @@ "integrationPill": { "connected": "Conectado", "errorReconnect": "Error — reconectar", - "warningServerError": "Conectado, error del servidor", + "warningServerError": "Conectado, problema de sincronización", "parkedReconnect": "Pausado — reconectar manualmente", "notConnected": "No conectado", "justNow": "ahora mismo", @@ -5291,8 +5291,26 @@ "ariaLabel": "Estado de la integración", "resumeCta": "Reconectar", "resumeSuccess": "Integración reanudada", - "resumeError": "Reconexión fallida" + "resumeError": "Reconexión fallida", + "stale": "Conectado, sin datos recientes", + "stalled": "Sincronización detenida", + "pendingSetup": "Esperando los primeros datos" }, + "integrationFreshness": { + "title": "Datos recibidos", + "summary": "{count} tipos de datos · más reciente {relative}", + "quiet": "{count} en silencio", + "workouts": "Entrenamientos" + }, + "integrationFallback": { + "description": "Configurado en esta cuenta pero sin tarjeta de ajustes propia. Esta fila existe para que una conexión no deje de funcionar sin que nadie lo note.", + "lastAttempt": "Último intento {relative}", + "noAttempt": "Ningún intento de sincronización registrado" + }, + "withingsViewData": "Ver tus mediciones", + "whoopViewData": "Ver tu recuperación", + "fitbitViewData": "Ver tu sueño", + "googleHealthViewData": "Ver tus pasos", "apiTokens": "Tokens de API", "apiTokensDescription": "Revisa y revoca tokens de API para el registro externo de tomas de medicación. Los tokens se emiten por medicamento desde su interruptor de endpoint de API.", "tokenMintMovedDescription": "Los tokens se emiten por medicamento. Abre un medicamento y activa su endpoint de API para obtener un token limitado únicamente a ese medicamento.", @@ -5852,17 +5870,14 @@ "title": "Apple Health", "description": "La sincronización en vivo se realiza desde la app HealthLog para iOS, no mediante una conexión web ni OAuth.", "status": { - "checking": "Comprobando datos de Apple Health…", - "dataReceived": "Datos de Apple Health recibidos", - "setup": "Configurar en la app de iOS", "unavailable": "Estado de Apple Health no disponible" }, - "lastDataLabel": "Últimos datos de Apple Health:", "setupTitle": "Activar la sincronización en vivo", "permissionStep": "En la app HealthLog para iOS, abre Ajustes → Apple Health y permite las categorías de salud que quieras compartir.", "backgroundStep": "Mantén activada la actualización en segundo plano. La app sincroniza datos nuevos automáticamente en primer plano y cuando iOS le concede tiempo en segundo plano.", "importNote": "¿Necesitas un historial anterior o no puedes usar la app de iOS? La carga XML/ZIP de Apple Health es una alternativa puntual y no activa la sincronización en vivo.", - "importAction": "Abrir importación puntual" + "importAction": "Abrir importación puntual", + "staleNote": "Hace más de una semana que no llegan datos de Apple Health. Abre la app HealthLog en tu iPhone y revisa los permisos de Apple Health." }, "garminInfo": { "title": "Garmin", diff --git a/messages/fr.json b/messages/fr.json index e2c6159af..a0a638b3d 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -5283,7 +5283,7 @@ "integrationPill": { "connected": "Connecté", "errorReconnect": "Erreur — reconnecter", - "warningServerError": "Connecté, erreur serveur", + "warningServerError": "Connecté, problème de synchronisation", "parkedReconnect": "En pause — reconnecter manuellement", "notConnected": "Non connecté", "justNow": "à l’instant", @@ -5293,8 +5293,26 @@ "ariaLabel": "Statut de l’intégration", "resumeCta": "Reconnecter", "resumeSuccess": "Intégration reprise", - "resumeError": "Échec de la reconnexion" + "resumeError": "Échec de la reconnexion", + "stale": "Connecté, aucune donnée récente", + "stalled": "Synchronisation arrêtée", + "pendingSetup": "En attente des premières données" }, + "integrationFreshness": { + "title": "Données reçues", + "summary": "{count} types de données · plus récent {relative}", + "quiet": "{count} silencieux", + "workouts": "Séances" + }, + "integrationFallback": { + "description": "Configuré sur ce compte sans carte de réglages dédiée. Cette ligne existe pour qu'une connexion ne puisse pas cesser de fonctionner sans que personne ne le voie.", + "lastAttempt": "Dernière tentative {relative}", + "noAttempt": "Aucune tentative de synchronisation enregistrée" + }, + "withingsViewData": "Voir vos mesures", + "whoopViewData": "Voir votre récupération", + "fitbitViewData": "Voir votre sommeil", + "googleHealthViewData": "Voir vos pas", "apiTokens": "Jetons API", "apiTokensDescription": "Consultez et révoquez les jetons API pour l’enregistrement externe des prises de médicaments. Les jetons sont émis par médicament depuis son interrupteur de point de terminaison API.", "tokenMintMovedDescription": "Les jetons sont émis par médicament. Ouvrez un médicament et activez son point de terminaison API pour obtenir un jeton limité à ce seul médicament.", @@ -5854,17 +5872,14 @@ "title": "Apple Health", "description": "La synchronisation en direct passe par l’app HealthLog pour iOS, et non par une connexion web ou OAuth.", "status": { - "checking": "Vérification des données Apple Health…", - "dataReceived": "Données Apple Health reçues", - "setup": "Configurer dans l’app iOS", "unavailable": "Statut Apple Health indisponible" }, - "lastDataLabel": "Dernières données Apple Health :", "setupTitle": "Activer la synchronisation en direct", "permissionStep": "Dans l’app HealthLog pour iOS, ouvre Réglages → Apple Health et autorise les catégories de santé que tu souhaites partager.", "backgroundStep": "Laisse l’actualisation en arrière-plan activée. L’app synchronise automatiquement les nouvelles données au premier plan et lorsque iOS lui accorde du temps en arrière-plan.", "importNote": "Besoin d’un ancien historique ou impossible d’utiliser l’app iOS ? L’envoi XML/ZIP Apple Health est une solution ponctuelle et n’active pas la synchronisation en direct.", - "importAction": "Ouvrir l’import ponctuel" + "importAction": "Ouvrir l’import ponctuel", + "staleNote": "Aucune donnée Apple Health n'est arrivée depuis plus d'une semaine. Ouvrez l'app HealthLog sur votre iPhone et vérifiez les autorisations Apple Health." }, "garminInfo": { "title": "Garmin", diff --git a/messages/it.json b/messages/it.json index ada7a6ab9..114b1fc07 100644 --- a/messages/it.json +++ b/messages/it.json @@ -5273,7 +5273,7 @@ "integrationPill": { "connected": "Connesso", "errorReconnect": "Errore — riconnetti", - "warningServerError": "Connesso, errore del server", + "warningServerError": "Connesso, problema di sincronizzazione", "parkedReconnect": "In pausa — riconnettere manualmente", "notConnected": "Non connesso", "justNow": "proprio ora", @@ -5283,8 +5283,26 @@ "ariaLabel": "Stato dell’integrazione", "resumeCta": "Riconnetti", "resumeSuccess": "Integrazione ripresa", - "resumeError": "Riconnessione fallita" + "resumeError": "Riconnessione fallita", + "stale": "Connesso, nessun dato recente", + "stalled": "Sincronizzazione ferma", + "pendingSetup": "In attesa dei primi dati" }, + "integrationFreshness": { + "title": "Dati ricevuti", + "summary": "{count} tipi di dati · più recente {relative}", + "quiet": "{count} in silenzio", + "workouts": "Allenamenti" + }, + "integrationFallback": { + "description": "Configurato su questo account ma senza una scheda di impostazioni propria. Questa riga esiste perché una connessione non possa smettere di funzionare senza che nessuno se ne accorga.", + "lastAttempt": "Ultimo tentativo {relative}", + "noAttempt": "Nessun tentativo di sincronizzazione registrato" + }, + "withingsViewData": "Vedi le tue misurazioni", + "whoopViewData": "Vedi il tuo recupero", + "fitbitViewData": "Vedi il tuo sonno", + "googleHealthViewData": "Vedi i tuoi passi", "apiTokens": "Token API", "apiTokensDescription": "Rivedi e revoca i token API per la registrazione esterna delle assunzioni di farmaci. I token vengono emessi per singolo farmaco dal relativo interruttore dell’endpoint API.", "tokenMintMovedDescription": "I token vengono emessi per singolo farmaco. Apri un farmaco e attiva il suo endpoint API per ottenere un token valido solo per quel farmaco.", @@ -5844,17 +5862,14 @@ "title": "Apple Health", "description": "La sincronizzazione in tempo reale avviene tramite l’app HealthLog per iOS, non tramite una connessione web o OAuth.", "status": { - "checking": "Controllo dei dati Apple Health…", - "dataReceived": "Dati Apple Health ricevuti", - "setup": "Configura nell’app iOS", "unavailable": "Stato di Apple Health non disponibile" }, - "lastDataLabel": "Ultimi dati Apple Health:", "setupTitle": "Attiva la sincronizzazione in tempo reale", "permissionStep": "Nell’app HealthLog per iOS, apri Impostazioni → Apple Health e autorizza le categorie di salute che vuoi condividere.", "backgroundStep": "Mantieni attivo l’aggiornamento in background. L’app sincronizza automaticamente i nuovi dati in primo piano e quando iOS concede tempo in background.", "importNote": "Ti serve lo storico precedente o non puoi usare l’app iOS? Il caricamento XML/ZIP di Apple Health è un’alternativa una tantum e non attiva la sincronizzazione in tempo reale.", - "importAction": "Apri importazione una tantum" + "importAction": "Apri importazione una tantum", + "staleNote": "Da più di una settimana non arrivano dati da Apple Health. Apri l'app HealthLog sull'iPhone e controlla i permessi di Apple Health." }, "garminInfo": { "title": "Garmin", diff --git a/messages/pl.json b/messages/pl.json index 309fb99b2..85bcb9a2f 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -5267,7 +5267,7 @@ "integrationPill": { "connected": "Połączono", "errorReconnect": "Błąd — połącz ponownie", - "warningServerError": "Połączono, błąd serwera", + "warningServerError": "Połączono, problem z synchronizacją", "parkedReconnect": "Wstrzymane — połącz ręcznie", "notConnected": "Nie połączono", "justNow": "przed chwilą", @@ -5277,8 +5277,26 @@ "ariaLabel": "Status integracji", "resumeCta": "Połącz ponownie", "resumeSuccess": "Integracja wznowiona", - "resumeError": "Ponowne połączenie nieudane" + "resumeError": "Ponowne połączenie nieudane", + "stale": "Połączono, brak nowych danych", + "stalled": "Synchronizacja zatrzymana", + "pendingSetup": "Czekam na pierwsze dane" }, + "integrationFreshness": { + "title": "Odebrane dane", + "summary": "{count} typów danych · najnowsze {relative}", + "quiet": "{count} bez danych", + "workouts": "Treningi" + }, + "integrationFallback": { + "description": "Skonfigurowane na tym koncie, ale bez własnej karty ustawień. Ten wiersz istnieje po to, by przerwane połączenie nie pozostało niezauważone.", + "lastAttempt": "Ostatnia próba {relative}", + "noAttempt": "Brak zapisanej próby synchronizacji" + }, + "withingsViewData": "Zobacz swoje pomiary", + "whoopViewData": "Zobacz swoją regenerację", + "fitbitViewData": "Zobacz swój sen", + "googleHealthViewData": "Zobacz swoje kroki", "apiTokens": "Tokeny API", "apiTokensDescription": "Przeglądaj i unieważniaj tokeny API do zewnętrznego rejestrowania przyjęć leków. Tokeny są wydawane dla pojedynczego leku z jego przełącznika punktu końcowego API.", "tokenMintMovedDescription": "Tokeny są wydawane dla pojedynczego leku. Otwórz lek i włącz jego punkt końcowy API, aby otrzymać token obejmujący wyłącznie ten lek.", @@ -5838,17 +5856,14 @@ "title": "Apple Health", "description": "Synchronizacja na żywo odbywa się przez aplikację HealthLog na iOS, a nie przez połączenie internetowe ani OAuth.", "status": { - "checking": "Sprawdzanie danych Apple Health…", - "dataReceived": "Odebrano dane Apple Health", - "setup": "Skonfiguruj w aplikacji iOS", "unavailable": "Stan Apple Health jest niedostępny" }, - "lastDataLabel": "Najnowsze dane Apple Health:", "setupTitle": "Włącz synchronizację na żywo", "permissionStep": "W aplikacji HealthLog na iOS otwórz Ustawienia → Apple Health i zezwól na udostępnianie wybranych kategorii zdrowotnych.", "backgroundStep": "Pozostaw włączone odświeżanie aplikacji w tle. Aplikacja synchronizuje nowe dane automatycznie na pierwszym planie oraz gdy iOS przydzieli jej czas w tle.", "importNote": "Potrzebujesz starszej historii lub nie możesz użyć aplikacji iOS? Przesłanie pliku XML/ZIP z Apple Health jest jednorazową alternatywą i nie włącza synchronizacji na żywo.", - "importAction": "Otwórz jednorazowy import" + "importAction": "Otwórz jednorazowy import", + "staleNote": "Od ponad tygodnia nie napłynęły żadne dane z Apple Health. Otwórz aplikację HealthLog na iPhonie i sprawdź uprawnienia Apple Health." }, "garminInfo": { "title": "Garmin", diff --git a/src/components/settings/integration-status-pill.tsx b/src/components/settings/integration-status-pill.tsx index b65c4adb9..35d8ef14d 100644 --- a/src/components/settings/integration-status-pill.tsx +++ b/src/components/settings/integration-status-pill.tsx @@ -4,6 +4,8 @@ import { CheckCircle2, AlertTriangle, CircleSlash, + Clock, + Info, PauseCircle, } from "lucide-react"; @@ -32,20 +34,33 @@ import { cn } from "@/lib/utils"; */ /** - * Pill states. + * Pill states — a thin projection of the server's sync-health verdict. * * - connected → happy path; relative "last sync" suffix is shown. - * - warning → connected but the upstream just returned a - * contract-mismatch error (v1.4.43 W4). - * - error → reauth-required / persistent transient error; - * "Error — reconnect" copy. - * - parked → v1.4.43 W14: >24h of persistent failures put the - * integration to sleep; "Pausiert — manuell wieder - * verbinden" copy + reconnect CTA. + * - stale → connected, but no new data for a while; suffix shown. + * - stalled → the sync stopped even attempting; suffix shown. + * - warning → attempting and failing (a 5xx, a rate limit, an upstream + * contract mismatch). Not the user's to fix. + * - error → reauth-required; "Error — reconnect" copy. + * - parked → >24h of persistent failures put the integration to + * sleep; "Paused — reconnect manually" copy + resume CTA. + * - pending-setup → configured but nothing has arrived yet. * - disconnected → user clicked Disconnect; "Not connected" copy. + * + * **Red is reserved for "your action fixes this".** A transient upstream + * failure carries the warning tone, not the destructive one: telling a user to + * reconnect cannot help against a 503, and ten fruitless reconnect clicks are + * the cost of saying otherwise. */ export type IntegrationPillState = - "connected" | "warning" | "error" | "parked" | "disconnected"; + | "connected" + | "stale" + | "stalled" + | "warning" + | "error" + | "parked" + | "pending-setup" + | "disconnected"; interface IntegrationStatusPillProps { state: IntegrationPillState; @@ -56,6 +71,8 @@ interface IntegrationStatusPillProps { lastSyncAt: Date | string | null; /** Override "now" for deterministic testing. Defaults to `new Date()`. */ now?: Date; + /** Override the testid so a card can keep its own established e2e hook. */ + testId?: string; className?: string; } @@ -68,7 +85,7 @@ function toDate(value: Date | string): Date { * string. We keep the buckets small and fixed (just-now / minutes / * hours / days) because the pill must fit on a 360 px wide card. */ -function formatRelative( +export function formatRelative( deltaMs: number, t: (key: string, params?: Record) => string, ): string { @@ -85,26 +102,37 @@ function formatRelative( return t("settings.integrationPill.daysAgo", { count: days }); } +/** The states that paint the semantic warning chip rather than a variant. */ +const WARNING_STATES = new Set([ + "stale", + "stalled", + "warning", + "parked", +]); + +/** The states whose label carries an inline relative timestamp. */ +const RELATIVE_STATES = new Set([ + "connected", + "stale", + "stalled", +]); + export function IntegrationStatusPill({ state, lastSyncAt, now, + testId, className, }: IntegrationStatusPillProps) { const { t } = useTranslations(); - // chipClass only applies to the `connected`, `warning`, and `parked` - // branches — `error` falls back to `variant="destructive"` and - // `disconnected` to `variant="outline"`. The warning and parked chips - // both carry the semantic `warning` tone (an AA-safe amber that clears - // contrast in light mode); their distinct labels + icons carry the - // meaning — "connected, but the upstream returned a contract-level error - // a reauth won't fix" vs "tried to keep going for 24h but the mismatch - // persists, manual intervention required" — distinct from the red - // reconnect pill (`error`). + // The chip classes apply to `connected` and to every warning-tone state — + // `error` falls back to `variant="destructive"`, `disconnected` and + // `pending-setup` to `variant="outline"`. The warning tone is an AA-safe + // amber that clears contrast in light mode; the distinct labels + icons + // carry which flavour of "not right" this is. const connectedChipClass = "border-success/30 bg-success/15 text-success"; const warningChipClass = "border-warning/30 bg-warning/15 text-warning"; - const parkedChipClass = "border-warning/30 bg-warning/15 text-warning"; let label: string; let icon: React.ReactNode; @@ -114,6 +142,14 @@ export function IntegrationStatusPill({ label = t("settings.integrationPill.connected"); icon =