support snapshots

This commit is contained in:
2026-03-15 15:04:18 -05:00
parent 28b23bc355
commit e7cfff3eca
10 changed files with 199 additions and 12 deletions

View File

@@ -1,8 +1,10 @@
import type {
AdminDiagnosticsDto,
BackupGuidanceDto,
AdminPermissionOptionDto,
AdminRoleDto,
AdminRoleInput,
SupportSnapshotDto,
AdminUserDto,
AdminUserInput,
ApiResponse,
@@ -136,6 +138,12 @@ export const api = {
getAdminDiagnostics(token: string) {
return request<AdminDiagnosticsDto>("/api/v1/admin/diagnostics", undefined, token);
},
getBackupGuidance(token: string) {
return request<BackupGuidanceDto>("/api/v1/admin/backup-guidance", undefined, token);
},
getSupportSnapshot(token: string) {
return request<SupportSnapshotDto>("/api/v1/admin/support-snapshot", undefined, token);
},
getAdminPermissions(token: string) {
return request<AdminPermissionOptionDto[]>("/api/v1/admin/permissions", undefined, token);
},

View File

@@ -1,4 +1,4 @@
import type { AdminDiagnosticsDto } from "@mrp/shared";
import type { AdminDiagnosticsDto, BackupGuidanceDto } from "@mrp/shared";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
@@ -20,6 +20,7 @@ function parseMetadata(metadataJson: string) {
export function AdminDiagnosticsPage() {
const { token } = useAuth();
const [diagnostics, setDiagnostics] = useState<AdminDiagnosticsDto | null>(null);
const [backupGuidance, setBackupGuidance] = useState<BackupGuidanceDto | null>(null);
const [status, setStatus] = useState("Loading diagnostics...");
useEffect(() => {
@@ -29,13 +30,13 @@ export function AdminDiagnosticsPage() {
let active = true;
api
.getAdminDiagnostics(token)
.then((nextDiagnostics) => {
Promise.all([api.getAdminDiagnostics(token), api.getBackupGuidance(token)])
.then(([nextDiagnostics, nextBackupGuidance]) => {
if (!active) {
return;
}
setDiagnostics(nextDiagnostics);
setBackupGuidance(nextBackupGuidance);
setStatus("Diagnostics loaded.");
})
.catch((error: Error) => {
@@ -50,10 +51,26 @@ export function AdminDiagnosticsPage() {
};
}, [token]);
if (!diagnostics) {
if (!diagnostics || !backupGuidance) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
}
async function handleExportSupportSnapshot() {
if (!token) {
return;
}
const snapshot = await api.getSupportSnapshot(token);
const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: "application/json" });
const objectUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = `mrp-codex-support-snapshot-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
link.click();
window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 60_000);
setStatus("Support snapshot exported.");
}
const summaryCards = [
["Server time", formatDateTime(diagnostics.serverTime)],
["Node runtime", diagnostics.nodeVersion],
@@ -97,6 +114,13 @@ export function AdminDiagnosticsPage() {
</p>
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={handleExportSupportSnapshot}
className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text"
>
Export support snapshot
</button>
<Link to="/settings/users" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
User management
</Link>
@@ -115,6 +139,47 @@ export function AdminDiagnosticsPage() {
</div>
</section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Backup And Restore</p>
<h3 className="mt-2 text-lg font-bold text-text">Operational backup workflow</h3>
<p className="mt-2 max-w-3xl text-sm text-muted">
Use these paths and steps as the support baseline for manual backup and restore procedures.
</p>
</div>
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-muted">
<div>Data: {backupGuidance.dataPath}</div>
<div>DB: {backupGuidance.databasePath}</div>
<div>Uploads: {backupGuidance.uploadsPath}</div>
</div>
</div>
<div className="mt-5 grid gap-4 xl:grid-cols-2">
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<p className="text-sm font-semibold text-text">Backup checklist</p>
<div className="mt-3 space-y-3">
{backupGuidance.backupSteps.map((step) => (
<div key={step.id}>
<p className="text-sm font-semibold text-text">{step.label}</p>
<p className="mt-1 text-sm text-muted">{step.detail}</p>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
<p className="text-sm font-semibold text-text">Restore checklist</p>
<div className="mt-3 space-y-3">
{backupGuidance.restoreSteps.map((step) => (
<div key={step.id}>
<p className="text-sm font-semibold text-text">{step.label}</p>
<p className="mt-1 text-sm text-muted">{step.detail}</p>
</div>
))}
</div>
</div>
</div>
</section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div>