'use client' /** * CompliancePanel — 合規狀態面板 (不含 AppLayout) * Sprint 5R: 從 /compliance/page.tsx 抽取 * @updated 2026-04-09 Claude Opus 4.6 Asia/Taipei */ import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' interface IncidentSummary { total_incidents: number resolved_rate: number severity_distribution: Array<{ severity: string; count: number }> } interface AutoRepairStats { approved_playbooks: number high_quality_playbooks: number total_executions: number overall_success_rate: number auto_repair_eligible: boolean } export function CompliancePanel() { const t = useTranslations('compliance') const [summary, setSummary] = useState(null) const [repairStats, setRepairStats] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { Promise.all([ fetch(`${API_BASE}/api/v1/stats/incidents/summary?days=30`).then(r => r.ok ? r.json() : null).catch(() => null), fetch(`${API_BASE}/api/v1/auto-repair/stats`).then(r => r.ok ? r.json() : null).catch(() => null), ]) .then(([s, r]) => { if (s) setSummary(s) if (r) setRepairStats(r) }) .catch(() => setError('load_failed')) .finally(() => setLoading(false)) }, []) const cardStyle = { background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '16px 18px' } const sevColors: Record = { P0: '#cc2200', P1: '#F59E0B', P2: '#4A90D9', P3: '#22C55E' } return (

{t('title')}

{t('subtitle')}

{loading ? (
{t('loading')}
) : error ? (
{t('error')}
) : ( <>
{summary && <>
{t('totalIncidents')}
{summary.total_incidents}
30 days
{t('resolvedRate')}
= 80 ? '#22C55E' : summary.resolved_rate >= 50 ? '#F59E0B' : '#cc2200' }}> {summary.resolved_rate.toFixed(1)}%
} {repairStats && <>
{t('approvedPlaybooks')}
{repairStats.approved_playbooks}
{t('highQualityPlaybooks')}: {repairStats.high_quality_playbooks}
{t('executionSuccessRate')}
= 0.8 ? '#22C55E' : '#F59E0B' }}> {(repairStats.overall_success_rate * 100).toFixed(1)}%
{t('autoRepairEligible')}
{repairStats.auto_repair_eligible ? t('yes') : t('no')}
}
{summary && summary.severity_distribution.length > 0 && (
Severity Distribution
{summary.severity_distribution.map(({ severity, count }) => (
{severity}
{count}
))}
)} )}
) }