Files
trustos/frontend/src/app/reports/page.tsx
drjones 2bdc085bc3
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / docker-build (push) Has been cancelled
Test / backend-test (push) Has been cancelled
Test / frontend-test (push) Has been cancelled
Test / security-scan (push) Has been cancelled
Major product upgrade: landing page, scanning UI, admin panel, working reports
Frontend:
- New public marketing landing page at / (hero, features, stats, how-it-works, CTA)
- New /scans page surfacing the scanning engine: asset health grid, one-click
  full scan, live scanner findings feed, 24h stats
- New /admin panel (was a 404 from the sidebar): audit report generation and
  instant PDF snapshot download
- Reports page: now visible to all roles, working PDF downloads, posture
  snapshot export for IT/admin
- Findings page: full-text search, sorting (severity/newest/title), severity
  count chips, CSV export
- Dashboard: scan activity strip, Run Scan + Export PDF quick actions
- Footprint page: summary stat cards
- api.ts: scanning, reports, and PDF download endpoints + types;
  fixed missing resolution_note on Finding type
- Removed unsupported eslint key from next.config.ts

Backend:
- Audit reports: list/get/PDF now open to executives and IT admins with strict
  tenant isolation (was trustos_admin-only, leaving tenants unable to see
  their own reports); PDF snapshot open to IT admins; generation stays
  admin-only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:42:24 +00:00

128 lines
4.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { api, type Report } from "@/lib/api";
import Sidebar from "@/components/Sidebar";
import { FileText, Download, RefreshCw } from "lucide-react";
export default function ReportsPage() {
const { tenantId, role, ready } = useAuth();
const [reports, setReports] = useState<Report[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const [snapshotting, setSnapshotting] = useState(false);
useEffect(() => {
if (!ready || !tenantId) return;
api.reports(tenantId)
.then(setReports)
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, [ready, tenantId]);
async function handleDownload(id: string) {
setDownloadingId(id);
try {
await api.downloadReportPdf(id);
} catch (e: any) {
setError(e.message || "Download failed");
} finally {
setDownloadingId(null);
}
}
async function handleSnapshot() {
if (!tenantId) return;
setSnapshotting(true);
try {
await api.downloadPdfSnapshot(tenantId);
} catch (e: any) {
setError(e.message || "Snapshot failed");
} finally {
setSnapshotting(false);
}
}
const canSnapshot = role === "it_admin" || role === "trustos_admin";
return (
<div className="flex min-h-screen bg-vault-black">
<Sidebar />
<main className="ml-64 flex-1 p-8">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-vault-text flex items-center gap-2">
<FileText className="w-6 h-6 text-vault-sapphire" />
Vault Audit Reports
</h1>
<p className="text-vault-muted text-sm mt-1">Point-in-time baseline reports and audit deliverables</p>
</div>
{canSnapshot && (
<button onClick={handleSnapshot} disabled={snapshotting} className="btn-primary">
{snapshotting
? <><RefreshCw className="w-4 h-4 animate-spin" /> Building PDF</>
: <><Download className="w-4 h-4" /> Current Posture PDF</>}
</button>
)}
</div>
{error && (
<div className="vault-card border-vault-crimson/40 bg-vault-crimsonDim/30 text-red-300 text-sm mb-6 py-3">
{error}
</div>
)}
{loading ? (
<div className="flex items-center justify-center h-40">
<div className="animate-spin w-6 h-6 border-2 border-vault-sapphire border-t-transparent rounded-full" />
</div>
) : reports.length === 0 ? (
<div className="vault-card text-center py-12">
<FileText className="w-8 h-8 text-vault-muted mx-auto mb-3" />
<p className="text-vault-text font-semibold">No audit reports yet</p>
<p className="text-vault-muted text-sm mt-1">
{role === "trustos_admin"
? "Generate the first Vault Audit from the Admin Panel."
: "Your TrustOS administrator will publish audit reports here."}
</p>
</div>
) : (
<div className="space-y-4">
{reports.map(r => (
<div key={r.id} className="vault-card">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
{r.is_baseline && (
<span className="vault-badge-info text-xs">Baseline</span>
)}
<p className="text-vault-text font-semibold">{r.title}</p>
</div>
<p className="text-vault-muted text-xs">
{new Date(r.report_date).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
{r.baseline_score != null && ` · Score at audit: ${Math.round(r.baseline_score)}`}
</p>
{r.executive_summary && (
<p className="text-vault-subtle text-sm mt-2 leading-relaxed line-clamp-2">{r.executive_summary}</p>
)}
</div>
<button
onClick={() => handleDownload(r.id)}
disabled={downloadingId === r.id}
className="btn-ghost border border-vault-border rounded-lg flex-shrink-0"
>
{downloadingId === r.id
? <><RefreshCw className="w-4 h-4 animate-spin" /> Preparing</>
: <><Download className="w-4 h-4" /> Download PDF</>}
</button>
</div>
</div>
))}
</div>
)}
</main>
</div>
);
}