From 289dac6bd1f57fae7e476dafaba1b86ac047f1e3 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 9 Apr 2026 15:43:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20S11+S12=20=E8=BC=89=E5=85=A5?= =?UTF-8?q?=E5=A4=B1=E6=95=97=E4=BF=AE=E5=BE=A9=20=E2=80=94=20Sprint=205R?= =?UTF-8?q?=20Phase=201A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S11: Tab 2 approvals API path 修正 (?status=pending → /pending) - S11: Tab 2 fetch 加 r.ok 檢查避免解析錯誤 JSON - S12: 安全合規改用 SecurityPanel + CompliancePanel (解決 double AppLayout) - S12: 知識庫改為 redirect 到 /knowledge-base (避免 lazy import 問題) - S12: 拓撲圖加入 useDashboardStore.connect() 啟動 SSE Co-Authored-By: Claude Sonnet 4.6 --- .../src/app/[locale]/knowledge-base/page.tsx | 9 +- apps/web/src/app/[locale]/knowledge/page.tsx | 27 ++-- apps/web/src/app/[locale]/page.tsx | 4 +- .../app/[locale]/security-compliance/page.tsx | 17 +-- apps/web/src/app/[locale]/topology/page.tsx | 10 ++ .../src/components/panels/CompliancePanel.tsx | 117 +++++++++++++++++ .../src/components/panels/SecurityPanel.tsx | 124 ++++++++++++++++++ apps/web/src/components/panels/index.ts | 2 + 8 files changed, 278 insertions(+), 32 deletions(-) create mode 100644 apps/web/src/components/panels/CompliancePanel.tsx create mode 100644 apps/web/src/components/panels/SecurityPanel.tsx diff --git a/apps/web/src/app/[locale]/knowledge-base/page.tsx b/apps/web/src/app/[locale]/knowledge-base/page.tsx index 45ce3223..cf66bd1a 100644 --- a/apps/web/src/app/[locale]/knowledge-base/page.tsx +++ b/apps/web/src/app/[locale]/knowledge-base/page.tsx @@ -215,8 +215,7 @@ export default function KnowledgeBasePage({ const totalCount = categories.reduce((sum, c) => sum + c.count, 0) - return ( - + const content = (
{/* 左側分類導航 */} @@ -527,6 +526,12 @@ export default function KnowledgeBasePage({ )}
+ ) + + return ( + + {content} ) } + diff --git a/apps/web/src/app/[locale]/knowledge/page.tsx b/apps/web/src/app/[locale]/knowledge/page.tsx index 93bd06b8..a91a271b 100644 --- a/apps/web/src/app/[locale]/knowledge/page.tsx +++ b/apps/web/src/app/[locale]/knowledge/page.tsx @@ -2,24 +2,19 @@ /** * 知識 (/knowledge) — Sprint 5 整合頁面 - * 直接載入現有 knowledge-base 內容 - * 零假數據: 原封不動使用現有頁面 - * 建立時間: 2026-04-08 (台北時區) + * 直接渲染 knowledge-base 的內容(不含 AppLayout) + * @updated 2026-04-09 Claude Opus 4.6 — 移除 lazy import 避免 double AppLayout */ -import { lazy, Suspense } from 'react' -import { useTranslations } from 'next-intl' -import { AppLayout } from '@/components/layout' - -const KnowledgeBaseContent = lazy(() => import('@/app/[locale]/knowledge-base/page')) +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' export default function KnowledgePage({ params }: { params: { locale: string } }) { - const t = useTranslations('common') - return ( - - {t('loading')}}> - - - - ) + const router = useRouter() + + useEffect(() => { + router.replace(`/${params.locale}/knowledge-base`) + }, [router, params.locale]) + + return null } diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 1f8cb2ec..5930fba4 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -44,8 +44,8 @@ function AlertsAndApprovalsTab() { useEffect(() => { Promise.all([ - fetch(`${API_BASE}/api/v1/incidents`).then(r => r.json()).catch(() => ({ incidents: [] })), - fetch(`${API_BASE}/api/v1/approvals?status=pending`).then(r => r.json()).catch(() => []), + fetch(`${API_BASE}/api/v1/incidents`).then(r => r.ok ? r.json() : { incidents: [] }).catch(() => ({ incidents: [] })), + fetch(`${API_BASE}/api/v1/approvals/pending`).then(r => r.ok ? r.json() : []).catch(() => []), ]).then(([incData, apprData]) => { setAlerts(incData.incidents ?? incData ?? []) setApprovals(Array.isArray(apprData) ? apprData : apprData.approvals ?? []) diff --git a/apps/web/src/app/[locale]/security-compliance/page.tsx b/apps/web/src/app/[locale]/security-compliance/page.tsx index 721d8139..fcc08856 100644 --- a/apps/web/src/app/[locale]/security-compliance/page.tsx +++ b/apps/web/src/app/[locale]/security-compliance/page.tsx @@ -3,28 +3,21 @@ /** * 安全合規 (/security-compliance) — Sprint 5 整合頁面 * 整合: 安全掃描 + 合規報告 - * 零假數據: 全部載入現有頁面內容 - * 建立時間: 2026-04-08 (台北時區) + * @updated 2026-04-09 Claude Opus 4.6 — 改用 Panel 元件解決 double AppLayout */ -import { lazy, Suspense } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' - -const SecurityContent = lazy(() => import('@/app/[locale]/security/page')) -const ComplianceContent = lazy(() => import('@/app/[locale]/compliance/page')) - -function Loading() { - return
載入中...
-} +import { SecurityPanel } from '@/components/panels/SecurityPanel' +import { CompliancePanel } from '@/components/panels/CompliancePanel' export default function SecurityCompliancePage({ params }: { params: { locale: string } }) { const t = useTranslations('nav') const tabs: TabConfig[] = [ - { id: 'security', label: t('security'), content: }> }, - { id: 'compliance', label: t('compliance'), content: }> }, + { id: 'security', label: t('security'), content: }, + { id: 'compliance', label: t('compliance'), content: }, ] return ( diff --git a/apps/web/src/app/[locale]/topology/page.tsx b/apps/web/src/app/[locale]/topology/page.tsx index e0814ae3..953610ed 100644 --- a/apps/web/src/app/[locale]/topology/page.tsx +++ b/apps/web/src/app/[locale]/topology/page.tsx @@ -11,13 +11,23 @@ * @updated 2026-04-09 Claude Code — Sprint 5 React Flow 完整升級 */ +import { useEffect } from 'react' import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { ServiceTopology } from '@/components/topology' +import { useDashboardStore } from '@/stores/dashboard.store' export default function TopologyPage({ params }: { params: { locale: string } }) { const t = useTranslations('topology') + // 確保 SSE 連接啟動,拓撲圖才有資料 + useEffect(() => { + const apiBase = process.env.NEXT_PUBLIC_API_URL + if (apiBase) { + useDashboardStore.getState().connect(apiBase) + } + }, []) + return (
diff --git a/apps/web/src/components/panels/CompliancePanel.tsx b/apps/web/src/components/panels/CompliancePanel.tsx new file mode 100644 index 00000000..0d1a6ff5 --- /dev/null +++ b/apps/web/src/components/panels/CompliancePanel.tsx @@ -0,0 +1,117 @@ +'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' + +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) + }) + .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}
+
+ ))} +
+
+ )} + + )} +
+ ) +} diff --git a/apps/web/src/components/panels/SecurityPanel.tsx b/apps/web/src/components/panels/SecurityPanel.tsx new file mode 100644 index 00000000..b1fe7cb8 --- /dev/null +++ b/apps/web/src/components/panels/SecurityPanel.tsx @@ -0,0 +1,124 @@ +'use client' + +/** + * SecurityPanel — 安全監控面板 (不含 AppLayout) + * Sprint 5R: 從 /security/page.tsx 抽取 + * @updated 2026-04-09 Claude Opus 4.6 Asia/Taipei + */ + +import { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' + +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_COLOR: Record = { + fatal: '#cc2200', error: '#cc2200', warning: '#F59E0B', info: '#4A90D9', debug: '#87867f', +} + +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) + + useEffect(() => { + 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 ?? []) + }) + .finally(() => setLoading(false)) + }, []) + + const cardStyle = { background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 12, padding: '16px 18px' } + + return ( +
+
+

{t('title')}

+

{t('subtitle')}

+
+ + {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : ( + <> + {stats && ( +
+ {[ + { label: t('totalIssues'), value: stats.total_issues, color: '#141413' }, + { label: t('criticalIssues'), value: stats.critical_count, color: stats.critical_count > 0 ? '#cc2200' : '#22C55E' }, + { label: 'Unresolved', value: stats.unresolved_issues, color: stats.unresolved_issues > 0 ? '#F59E0B' : '#22C55E' }, + { label: t('errorRate'), value: stats.error_count_24h != null ? `${stats.error_count_24h}/24h` : '—', color: '#141413' }, + ].map(card => ( +
+
{card.label}
+
{card.value}
+
+ ))} +
+ )} + +
+
+ {t('recentIssues')} ({issues.length}) +
+ {issues.length === 0 ? ( +
{t('noData')}
+ ) : ( + + + + {[t('issue'), 'Level', t('count'), 'Last Seen'].map(col => ( + + ))} + + + + {issues.map(issue => ( + + + + + + + ))} + +
{col}
+
{issue.title}
+ {issue.culprit &&
{issue.culprit}
} +
+ {issue.level} + {issue.count} + {new Date(issue.last_seen).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} +
+ )} +
+ + )} +
+ ) +} diff --git a/apps/web/src/components/panels/index.ts b/apps/web/src/components/panels/index.ts index f9d478ee..89daa75e 100644 --- a/apps/web/src/components/panels/index.ts +++ b/apps/web/src/components/panels/index.ts @@ -21,3 +21,5 @@ export { TicketsPanel } from './TicketsPanel' export { CostPanel } from './CostPanel' export { ActionLogsPanel } from './ActionLogsPanel' export { BillingPanel } from './BillingPanel' +export { SecurityPanel } from './SecurityPanel' +export { CompliancePanel } from './CompliancePanel'