This commit is contained in:
2026-03-15 14:11:21 -05:00
parent 1fcb0c5480
commit 857d34397e
28 changed files with 848 additions and 45 deletions

View File

@@ -1,4 +1,5 @@
import type {
AdminDiagnosticsDto,
ApiResponse,
CompanyProfileDto,
CompanyProfileInput,
@@ -127,6 +128,9 @@ export const api = {
me(token: string) {
return request<LoginResponse["user"]>("/api/v1/auth/me", undefined, token);
},
getAdminDiagnostics(token: string) {
return request<AdminDiagnosticsDto>("/api/v1/admin/diagnostics", undefined, token);
},
getCompanyProfile(token: string) {
return request<CompanyProfileDto>("/api/v1/company-profile", undefined, token);
},

View File

@@ -9,6 +9,7 @@ import { ProtectedRoute } from "./components/ProtectedRoute";
import { AuthProvider } from "./auth/AuthProvider";
import { DashboardPage } from "./modules/dashboard/DashboardPage";
import { LoginPage } from "./modules/login/LoginPage";
import { AdminDiagnosticsPage } from "./modules/settings/AdminDiagnosticsPage";
import { CompanySettingsPage } from "./modules/settings/CompanySettingsPage";
import { CrmDetailPage } from "./modules/crm/CrmDetailPage";
import { CrmFormPage } from "./modules/crm/CrmFormPage";
@@ -54,6 +55,10 @@ const router = createBrowserRouter([
element: <ProtectedRoute requiredPermissions={[permissions.companyRead]} />,
children: [{ path: "/settings/company", element: <CompanySettingsPage /> }],
},
{
element: <ProtectedRoute requiredPermissions={[permissions.adminManage]} />,
children: [{ path: "/settings/admin-diagnostics", element: <AdminDiagnosticsPage /> }],
},
{
element: <ProtectedRoute requiredPermissions={[permissions.crmRead]} />,
children: [

View File

@@ -0,0 +1,169 @@
import type { AdminDiagnosticsDto } from "@mrp/shared";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { useAuth } from "../../auth/AuthProvider";
import { api } from "../../lib/api";
function formatDateTime(value: string) {
return new Date(value).toLocaleString();
}
function parseMetadata(metadataJson: string) {
try {
return JSON.parse(metadataJson) as Record<string, unknown>;
} catch {
return {};
}
}
export function AdminDiagnosticsPage() {
const { token } = useAuth();
const [diagnostics, setDiagnostics] = useState<AdminDiagnosticsDto | null>(null);
const [status, setStatus] = useState("Loading diagnostics...");
useEffect(() => {
if (!token) {
return;
}
let active = true;
api
.getAdminDiagnostics(token)
.then((nextDiagnostics) => {
if (!active) {
return;
}
setDiagnostics(nextDiagnostics);
setStatus("Diagnostics loaded.");
})
.catch((error: Error) => {
if (!active) {
return;
}
setStatus(error.message || "Unable to load diagnostics.");
});
return () => {
active = false;
};
}, [token]);
if (!diagnostics) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
}
const summaryCards = [
["Server time", formatDateTime(diagnostics.serverTime)],
["Node runtime", diagnostics.nodeVersion],
["Audit events", diagnostics.auditEventCount.toString()],
["Active users", `${diagnostics.activeUserCount} / ${diagnostics.userCount}`],
["Sales docs", diagnostics.salesDocumentCount.toString()],
["Work orders", diagnostics.workOrderCount.toString()],
["Projects", diagnostics.projectCount.toString()],
["Attachments", diagnostics.attachmentCount.toString()],
];
const footprintCards = [
["Database URL", diagnostics.databaseUrl],
["Data directory", diagnostics.dataDir],
["Uploads directory", diagnostics.uploadsDir],
["Client origin", diagnostics.clientOrigin],
["Company profile", diagnostics.companyProfilePresent ? "Present" : "Missing"],
["Roles / permissions", `${diagnostics.roleCount} / ${diagnostics.permissionCount}`],
["Customers / vendors", `${diagnostics.customerCount} / ${diagnostics.vendorCount}`],
["Inventory / warehouses", `${diagnostics.inventoryItemCount} / ${diagnostics.warehouseCount}`],
["Purchase orders", diagnostics.purchaseOrderCount.toString()],
["Shipments", diagnostics.shipmentCount.toString()],
];
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">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin Diagnostics</p>
<h3 className="mt-2 text-lg font-bold text-text">Operational runtime and audit visibility</h3>
<p className="mt-2 max-w-3xl text-sm text-muted">
This view surfaces environment footprint, record counts, and recent change activity so admin review does not require direct database access.
</p>
</div>
<div className="flex flex-wrap gap-3">
<Link to="/settings/company" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
Company settings
</Link>
</div>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{summaryCards.map(([label, value]) => (
<div key={label} className="rounded-3xl border border-line/70 bg-page/70 p-4">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">{label}</p>
<p className="mt-3 text-lg font-bold 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">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">System Footprint</p>
<div className="mt-5 grid gap-3 xl:grid-cols-2">
{footprintCards.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 break-all 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">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Recent Audit Trail</p>
<h3 className="mt-2 text-lg font-bold text-text">Latest cross-module write activity</h3>
</div>
<p className="text-sm text-muted">{status}</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">Actor</th>
<th className="px-3 py-3">Entity</th>
<th className="px-3 py-3">Action</th>
<th className="px-3 py-3">Summary</th>
<th className="px-3 py-3">Metadata</th>
</tr>
</thead>
<tbody className="divide-y divide-line/70">
{diagnostics.recentAuditEvents.map((event) => {
const metadata = parseMetadata(event.metadataJson);
return (
<tr key={event.id} className="align-top">
<td className="px-3 py-3 text-muted">{formatDateTime(event.createdAt)}</td>
<td className="px-3 py-3 text-text">{event.actorName ?? "System"}</td>
<td className="px-3 py-3 text-text">
<div>{event.entityType}</div>
{event.entityId ? <div className="text-xs text-muted">{event.entityId}</div> : null}
</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">
{event.action}
</span>
</td>
<td className="px-3 py-3 text-text">{event.summary}</td>
<td className="px-3 py-3 text-xs text-muted">
{Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : "No metadata"}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import type { CompanyProfileInput } from "@mrp/shared";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { useAuth } from "../../auth/AuthProvider";
@@ -6,7 +7,7 @@ import { api } from "../../lib/api";
import { useTheme } from "../../theme/ThemeProvider";
export function CompanySettingsPage() {
const { token } = useAuth();
const { token, user } = useAuth();
const { applyBrandProfile } = useTheme();
const [form, setForm] = useState<CompanyProfileInput | null>(null);
const [companyId, setCompanyId] = useState<string | null>(null);
@@ -145,6 +146,20 @@ export function CompanySettingsPage() {
return (
<form className="space-y-6" onSubmit={handleSave}>
{user?.permissions.includes("admin.manage") ? (
<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-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin</p>
<h3 className="mt-2 text-lg font-bold text-text">Diagnostics and audit trail</h3>
<p className="mt-2 text-sm text-muted">Review runtime footprint and recent change activity from the admin diagnostics surface.</p>
</div>
<Link to="/settings/admin-diagnostics" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
Open diagnostics
</Link>
</div>
</section>
) : null}
<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-6 lg:flex-row lg:items-start lg:justify-between">
<div>