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
This commit is contained in:
86
frontend/src/components/RiskDial.tsx
Normal file
86
frontend/src/components/RiskDial.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user