backup and restore
This commit is contained in:
@@ -8,8 +8,8 @@ import type {
|
||||
AdminUserInput,
|
||||
SupportSnapshotDto,
|
||||
AuditEventDto,
|
||||
SupportLogEntryDto,
|
||||
} from "@mrp/shared";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
import { env } from "../../config/env.js";
|
||||
import { paths } from "../../config/paths.js";
|
||||
@@ -17,6 +17,7 @@ import { logAuditEvent } from "../../lib/audit.js";
|
||||
import { hashPassword } from "../../lib/password.js";
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
import { getLatestStartupReport } from "../../lib/startup-state.js";
|
||||
import { getSupportLogCount, listSupportLogs } from "../../lib/support-log.js";
|
||||
|
||||
function mapAuditEvent(record: {
|
||||
id: string;
|
||||
@@ -45,6 +46,17 @@ function mapAuditEvent(record: {
|
||||
};
|
||||
}
|
||||
|
||||
function mapSupportLogEntry(record: SupportLogEntryDto): SupportLogEntryDto {
|
||||
return {
|
||||
id: record.id,
|
||||
level: record.level,
|
||||
source: record.source,
|
||||
message: record.message,
|
||||
contextJson: record.contextJson,
|
||||
createdAt: record.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function mapRole(record: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -468,6 +480,7 @@ export async function updateAdminUser(userId: string, payload: AdminUserInput, a
|
||||
|
||||
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||
const startupReport = getLatestStartupReport();
|
||||
const recentSupportLogs = listSupportLogs(50);
|
||||
const [
|
||||
companyProfile,
|
||||
userCount,
|
||||
@@ -519,8 +532,6 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||
}),
|
||||
]);
|
||||
|
||||
await Promise.all([fs.access(paths.dataDir), fs.access(paths.uploadsDir)]);
|
||||
|
||||
return {
|
||||
serverTime: new Date().toISOString(),
|
||||
nodeVersion: process.version,
|
||||
@@ -544,9 +555,10 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||
shipmentCount,
|
||||
attachmentCount,
|
||||
auditEventCount,
|
||||
startupStatus: startupReport.status,
|
||||
startupChecks: startupReport.checks,
|
||||
supportLogCount: getSupportLogCount(),
|
||||
startup: startupReport,
|
||||
recentAuditEvents: recentAuditEvents.map(mapAuditEvent),
|
||||
recentSupportLogs: recentSupportLogs.map(mapSupportLogEntry),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -600,11 +612,70 @@ export function getBackupGuidance(): BackupGuidanceDto {
|
||||
detail: "Confirm admin login, attachment access, and PDF generation after restore to verify the operational surface is healthy.",
|
||||
},
|
||||
],
|
||||
verificationChecklist: [
|
||||
{
|
||||
id: "backup-size-check",
|
||||
label: "Confirm backup contains data and uploads",
|
||||
detail: "Verify the backup archive or copied directory includes the SQLite database and uploads tree rather than only one of them.",
|
||||
evidence: "Directory listing or archive manifest showing prisma/app.db and uploads/ content.",
|
||||
},
|
||||
{
|
||||
id: "timestamp-check",
|
||||
label: "Check backup freshness",
|
||||
detail: "Confirm the backup timestamp matches the expected backup window and is newer than the last major data-entry period you need to protect.",
|
||||
evidence: "Backup timestamp recorded in your scheduler, NAS share, or copied folder metadata.",
|
||||
},
|
||||
{
|
||||
id: "snapshot-export",
|
||||
label: "Capture a support snapshot with the backup",
|
||||
detail: "Export the support snapshot from diagnostics when taking a formal backup so the runtime state and active-user footprint are recorded alongside it.",
|
||||
evidence: "JSON support snapshot stored with the backup set or support ticket.",
|
||||
},
|
||||
{
|
||||
id: "app-stop-check",
|
||||
label: "Verify writes were stopped before copy",
|
||||
detail: "Use a controlled maintenance stop or container stop before backup to reduce the chance of a partial SQLite copy.",
|
||||
evidence: "Maintenance log entry, Docker stop event, or operator note recorded with the backup.",
|
||||
},
|
||||
],
|
||||
restoreDrillSteps: [
|
||||
{
|
||||
id: "prepare-drill-target",
|
||||
label: "Prepare isolated restore target",
|
||||
detail: "Restore into an isolated container or duplicate environment instead of the live production instance.",
|
||||
expectedOutcome: "A clean target environment is ready to receive the backed-up data directory without impacting production.",
|
||||
},
|
||||
{
|
||||
id: "load-backed-up-data",
|
||||
label: "Load the full backup set",
|
||||
detail: `Restore the full backed-up data directory so ${paths.prismaDir}/app.db and uploads are returned together.`,
|
||||
expectedOutcome: "The restore target contains both database and file assets with the original directory structure intact.",
|
||||
},
|
||||
{
|
||||
id: "boot-restored-app",
|
||||
label: "Start the restored application",
|
||||
detail: "Launch the restored app and allow startup validation plus migrations to complete normally.",
|
||||
expectedOutcome: "The application starts without startup-validation failures and the diagnostics page loads.",
|
||||
},
|
||||
{
|
||||
id: "run-functional-checks",
|
||||
label: "Run post-restore functional checks",
|
||||
detail: "Verify login, one attachment download, one PDF render, and one representative transactional detail page such as inventory, purchasing, or shipping.",
|
||||
expectedOutcome: "Core operational flows work in the restored environment and file/PDF dependencies remain valid.",
|
||||
},
|
||||
{
|
||||
id: "record-drill-results",
|
||||
label: "Record restore-drill results",
|
||||
detail: "Capture the drill date, backup source used, startup status, and any gaps discovered so future recovery work improves over time.",
|
||||
expectedOutcome: "A dated restore-drill record exists for support and disaster-recovery review.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSupportSnapshot(): Promise<SupportSnapshotDto> {
|
||||
const diagnostics = await getAdminDiagnostics();
|
||||
const backupGuidance = getBackupGuidance();
|
||||
const [users, roles] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where: { isActive: true },
|
||||
@@ -620,5 +691,11 @@ export async function getSupportSnapshot(): Promise<SupportSnapshotDto> {
|
||||
userCount: diagnostics.userCount,
|
||||
roleCount: roles,
|
||||
activeUserEmails: users.map((user) => user.email),
|
||||
backupGuidance,
|
||||
recentSupportLogs: diagnostics.recentSupportLogs,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSupportLogs() {
|
||||
return listSupportLogs(100).map(mapSupportLogEntry);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user