feat(nutrients): tell the account holder when a sync arrived but nothing landed

GET /api/nutrients now looks up the most recent nutrient.batch.ingest
audit row when the window is genuinely empty, and surfaces it as
lastAttempt only when that call landed nothing (inserted === 0 &&
updated === 0). A real success elsewhere is left alone, so an empty
window for an unrelated reason never gets reframed as a failure.

The lookup runs on the zero-rows path only, never on the happy path,
and stays counts-and-reason-codes only - no user-typed content, no
per-entry health values. The reason code never reaches the page as
text: each of the four closed skip reasons maps to its own human
sentence, and the two that have no useful action for the account
holder (unit_mismatch, upsert_failed) say plainly that the entries
couldn't be stored rather than inventing advice. day_invalid is the
one case worth naming a real cause for - a clock or time zone mismatch
on the sending side - without claiming it was a phone, since any
token holder can call this route.

This depends on AuditLog retention: cleanupOldAuditLogs prunes rows
past 365 days by default, so a dormant account's last failed attempt
can age out and the explanation silently reverts to the plain empty
state. Documented at the lookup for whoever next touches that job.
This commit is contained in:
Marc-André Bombeck committed 2026-07-30 23:30:28 +02:00
1 parent 3ec3a8c6aa
commit 12d3239de0
12 files changed
+374 -17

No files matched your search

+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Nährstoffe aktivieren",
"emptyTitle": "Noch keine Nährstoffdaten",
"emptyDescription": "Daten kommen aus der Synchronisierung der Gesundheits-App deines Telefons oder einem manuellen Wassereintrag.",
"loadError": "Deine Nährstoffdaten konnten nicht geladen werden"
"loadError": "Deine Nährstoffdaten konnten nicht geladen werden",
"lastAttemptTitle": "Eine Synchronisierung ist angekommen, aber nichts konnte gespeichert werden",
"lastAttemptAt": "Letzter Versuch: {date}",
"lastAttemptReason": {
"unitMismatch": "Die übermittelten Werte entsprachen nicht den Einheiten, die diese App für diese Nährstoffe erwartet, daher konnten sie nicht gespeichert werden.",
"valueOutOfRange": "Die übermittelten Mengen lagen höher, als diese App plausibel speichern kann, daher konnten sie nicht gesichert werden.",
"dayInvalid": "Die Einträge trugen ein Datum, das der Server nicht akzeptieren konnte. Das kann passieren, wenn Uhrzeit oder Zeitzone auf der sendenden Seite falsch eingestellt sind — das lohnt sich zu prüfen.",
"upsertFailed": "Diese Einträge haben den Server erreicht, aber ein Problem auf unserer Seite hat verhindert, dass sie gespeichert wurden. Du musst nichts weiter tun.",
"unknown": "Diese Einträge sind angekommen, konnten aber nicht gespeichert werden."
}
},
"hydration": {
"referenceMeta": "Richtwert {value} {unit}/Tag · EFSA",
+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Turn on nutrients",
"emptyTitle": "No nutrient data yet",
"emptyDescription": "Data arrives from your phone's health app sync or a manual water entry.",
"loadError": "Your nutrient data could not be loaded"
"loadError": "Your nutrient data could not be loaded",
"lastAttemptTitle": "A sync arrived, but nothing could be stored",
"lastAttemptAt": "Last attempt: {date}",
"lastAttemptReason": {
"unitMismatch": "The values that arrived didn't match the units this app expects for these nutrients, so they couldn't be stored.",
"valueOutOfRange": "The amounts that arrived were higher than this app can plausibly store, so they couldn't be saved.",
"dayInvalid": "The entries carried a date the server couldn't accept. This can happen when the clock or time zone on the sending side is wrong — it's worth checking.",
"upsertFailed": "These entries reached the server, but a problem on this end kept them from being saved. There's nothing you need to do.",
"unknown": "These entries arrived but couldn't be stored."
}
},
"hydration": {
"referenceMeta": "Reference intake {value} {unit}/day · EFSA",
+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Activar nutrientes",
"emptyTitle": "Aún no hay datos de nutrientes",
"emptyDescription": "Los datos llegan desde la sincronización de la app de salud de tu teléfono o desde una entrada manual de agua.",
"loadError": "No se pudieron cargar tus datos de nutrientes"
"loadError": "No se pudieron cargar tus datos de nutrientes",
"lastAttemptTitle": "Se recibió una sincronización, pero no se pudo guardar nada",
"lastAttemptAt": "Último intento: {date}",
"lastAttemptReason": {
"unitMismatch": "Los valores recibidos no coincidían con las unidades que esta app espera para estos nutrientes, así que no se pudieron guardar.",
"valueOutOfRange": "Las cantidades recibidas eran superiores a lo que esta app puede almacenar de forma plausible, así que no se pudieron guardar.",
"dayInvalid": "Las entradas llevaban una fecha que el servidor no pudo aceptar. Esto puede ocurrir cuando el reloj o la zona horaria del lado que envía los datos están mal configurados; merece la pena revisarlo.",
"upsertFailed": "Estas entradas llegaron al servidor, pero un problema de nuestro lado impidió guardarlas. No tienes que hacer nada.",
"unknown": "Estas entradas llegaron, pero no se pudieron guardar."
}
},
"hydration": {
"referenceMeta": "Ingesta de referencia {value} {unit}/día · EFSA",
+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Activer les nutriments",
"emptyTitle": "Pas encore de données nutritionnelles",
"emptyDescription": "Les données proviennent de la synchronisation de l'appli santé de votre téléphone ou d'une saisie manuelle d'eau.",
"loadError": "Tes données de nutriments n'ont pas pu être chargées"
"loadError": "Tes données de nutriments n'ont pas pu être chargées",
"lastAttemptTitle": "Une synchronisation est arrivée, mais rien n'a pu être enregistré",
"lastAttemptAt": "Dernière tentative : {date}",
"lastAttemptReason": {
"unitMismatch": "Les valeurs reçues ne correspondaient pas aux unités attendues par l'appli pour ces nutriments, elles n'ont donc pas pu être enregistrées.",
"valueOutOfRange": "Les quantités reçues étaient supérieures à ce que l'appli peut raisonnablement enregistrer, elles n'ont donc pas pu être sauvegardées.",
"dayInvalid": "Les entrées portaient une date que le serveur n'a pas pu accepter. Cela peut arriver quand l'horloge ou le fuseau horaire du côté qui envoie les données est incorrect — cela vaut la peine de vérifier.",
"upsertFailed": "Ces entrées sont arrivées jusqu'au serveur, mais un problème de notre côté a empêché leur enregistrement. Tu n'as rien à faire.",
"unknown": "Ces entrées sont arrivées mais n'ont pas pu être enregistrées."
}
},
"hydration": {
"referenceMeta": "Apport de référence {value} {unit}/jour · EFSA",
+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Attiva nutrienti",
"emptyTitle": "Ancora nessun dato sui nutrienti",
"emptyDescription": "I dati arrivano dalla sincronizzazione dell'app salute del telefono o da una voce di acqua inserita manualmente.",
"loadError": "Non è stato possibile caricare i tuoi dati sui nutrienti"
"loadError": "Non è stato possibile caricare i tuoi dati sui nutrienti",
"lastAttemptTitle": "È arrivata una sincronizzazione, ma non è stato possibile salvare nulla",
"lastAttemptAt": "Ultimo tentativo: {date}",
"lastAttemptReason": {
"unitMismatch": "I valori ricevuti non corrispondevano alle unità che questa app si aspetta per questi nutrienti, quindi non è stato possibile salvarli.",
"valueOutOfRange": "Le quantità ricevute erano superiori a quanto questa app può plausibilmente memorizzare, quindi non è stato possibile salvarle.",
"dayInvalid": "Le voci riportavano una data che il server non ha potuto accettare. Questo può succedere quando l'orologio o il fuso orario dal lato che invia i dati non sono corretti: vale la pena controllare.",
"upsertFailed": "Queste voci hanno raggiunto il server, ma un problema da questa parte ne ha impedito il salvataggio. Non devi fare nulla.",
"unknown": "Queste voci sono arrivate ma non è stato possibile salvarle."
}
},
"hydration": {
"referenceMeta": "Apporto di riferimento {value} {unit}/giorno · EFSA",
+10 -1
View File
@@ -9111,7 +9111,16 @@
"moduleOffCta": "Włącz składniki odżywcze",
"emptyTitle": "Brak jeszcze danych o składnikach odżywczych",
"emptyDescription": "Dane pochodzą z synchronizacji aplikacji zdrowia telefonu lub z ręcznego wpisu wody.",
"loadError": "Nie udało się wczytać danych o składnikach odżywczych"
"loadError": "Nie udało się wczytać danych o składnikach odżywczych",
"lastAttemptTitle": "Synchronizacja dotarła, ale nic nie udało się zapisać",
"lastAttemptAt": "Ostatnia próba: {date}",
"lastAttemptReason": {
"unitMismatch": "Otrzymane wartości nie pasowały do jednostek, których ta aplikacja oczekuje dla tych składników odżywczych, więc nie udało się ich zapisać.",
"valueOutOfRange": "Otrzymane ilości były wyższe, niż ta aplikacja może wiarygodnie zapisać, więc nie udało się ich zapisać.",
"dayInvalid": "Wpisy zawierały datę, której serwer nie mógł zaakceptować. Może się to zdarzyć, gdy zegar lub strefa czasowa po stronie wysyłającej są nieprawidłowe — warto to sprawdzić.",
"upsertFailed": "Te wpisy dotarły do serwera, ale problem po naszej stronie uniemożliwił ich zapisanie. Nie musisz nic robić.",
"unknown": "Te wpisy dotarły, ale nie udało się ich zapisać."
}
},
"hydration": {
"referenceMeta": "Zalecane spożycie {value} {unit}/dzień · EFSA",