HuggingClaw-MissionControl / src /components /panels /security-audit-panel.tsx
nyk
feat: full i18n — 1752 keys across 10 languages, all panels translated (#326)
b180108 unverified
Raw
History Blame Contribute Delete
34.3 kB
'use client'
import { useState, useCallback } from 'react'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { Loader } from '@/components/ui/loader'
import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll'
import { useNavigateToPanel } from '@/lib/navigation'
import {
LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
} from 'recharts'
interface AuthEvent {
id: number
type: string
actor: string
ip: string
timestamp: number
detail: string
}
interface AgentTrust {
agentId: number
name: string
trustScore: number
flagged: boolean
lastEval: number
}
interface SecretAlert {
id: number
file: string
line: number
type: string
preview: string
detectedAt: number
resolved: boolean
}
interface ToolAuditEntry {
tool: string
calls: number
successes: number
failures: number
}
interface RateLimitSignal {
ip: string
hits: number
agent?: string
lastHit: number
}
interface InjectionAttempt {
id: number
type: string
source: string
input: string
blocked: boolean
timestamp: number
}
interface TimelinePoint {
timestamp: string
authEvents: number
injectionAttempts: number
secretAlerts: number
toolCalls: number
}
interface EvalScore {
layer: string
score: number
maxScore: number
}
interface AgentEval {
agentId: number
name: string
scores: EvalScore[]
convergence: number
driftDetected: boolean
lastEvalAt: number
}
type CheckSeverity = 'critical' | 'high' | 'medium' | 'low'
interface ScanCheck {
id: string
name: string
status: 'pass' | 'fail' | 'warn'
detail: string
fix: string
severity?: CheckSeverity
}
interface ScanCategory {
score: number
checks: ScanCheck[]
}
interface ScanData {
score: number
overall: string
categories: Record<string, ScanCategory>
}
interface SecurityAuditData {
posture: { score: number; level: string }
scan?: ScanData
authEvents: AuthEvent[]
agentTrust: AgentTrust[]
secretAlerts: SecretAlert[]
toolAudit: ToolAuditEntry[]
rateLimits: RateLimitSignal[]
injectionAttempts: InjectionAttempt[]
timeline: TimelinePoint[]
}
interface AgentEvalsData {
agents: AgentEval[]
overallConvergence: number
driftAlerts: string[]
}
const SCAN_STATUS_ICON: Record<string, string> = { pass: '+', fail: 'x', warn: '!' }
const SCAN_STATUS_COLOR: Record<string, string> = { pass: 'text-green-400', fail: 'text-red-400', warn: 'text-amber-400' }
const SEVERITY_BADGE: Record<CheckSeverity, { label: string; className: string }> = {
critical: { label: 'C', className: 'bg-red-500/20 text-red-400' },
high: { label: 'H', className: 'bg-orange-500/20 text-orange-400' },
medium: { label: 'M', className: 'bg-amber-500/20 text-amber-400' },
low: { label: 'L', className: 'bg-blue-500/20 text-blue-300' },
}
function ScanCategoryRow({ label, icon, category, failingCount }: {
label: string; icon: string; category: ScanCategory; failingCount: number
}) {
const t = useTranslations('securityAudit')
const [expanded, setExpanded] = useState(false)
return (
<div className="border border-border/50 rounded-lg overflow-hidden">
<button
onClick={() => setExpanded(!expanded)}
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-secondary/50 transition-colors"
>
<span className="w-5 h-5 rounded bg-secondary flex items-center justify-center text-xs font-mono text-muted-foreground">
{icon}
</span>
<span className="flex-1 text-sm font-medium">{label}</span>
<span className={`text-xs tabular-nums ${category.score >= 80 ? 'text-green-400' : category.score >= 50 ? 'text-amber-400' : 'text-red-400'}`}>
{category.score}%
</span>
{failingCount > 0 && (
<span className="text-xs text-muted-foreground">
{t('issueCount', { count: failingCount })}
</span>
)}
<span className="text-xs text-muted-foreground/50">{expanded ? '-' : '+'}</span>
</button>
{expanded && (
<div className="border-t border-border/30 px-3 py-2 space-y-1.5 bg-secondary/20">
{[...category.checks].sort((a, b) => {
if (a.status === 'pass' && b.status !== 'pass') return 1
if (a.status !== 'pass' && b.status === 'pass') return -1
const sev: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
return (sev[a.severity ?? 'medium'] ?? 2) - (sev[b.severity ?? 'medium'] ?? 2)
}).map(check => (
<div key={check.id} className="flex items-start gap-2 py-1">
<span className={`font-mono text-xs mt-0.5 w-4 shrink-0 ${SCAN_STATUS_COLOR[check.status]}`}>
[{SCAN_STATUS_ICON[check.status]}]
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm">{check.name}</span>
{check.severity && (
<span className={`text-2xs px-1 py-0.5 rounded font-mono leading-none ${SEVERITY_BADGE[check.severity].className}`}>
{SEVERITY_BADGE[check.severity].label}
</span>
)}
</div>
<p className="text-xs text-muted-foreground">{check.detail}</p>
{check.fix && check.status !== 'pass' && (
<p className="text-xs text-primary/70 mt-0.5">{t('fixPrefix', { fix: check.fix })}</p>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}
export function SecurityAuditPanel() {
const t = useTranslations('securityAudit')
const { setSecurityPosture } = useMissionControl()
const navigateToPanel = useNavigateToPanel()
const [selectedTimeframe, setSelectedTimeframe] = useState<'hour' | 'day' | 'week' | 'month'>('day')
const [data, setData] = useState<SecurityAuditData | null>(null)
const [evalsData, setEvalsData] = useState<AgentEvalsData | null>(null)
const [isLoading, setIsLoading] = useState(false)
const fetchData = useCallback(async () => {
setIsLoading(true)
try {
const [auditRes, evalsRes] = await Promise.all([
fetch(`/api/security-audit?timeframe=${selectedTimeframe}`),
fetch(`/api/agents/evals?timeframe=${selectedTimeframe}`),
])
if (auditRes.ok) {
const audit = await auditRes.json()
// API returns authEvents as { loginFailures, tokenRotations, accessDenials, recentEvents }
// but the panel expects authEvents to be an array of AuthEvent
if (audit.authEvents && !Array.isArray(audit.authEvents)) {
const events = audit.authEvents.recentEvents || []
audit.authEvents = events.map((e: any, i: number) => ({
id: i,
type: (e.event_type || '').replace('auth.', ''),
actor: e.agent_name || 'unknown',
ip: e.ip_address || '',
timestamp: e.created_at || 0,
detail: e.detail || '',
}))
}
// agentTrust: { agents: [...], flaggedCount } → AgentTrust[]
if (audit.agentTrust && !Array.isArray(audit.agentTrust)) {
const agents = audit.agentTrust.agents || []
const flaggedThreshold = 0.8
audit.agentTrust = agents.map((a: any, i: number) => ({
agentId: i,
name: a.name,
trustScore: a.score,
flagged: a.score < flaggedThreshold,
}))
}
// secretExposures → secretAlerts
if (audit.secretExposures && !audit.secretAlerts) {
const recent = audit.secretExposures.recent || []
audit.secretAlerts = recent.map((e: any, i: number) => ({
id: i,
file: e.detail || '',
line: 0,
type: (e.event_type || '').replace('secret.', ''),
preview: e.detail || '',
detectedAt: e.created_at || 0,
resolved: false,
}))
}
if (!Array.isArray(audit.secretAlerts)) audit.secretAlerts = []
// mcpAudit → toolAudit
if (audit.mcpAudit && !audit.toolAudit) {
const topTools = audit.mcpAudit.topTools || []
audit.toolAudit = topTools.map((t: any) => ({
tool: t.name,
calls: t.count,
successes: t.count,
failures: 0,
}))
}
if (!Array.isArray(audit.toolAudit)) audit.toolAudit = []
// rateLimits: { totalHits, byIp } → RateLimitSignal[]
if (audit.rateLimits && !Array.isArray(audit.rateLimits)) {
const byIp = audit.rateLimits.byIp || []
audit.rateLimits = byIp.map((r: any) => ({
ip: r.ip,
hits: r.count,
lastHit: 0,
}))
}
// injectionAttempts: { total, recent } → InjectionAttempt[]
if (audit.injectionAttempts && !Array.isArray(audit.injectionAttempts)) {
const recent = audit.injectionAttempts.recent || []
audit.injectionAttempts = recent.map((e: any, i: number) => ({
id: i,
type: (e.event_type || '').replace('injection.', ''),
source: e.agent_name || e.ip_address || 'unknown',
input: e.detail || '',
blocked: true,
timestamp: e.created_at || 0,
}))
}
// timeline: [{timestamp, eventCount, severity}] → [{timestamp, authEvents, ...}]
if (Array.isArray(audit.timeline)) {
audit.timeline = audit.timeline.map((t: any) => ({
timestamp: t.timestamp,
authEvents: t.eventCount || 0,
injectionAttempts: 0,
secretAlerts: 0,
toolCalls: 0,
}))
}
setData(audit)
if (audit.posture) {
setSecurityPosture(audit.posture)
}
}
if (evalsRes.ok) {
const evals = await evalsRes.json()
setEvalsData(evals)
}
} catch {
// Silent failure — data will remain stale
} finally {
setIsLoading(false)
}
}, [selectedTimeframe, setSecurityPosture])
useSmartPoll(fetchData, 30_000)
const postureColor = (score: number) => {
if (score >= 80) return 'text-green-400'
if (score >= 60) return 'text-yellow-400'
if (score >= 40) return 'text-orange-400'
return 'text-red-400'
}
const postureRingColor = (score: number) => {
if (score >= 80) return 'stroke-green-500'
if (score >= 60) return 'stroke-yellow-500'
if (score >= 40) return 'stroke-orange-500'
return 'stroke-red-500'
}
const postureBgColor = (level: string) => {
switch (level) {
case 'hardened': return 'bg-green-500/15 text-green-400'
case 'secure': return 'bg-green-500/10 text-green-300'
case 'needs-attention': return 'bg-yellow-500/15 text-yellow-400'
case 'at-risk': return 'bg-red-500/15 text-red-400'
default: return 'bg-muted text-muted-foreground'
}
}
const trustBarColor = (score: number) => {
if (score >= 0.8) return 'bg-green-500'
if (score >= 0.5) return 'bg-yellow-500'
return 'bg-red-500'
}
const formatTime = (ts: number) => new Date(ts * 1000).toLocaleString([], {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
})
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="border-b border-border pb-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">{t('title')}</h1>
<p className="text-muted-foreground mt-2">
{t('subtitle')}
</p>
</div>
<div className="flex items-center gap-4">
{isLoading && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary" />
)}
<div className="flex space-x-2">
{(['hour', 'day', 'week', 'month'] as const).map((tf) => (
<Button
key={tf}
onClick={() => setSelectedTimeframe(tf)}
variant={selectedTimeframe === tf ? 'default' : 'secondary'}
>
{t(`timeframe${tf.charAt(0).toUpperCase() + tf.slice(1)}` as 'timeframeHour' | 'timeframeDay' | 'timeframeWeek' | 'timeframeMonth')}
</Button>
))}
</div>
</div>
</div>
</div>
{!data ? (
<Loader variant="panel" label={t('loadingSecurityData')} />
) : (
<div className="space-y-6">
{/* Posture Score Header */}
<div className="bg-card border border-border rounded-lg p-6">
<div className="flex items-center gap-6">
{/* Circular gauge */}
<div className="relative w-24 h-24 flex-shrink-0">
<svg viewBox="0 0 36 36" className="w-24 h-24 -rotate-90">
<circle cx="18" cy="18" r="15.9" fill="none" className="stroke-muted" strokeWidth="2.5" />
<circle
cx="18" cy="18" r="15.9" fill="none"
className={postureRingColor(data.posture.score)}
strokeWidth="2.5"
strokeDasharray={`${data.posture.score} ${100 - data.posture.score}`}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${postureColor(data.posture.score)}`}>
{data.posture.score}
</span>
</div>
</div>
<div>
<h2 className="text-xl font-semibold text-foreground">{t('securityPosture')}</h2>
<span className={`inline-block mt-1 px-2 py-0.5 text-xs font-medium rounded ${postureBgColor(data.posture.level)}`}>
{data.posture.level}
</span>
<p className="text-sm text-muted-foreground mt-2">
{t('blendedScore')}
</p>
</div>
</div>
</div>
{/* Infrastructure Scan Categories */}
{data.scan && (
<div className="bg-card border border-border rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">{t('infrastructureScan')}</h2>
<span className={`text-sm font-bold tabular-nums ${postureColor(data.scan.score)}`}>
{data.scan.score}/100
</span>
</div>
<div className="space-y-2">
{Object.entries(data.scan.categories).map(([key, cat]) => {
const scanCategoryLabels: Record<string, string> = { credentials: t('scanCredentials'), network: t('scanNetwork'), openclaw: t('scanOpenclaw'), runtime: t('scanRuntime'), os: t('scanOs') }
const label = scanCategoryLabels[key] || key
const icon = { credentials: 'K', network: 'N', openclaw: 'O', runtime: 'R', os: 'S' }[key] || key[0].toUpperCase()
const failing = cat.checks.filter(c => c.status !== 'pass')
return (
<ScanCategoryRow key={key} label={label} icon={icon} category={cat} failingCount={failing.length} />
)
})}
</div>
</div>
)}
{/* Auth Events + Agent Trust */}
<div className="grid lg:grid-cols-2 gap-6">
{/* Auth Events */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('authEvents')}</h2>
{data.authEvents.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">{t('noAuthEvents')}</p>
) : (
<div className="overflow-x-auto max-h-64 overflow-y-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-card">
<tr className="text-left text-muted-foreground text-xs">
<th className="pb-2 pr-3">{t('colType')}</th>
<th className="pb-2 pr-3">{t('colActor')}</th>
<th className="pb-2 pr-3">{t('colIP')}</th>
<th className="pb-2">{t('colTime')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border/50">
{data.authEvents.map(evt => (
<tr key={evt.id} className="text-xs">
<td className="py-1.5 pr-3">
<span className={`px-1.5 py-0.5 rounded text-2xs font-medium ${
evt.type === 'login_failure' ? 'bg-red-500/15 text-red-400'
: evt.type === 'token_rotation' ? 'bg-blue-500/15 text-blue-400'
: 'bg-muted text-muted-foreground'
}`}>
{evt.type.replace(/_/g, ' ')}
</span>
</td>
<td className="py-1.5 pr-3 text-foreground">{evt.actor}</td>
<td className="py-1.5 pr-3 font-mono text-muted-foreground">{evt.ip}</td>
<td className="py-1.5 text-muted-foreground">{formatTime(evt.timestamp)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Agent Trust Scores */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('agentTrustScores')}</h2>
{data.agentTrust.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">{t('noAgentTrustData')}</p>
) : (
<div className="grid grid-cols-2 gap-3 max-h-64 overflow-y-auto">
{data.agentTrust.map(agent => (
<div
key={agent.agentId}
className={`p-3 rounded-lg border ${
agent.flagged ? 'border-red-500/50 bg-red-500/5' : 'border-border bg-secondary'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
{agent.flagged && (
<span className="text-2xs px-1.5 py-0.5 rounded bg-red-500/15 text-red-400 shrink-0 ml-1">{t('flagged')}</span>
)}
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full ${trustBarColor(agent.trustScore)}`}
style={{ width: `${agent.trustScore * 100}%` }}
/>
</div>
<div className="text-2xs text-muted-foreground mt-1">{(agent.trustScore * 100).toFixed(0)}%</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Secret Exposure Alerts */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('secretExposureAlerts')}</h2>
{data.secretAlerts.length === 0 ? (
<div className="flex items-center gap-2 py-4 justify-center">
<svg className="w-5 h-5 text-green-400" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 1a5 5 0 015 5v2a2 2 0 01-2 2H5a2 2 0 01-2-2V6a5 5 0 015-5z" />
<path d="M5.5 14h5M6.5 12v2M9.5 12v2" />
</svg>
<span className="text-sm font-medium text-green-400">{t('noSecretsDetected')}</span>
</div>
) : (
<div className="overflow-x-auto max-h-48 overflow-y-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-card">
<tr className="text-left text-muted-foreground text-xs">
<th className="pb-2 pr-3">{t('colType')}</th>
<th className="pb-2 pr-3">{t('colFile')}</th>
<th className="pb-2 pr-3">{t('colPreview')}</th>
<th className="pb-2 pr-3">{t('colStatus')}</th>
<th className="pb-2">{t('colDetected')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border/50">
{data.secretAlerts.map(alert => (
<tr key={alert.id} className="text-xs">
<td className="py-1.5 pr-3">
<span className="px-1.5 py-0.5 rounded bg-red-500/15 text-red-400 text-2xs font-medium">{alert.type}</span>
</td>
<td className="py-1.5 pr-3 font-mono text-foreground">{alert.file}:{alert.line}</td>
<td className="py-1.5 pr-3 font-mono text-muted-foreground max-w-48 truncate">{alert.preview}</td>
<td className="py-1.5 pr-3">
<span className={`text-2xs ${alert.resolved ? 'text-green-400' : 'text-red-400'}`}>
{alert.resolved ? t('statusResolved') : t('statusActive')}
</span>
</td>
<td className="py-1.5 text-muted-foreground">{formatTime(alert.detectedAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* MCP Tool Audit + Rate Limits */}
<div className="grid lg:grid-cols-2 gap-6">
{/* MCP Tool Audit BarChart */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('mcpToolAudit')}</h2>
{data.toolAudit.length === 0 ? (
<div className="h-48 flex items-center justify-center text-muted-foreground text-sm">{t('noToolUsageData')}</div>
) : (
<div className="h-48">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.toolAudit}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tool" angle={-45} textAnchor="end" height={60} interval={0} tick={{ fontSize: 10 }} />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="successes" stackId="a" fill="#22c55e" name={t('chartSuccess')} />
<Bar dataKey="failures" stackId="a" fill="#ef4444" name={t('chartFailure')} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* Rate Limit / Abuse Signals */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('rateLimitAbuseSignals')}</h2>
{data.rateLimits.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">{t('noRateLimitSignals')}</p>
) : (
<div className="space-y-2 max-h-48 overflow-y-auto">
{data.rateLimits.map((rl, i) => (
<div key={i} className="flex items-center justify-between p-2 bg-secondary rounded-lg text-sm">
<div>
<span className="font-mono text-foreground">{rl.ip}</span>
{rl.agent && <span className="ml-2 text-xs text-muted-foreground">({rl.agent})</span>}
</div>
<div className="flex items-center gap-2">
<span className={`text-xs font-medium ${rl.hits > 100 ? 'text-red-400' : rl.hits > 50 ? 'text-yellow-400' : 'text-muted-foreground'}`}>
{t('hits', { hits: rl.hits })}
</span>
<span className="text-2xs text-muted-foreground">{formatTime(rl.lastHit)}</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Injection Attempts */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('injectionAttempts')}</h2>
{data.injectionAttempts.length === 0 ? (
<div className="flex items-center gap-2 py-4 justify-center">
<svg className="w-5 h-5 text-green-400" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 1l6 3v4c0 3.5-2.5 6.5-6 7.5C4.5 14.5 2 11.5 2 8V4l6-3z" />
<path d="M5.5 8l2 2 3.5-3.5" />
</svg>
<span className="text-sm font-medium text-green-400">{t('noInjectionAttempts')}</span>
</div>
) : (
<div className="overflow-x-auto max-h-48 overflow-y-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-card">
<tr className="text-left text-muted-foreground text-xs">
<th className="pb-2 pr-3">{t('colType')}</th>
<th className="pb-2 pr-3">{t('colSource')}</th>
<th className="pb-2 pr-3">{t('colInput')}</th>
<th className="pb-2 pr-3">{t('colStatus')}</th>
<th className="pb-2">{t('colTime')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border/50">
{data.injectionAttempts.map(attempt => (
<tr key={attempt.id} className="text-xs">
<td className="py-1.5 pr-3">
<span className="px-1.5 py-0.5 rounded bg-orange-500/15 text-orange-400 text-2xs font-medium">{attempt.type}</span>
</td>
<td className="py-1.5 pr-3 text-foreground">{attempt.source}</td>
<td className="py-1.5 pr-3 font-mono text-muted-foreground max-w-48 truncate">{attempt.input}</td>
<td className="py-1.5 pr-3">
<span className={`text-2xs font-medium ${attempt.blocked ? 'text-green-400' : 'text-red-400'}`}>
{attempt.blocked ? t('statusBlocked') : t('statusPassed')}
</span>
</td>
<td className="py-1.5 text-muted-foreground">{formatTime(attempt.timestamp)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Timeline */}
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('securityTimeline', { timeframe: selectedTimeframe })}</h2>
{data.timeline.length === 0 ? (
<div className="h-48 flex items-center justify-center text-muted-foreground text-sm">{t('noTimelineData')}</div>
) : (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data.timeline.map(p => ({
...p,
time: new Date(p.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
}))}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="authEvents" stroke="#8884d8" strokeWidth={2} name={t('chartAuthEvents')} />
<Line type="monotone" dataKey="injectionAttempts" stroke="#ef4444" strokeWidth={2} name={t('chartInjections')} />
<Line type="monotone" dataKey="secretAlerts" stroke="#f59e0b" strokeWidth={2} name={t('chartSecrets')} />
<Line type="monotone" dataKey="toolCalls" stroke="#22c55e" strokeWidth={2} name={t('chartToolCalls')} />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* Agent Eval Dashboard */}
{evalsData && (
<div className="bg-card border border-border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">{t('agentEvalDashboard')}</h2>
{/* Convergence gauge + drift alerts */}
<div className="flex items-center gap-6 mb-6">
<div className="relative w-20 h-20 flex-shrink-0">
<svg viewBox="0 0 36 36" className="w-20 h-20 -rotate-90">
<circle cx="18" cy="18" r="15.9" fill="none" className="stroke-muted" strokeWidth="2.5" />
<circle
cx="18" cy="18" r="15.9" fill="none"
className={postureRingColor(evalsData.overallConvergence)}
strokeWidth="2.5"
strokeDasharray={`${evalsData.overallConvergence} ${100 - evalsData.overallConvergence}`}
strokeLinecap="round"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-lg font-bold ${postureColor(evalsData.overallConvergence)}`}>
{evalsData.overallConvergence}
</span>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-foreground">{t('overallConvergence')}</h3>
<p className="text-xs text-muted-foreground">{t('crossAgentAlignment')}</p>
{evalsData.driftAlerts.length > 0 && (
<div className="mt-2 space-y-1">
{evalsData.driftAlerts.map((alert, i) => (
<div key={i} className="text-xs text-red-400 flex items-center gap-1">
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M8 1l7 14H1L8 1z" />
<path d="M8 6v4M8 12v1" />
</svg>
{alert}
</div>
))}
</div>
)}
</div>
</div>
{/* Per-agent eval scores */}
{evalsData.agents.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">{t('noEvalData')}</p>
) : (
<div className="space-y-3">
{evalsData.agents.map(agent => (
<div
key={agent.agentId}
className={`p-4 rounded-lg border ${
agent.driftDetected ? 'border-red-500/50 bg-red-500/5' : 'border-border bg-secondary'
}`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">{agent.name}</span>
{agent.driftDetected && (
<span className="text-2xs px-1.5 py-0.5 rounded bg-red-500/15 text-red-400">{t('drift')}</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t('convergence')}</span>
<span className={`text-sm font-bold ${postureColor(agent.convergence)}`}>{agent.convergence}%</span>
</div>
</div>
<div className="grid grid-cols-4 gap-2">
{agent.scores.map(s => (
<div key={s.layer} className="text-center">
<div className="text-2xs text-muted-foreground mb-1 truncate">{s.layer}</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className={`h-1.5 rounded-full ${
(s.score / s.maxScore) >= 0.8 ? 'bg-green-500'
: (s.score / s.maxScore) >= 0.5 ? 'bg-yellow-500'
: 'bg-red-500'
}`}
style={{ width: `${(s.score / s.maxScore) * 100}%` }}
/>
</div>
<div className="text-2xs text-foreground mt-0.5">{s.score}/{s.maxScore}</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
</div>
)
}