diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml
index 8c734b58e..dac00eb9e 100644
--- a/docs/api/openapi.yaml
+++ b/docs/api/openapi.yaml
@@ -12100,10 +12100,11 @@ components:
items:
$ref: "#/components/schemas/AppleHealthBatchEntry"
syncTrigger:
- description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake)
- so an operator can see, per batch, which trigger produced it. Recorded on the ingest wide event and nowhere
- else — it does not affect dedup, attribution, or how any sample is stored. Optional and backward-compatible:
- pre-#66 clients omit it."
+ description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake).
+ Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt`
+ for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not
+ affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients
+ omit it."
type: string
enum:
- foreground
@@ -32631,6 +32632,27 @@ components:
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When HealthKit last synced for this user; null when never (and always null on the PATCH echo).
+ lastSyncTrigger:
+ description: What the client declared triggered the most recent accepted batch. Null when the client sent no trigger —
+ an older build, or a batch that predates the field. Present on the GET read; omitted from the PATCH echo.
+ anyOf:
+ - type: string
+ enum:
+ - foreground
+ - background
+ - push
+ description: What triggered a HealthKit batch.
+ - type: "null"
+ lastBackgroundSyncAt:
+ description: When a batch last arrived without the app being opened (a `background` or `push` trigger). Null when that
+ has never happened. `lastSyncedAt` alone cannot separate a phone that delivers on its own from one that only
+ delivers while the app is open; this field is what does. Present on the GET read; omitted from the PATCH
+ echo.
+ anyOf:
+ - type: string
+ format: date-time
+ pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
+ - type: "null"
syncHealth:
description: Present on the GET read; omitted from the PATCH echo.
$ref: "#/components/schemas/SyncHealth"
diff --git a/messages/de.json b/messages/de.json
index 6cacb7616..5c3cc6864 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "vor {count} min",
"hoursAgo": "vor {count} Std.",
"daysAgo": "vor {count} d",
- "ariaLabel": "Integrationsstatus",
"resumeCta": "Wieder verbinden",
"resumeSuccess": "Integration wieder aktiv",
"resumeError": "Wieder verbinden fehlgeschlagen",
@@ -5841,7 +5840,20 @@
"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",
- "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."
+ "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.",
+ "delivery": {
+ "title": "Übermittlung",
+ "lastTriggerLabel": "Letzte Synchronisierung kam",
+ "lastBackgroundLabel": "Letzte Hintergrund-Synchronisierung",
+ "backgroundNever": "Nie — bisher kam jede Synchronisierung bei geöffneter App an",
+ "noSyncYet": "Es ist noch keine Synchronisierung angekommen, daher gibt es hier nichts zu berichten.",
+ "trigger": {
+ "foreground": "bei geöffneter App",
+ "background": "im Hintergrund",
+ "push": "durch einen Push-Weckruf",
+ "unknown": "Auslöser von der App nicht gemeldet"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/messages/en.json b/messages/en.json
index f7e401020..f93b91fdf 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "{count} min ago",
"hoursAgo": "{count} h ago",
"daysAgo": "{count} d ago",
- "ariaLabel": "Integration status",
"resumeCta": "Reconnect",
"resumeSuccess": "Integration resumed",
"resumeError": "Reconnect failed",
@@ -5841,7 +5840,20 @@
"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",
- "staleNote": "No Apple Health data has arrived for over a week. Open the HealthLog app on your iPhone and check the Apple Health permissions."
+ "staleNote": "No Apple Health data has arrived for over a week. Open the HealthLog app on your iPhone and check the Apple Health permissions.",
+ "delivery": {
+ "title": "Delivery",
+ "lastTriggerLabel": "Latest sync arrived",
+ "lastBackgroundLabel": "Last background sync",
+ "backgroundNever": "Never — every sync so far arrived with the app open",
+ "noSyncYet": "No sync has arrived yet, so there is nothing to report here.",
+ "trigger": {
+ "foreground": "with the app open",
+ "background": "in the background",
+ "push": "from a push wake",
+ "unknown": "trigger not reported by the app"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/messages/es.json b/messages/es.json
index a856d5850..a1248c1fe 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "hace {count} min",
"hoursAgo": "hace {count} h",
"daysAgo": "hace {count} d",
- "ariaLabel": "Estado de la integración",
"resumeCta": "Reconectar",
"resumeSuccess": "Integración reanudada",
"resumeError": "Reconexión fallida",
@@ -5841,7 +5840,20 @@
"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",
- "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."
+ "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.",
+ "delivery": {
+ "title": "Entrega",
+ "lastTriggerLabel": "La última sincronización llegó",
+ "lastBackgroundLabel": "Última sincronización en segundo plano",
+ "backgroundNever": "Nunca — hasta ahora todas las sincronizaciones llegaron con la app abierta",
+ "noSyncYet": "Aún no ha llegado ninguna sincronización, así que no hay nada que informar aquí.",
+ "trigger": {
+ "foreground": "con la app abierta",
+ "background": "en segundo plano",
+ "push": "desde un aviso push",
+ "unknown": "la app no informó del desencadenante"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/messages/fr.json b/messages/fr.json
index 052b02a26..79d87c87a 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "il y a {count} min",
"hoursAgo": "il y a {count} h",
"daysAgo": "il y a {count} j",
- "ariaLabel": "Statut de l’intégration",
"resumeCta": "Reconnecter",
"resumeSuccess": "Intégration reprise",
"resumeError": "Échec de la reconnexion",
@@ -5841,7 +5840,20 @@
"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",
- "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."
+ "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.",
+ "delivery": {
+ "title": "Livraison",
+ "lastTriggerLabel": "La dernière synchronisation est arrivée",
+ "lastBackgroundLabel": "Dernière synchronisation en arrière-plan",
+ "backgroundNever": "Jamais — jusqu'ici chaque synchronisation est arrivée avec l'app ouverte",
+ "noSyncYet": "Aucune synchronisation n'est encore arrivée, il n'y a donc rien à signaler ici.",
+ "trigger": {
+ "foreground": "avec l'app ouverte",
+ "background": "en arrière-plan",
+ "push": "depuis un réveil push",
+ "unknown": "déclencheur non signalé par l'app"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/messages/it.json b/messages/it.json
index deda44729..a192e831f 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "{count} min fa",
"hoursAgo": "{count} h fa",
"daysAgo": "{count} g fa",
- "ariaLabel": "Stato dell’integrazione",
"resumeCta": "Riconnetti",
"resumeSuccess": "Integrazione ripresa",
"resumeError": "Riconnessione fallita",
@@ -5841,7 +5840,20 @@
"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",
- "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."
+ "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.",
+ "delivery": {
+ "title": "Consegna",
+ "lastTriggerLabel": "L'ultima sincronizzazione è arrivata",
+ "lastBackgroundLabel": "Ultima sincronizzazione in background",
+ "backgroundNever": "Mai — finora ogni sincronizzazione è arrivata con l'app aperta",
+ "noSyncYet": "Non è ancora arrivata alcuna sincronizzazione, quindi qui non c'è nulla da riferire.",
+ "trigger": {
+ "foreground": "con l'app aperta",
+ "background": "in background",
+ "push": "da una sveglia push",
+ "unknown": "trigger non segnalato dall'app"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/messages/pl.json b/messages/pl.json
index cb49beb28..c7281731c 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -5297,7 +5297,6 @@
"minutesAgo": "{count} min temu",
"hoursAgo": "{count} godz. temu",
"daysAgo": "{count} dni temu",
- "ariaLabel": "Status integracji",
"resumeCta": "Połącz ponownie",
"resumeSuccess": "Integracja wznowiona",
"resumeError": "Ponowne połączenie nieudane",
@@ -5841,7 +5840,20 @@
"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",
- "staleNote": "Od ponad tygodnia nie napłynęły żadne dane z Apple Health. Otwórz aplikację HealthLog na iPhonie i sprawdź uprawnienia Apple Health."
+ "staleNote": "Od ponad tygodnia nie napłynęły żadne dane z Apple Health. Otwórz aplikację HealthLog na iPhonie i sprawdź uprawnienia Apple Health.",
+ "delivery": {
+ "title": "Dostarczanie",
+ "lastTriggerLabel": "Ostatnia synchronizacja dotarła",
+ "lastBackgroundLabel": "Ostatnia synchronizacja w tle",
+ "backgroundNever": "Nigdy — jak dotąd każda synchronizacja dotarła przy otwartej aplikacji",
+ "noSyncYet": "Żadna synchronizacja jeszcze nie dotarła, więc nie ma tu nic do zgłoszenia.",
+ "trigger": {
+ "foreground": "przy otwartej aplikacji",
+ "background": "w tle",
+ "push": "z wybudzenia push",
+ "unknown": "aplikacja nie zgłosiła wyzwalacza"
+ }
+ }
},
"garminInfo": {
"title": "Garmin",
diff --git a/prisma/migrations/0281_integration_failing_legs_and_healthkit_trigger/migration.sql b/prisma/migrations/0281_integration_failing_legs_and_healthkit_trigger/migration.sql
new file mode 100644
index 000000000..c49c4a46c
--- /dev/null
+++ b/prisma/migrations/0281_integration_failing_legs_and_healthkit_trigger/migration.sql
@@ -0,0 +1,38 @@
+-- The integration ledger stops erasing itself, and the HealthKit trigger gets
+-- somewhere to live.
+--
+-- 1. `failing_leg` → `failing_legs`
+--
+-- `failing_leg` was a single last-writer-wins slot. Withings runs four legs on
+-- four crons and WHOOP four resources on four more, so two legs failing in
+-- overlapping windows overwrote each other: the row remembered whichever failed
+-- last, and a success from that leg then cleared an error the other one was
+-- still causing. The slot becomes a set. A failure adds its leg, a success
+-- removes its own, and the row's error state, per-kind buckets and streak
+-- anchor clear only when the set is empty.
+--
+-- The array is ordered oldest-failure-first, so the last element is the leg that
+-- owns whatever message `last_error` holds.
+--
+-- 2. `users`: the HealthKit sync trigger, persisted
+--
+-- The batch route has accepted an optional `syncTrigger` since v1.32.8 and put
+-- it on the wide event, where no human reads it. Persisting it — plus the
+-- instant of the most recent background-triggered batch — is what lets the
+-- Apple Health card answer whether the phone is delivering on its own or only
+-- when the app is open. Both nullable and additive.
+
+ALTER TABLE "integration_statuses" ADD COLUMN "failing_legs" JSONB;
+
+-- Carry every attributed leg forward as a one-element set. Rows whose
+-- `failing_leg` is NULL become NULL too, not an empty array: NULL and `[]` both
+-- read as "failing, but unattributed", and leaving them NULL keeps the backfill
+-- to the rows that actually carry information.
+UPDATE "integration_statuses"
+SET "failing_legs" = jsonb_build_array("failing_leg")
+WHERE "failing_leg" IS NOT NULL;
+
+ALTER TABLE "integration_statuses" DROP COLUMN "failing_leg";
+
+ALTER TABLE "users" ADD COLUMN "healthkit_last_sync_trigger" TEXT;
+ALTER TABLE "users" ADD COLUMN "healthkit_last_background_sync_at" TIMESTAMP(3);
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 367884b30..60e1fecf7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -319,9 +319,9 @@ model User {
// must never be the OpenAI key. Separate columns make that structural
// instead of a comment. The base URL passes the same SSRF floor as the
// Local provider (`isPublicUrl` unless the operator allowlisted the host).
- aiCompatBaseUrl String? @map("ai_compat_base_url")
- aiCompatKeyEncrypted String? @map("ai_compat_key_encrypted")
- aiCompatModel String? @map("ai_compat_model")
+ aiCompatBaseUrl String? @map("ai_compat_base_url")
+ aiCompatKeyEncrypted String? @map("ai_compat_key_encrypted")
+ aiCompatModel String? @map("ai_compat_model")
// v1.4.16 phase B5b — ordered fallback chain for multi-provider AI
// redundancy. Each entry is `{ providerType, priority, enabled }`,
@@ -415,8 +415,20 @@ model User {
// HealthKit (iOS) integration config — list of per-metric direction toggles.
// Shape: { entries: [{ id, kind, direction, enabled }], lastSyncedAt }
- healthKitConfigJson Json? @map("healthkit_config_json")
- healthKitLastSyncedAt DateTime? @map("healthkit_last_synced_at")
+ healthKitConfigJson Json? @map("healthkit_config_json")
+ healthKitLastSyncedAt DateTime? @map("healthkit_last_synced_at")
+ // What triggered the most recent accepted HealthKit batch — "foreground",
+ // "background" or "push", exactly as the client declared it on the write.
+ // Null when the client sent no trigger (an older build, or a batch that
+ // predates the field).
+ //
+ // `healthKitLastBackgroundSyncAt` dates the most recent batch that arrived
+ // WITHOUT the app being opened. It is the one instrument that tells "the
+ // phone is delivering on its own" apart from "every arrival happened while
+ // the app was on screen", which is otherwise indistinguishable from the
+ // outside — `healthKitLastSyncedAt` alone looks identical either way.
+ healthKitLastSyncTrigger String? @map("healthkit_last_sync_trigger")
+ healthKitLastBackgroundSyncAt DateTime? @map("healthkit_last_background_sync_at")
// Display name (optional override of username for greetings).
displayName String? @map("display_name")
@@ -4175,11 +4187,21 @@ model IntegrationStatus {
// blip apart from a dead pipe. Cleared by a success that is entitled to
// clear it, by a reconnect, a disconnect, and a resume.
failingSinceAt DateTime? @map("failing_since_at")
- // Which sync leg owns the error currently on the row. `null` = the provider
- // has a single leg, or the failure predates leg attribution. A success from
- // a DIFFERENT leg may not clear an error this one did not cause — see
- // `recordSyncSuccess`.
- failingLeg String? @map("failing_leg")
+ // Every sync leg currently known to be failing, oldest failure first, as a
+ // JSON string array. A single slot could only ever remember one leg, so two
+ // legs of the same provider failing in overlapping windows erased each other
+ // and the row forgot one of them. A failure adds its leg; a success removes
+ // its own; the error state, the buckets and `failingSinceAt` clear only when
+ // the array is empty — see `recordSyncSuccess`.
+ //
+ // The LAST element is the leg that failed most recently and therefore owns
+ // whatever message `lastError` holds. When that leg recovers while others are
+ // still failing, `lastError` is cleared rather than left describing a leg that
+ // is now healthy.
+ //
+ // An empty array means "failing, but unattributed" (a single-leg provider, or
+ // a failure recorded without a leg), under which any success clears the row.
+ failingLegs Json? @map("failing_legs")
alertedAt DateTime? @map("alerted_at") // last time admin Telegram alert fired (idempotency guard)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
diff --git a/src/app/api/integrations/healthkit/route.ts b/src/app/api/integrations/healthkit/route.ts
index b2b354faa..8509b12ba 100644
--- a/src/app/api/integrations/healthkit/route.ts
+++ b/src/app/api/integrations/healthkit/route.ts
@@ -130,12 +130,27 @@ export const GET = apiHandler(async () => {
const row = await prisma.user.findUnique({
where: { id: user.id },
- select: { healthKitConfigJson: true, healthKitLastSyncedAt: true },
+ select: {
+ healthKitConfigJson: true,
+ healthKitLastSyncedAt: true,
+ healthKitLastSyncTrigger: true,
+ healthKitLastBackgroundSyncAt: true,
+ },
});
const stored = parseStored(row?.healthKitConfigJson ?? null);
const entries = mergeWithDefaults(stored);
const lastSyncedAt = row?.healthKitLastSyncedAt?.toISOString() ?? null;
+ // What triggered the most recent accepted batch, and when data last arrived
+ // without the app being opened. `lastSyncedAt` alone cannot separate those
+ // two cases, which is the whole diagnostic: a card that only ever says "last
+ // sync 10 minutes ago" reads the same whether the phone is delivering by
+ // itself or whether every arrival was the user opening the app. Null in
+ // either field is honest absence — an older client that declares no trigger,
+ // or a pipe that has never delivered in the background.
+ const lastSyncTrigger = row?.healthKitLastSyncTrigger ?? null;
+ const lastBackgroundSyncAt =
+ row?.healthKitLastBackgroundSyncAt?.toISOString() ?? null;
// Apple Health is push-based: the iOS app delivers and nothing here polls, so
// liveness is a data question, not an attempt question. Without a threshold a
@@ -157,6 +172,8 @@ export const GET = apiHandler(async () => {
return apiSuccess({
entries,
lastSyncedAt,
+ lastSyncTrigger,
+ lastBackgroundSyncAt,
syncHealth,
metricFreshness: classifyMetricFreshness(samples, syncHealth.verdict),
});
diff --git a/src/app/api/measurements/batch/route.ts b/src/app/api/measurements/batch/route.ts
index 77bd7252f..68cee2ff2 100644
--- a/src/app/api/measurements/batch/route.ts
+++ b/src/app/api/measurements/batch/route.ts
@@ -169,12 +169,14 @@ const batchEntrySchema = z.object({
});
// v1.32.8 (iOS #66) — optional per-REQUEST diagnostic tag naming what woke the
-// client for this sync. Purely observational: it flows into the
-// `measurement.batch.ingest` wide event and NOTHING else — never persisted,
-// never part of the dedup key, never an input to attribution or how a sample is
-// stored. Only the client knows its own wake reason, so it is client-asserted
-// by necessity; being diagnostic-only, that carries no trust surface. Optional
-// so every pre-#66 caller stays byte-for-byte unchanged.
+// client for this sync. Purely diagnostic: it flows into the
+// `measurement.batch.ingest` wide event and onto the account's HealthKit
+// diagnostic columns, which the Apple Health card reads. It is never part of
+// the dedup key and never an input to attribution or how a sample is stored —
+// sending it or omitting it produces byte-identical row outcomes. Only the
+// client knows its own wake reason, so it is client-asserted by necessity;
+// being diagnostic-only, that carries no trust surface. Optional so every
+// pre-#66 caller stays byte-for-byte unchanged.
const syncTriggerEnum = z.enum(["foreground", "background", "push"]);
const batchPayloadSchema = z.object({
@@ -644,10 +646,28 @@ async function postBatch(request: NextRequest): Promise
+ {t("settings.appleHealth.delivery.noSyncYet")}
+
+ {t("settings.appleHealth.delivery.title")}
+
+ {status.lastSyncedAt ? (
+
+
+ ) : (
+