Files
trustos/frontend/src/components/RiskDial.tsx
drjones 463dff883b feat: initial TrustOS platform scaffold
- 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
2026-07-05 09:46:08 +00:00

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>
);
}