- FastAPI backend: auth, findings, dashboard, attack paths, footprint, AI translator, risk calculator, PDF report generator - Next.js frontend: Vault dashboard, login, findings table, finding detail with AI coach, digital footprint, reports - PostgreSQL data model: tenants, users, assets, findings, risk scores, audit reports, attack paths - Docker Compose + Dockerfiles for all services - Demo seed data: Acme Corp with 6 findings and 90-day risk score history - AI Risk Translator (OpenAI/Anthropic) with plain-English business impact - Role-based access: executive / it_admin / trustos_admin - Scope-lock engine: authorization required before any assessment Stage 1-8 complete: Phase 1 Vault Audit product ready
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
"use client";
|
|
import {
|
|
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
|
|
ResponsiveContainer, ReferenceLine
|
|
} from "recharts";
|
|
|
|
interface ScoreTrendProps {
|
|
data: { date: string; score: number }[];
|
|
}
|
|
|
|
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
if (active && payload?.length) {
|
|
return (
|
|
<div className="bg-vault-dark border border-vault-border rounded-lg px-3 py-2 shadow-xl">
|
|
<p className="text-vault-muted text-xs mb-1">{label}</p>
|
|
<p className="text-vault-sapphireLight font-bold text-sm">Score: {payload[0].value}</p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
export default function ScoreTrend({ data }: ScoreTrendProps) {
|
|
// Thin the data to ~30 points for readability
|
|
const step = Math.ceil(data.length / 30);
|
|
const thinned = data.filter((_, i) => i % step === 0 || i === data.length - 1);
|
|
|
|
// Format dates to short labels
|
|
const formatted = thinned.map(d => ({
|
|
...d,
|
|
shortDate: d.date.slice(5), // MM-DD
|
|
}));
|
|
|
|
return (
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<LineChart data={formatted} margin={{ top: 5, right: 10, left: -20, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id="scoreGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#3b82d4" stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor="#3b82d4" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#2d3447" />
|
|
<XAxis
|
|
dataKey="shortDate"
|
|
tick={{ fill: "#64748b", fontSize: 10 }}
|
|
axisLine={{ stroke: "#2d3447" }}
|
|
tickLine={false}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
tick={{ fill: "#64748b", fontSize: 10 }}
|
|
axisLine={false}
|
|
tickLine={false}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<ReferenceLine y={75} stroke="#3b82d430" strokeDasharray="4 4" />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="score"
|
|
stroke="#3b82d4"
|
|
strokeWidth={2}
|
|
dot={false}
|
|
activeDot={{ r: 5, fill: "#3b82d4", stroke: "#0a0d14", strokeWidth: 2 }}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|