From d9afb23067a93aa27eb16d89b6ad7cccd908f476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 27 Jul 2026 12:02:01 +0200 Subject: [PATCH] fix(backup): restore the nutrient day totals the export has always carried MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The export has written nutrient day totals since v1.29. The canonical schema never declared the field, so `.passthrough()` carried it through parsing and no reader ever looked at it: every water and vitamin total in the file was discarded on restore, and the restore reported success. Declared in the contract, cleared and re-created beside its siblings, and counted in both places that count — what the file holds and what the restore replaced. A count that stops at the domains someone remembered is how this stayed quiet. --- .../api/admin/backups/[id]/restore/route.ts | 22 ++++++++++++++++++ src/lib/validations/backup.ts | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/app/api/admin/backups/[id]/restore/route.ts b/src/app/api/admin/backups/[id]/restore/route.ts index 879a0a42e..2f5660421 100644 --- a/src/app/api/admin/backups/[id]/restore/route.ts +++ b/src/app/api/admin/backups/[id]/restore/route.ts @@ -58,6 +58,7 @@ interface RestoreResponse { cycleProfile: number; labResults: number; biomarkers: number; + nutrientDays: number; illnessEpisodes: number; allergies: number; familyHistory: number; @@ -328,6 +329,12 @@ const handler = apiHandler( const biomarkers = await tx.biomarker.deleteMany({ where: { userId: ownerId }, }); + // The export has written nutrient day totals since v1.29 and nothing + // here ever read them back: water and vitamin history was carried in + // the file, discarded on restore, and the restore reported success. + const nutrientDays = await tx.nutrientIntakeDay.deleteMany({ + where: { userId: ownerId }, + }); if (payload.appSettings) { const settings = payload.appSettings; @@ -968,6 +975,19 @@ const handler = apiHandler( }); } + if (payload.nutrientDays.length > 0) { + await tx.nutrientIntakeDay.createMany({ + data: payload.nutrientDays.map((n) => ({ + userId: ownerId, + day: n.day, + nutrient: n.nutrient, + amount: n.amount, + unit: n.unit, + source: n.source, + })), + }); + } + if (payload.documents.length > 0) { await tx.inboundDocument.createMany({ data: payload.documents.map((document) => ({ @@ -1018,6 +1038,7 @@ const handler = apiHandler( cycleProfile: cycleCleared.cycleProfile, labResults: labResults.count, biomarkers: biomarkers.count, + nutrientDays: nutrientDays.count, illnessEpisodes: illnessEpisodes.count, allergies: allergies.count, familyHistory: familyHistory.count, @@ -1140,6 +1161,7 @@ const handler = apiHandler( cycleDayLogs: summary.cycleDayLogs, labResults: summary.labResults, biomarkers: summary.biomarkers, + nutrientDays: summary.nutrientDays, illnessEpisodes: summary.illnessEpisodes, illnessDayLogs: summary.illnessDayLogs, allergies: summary.allergies, diff --git a/src/lib/validations/backup.ts b/src/lib/validations/backup.ts index b204181b3..0af8a3c5b 100644 --- a/src/lib/validations/backup.ts +++ b/src/lib/validations/backup.ts @@ -314,6 +314,26 @@ const cycleProfileSchema = z }) .passthrough(); +/** + * A day's total for one nutrient, from one source. + * + * The export has written these since v1.29 and the canonical schema never + * declared them, so `.passthrough()` carried the key through parsing and no + * reader ever looked at it. The restore therefore dropped every water and + * vitamin total on the floor while reporting success. + */ +const nutrientDaySchema = z + .object({ + day: z.string().min(1), + nutrient: z.string().min(1), + amount: z.number(), + unit: z.string().min(1), + // Closed set in app code rather than a DB enum; kept as a string here so a + // future source does not make an old file unparseable. + source: z.string().min(1), + }) + .passthrough(); + /** * A symptom the account created, as opposed to the seeded catalogue. * @@ -595,6 +615,7 @@ export const backupPayloadSchema = z familyHistory: z.array(familyHistoryBackupSchema).default([]), workouts: z.array(workoutBackupSchema).default([]), documents: z.array(documentBackupSchema).default([]), + nutrientDays: z.array(nutrientDaySchema).default([]), manifest: backupManifestSchema.nullable().default(null), }) .passthrough() @@ -632,6 +653,7 @@ export interface BackupSummary { cycleDayLogs: number; /** Lab results in the backup. */ labResults: number; + nutrientDays: number; /** User-scoped biomarker catalog entries in the backup. */ biomarkers: number; /** Illness episodes, including flares/exacerbations. */ @@ -659,6 +681,7 @@ export function summarizeBackup(payload: BackupPayload): BackupSummary { moodEntries: payload.moodEntries.length, cycles: payload.cycles.length, cycleDayLogs: payload.cycleDayLogs.length, + nutrientDays: payload.nutrientDays.length, labResults: payload.labResults.length, biomarkers: payload.biomarkers.length, illnessEpisodes: payload.illnessEpisodes.length,