backup and restore
This commit is contained in:
@@ -4,6 +4,7 @@ import type {
|
||||
AdminPermissionOptionDto,
|
||||
AdminRoleDto,
|
||||
AdminRoleInput,
|
||||
SupportLogEntryDto,
|
||||
SupportSnapshotDto,
|
||||
AdminUserDto,
|
||||
AdminUserInput,
|
||||
@@ -144,6 +145,9 @@ export const api = {
|
||||
getSupportSnapshot(token: string) {
|
||||
return request<SupportSnapshotDto>("/api/v1/admin/support-snapshot", undefined, token);
|
||||
},
|
||||
getSupportLogs(token: string) {
|
||||
return request<SupportLogEntryDto[]>("/api/v1/admin/support-logs", undefined, token);
|
||||
},
|
||||
getAdminPermissions(token: string) {
|
||||
return request<AdminPermissionOptionDto[]>("/api/v1/admin/permissions", undefined, token);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AdminDiagnosticsDto, BackupGuidanceDto } from "@mrp/shared";
|
||||
import type { AdminDiagnosticsDto, BackupGuidanceDto, SupportLogEntryDto } from "@mrp/shared";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -21,6 +21,7 @@ export function AdminDiagnosticsPage() {
|
||||
const { token } = useAuth();
|
||||
const [diagnostics, setDiagnostics] = useState<AdminDiagnosticsDto | null>(null);
|
||||
const [backupGuidance, setBackupGuidance] = useState<BackupGuidanceDto | null>(null);
|
||||
const [supportLogs, setSupportLogs] = useState<SupportLogEntryDto[]>([]);
|
||||
const [status, setStatus] = useState("Loading diagnostics...");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,13 +31,14 @@ export function AdminDiagnosticsPage() {
|
||||
|
||||
let active = true;
|
||||
|
||||
Promise.all([api.getAdminDiagnostics(token), api.getBackupGuidance(token)])
|
||||
.then(([nextDiagnostics, nextBackupGuidance]) => {
|
||||
Promise.all([api.getAdminDiagnostics(token), api.getBackupGuidance(token), api.getSupportLogs(token)])
|
||||
.then(([nextDiagnostics, nextBackupGuidance, nextSupportLogs]) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setDiagnostics(nextDiagnostics);
|
||||
setBackupGuidance(nextBackupGuidance);
|
||||
setSupportLogs(nextSupportLogs);
|
||||
setStatus("Diagnostics loaded.");
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
@@ -71,10 +73,28 @@ export function AdminDiagnosticsPage() {
|
||||
setStatus("Support snapshot exported.");
|
||||
}
|
||||
|
||||
async function handleExportSupportLogs() {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = await api.getSupportLogs(token);
|
||||
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: "application/json" });
|
||||
const objectUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = objectUrl;
|
||||
link.download = `mrp-codex-support-logs-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
|
||||
link.click();
|
||||
window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 60_000);
|
||||
setSupportLogs(logs);
|
||||
setStatus("Support logs exported.");
|
||||
}
|
||||
|
||||
const summaryCards = [
|
||||
["Server time", formatDateTime(diagnostics.serverTime)],
|
||||
["Node runtime", diagnostics.nodeVersion],
|
||||
["Audit events", diagnostics.auditEventCount.toString()],
|
||||
["Support logs", diagnostics.supportLogCount.toString()],
|
||||
["Active users", `${diagnostics.activeUserCount} / ${diagnostics.userCount}`],
|
||||
["Sales docs", diagnostics.salesDocumentCount.toString()],
|
||||
["Work orders", diagnostics.workOrderCount.toString()],
|
||||
@@ -96,12 +116,18 @@ export function AdminDiagnosticsPage() {
|
||||
];
|
||||
|
||||
const startupStatusTone =
|
||||
diagnostics.startupStatus === "PASS"
|
||||
diagnostics.startup.status === "PASS"
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-500/15 dark:text-emerald-200"
|
||||
: diagnostics.startupStatus === "WARN"
|
||||
: diagnostics.startup.status === "WARN"
|
||||
? "bg-amber-100 text-amber-800 dark:bg-amber-500/15 dark:text-amber-200"
|
||||
: "bg-rose-100 text-rose-800 dark:bg-rose-500/15 dark:text-rose-200";
|
||||
|
||||
const startupSummaryCards = [
|
||||
["Generated", formatDateTime(diagnostics.startup.generatedAt)],
|
||||
["Duration", `${diagnostics.startup.durationMs} ms`],
|
||||
["Pass / Warn / Fail", `${diagnostics.startup.passCount} / ${diagnostics.startup.warnCount} / ${diagnostics.startup.failCount}`],
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
||||
@@ -119,7 +145,14 @@ export function AdminDiagnosticsPage() {
|
||||
onClick={handleExportSupportSnapshot}
|
||||
className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text"
|
||||
>
|
||||
Export support snapshot
|
||||
Export support bundle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExportSupportLogs}
|
||||
className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text"
|
||||
>
|
||||
Export support logs
|
||||
</button>
|
||||
<Link to="/settings/users" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
User management
|
||||
@@ -178,6 +211,32 @@ export function AdminDiagnosticsPage() {
|
||||
</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 verification checklist</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{backupGuidance.verificationChecklist.map((item) => (
|
||||
<div key={item.id}>
|
||||
<p className="text-sm font-semibold text-text">{item.label}</p>
|
||||
<p className="mt-1 text-sm text-muted">{item.detail}</p>
|
||||
<p className="mt-1 text-xs text-muted">Evidence: {item.evidence}</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 drill runbook</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{backupGuidance.restoreDrillSteps.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>
|
||||
<p className="mt-1 text-xs text-muted">Expected outcome: {step.expectedOutcome}</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">
|
||||
@@ -187,11 +246,11 @@ export function AdminDiagnosticsPage() {
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Boot-time readiness checks</h3>
|
||||
</div>
|
||||
<span className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${startupStatusTone}`}>
|
||||
{diagnostics.startupStatus}
|
||||
{diagnostics.startup.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
||||
{diagnostics.startupChecks.map((check) => (
|
||||
{diagnostics.startup.checks.map((check) => (
|
||||
<div key={check.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-text">{check.label}</p>
|
||||
@@ -201,6 +260,14 @@ export function AdminDiagnosticsPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 grid gap-3 lg:grid-cols-3">
|
||||
{startupSummaryCards.map(([label, value]) => (
|
||||
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
|
||||
<p className="mt-2 text-sm text-text">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
||||
@@ -215,6 +282,49 @@ 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 items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Support Logs</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Recent runtime warnings and failures</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted">{supportLogs.length} entries loaded</p>
|
||||
</div>
|
||||
<div className="mt-5 overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
<th className="px-3 py-3">When</th>
|
||||
<th className="px-3 py-3">Level</th>
|
||||
<th className="px-3 py-3">Source</th>
|
||||
<th className="px-3 py-3">Message</th>
|
||||
<th className="px-3 py-3">Context</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line/70">
|
||||
{supportLogs.map((entry) => {
|
||||
const context = parseMetadata(entry.contextJson);
|
||||
return (
|
||||
<tr key={entry.id} className="align-top">
|
||||
<td className="px-3 py-3 text-muted">{formatDateTime(entry.createdAt)}</td>
|
||||
<td className="px-3 py-3">
|
||||
<span className="rounded-full bg-page px-2 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-text">
|
||||
{entry.level}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-text">{entry.source}</td>
|
||||
<td className="px-3 py-3 text-text">{entry.message}</td>
|
||||
<td className="px-3 py-3 text-xs text-muted">
|
||||
{Object.keys(context).length > 0 ? JSON.stringify(context) : "No context"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 items-center justify-between gap-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user