- {[
- { level: '🔴', name: 'CrashLoopBackOff', meta: 'awoooi-api · K3s', age: '2m', active: true },
- { level: '🟡', name: 'HighCPU 92%', meta: 'awoooi-web · K3s', age: '5m', active: false },
- { level: '🟡', name: 'PostgresDiskFull', meta: '.110 主機層 94%', age: '8m', active: false },
- { level: '🟢', name: 'OOMKilled → 修復', meta: 'awoooi-worker', age: '12m', active: false },
- ].map(a => (
-
-
{a.level}
-
-
{a.name}
-
{a.meta}
+ {activeIncidents.length > 0 ? (
+ activeIncidents.slice(0, 5).map((inc, i) => (
+
+
{severityEmoji(inc.severity)}
+
+
{inc.incident_id}
+
{inc.affected_services.join(', ') || inc.status}
+
+
{inc.severity}
-
{a.age}
+ ))
+ ) : (
+
+
+
{t('noActiveAlerts')}
- ))}
+ )}
@@ -125,10 +163,10 @@ export function NeuralLiveCenter({ stats, history }: Props) {
{/* ── Center: Chain visualization ── */}
- {t('chainTitle')} — CrashLoopBackOff · awoooi-api
+ {t('chainTitle')}{latestItem ? ` — ${latestItem.incident_id} · ${latestItem.playbook_name}` : ''}
- {CHAIN_NODES.map((node, i) => (
+ {chainNodes.map((node, i) => (
{node.label}
@@ -139,7 +177,7 @@ export function NeuralLiveCenter({ stats, history }: Props) {
{node.state === 'waiting'&&
{t('nodeWaiting')}}
- {i < CHAIN_NODES.length - 1 && (
+ {i < chainNodes.length - 1 && (
)}
@@ -166,41 +204,48 @@ export function NeuralLiveCenter({ stats, history }: Props) {
{/* ── Right: Execution log stream ── */}
{t('execStream')}
-
- {displayHistory.map(item => {
- const scheme = SCHEME_CONFIG[item.uri_scheme as UriScheme] ?? SCHEME_CONFIG['kubectl://']
- const statusCfg = STATUS_CONFIG[item.status as RepairStatus]
- const StatusIcon = statusCfg.Icon
- const elapsed = Math.round((Date.now() - new Date(item.executed_at).getTime()) / 60000)
+ {history.length > 0 ? (
+
+ {history.map(item => {
+ const scheme = SCHEME_CONFIG[item.uri_scheme as UriScheme] ?? SCHEME_CONFIG['kubectl://']
+ const statusCfg = STATUS_CONFIG[item.status as RepairStatus]
+ const StatusIcon = statusCfg.Icon
+ const elapsed = Math.round((Date.now() - new Date(item.executed_at).getTime()) / 60000)
- return (
-
-
-
- {elapsed}m
-
-
- {scheme.label}
-
-
+ return (
+
+
+
+ {elapsed}m
+
+
+ {scheme.label}
+
+
+
+
{item.playbook_name}
+ {item.duration_ms && (
+
{(item.duration_ms / 1000).toFixed(1)}s
+ )}
+ {item.error && (
+
{item.error}
+ )}
+ {item.status === 'pending_approval' && (
+
{t('waitingApproval')}
+ )}
+ {item.rag_confidence && (
+
RAG {item.rag_confidence.toFixed(2)}
+ )}
-
{item.playbook_name}
- {item.duration_ms && (
-
{(item.duration_ms / 1000).toFixed(1)}s
- )}
- {item.error && (
-
{item.error}
- )}
- {item.status === 'pending_approval' && (
-
{t('waitingApproval')}
- )}
- {item.rag_confidence && (
-
RAG {item.rag_confidence.toFixed(2)}
- )}
-
- )
- })}
-
+ )
+ })}
+
+ ) : (
+
+ )}
diff --git a/apps/web/src/components/neural-command/NeuralStats.tsx b/apps/web/src/components/neural-command/NeuralStats.tsx
index edb90280..007f7590 100644
--- a/apps/web/src/components/neural-command/NeuralStats.tsx
+++ b/apps/web/src/components/neural-command/NeuralStats.tsx
@@ -4,60 +4,92 @@
* NeuralStats - 統計 & 歷史面板
* ================================
* 5 KPI + 執行路徑分佈 + Playbook 排名 + 時間軸
+ *
+ * 2026-04-07 Claude Code: Sprint F 打假行動 — 移除所有 MOCK 常數
+ * 統帥鐵律: 所有數據從 stats/playbooks/history props 計算,禁止寫死!
*/
+import { useMemo } from 'react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
-import { CheckCircle2, XCircle, Clock, TrendingUp, TrendingDown } from 'lucide-react'
+import { Inbox } from 'lucide-react'
import type { AutoRepairStats, PlaybookItem, RepairHistoryItem } from './types'
interface Props {
stats: AutoRepairStats | null
playbooks: PlaybookItem[]
history: RepairHistoryItem[]
+ pendingCount: number
}
-const SCHEME_STATS = [
- { scheme: 'kubectl://', icon: '☸️', color: 'bg-blue-500', textColor: 'text-blue-500', count: 116, rate: 91, pct: 74 },
- { scheme: 'openclaw://', icon: '🦞', color: 'bg-orange-500', textColor: 'text-orange-500', count: 34, rate: 76, pct: 22 },
- { scheme: 'ansible://', icon: '⚙️', color: 'bg-purple-500', textColor: 'text-purple-500', count: 6, rate: 100, pct: 4 },
-]
-
-const PLAYBOOK_RANKINGS = [
- { name: 'crashloop-pod-delete', type: 'kubectl', rate: 94, count: 52, tag: 'blue' },
- { name: 'vacuum_postgres', type: 'ansible', rate: 100, count: 4, tag: 'purple' },
- { name: 'high-cpu-restart', type: 'kubectl', rate: 88, count: 33, tag: 'blue' },
- { name: 'openclaw-down-repair', type: 'openclaw', rate: 76, count: 21, tag: 'orange' },
- { name: 'oom-killed-pod-delete',type: 'kubectl', rate: 89, count: 28, tag: 'blue' },
-]
-
const TYPE_BADGE: Record
= {
kubectl: 'bg-blue-500/10 text-blue-500 border border-blue-500/25',
ansible: 'bg-purple-500/10 text-purple-500 border border-purple-500/25',
openclaw: 'bg-orange-500/10 text-orange-500 border border-orange-500/25',
}
-const MOCK_HISTORY: RepairHistoryItem[] = [
- { id: '1', incident_id: 'INC-001', playbook_id: 'PB-1', playbook_name: 'crashloop-pod-delete', action_type: 'kubectl', uri_scheme: 'kubectl://', command: 'kubectl delete pod awoooi-api-7f9d4', status: 'success', executed_at: new Date(Date.now() - 2 * 60000).toISOString(), duration_ms: 2300, error: null, rag_confidence: 0.94 },
- { id: '2', incident_id: 'INC-002', playbook_id: 'PB-2', playbook_name: 'vacuum_postgres', action_type: 'ssh_command', uri_scheme: 'ansible://', command: 'ansible://192.168.0.188/vacuum_postgres.yml', status: 'pending_approval', executed_at: new Date(Date.now() - 5 * 60000).toISOString(), duration_ms: null, error: null, rag_confidence: 0.91 },
- { id: '3', incident_id: 'INC-003', playbook_id: 'PB-3', playbook_name: 'openclaw-down-repair', action_type: 'ssh_command', uri_scheme: 'openclaw://', command: 'openclaw://docker-110/sentry', status: 'success', executed_at: new Date(Date.now() - 8 * 60000).toISOString(), duration_ms: 45000, error: null, rag_confidence: 0.88 },
- { id: '4', incident_id: 'INC-004', playbook_id: 'PB-4', playbook_name: 'high-cpu-restart', action_type: 'kubectl', uri_scheme: 'kubectl://', command: 'kubectl rollout restart deployment/awoooi-web', status: 'failed', executed_at: new Date(Date.now() - 15 * 60000).toISOString(), duration_ms: null, error: 'SSH timeout 60s', rag_confidence: 0.82 },
- { id: '5', incident_id: 'INC-005', playbook_id: 'PB-5', playbook_name: 'oom-killed-pod-delete', action_type: 'kubectl', uri_scheme: 'kubectl://', command: 'kubectl delete pod awoooi-worker-abc', status: 'success', executed_at: new Date(Date.now() - 17 * 60000).toISOString(), duration_ms: 1800, error: null, rag_confidence: 0.89 },
-]
+const SCHEME_ICON: Record = {
+ 'kubectl://': { icon: '☸️', color: 'bg-blue-500', textColor: 'text-blue-500', bg: 'bg-blue-500/10' },
+ 'openclaw://': { icon: '🦞', color: 'bg-orange-500', textColor: 'text-orange-500', bg: 'bg-orange-500/10' },
+ 'ansible://': { icon: '⚙️', color: 'bg-purple-500', textColor: 'text-purple-500', bg: 'bg-purple-500/10' },
+}
-export function NeuralStats({ stats, playbooks, history }: Props) {
+export function NeuralStats({ stats, playbooks, history, pendingCount }: Props) {
const t = useTranslations('neuralCommand')
- const displayHistory = history.length > 0 ? history : MOCK_HISTORY
- const successRate = stats?.overall_success_rate ?? 0.87
- const totalExec = stats?.total_executions ?? 156
+ // 從 history 聚合 scheme 分佈
+ const schemeStats = useMemo(() => {
+ const counts: Record = {}
+ for (const item of history) {
+ const scheme = item.uri_scheme
+ if (!counts[scheme]) counts[scheme] = { count: 0, successCount: 0 }
+ counts[scheme].count++
+ if (item.status === 'success') counts[scheme].successCount++
+ }
+ const total = history.length || 1
+ return Object.entries(counts).map(([scheme, { count, successCount }]) => ({
+ scheme,
+ ...(SCHEME_ICON[scheme] ?? SCHEME_ICON['kubectl://']),
+ count,
+ rate: count > 0 ? Math.round((successCount / count) * 100) : 0,
+ pct: Math.round((count / total) * 100),
+ })).sort((a, b) => b.count - a.count)
+ }, [history])
+
+ // 從 playbooks 排序 (按成功率,有執行次數的優先)
+ const playbookRankings = useMemo(() => {
+ return playbooks
+ .filter(p => p.success_count + p.failure_count > 0)
+ .map(p => {
+ const total = p.success_count + p.failure_count
+ return {
+ name: p.name,
+ type: p.tags.includes('ansible') ? 'ansible' : p.tags.includes('openclaw') ? 'openclaw' : 'kubectl',
+ rate: total > 0 ? Math.round((p.success_count / total) * 100) : 0,
+ count: total,
+ }
+ })
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 8)
+ }, [playbooks])
+
+ // 從 history 計算平均時長
+ const avgDuration = useMemo(() => {
+ const durations = history.filter(h => h.duration_ms != null).map(h => h.duration_ms!)
+ if (durations.length === 0) return '--'
+ const avg = durations.reduce((a, b) => a + b, 0) / durations.length
+ return `${(avg / 1000).toFixed(1)}s`
+ }, [history])
+
+ const successRate = stats?.overall_success_rate ?? null
+ const totalExec = stats?.total_executions ?? null
const KPIs = [
- { label: t('kpiSuccessRate'), value: `${Math.round(successRate * 100)}%`, color: 'text-green-500', trend: t('trendUp', { n: 3 }) },
- { label: t('kpiTotalExec'), value: totalExec, color: 'text-blue-500', trend: t('trendUp', { n: 23 }) },
- { label: t('kpiPlaybooks'), value: stats?.approved_playbooks ?? 10, color: 'text-orange-500', trend: '5 K3s · 5 Docker' },
- { label: t('kpiAvgDuration'), value: '4.2s', color: 'text-yellow-500', trend: t('trendDown', { n: 1.1 }) },
- { label: t('kpiPendingAppr'), value: 2, color: 'text-purple-500', trend: 'ansible:// 類型' },
+ { label: t('kpiSuccessRate'), value: successRate != null ? `${Math.round(successRate * 100)}%` : '--', color: 'text-green-500' },
+ { label: t('kpiTotalExec'), value: totalExec ?? '--', color: 'text-blue-500' },
+ { label: t('kpiPlaybooks'), value: stats?.approved_playbooks ?? '--', color: 'text-orange-500' },
+ { label: t('kpiAvgDuration'), value: avgDuration, color: 'text-yellow-500' },
+ { label: t('kpiPendingAppr'), value: pendingCount, color: 'text-purple-500' },
]
return (
@@ -65,11 +97,10 @@ export function NeuralStats({ stats, playbooks, history }: Props) {
{/* ── KPI strip ── */}
- {KPIs.map(({ label, value, color, trend }) => (
+ {KPIs.map(({ label, value, color }) => (
{value}
{label}
-
{trend}
))}
@@ -79,118 +110,136 @@ export function NeuralStats({ stats, playbooks, history }: Props) {
{/* ── Scheme breakdown ── */}
{t('schemeBreakdown')}
-
- {SCHEME_STATS.map(s => (
-
-
{s.icon}
-
{s.scheme}
-
-
+ {schemeStats.length > 0 ? (
+
+ {schemeStats.map(s => (
+
+
{s.icon}
+
{s.scheme}
+
+
{s.count}
+
{s.rate}%
-
{s.count}
-
{s.rate}%
-
- ))}
-
+ ))}
+
+ ) : (
+
+ )}
{/* ── Playbook ranking ── */}
{t('playbookRanking')}
-
-
-
- | {t('thName')} |
- {t('thType')} |
- {t('thRate')} |
- {t('thCount')} |
-
-
-
- {PLAYBOOK_RANKINGS.map(pb => (
-
- | {pb.name} |
-
-
- {pb.type}
-
- |
-
-
- |
- {pb.count} |
+ {playbookRankings.length > 0 ? (
+
+
+
+ | {t('thName')} |
+ {t('thType')} |
+ {t('thRate')} |
+ {t('thCount')} |
- ))}
-
-
+
+
+ {playbookRankings.map(pb => (
+
+ | {pb.name} |
+
+
+ {pb.type}
+
+ |
+
+
+ |
+ {pb.count} |
+
+ ))}
+
+
+ ) : (
+
+ )}
{/* ── History timeline ── */}
{t('historyTimeline')}
-
- {displayHistory.map(item => {
- const elapsed = Math.round((Date.now() - new Date(item.executed_at).getTime()) / 60000)
- const isSuccess = item.status === 'success'
- const isPending = item.status === 'pending_approval'
- const isFailed = item.status === 'failed'
+ {history.length > 0 ? (
+
+ {history.map(item => {
+ const elapsed = Math.round((Date.now() - new Date(item.executed_at).getTime()) / 60000)
+ const isSuccess = item.status === 'success'
+ const isPending = item.status === 'pending_approval'
+ const isFailed = item.status === 'failed'
- return (
-
- {/* Timeline dot */}
-
-
- {/* Content */}
-
-
-
- {item.playbook_name}
- {isSuccess && ' ✅'}
- {isFailed && ' ❌'}
- {isPending && ' ⏳'}
-
+ return (
+
+ {/* Timeline dot */}
+
-
-
{elapsed}m {t('ago')}
-
- {item.uri_scheme}{item.command.replace(/^[a-z]+:\/\//, '')}
-
- {item.duration_ms && (
-
- {(item.duration_ms / 1000).toFixed(1)}s
-
- )}
- {item.rag_confidence && (
-
- 🦞 RAG {item.rag_confidence.toFixed(2)}
-
- )}
- {isPending && (
-
{t('waitingApproval')}
- )}
- {isFailed && item.error && (
-
{item.error}
- )}
+
+ {/* Content */}
+
+
+
+ {item.playbook_name}
+
+
+
+ {elapsed}m {t('ago')}
+
+ {item.uri_scheme}{item.command.replace(/^[a-z]+:\/\//, '')}
+
+ {item.duration_ms && (
+
+ {(item.duration_ms / 1000).toFixed(1)}s
+
+ )}
+ {item.rag_confidence && (
+
+ RAG {item.rag_confidence.toFixed(2)}
+
+ )}
+ {isPending && (
+ {t('waitingApproval')}
+ )}
+ {isFailed && item.error && (
+ {item.error}
+ )}
+
-
- )
- })}
-
+ )
+ })}
+
+ ) : (
+
+ )}
)
diff --git a/apps/web/src/components/neural-command/types.ts b/apps/web/src/components/neural-command/types.ts
index f789edbb..85114a96 100644
--- a/apps/web/src/components/neural-command/types.ts
+++ b/apps/web/src/components/neural-command/types.ts
@@ -42,3 +42,30 @@ export interface RepairHistoryItem {
}
export type NeuralTab = 'preflight' | 'live' | 'stats' | 'approval'
+
+export interface PendingApprovalItem {
+ id: string
+ action: string
+ description: string
+ status: string
+ risk_level: string
+ blast_radius: {
+ affected_pods: number
+ estimated_downtime: string
+ related_services: string[]
+ data_impact: string
+ }
+ dry_run_checks: Array<{ name: string; passed: boolean; message?: string }>
+ requested_by: string
+ created_at: string
+ incident_id?: string | null
+}
+
+export interface ActiveIncident {
+ incident_id: string
+ status: string
+ severity: string
+ signal_count: number
+ affected_services: string[]
+ created_at: string
+}
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts
index e3048b62..231a61d4 100644
--- a/apps/web/src/lib/api-client.ts
+++ b/apps/web/src/lib/api-client.ts
@@ -5,6 +5,8 @@
* 統帥鐵律: 禁止任何 Fallback IP,環境變數缺失即噴錯
*/
+import { CURRENT_USER } from '@/lib/constants/user'
+
// 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 Fallback
const getApiBaseUrl = (): string => {
const url = process.env.NEXT_PUBLIC_API_URL
@@ -114,7 +116,7 @@ export const apiClient = {
}>(res)
},
- async signApproval(approvalId: string, signer: string = 'commander', comment?: string, csrfToken?: string | null) {
+ async signApproval(approvalId: string, signer: string = CURRENT_USER.id, comment?: string, csrfToken?: string | null) {
// Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code)
const headers: Record
= { 'Content-Type': 'application/json' }
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
@@ -151,8 +153,8 @@ export const apiClient = {
headers,
credentials: 'include',
body: JSON.stringify({
- rejector_id: 'commander',
- rejector_name: 'Commander',
+ rejector_id: CURRENT_USER.id,
+ rejector_name: CURRENT_USER.name,
reason: reason || 'Rejected via WarRoom',
}),
})
diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts
index d10b993b..24158c78 100644
--- a/apps/web/src/lib/constants/index.ts
+++ b/apps/web/src/lib/constants/index.ts
@@ -12,3 +12,4 @@ export * from './z-index'
export * from './shortcuts'
export * from './animations'
export * from './sse-states'
+export * from './user'
diff --git a/apps/web/src/lib/constants/user.ts b/apps/web/src/lib/constants/user.ts
new file mode 100644
index 00000000..615ae162
--- /dev/null
+++ b/apps/web/src/lib/constants/user.ts
@@ -0,0 +1,13 @@
+/**
+ * 當前用戶常數
+ * =====================================
+ * 統帥是唯一操作者,在多用戶認證系統實作前使用固定身份。
+ * 禁止使用 'demo-user'、'user-001'、'Demo User' 等假身份。
+ *
+ * 2026-04-07 Claude Code: Sprint F 打假行動 — 消除假用戶身份
+ */
+
+export const CURRENT_USER = {
+ id: 'commander',
+ name: '統帥',
+} as const
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 83e5fb86..a9bb9dab 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -5,6 +5,30 @@
---
+## 📍 當前狀態 (2026-04-07 Sprint F 打假行動完成 → Sprint 4 Phase A 待實作)
+
+| 項目 | 狀態 | 說明 |
+|------|------|------|
+| Sprint F 計畫文件 | ✅ | `docs/superpowers/plans/2026-04-07-sprint-f-fake-data-purge.md` |
+| 首席架構師審查 | ✅ 100% | Fully Approved (統帥指示: 立即啟動) |
+| P0: Neural Command 假數據清除 | ✅ | NeuralLiveCenter + NeuralStats + NeuralApprovalPanel |
+| P1: 假用戶身份清除 | ✅ | 7處 demo-user → commander/統帥 |
+| P2: Demo 匯出清理 | ✅ | 6處 Demo 組件/Mock 常數刪除 |
+| P3: /demo 頁面保護 | ✅ | NEXT_PUBLIC_ENABLE_DEMO 環境變數 |
+| i18n 新增翻譯 | ✅ | 22 個新 key (zh-TW + en) |
+| TypeScript 驗證 | ✅ | 零新增錯誤,假數據搜尋零殘留 |
+| project_sprint_f_fake_data_purge.md | ✅ | Memory 更新 |
+| project_current_status.md | ✅ | 狀態快照更新 |
+| 首席架構師 Review | ✅ 98/100 | C1(CURRENT_USER統一) + C2(未使用import) 已修正 |
+| C1 修正: CURRENT_USER 統一 | ✅ | 10+處 'commander'/'統帥' → CURRENT_USER 常數 |
+| C2 修正: NeuralStats import 清理 | ✅ | 移除未使用的 lucide import |
+| 漏網之魚修復 | ✅ | openclaw-state-machine.tsx War Room User → CURRENT_USER |
+| 最終驗證 | ✅ | 'commander' 僅存於 user.ts 定義,TypeScript 零新增錯誤 |
+
+**下一步**: Sprint 4 Phase A 實作 (A1→A2→A3: Model + Redis 計數器)
+
+---
+
## 📍 當前狀態 (2026-04-07 Sprint 4 規劃完成 → Phase A 待實作)
| 項目 | 狀態 | 說明 |
diff --git a/docs/superpowers/plans/2026-04-07-sprint-f-fake-data-purge.md b/docs/superpowers/plans/2026-04-07-sprint-f-fake-data-purge.md
new file mode 100644
index 00000000..ad278a47
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-07-sprint-f-fake-data-purge.md
@@ -0,0 +1,187 @@
+# Sprint F: 前端打假行動 (Fake Data Purge)
+
+> **建立**: 2026-04-07 (台北時區)
+> **建立者**: Claude Code
+> **首席架構師審查**: ✅ 100% Fully Approved (統帥原話: "請立即啟動實作程序")
+> **優先級**: 🔴 P0 — 直接影響系統可信度 (Trust)
+
+---
+
+## 背景
+
+前端審計發現 **29 處假數據/假頁面**,嚴重違反 `feedback_no_fake_data.md` 鐵律。
+核心問題: Neural Command 頁面後端 API 已全部打通 (Sprint 2/3),但前端子組件殘留大量 Mock 常數。
+
+## 與現有工作的衝突分析
+
+| 現有工作 | 衝突? | 分析 |
+|----------|-------|------|
+| Sprint 3 SSH_COMMAND (已完成) | ❌ 無衝突 | Sprint F 修復的正是 Sprint 3 前端組件的假數據 |
+| Sprint 4 告警處置統計 (待實作) | ⚠️ 低衝突 | Sprint 4 Phase E (前端頁面) 會新增組件,Sprint F 修的是現有組件。建議 **Sprint F 先做**,確保新組件從一開始就用真實數據 |
+| Phase 24 AI Router (進行中) | ❌ 無衝突 | 後端路由重構,不涉及前端 |
+| Phase O 可觀測性 | ❌ 無衝突 | SigNoz/監控整合,不涉及 Neural Command UI |
+
+**結論**: Sprint F 應在 Sprint 4 Phase A 之前執行,作為 Sprint 3.5 的技術債清理。
+
+---
+
+## 執行順序
+
+```
+Sprint 3 (✅ 完成) → Sprint F (打假行動) → Sprint 4 (告警處置統計)
+ ^^^^^^^^^^^^^^^^
+ 當前執行點
+```
+
+---
+
+## Phase P0: Neural Command 假數據清除 (核心修復)
+
+### P0-1: NeuralLiveCenter.tsx
+
+**問題**: 6 處假數據 + 假歷史 + 假告警雷達
+**後端 API**: 已有 `stats` + `history` props 從 page.tsx 傳入
+
+| 修復項 | 現況 (假) | 修復方案 |
+|--------|-----------|----------|
+| L51-57: `displayHistory` fallback | 5 筆假 INC-1~5 | 無資料時顯示 `EmptyState` (`t('noHistory')`) |
+| L77: todayMatches | 寫死 `23` | 從 `stats` 計算或顯示 `--` |
+| L78: RAG Confidence | 寫死 `0.87` | 從 `stats` 取或顯示 `--` |
+| L93: 執行成功率 | 寫死 `136/156` | `stats.total_executions * stats.overall_success_rate / stats.total_executions` |
+| L94: 平均時長 | 寫死 `4.2s` | 從 `history` 計算平均 `duration_ms`,無資料顯示 `--` |
+| L95: 待審批 | 寫死 `2` | 新增 `pendingCount` prop 從 page.tsx 傳入 |
+| L103-108: Alert Radar | 4 筆假告警 | 從 `history` 最新 4 筆衍生,或從 `/api/v1/incidents` 取活躍告警 |
+| L34-39: CHAIN_NODES | 假鏈路 | 無活躍修復時顯示 idle 狀態,有時從最新 history item 衍生 |
+
+**新增 Props**:
+```typescript
+interface Props {
+ stats: AutoRepairStats | null
+ history: RepairHistoryItem[]
+ pendingCount: number // 新增: 從 page.tsx 傳入
+ activeIncidents?: any[] // 新增: 活躍告警 (可選)
+}
+```
+
+### P0-2: NeuralStats.tsx
+
+**問題**: SCHEME_STATS + PLAYBOOK_RANKINGS + MOCK_HISTORY + 假 KPI
+**後端 API**: 已有 `stats` + `playbooks` + `history` props
+
+| 修復項 | 現況 (假) | 修復方案 |
+|--------|-----------|----------|
+| L20-23: SCHEME_STATS | 寫死 116/34/6 | 從 `history` 聚合計算 scheme 分佈 |
+| L26-31: PLAYBOOK_RANKINGS | 寫死 5 個排名 | 從 `playbooks` prop 排序 (success_count/total) |
+| L40-46: MOCK_HISTORY | 5 筆假歷史 | 移除,直接用 `history` prop |
+| L52: successRate fallback | `?? 0.87` | `?? 0` + 無資料時顯示 `--` |
+| L53: totalExec fallback | `?? 156` | `?? 0` |
+| L59: 平均時長 | 寫死 `4.2s` | 從 `history` 計算 |
+| L60: 待審批 | 寫死 `2` | 新增 prop `pendingCount` |
+
+### P0-3: NeuralApprovalPanel.tsx
+
+**問題**: 整個面板用 MOCK_PENDING 假審批
+**後端 API**: `/api/v1/approvals/pending` 已存在
+
+| 修復項 | 現況 (假) | 修復方案 |
+|--------|-----------|----------|
+| L17-33: MOCK_PENDING | 假審批 APR-2026-0042 | 接收 `approvals` prop (PendingApprovalsResponse) |
+| 無 pending 時 | 永遠顯示假審批 | 顯示 "目前無待審批項目" EmptyState |
+| 審批動作 | 純前端 state | 呼叫真實 `/api/v1/approvals/{id}/sign` 或 `/reject` |
+
+**新增 Props**:
+```typescript
+interface Props {
+ approvals: ApprovalItem[] // 從 page.tsx 傳入
+ onApprove: (id: string) => Promise
+ onReject: (id: string) => Promise
+}
+```
+
+### P0-4: page.tsx (Neural Command) 修改
+
+page.tsx 已在 fetch 四個 API,但需要:
+1. 將 `pendingApprovals` count 和 approvals list 傳給子組件
+2. 新增 incidents fetch 給 Alert Radar
+
+---
+
+## Phase P1: 假用戶身份清除
+
+**影響**: 7 處使用 `demo-user` / `user-001` / `Demo User`
+
+| 檔案 | 修復方案 |
+|------|----------|
+| live-approval-panel.tsx:101-102 | 改用 `AWOOOI_USER_ID` / `AWOOOI_USER_NAME` 環境變數或 constants |
+| conversational-view.tsx:65-66 | 同上 |
+| batch-mode-selector.tsx:74-75 | 同上 |
+| hitl-section.tsx:238,258 | 同上 |
+| ai-command-panel.tsx:54,60 | 同上 |
+
+**方案**: 建立 `src/constants/user.ts`:
+```typescript
+// 統帥是唯一用戶,在多用戶系統實作前使用固定身份
+export const CURRENT_USER = {
+ id: 'commander',
+ name: '統帥',
+} as const
+```
+
+---
+
+## Phase P2: Demo 組件清理
+
+| 檔案 | 動作 |
+|------|------|
+| charts/global-pulse-chart.tsx:190-223 | 刪除 `GlobalPulseChartDemo` |
+| charts/ai-process-stepper.tsx:236-273 | 刪除 `AIProcessStepperDemo` |
+| charts/index.ts:8-9 | 移除 Demo 匯出 |
+| approval/index.ts:32-98 | 刪除 `MOCK_APPROVAL_*` 三個常數 |
+| incident/thinking-terminal.tsx:359-387 | 刪除 `DEMO_DECISION_CHAIN` |
+| incident/index.ts:8 | 移除匯出 |
+
+**前提確認**: 搜尋確認這些 export 沒有被任何生產頁面 import。
+
+---
+
+## Phase P3: Demo 頁面
+
+| 動作 | 說明 |
+|------|------|
+| 保留但加保護 | `/demo` 路由加 `NEXT_PUBLIC_ENABLE_DEMO=true` 環境變數保護 |
+
+**理由**: Demo 頁面用於內部測試 (LiveDashboard + HITLSection 都接真實 API),但不應在生產暴露。
+
+---
+
+## 實施順序 (無衝突)
+
+```
+P0-4 (page.tsx 增加 props 傳遞)
+ ↓
+P0-1 (NeuralLiveCenter) ─── 可平行 ──→ P0-2 (NeuralStats)
+ ↓
+P0-3 (NeuralApprovalPanel)
+ ↓
+P1 (假用戶身份)
+ ↓
+P2 (Demo 組件清理)
+ ↓
+P3 (Demo 頁面保護)
+ ↓
+驗證 + 文件更新
+```
+
+**P0-1 和 P0-2 可平行** (無依賴),P0-3 需要 P0-4 先完成 (approvals prop)。
+
+---
+
+## 驗證清單
+
+- [ ] `npm run build` 通過 (無 TypeScript 錯誤)
+- [ ] Neural Command 頁面四個 Tab 正常渲染
+- [ ] 無資料時顯示 `--` 或 EmptyState (非假數據)
+- [ ] 有真實資料時正確顯示
+- [ ] 審批操作呼叫真實 API
+- [ ] 全域搜尋 `MOCK_` / `DEMO_` / `demo-user` 零殘留
+- [ ] `grep -r "136/156\|0\.87\|4\.2s" apps/web/src/` 零殘留