mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
Several surfaces promised more than the per-account copies deliver: - The full-backup export card said it had the same shape as the weekly copy. It is a portable export with documents as metadata only; it now says so, and that it cannot recreate an account on another server. - The export page offered to "import from a backup". The JSON import adds measurements and mood entries; it is now named that way, and its help states the 16 MB and 10,000-entries-per-list limits from the constants the route enforces. - The weekly copy is labelled an account rollback on this server, and the backups page says a copy cannot rebuild a lost server. The upload says the account must exist here. Two restore refusals pointed to a "disaster-recovery" copy; they now name the copies that exist. - The instance-settings restore option was described as being for rebuilding a host. docs/ops/backup-restore.md is rewritten short and accurate: what each copy is, the deletion grace window, that database dumps keep a deleted account until they age out, how to restore one account, and a link to the disaster-recovery procedure for a lost server.
123 lines
5.7 KiB
TypeScript
123 lines
5.7 KiB
TypeScript
/**
|
|
* The instance settings, the one section of a backup file that is not the
|
|
* account's. Both backup ends in one file.
|
|
*
|
|
* A disaster-recovery payload carries the singleton `app_settings` row —
|
|
* registration, the MFA requirement, the default locale and timezone, module
|
|
* availability, the notification and AI configuration, the document size cap
|
|
* and quota. Writing it back as a side effect of restoring ONE account
|
|
* reconfigures the host for everybody on it, silently and hours or weeks out
|
|
* of date: an operator putting a single record back on Wednesday has no
|
|
* reason to expect Monday's registration switch and Monday's upload cap to
|
|
* come with it. Putting the instance settings themselves back is a real
|
|
* case, so the write stays available — as an answer the operator gives
|
|
* (`restoreInstanceSettings`), not one the account restore assumes. An
|
|
* account copy does not rebuild a lost host; a database dump and the keys do
|
|
* (`docs/ops/disaster-recovery.md`).
|
|
*/
|
|
import { DEFAULT_REASONING_MAX_EFFORT } from "@/lib/ai/reasoning/levels";
|
|
import type {
|
|
BackupExportContext,
|
|
BackupExportPart,
|
|
} from "@/lib/export/export-context";
|
|
import type { RestoreContext } from "@/lib/export/restore-context";
|
|
import { foldLegacyCoachAvailability } from "@/lib/modules/operator-availability";
|
|
|
|
// ── Export ───────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* The instance settings slice: the singleton row, in a disaster-recovery
|
|
* payload only, with its timestamps and its quota made JSON-safe.
|
|
*/
|
|
export async function buildAppSettingsBackupPart(
|
|
ctx: BackupExportContext,
|
|
): Promise<BackupExportPart> {
|
|
const appSettings = ctx.disasterRecovery
|
|
? await ctx.prisma.appSettings.findUnique({ where: { id: "singleton" } })
|
|
: null;
|
|
return {
|
|
section: {
|
|
appSettings:
|
|
ctx.disasterRecovery && appSettings
|
|
? {
|
|
...appSettings,
|
|
adminCodexTokenExpiresAt:
|
|
appSettings.adminCodexTokenExpiresAt?.toISOString() ?? null,
|
|
adminCodexConnectedAt:
|
|
appSettings.adminCodexConnectedAt?.toISOString() ?? null,
|
|
documentQuotaBytes: appSettings.documentQuotaBytes.toString(),
|
|
}
|
|
: null,
|
|
},
|
|
counts: {},
|
|
};
|
|
}
|
|
|
|
// ── Restore ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function restoreAppSettings(ctx: RestoreContext): Promise<void> {
|
|
const { tx, payload } = ctx;
|
|
if (!(payload.appSettings && ctx.restoreInstanceSettings)) return;
|
|
const settings = payload.appSettings;
|
|
// A file from before 0343 carries the Coach's operator answer in module
|
|
// availability; carry it into the Coach switch.
|
|
const coachFold = foldLegacyCoachAvailability(
|
|
settings.moduleAvailabilityJson,
|
|
settings.assistantCoachEnabled,
|
|
);
|
|
const settingsData = {
|
|
registrationEnabled: settings.registrationEnabled,
|
|
mfaRequired: settings.mfaRequired,
|
|
defaultLocale: settings.defaultLocale,
|
|
telegramGlobal: settings.telegramGlobal,
|
|
ntfyGlobal: settings.ntfyGlobal,
|
|
webPushGlobal: settings.webPushGlobal,
|
|
webPushVapidPublicKey: settings.webPushVapidPublicKey,
|
|
webPushVapidPrivateKeyEncrypted: settings.webPushVapidPrivateKeyEncrypted,
|
|
webPushVapidSubject: settings.webPushVapidSubject,
|
|
apiGlobal: settings.apiGlobal,
|
|
umamiEnabled: settings.umamiEnabled,
|
|
umamiScriptUrl: settings.umamiScriptUrl,
|
|
umamiWebsiteId: settings.umamiWebsiteId,
|
|
glitchtipEnabled: settings.glitchtipEnabled,
|
|
glitchtipDsn: settings.glitchtipDsn,
|
|
glitchtipEnvironment: settings.glitchtipEnvironment,
|
|
reminderLateMinutes: settings.reminderLateMinutes,
|
|
reminderMissedMinutes: settings.reminderMissedMinutes,
|
|
adminAiKeyEncrypted: settings.adminAiKeyEncrypted,
|
|
adminAiModel: settings.adminAiModel,
|
|
adminAiBaseUrl: settings.adminAiBaseUrl,
|
|
adminCodexAccessTokenEncrypted: settings.adminCodexAccessTokenEncrypted,
|
|
adminCodexRefreshTokenEncrypted: settings.adminCodexRefreshTokenEncrypted,
|
|
adminCodexAccountIdEncrypted: settings.adminCodexAccountIdEncrypted,
|
|
adminCodexTokenExpiresAt: settings.adminCodexTokenExpiresAt
|
|
? new Date(settings.adminCodexTokenExpiresAt)
|
|
: null,
|
|
adminCodexConnectedAt: settings.adminCodexConnectedAt
|
|
? new Date(settings.adminCodexConnectedAt)
|
|
: null,
|
|
adminCodexConnectionStatus: settings.adminCodexConnectionStatus,
|
|
adminAiInsightsFeedbackSummary:
|
|
settings.adminAiInsightsFeedbackSummary as never,
|
|
defaultUserTimezone: settings.defaultUserTimezone,
|
|
assistantEnabled: settings.assistantEnabled,
|
|
assistantCoachEnabled: coachFold.assistantCoachEnabled,
|
|
assistantBriefingEnabled: settings.assistantBriefingEnabled,
|
|
assistantInsightStatusEnabled: settings.assistantInsightStatusEnabled,
|
|
assistantDocumentAiEnabled: settings.assistantDocumentAiEnabled ?? true,
|
|
// Absent on a file written before the controls existed, which restores
|
|
// them at their defaults: allowed, no cap.
|
|
aiReasoningEnabled: settings.aiReasoningEnabled ?? true,
|
|
aiReasoningMaxEffort:
|
|
settings.aiReasoningMaxEffort ?? DEFAULT_REASONING_MAX_EFFORT,
|
|
moduleAvailabilityJson: coachFold.moduleAvailabilityJson as never,
|
|
documentMaxFileBytes: settings.documentMaxFileBytes,
|
|
documentQuotaBytes: BigInt(settings.documentQuotaBytes),
|
|
};
|
|
await tx.appSettings.upsert({
|
|
where: { id: settings.id },
|
|
create: { id: settings.id, ...settingsData },
|
|
update: settingsData,
|
|
});
|
|
}
|