diff --git a/messages/de.json b/messages/de.json index ad76c0b03..503a99d3f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -6243,7 +6243,7 @@ "restore": "Wiederherstellen", "restoreAria": "Backup für {username} wiederherstellen", "restoreTitle": "Aus Backup wiederherstellen", - "restoreDescription": "Dies ersetzt ALLE aktuellen Daten von {username} mit dem Snapshot von {when}. Vorhandene Messungen, Medikamente, Einnahmen, Stimmungseinträge, Benachrichtigungs-Kanäle, Push-Abos und Telegram-Lösch-Aufträge werden entfernt.", + "restoreDescription": "Dies ersetzt ALLE aktuellen Daten von {username} mit dem Snapshot von {when}. Vorhandene Messungen, Medikamente, Einnahmen, Stimmungseinträge, Benachrichtigungs-Kanäle, Push-Abos und Telegram-Lösch-Aufträge werden entfernt. Falls im Snapshot enthalten, überschreiben instanzweite Einstellungen die Konfiguration der gesamten Installation.", "restorePromptLabel": "Tippe RESTORE zum Fortfahren", "restoreConfirm": "Jetzt wiederherstellen", "restoreInProgress": "Wiederherstellung läuft…", diff --git a/messages/en.json b/messages/en.json index 7f141bc88..fce2bb3e3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -6243,7 +6243,7 @@ "restore": "Restore", "restoreAria": "Restore from backup for {username}", "restoreTitle": "Restore from backup", - "restoreDescription": "This will replace ALL current data for {username} with the snapshot from {when}. Existing measurements, medications, intake events, mood entries, notification channels, push subscriptions and queued Telegram deletions will be deleted.", + "restoreDescription": "This will replace ALL current data for {username} with the snapshot from {when}. Existing measurements, medications, intake events, mood entries, notification channels, push subscriptions and queued Telegram deletions will be deleted. If included in the snapshot, instance-wide settings overwrite the configuration for the entire installation.", "restorePromptLabel": "Type RESTORE to continue", "restoreConfirm": "Restore now", "restoreInProgress": "Restoring…", diff --git a/src/app/api/admin/backups/[id]/restore/route.ts b/src/app/api/admin/backups/[id]/restore/route.ts index 7dd1c6afd..0bb3e75ff 100644 --- a/src/app/api/admin/backups/[id]/restore/route.ts +++ b/src/app/api/admin/backups/[id]/restore/route.ts @@ -100,7 +100,7 @@ const handler = apiHandler( details: { reason: "missing_confirmation", backupId: id }, }); return apiError( - "Confirmation token missing — body must include confirm: 'RESTORE'", + "Confirmation token missing — restoring user data and included instance-wide settings requires confirm: 'RESTORE'", 422, ); } @@ -403,13 +403,11 @@ const handler = apiHandler( unit: measurement.unit, source: measurement.source ?? "MANUAL", measuredAt: new Date(measurement.measuredAt), - notes: measurement.notes ?? null, + notes: null, notesEncrypted: - measurement.notesEncrypted !== undefined - ? measurement.notesEncrypted === null - ? null - : decodeEncryptedBytes(measurement.notesEncrypted) - : encryptNote(measurement.notes ?? null), + measurement.notesEncrypted == null + ? encryptNote(measurement.notes ?? null) + : decodeEncryptedBytes(measurement.notesEncrypted), externalId: measurement.externalId ?? null, externalSourceVersion: measurement.externalSourceVersion ?? null, glucoseContext: (measurement.glucoseContext ?? null) as never, diff --git a/src/app/api/export/__tests__/per-type-routes.test.ts b/src/app/api/export/__tests__/per-type-routes.test.ts index f442c3e15..b46280d47 100644 --- a/src/app/api/export/__tests__/per-type-routes.test.ts +++ b/src/app/api/export/__tests__/per-type-routes.test.ts @@ -421,7 +421,7 @@ describe("GET /api/export/full-backup", () => { expect(res.headers.get("content-disposition")).toMatch(/\.json"/); const json = await res.json(); expect(json).toMatchObject({ - schemaVersion: "1", + schemaVersion: "2", userId: "user-1", measurements: [], medications: [], diff --git a/src/components/admin/__tests__/backups-section-empty.test.tsx b/src/components/admin/__tests__/backups-section-empty.test.tsx index 8029c7cf9..bf16a336d 100644 --- a/src/components/admin/__tests__/backups-section-empty.test.tsx +++ b/src/components/admin/__tests__/backups-section-empty.test.tsx @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; +import de from "../../../../messages/de.json"; +import en from "../../../../messages/en.json"; /** * v1.4.15 phase-C5 — `/admin/backups` empty state. @@ -76,4 +78,13 @@ describe("BackupsSection — empty state", () => { const matches = (html.match(/Backup now/g) ?? []).length; expect(matches).toBeGreaterThanOrEqual(2); }); + + it("warns in both catalogs that restore overwrites instance-wide settings", () => { + expect(de.admin.section.backups.restoreDescription).toMatch( + /instanzweite Einstellungen/i, + ); + expect(en.admin.section.backups.restoreDescription).toMatch( + /instance-wide settings/i, + ); + }); }); diff --git a/src/lib/export/__tests__/full-backup-payload.test.ts b/src/lib/export/__tests__/full-backup-payload.test.ts index 30b553b4c..fd5a23f56 100644 --- a/src/lib/export/__tests__/full-backup-payload.test.ts +++ b/src/lib/export/__tests__/full-backup-payload.test.ts @@ -76,7 +76,7 @@ const appSettings = { assistantHealthScoreExplainerEnabled: false, moduleAvailabilityJson: { nutrients: false, insights: true }, documentMaxFileBytes: 12_345_678, - documentQuotaBytes: 9_876_543_210n, + documentQuotaBytes: BigInt("9876543210"), }; function makePrisma() { diff --git a/tests/integration/admin-backups-canonical-roundtrip.test.ts b/tests/integration/admin-backups-canonical-roundtrip.test.ts index 6649ffa2f..d0a24c222 100644 --- a/tests/integration/admin-backups-canonical-roundtrip.test.ts +++ b/tests/integration/admin-backups-canonical-roundtrip.test.ts @@ -159,7 +159,7 @@ describe("canonical disaster-recovery backup round-trip", () => { assistantHealthScoreExplainerEnabled: false, moduleAvailabilityJson: { insights: true, nutrients: false }, documentMaxFileBytes: 12_345_678, - documentQuotaBytes: 9_876_543_210n, + documentQuotaBytes: BigInt("9876543210"), }, }); @@ -917,12 +917,12 @@ describe("canonical disaster-recovery backup round-trip", () => { const originalCreateMany = tx.measurement.createMany.bind( tx.measurement, ); - vi.spyOn(tx.measurement, "upsert").mockImplementation(async (args) => { + vi.spyOn(tx.measurement, "upsert").mockImplementation((args) => { stableWriteQueries += 1; return originalUpsert(args); }); vi.spyOn(tx.measurement, "createMany").mockImplementation( - async (args) => { + (args) => { stableWriteQueries += 1; return originalCreateMany(args); }, diff --git a/tests/integration/admin-backups-restore.test.ts b/tests/integration/admin-backups-restore.test.ts index 952f1a188..6e0ee8b29 100644 --- a/tests/integration/admin-backups-restore.test.ts +++ b/tests/integration/admin-backups-restore.test.ts @@ -100,7 +100,8 @@ describe("POST /api/admin/backups/[id]/restore", () => { unit: "kg", measuredAt: "2026-05-08T07:00:00.000Z", source: "MANUAL", - notes: null, + notes: "private legacy note", + notesEncrypted: null, }, { type: "PULSE", @@ -221,6 +222,8 @@ describe("POST /api/admin/backups/[id]/restore", () => { expect(measurements[0]!.type).toBe("WEIGHT"); expect(measurements[0]!.value).toBe(80.5); expect(measurements[1]!.type).toBe("PULSE"); + expect(measurements[0]!.notes).toBeNull(); + expect(measurements[0]!.notesEncrypted).not.toBeNull(); const medications = await prisma.medication.findMany({ where: { userId: admin.id }, @@ -303,6 +306,8 @@ describe("POST /api/admin/backups/[id]/restore", () => { }); expect(res.status).toBe(422); + const error = (await res.json()) as { error: string }; + expect(error.error).toMatch(/instance-wide settings/i); const denied = await prisma.auditLog.count({ where: { action: "admin.backups.restore.denied",