feat(backup): carry the health profile and custom metrics through export and restore

Both were classified as backed up when the backup plan was written and carried
by nothing. The custom metric is live data loss: a series the account defined
itself is the one kind no integration can re-sync, so a restore rebuilt the
account without the metric and every one of its readings, and reported success.

Builder and restore live in one file beside each other, the way the cycle pair
does — a grep of the restore route alone does not find either, because the route
delegates to both.

Readings nest inside the metric that owns them rather than referencing it by
name, so the restore has no parent to look up and no lookup to miss. A duplicate
metric name and an undecodable ciphertext field both throw and name the value.
Intake events whose medication did not restore now throw with the unresolved
reference instead of being filtered away into a dose history that comes back
short while the restore reports success.
This commit is contained in:
Marc-André Bombeck committed 2026-07-27 23:04:44 +02:00
1 parent ffe97d9ad9
commit c6593b292f
15 files changed
+826 -7

No files matched your search

@@ -18,6 +18,10 @@ vi.mock("@/lib/db", () => ({
moodEntry: { findMany: vi.fn() },
moodTag: { findMany: vi.fn() },
nutrientIntakeDay: { findMany: vi.fn().mockResolvedValue([]) },
// Durable self-context + user-defined metrics, read by
// `buildProfileBackupSection`.
userHealthProfile: { findUnique: vi.fn().mockResolvedValue(null) },
customMetric: { findMany: vi.fn().mockResolvedValue([]) },
// v1.15.0 — cycle tables read by the full-backup helper.
cycleProfile: { findUnique: vi.fn() },
menstrualCycle: { findMany: vi.fn() },
@@ -16,6 +16,10 @@ vi.mock("@/lib/db", () => ({
moodEntry: { findMany: vi.fn().mockResolvedValue([]) },
moodTag: { findMany: vi.fn().mockResolvedValue([]) },
nutrientIntakeDay: { findMany: vi.fn().mockResolvedValue([]) },
// Durable self-context + user-defined metrics, read by
// `buildProfileBackupSection`.
userHealthProfile: { findUnique: vi.fn().mockResolvedValue(null) },
customMetric: { findMany: vi.fn().mockResolvedValue([]) },
cycleProfile: { findUnique: vi.fn().mockResolvedValue(null) },
menstrualCycle: { findMany: vi.fn().mockResolvedValue([]) },
cycleDayLog: { findMany: vi.fn().mockResolvedValue([]) },
+3
View File
@@ -117,6 +117,9 @@ export const POST = apiHandler(async (request: NextRequest) => {
export_workout_count: counts.workouts,
export_document_count: counts.documents,
export_nutrient_day_count: counts.nutrientDays,
export_health_profile_count: counts.healthProfile,
export_custom_metric_count: counts.customMetrics,
export_custom_metric_entry_count: counts.customMetricEntries,
export_archive_bytes: archive.byteLength,
},
});
+3
View File
@@ -76,6 +76,9 @@ export const GET = apiHandler(async (request: NextRequest) => {
export_workout_count: counts.workouts,
export_document_count: counts.documents,
export_nutrient_day_count: counts.nutrientDays,
export_health_profile_count: counts.healthProfile,
export_custom_metric_count: counts.customMetrics,
export_custom_metric_entry_count: counts.customMetricEntries,
},
});