'use client' /** * SecurityPanel — 安全監控面板 (不含 AppLayout) * Sprint 5R: 從 /security/page.tsx 抽取 * @updated 2026-07-03 — UI/UX 全面升級,Tailwind + 彩色 Stat Cards + 問題列表 */ import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { AlertCircle, AlertTriangle, Bug, CheckCircle2, Clock, RefreshCw, ShieldAlert, ShieldCheck, XCircle, } from 'lucide-react' import { cn } from '@/lib/utils' import { IwoooSReadOnlyBridge } from '@/components/security/iwooos-read-only-bridge' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' interface IssueStats { total_issues: number unresolved_issues: number critical_count: number error_count_24h: number } interface SentryIssue { id: string title: string culprit: string | null level: string count: number first_seen: string last_seen: string } const LEVEL_CFG: Record = { fatal: { badge: 'bg-[#fff0ed] border-[#f5b8b8] text-[#9f2f25]', icon: XCircle }, error: { badge: 'bg-[#fff0ed] border-[#f5b8b8] text-[#9f2f25]', icon: AlertCircle }, warning: { badge: 'bg-[#fff7e8] border-[#d9b36f] text-[#8a5a08]', icon: AlertTriangle }, info: { badge: 'bg-[#f0f6ff] border-[#c7dcf7] text-[#1f5b9b]', icon: Bug }, debug: { badge: 'bg-[#f5f4ed] border-[#e0ddd4] text-[#77736a]', icon: Bug }, } export function SecurityPanel() { const t = useTranslations('security') const [stats, setStats] = useState(null) const [issues, setIssues] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchData = () => { setLoading(true) setError(null) Promise.all([ fetch(`${API_BASE}/api/v1/errors/stats`).then(r => r.ok ? r.json() : null).catch(() => null), fetch(`${API_BASE}/api/v1/errors/issues?limit=20`).then(r => r.ok ? r.json() : { issues: [] }).catch(() => ({ issues: [] })), ]) .then(([statsData, listData]) => { if (statsData) setStats(statsData) setIssues(listData?.issues ?? []) }) .catch(() => setError('load_failed')) .finally(() => setLoading(false)) } useEffect(() => { fetchData() }, []) // eslint-disable-line react-hooks/exhaustive-deps const statCards = stats ? [ { label: t('totalIssues'), value: stats.total_issues.toLocaleString(), icon: Bug, accent: 'text-[#141413]', bg: 'bg-white', border: 'border-[#e0ddd4]', }, { label: t('criticalIssues'), value: stats.critical_count.toLocaleString(), icon: XCircle, accent: stats.critical_count > 0 ? 'text-[#9f2f25]' : 'text-[#17602a]', bg: stats.critical_count > 0 ? 'bg-[#fff0ed]' : 'bg-[#f0faf2]', border: stats.critical_count > 0 ? 'border-[#f5b8b8]' : 'border-[#8fc29a]', }, { label: t('unresolvedIssues'), value: stats.unresolved_issues.toLocaleString(), icon: AlertCircle, accent: stats.unresolved_issues > 0 ? 'text-[#8a5a08]' : 'text-[#17602a]', bg: stats.unresolved_issues > 0 ? 'bg-[#fff7e8]' : 'bg-[#f0faf2]', border: stats.unresolved_issues > 0 ? 'border-[#d9b36f]' : 'border-[#8fc29a]', }, { label: t('errorRate'), value: stats.error_count_24h != null ? `${stats.error_count_24h}/24h` : '—', icon: Clock, accent: 'text-[#4A90D9]', bg: 'bg-[#f0f6ff]', border: 'border-[#c7dcf7]', }, ] : [] return (
{/* Header */}

Security

{t('title')}

{t('subtitle')}

{/* IwoooS Bridge */}
{/* Loading */} {loading && (
{[0,1,2,3].map(i => (
))}
)} {/* Error */} {!loading && error && (
)} {/* Content */} {!loading && !error && (
{/* Stat cards */} {statCards.length > 0 && (
{statCards.map(({ label, value, icon: Icon, accent, bg, border }) => (

{label}

{value}

))}
)} {/* Recent Issues */}
{issues.length === 0 ? (
{issues.length}
{issues.length === 0 ? (
) : (
{/* Header */}
{[t('issue'), t('level'), t('count'), t('lastSeen')].map(col => ( {col} ))}
{issues.map(issue => { const cfg = LEVEL_CFG[issue.level] ?? LEVEL_CFG['debug'] const LvIcon = cfg.icon return (

{issue.title}

{issue.culprit && (

{issue.culprit}

)}
{issue.count.toLocaleString()} {new Date(issue.last_seen).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', })}
) })}
)}
)}
) }