- 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
87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
"use client";
|
|
import { useEffect, useRef } from "react";
|
|
|
|
interface RiskDialProps {
|
|
score: number;
|
|
size?: number;
|
|
}
|
|
|
|
export default function RiskDial({ score, size = 200 }: RiskDialProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = size * dpr;
|
|
canvas.height = size * dpr;
|
|
ctx.scale(dpr, dpr);
|
|
|
|
const cx = size / 2;
|
|
const cy = size / 2;
|
|
const radius = size * 0.38;
|
|
const startAngle = Math.PI * 0.75; // 135°
|
|
const endAngle = Math.PI * 2.25; // 405° (full 270° arc)
|
|
const valueAngle = startAngle + (endAngle - startAngle) * (score / 100);
|
|
|
|
// Background track
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, radius, startAngle, endAngle);
|
|
ctx.strokeStyle = "#2d3447";
|
|
ctx.lineWidth = 12;
|
|
ctx.lineCap = "round";
|
|
ctx.stroke();
|
|
|
|
// Score color
|
|
const getColor = (s: number) => {
|
|
if (s >= 75) return "#3b82d4"; // sapphire — healthy
|
|
if (s >= 50) return "#d97706"; // amber — warning
|
|
return "#dc2626"; // crimson — critical
|
|
};
|
|
|
|
// Score arc
|
|
if (score > 0) {
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, radius, startAngle, valueAngle);
|
|
ctx.strokeStyle = getColor(score);
|
|
ctx.lineWidth = 12;
|
|
ctx.lineCap = "round";
|
|
ctx.stroke();
|
|
|
|
// Glow
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, radius, startAngle, valueAngle);
|
|
ctx.strokeStyle = getColor(score) + "40";
|
|
ctx.lineWidth = 20;
|
|
ctx.lineCap = "round";
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Score number
|
|
ctx.fillStyle = "#e2e8f0";
|
|
ctx.font = `bold ${size * 0.22}px system-ui, sans-serif`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillText(Math.round(score).toString(), cx, cy - 4);
|
|
|
|
// Label
|
|
ctx.fillStyle = "#64748b";
|
|
ctx.font = `${size * 0.07}px system-ui, sans-serif`;
|
|
ctx.fillText("/ 100", cx, cy + size * 0.14);
|
|
}, [score, size]);
|
|
|
|
return (
|
|
<div className="flex flex-col items-center">
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{ width: size, height: size }}
|
|
className="drop-shadow-lg"
|
|
/>
|
|
<p className="text-vault-muted text-xs mt-1">Cyber Health Score</p>
|
|
</div>
|
|
);
|
|
}
|