Major product upgrade: landing page, scanning UI, admin panel, working reports
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

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>
This commit is contained in:
drjones
2026-07-07 14:42:24 +00:00
parent d92a7c057d
commit 2bdc085bc3
12 changed files with 1024 additions and 77 deletions

View File

@@ -1,68 +1,91 @@
"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/hooks/useAuth";
import { api } from "@/lib/api";
import { api, type Report } from "@/lib/api";
import Sidebar from "@/components/Sidebar";
import { FileText, Download, CheckCircle } from "lucide-react";
interface Report {
id: string;
tenant_id: string;
title: string;
report_date: string;
baseline_score: number | null;
executive_summary: string | null;
pdf_path: string | null;
is_baseline: boolean;
generated_by: string | null;
created_at: string;
}
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 || role !== "trustos_admin") {
setLoading(false);
return;
}
// Only admins see this — public endpoint for tenants to view their own would come later
fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/api/v1/audit-reports?tenant_id=${tenantId}`, {
headers: { Authorization: `Bearer ${localStorage.getItem("trustos_token")}` }
})
.then(r => r.json())
if (!ready || !tenantId) return;
api.reports(tenantId)
.then(setReports)
.catch(() => {})
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, [ready, tenantId, role]);
}, [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">
<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 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>
) : role !== "trustos_admin" ? (
<div className="vault-card">
<p className="text-vault-muted text-sm">Audit reports are managed by your TrustOS administrator.</p>
</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">Generate the first Vault Audit from the admin panel.</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">
@@ -78,21 +101,21 @@ export default function ReportsPage() {
</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 && ` · Score at audit: ${Math.round(r.baseline_score)}`}
{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>
<div className="flex items-center gap-2">
{r.pdf_path ? (
<span className="flex items-center gap-1.5 text-emerald-400 text-xs">
<CheckCircle className="w-3.5 h-3.5" /> PDF ready
</span>
) : (
<span className="text-vault-muted text-xs">PDF generating</span>
)}
</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>
))}