# Sprint 5 指令中心重設計 — 完整解決方案與整合計畫

> **建立**: 2026-04-08 (台北)
> **建立者**: Claude Code
> **狀態**: 📋 待統帥審核
> **ADR**: ADR-060 React Flow + elkjs (已批准)

---

## 一、現有架構與資料流完整地圖

### 1.1 資料從哪裡來？

```
┌─────────────────────────────────────────────────────────┐
│                    資料源 (SSOT)                          │
│  ops/monitoring/service-registry.yaml (717行)            │
│  定義: 6 K8s + 10 Docker + 1 Systemd + 6 Nodes +       │
│        6 Pages + 8 API + 4 AI = 41 個受監控實體          │
│        + 15 個告警模板 + 9 個自動修復動作                │
└────────────────────────┬────────────────────────────────┘
                         │
           ┌─────────────┼─────────────────┐
           ▼             ▼                 ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Prometheus   │ │ Alertmanager │ │ Blackbox Exporter │
│ 22 targets   │ │ 71 rules     │ │ HTTP/TCP probes   │
└──────┬───────┘ └──────┬───────┘ └────────┬──────────┘
       │                │                   │
       ▼                ▼                   ▼
┌──────────────────────────────────────────────────────┐
│               後端 API 層                              │
│  apps/api/src/services/host_aggregator.py (500+行)    │
│  HostAggregator.fetch_all() → 4 主機並行 TCP/HTTP 探測│
│  回傳: AggregatedStatus {hosts, services, metrics}    │
└────────────────────────┬─────────────────────────────┘
                         │
           ┌─────────────┼─────────────────┐
           ▼             ▼                 ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ GET          │ │ GET          │ │ GET              │
│ /dashboard   │ │ /dashboard/  │ │ /dashboard/hosts │
│ (聚合資料)   │ │ stream (SSE) │ │ (4主機概覽)      │
│ 389行        │ │ 每30秒推送   │ │                  │
└──────┬───────┘ └──────┬───────┘ └────────┬──────────┘
       │                │                   │
       ▼                ▼                   ▼
┌──────────────────────────────────────────────────────┐
│               前端 Store 層                            │
│  apps/web/src/stores/dashboard.store.ts (449行)       │
│  Zustand: Host[] + HostService[] + HostMetrics        │
│  SSE 自動重連 (exponential backoff)                   │
└────────────────────────┬─────────────────────────────┘
                         │
           ┌─────────────┼─────────────────┐
           ▼             ▼                 ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ page.tsx     │ │ topology/    │ │ neural-command/   │
│ (首頁)       │ │ page.tsx     │ │ page.tsx          │
│ HostGrid     │ │ (103行,簡單) │ │ (4元件,1052行)    │
│ MetricsStrip │ │              │ │                   │
└──────────────┘ └──────────────┘ └──────────────────┘
```

### 1.2 現有 API 回傳的完整資料結構

```typescript
// GET /api/v1/dashboard 回傳:
interface DashboardResponse {
  timestamp: string
  environment: string
  mock_mode: boolean
  overall_status: 'healthy' | 'degraded' | 'unhealthy'
  hosts: HostStatusResponse[]      // 4 個主機
  alerts_count: number
  pending_approvals: number
}

interface HostStatusResponse {
  ip: string                       // "192.168.0.110"
  name: string                     // "DevOps 金庫"
  role: string                     // "devops" | "security" | "k3s" | "ai_web"
  status: string                   // "healthy" | "degraded" | "unhealthy" | "unreachable"
  services: ServiceStatus[]        // 每主機的服務清單
  metrics: HostMetricsResponse     // CPU/Memory/Disk/Load/Baseline
  last_check: string
}

interface ServiceStatus {
  name: string                     // "PostgreSQL"
  status: 'up' | 'down' | 'degraded'
  port: number                     // 5432
  latency_ms: number               // 2.3
  error: string | null
}

interface HostMetricsResponse {
  cpu_percent: number              // 45.2
  memory_percent: number           // 62.8
  disk_percent: number             // 71.0
  load_avg_1m: number              // 1.23
  uptime_hours: number             // 720
  cpu_baseline: BaselineResponse   // {baseline: 50, std: 10, sigma: -0.48}
  memory_baseline: BaselineResponse
}
```

**結論: 拓撲圖需要的 90% 資料已經在 API 裡了。**

### 1.3 缺什麼？需要擴充什麼？

| 需要的資料 | 現有來源 | 缺口 | 解決方案 |
|-----------|---------|------|---------|
| 主機清單 + 狀態 | ✅ `/dashboard` hosts | — | 直接用 |
| 每主機的服務 | ✅ `/dashboard` hosts[].services | — | 直接用 |
| CPU/Memory 指標 | ✅ `/dashboard` hosts[].metrics | — | 直接用 |
| 即時更新 | ✅ `/dashboard/stream` SSE | — | 直接用 |
| **服務間依賴關係** | ❌ 不存在 | **缺** | **新增靜態定義** |
| **群組分類 (拓撲層級)** | ⚠️ role 欄位只有 4 種 | **部分** | **擴充 role + group** |
| **AI 介入狀態** | ⚠️ 沒有即時 field | **缺** | **擴充 SSE 事件** |
| **K8s Pod 明細** | ⚠️ 只有 service 級 | **缺** | **新增 k3s_monitor 整合** |

---

## 二、完整解決方案 — 分四個工作包

### 工作包 1: 前端拓撲元件 (核心)

**目標**: 建立 React Flow + elkjs 的嵌套群組拓撲圖元件

#### WP1-1: 安裝依賴

```bash
cd apps/web
npm install @xyflow/react elkjs
```

**影響檔案**: 
- `apps/web/package.json` (新增 2 deps)
- `apps/web/package-lock.json`

**驗證**: 
```bash
cd apps/web && node -e "require('@xyflow/react'); require('elkjs'); console.log('OK')"
```

---

#### WP1-2: 建立元件目錄

**新增檔案清單** (8 個新檔案):

| 檔案 | 行數(預估) | 功能 |
|------|-----------|------|
| `src/components/topology/index.ts` | 10 | 匯出 |
| `src/components/topology/ServiceTopology.tsx` | 150 | 主元件: ReactFlow + Controls + MiniMap |
| `src/components/topology/nodes/GroupNode.tsx` | 120 | 群組節點: 展開/收合 + 摘要資訊 |
| `src/components/topology/nodes/ServiceNode.tsx` | 80 | 服務節點: 狀態燈 + 名稱 + 指標 (memo) |
| `src/components/topology/edges/TopologyEdge.tsx` | 60 | 自定義邊線: 漸層 + 動畫 |
| `src/components/topology/hooks/useTopologyData.ts` | 100 | 從 dashboard.store → nodes/edges 轉換 |
| `src/components/topology/hooks/useElkLayout.ts` | 80 | elkjs 嵌套佈局計算 |
| `src/components/topology/topology.css` | 30 | 邊線動畫 keyframes |

**總新增**: ~630 行

---

#### WP1-3: ServiceNode.tsx 詳細設計

```typescript
// 與現有架構的整合點:
// 1. 讀取 dashboard.store.ts 的 HostService 型別 (已定義)
// 2. 色彩來自 tailwind.config.ts 的 status.* (已定義)
// 3. Icon 來自 lucide-react (已安裝)
// 4. 字型 DM Mono / JetBrains Mono (已載入)

import { memo } from 'react'
import { Handle, Position, type NodeProps } from '@xyflow/react'
import { Server, Database, Globe, Brain, Shield, Box } from 'lucide-react'
import { cn } from '@/lib/utils'

// 服務類型 → Lucide icon 映射
const SERVICE_ICONS: Record<string, React.ElementType> = {
  'PostgreSQL': Database,
  'Redis': Database,
  'Ollama': Brain,
  'OpenClaw': Brain,
  'Harbor': Box,
  'Sentry': Shield,
  'Nginx': Globe,
  'K3s API': Server,
  // ... 完整映射
}

// 狀態 → tailwind.config.ts 色彩
const STATUS_COLORS = {
  up: 'border-status-healthy text-status-healthy',        // #22C55E
  down: 'border-status-severe text-status-severe',         // #FF3300
  degraded: 'border-status-warning text-status-warning',   // #F59E0B
  healthy: 'border-status-healthy text-status-healthy',
  thinking: 'border-status-thinking text-status-thinking',  // #8B5CF6
}

export const ServiceNode = memo(({ data }: NodeProps) => {
  const Icon = SERVICE_ICONS[data.name] || Server
  const colorClass = STATUS_COLORS[data.status] || 'border-border'
  
  return (
    <div className={cn(
      'bg-card border-[0.5px] rounded-[8px] px-3 py-2 min-w-[120px]',
      'shadow-sm hover:shadow-md transition-shadow',
      'font-body text-xs',
      colorClass
    )}>
      <Handle type="target" position={Position.Left} className="!bg-border !w-1.5 !h-1.5" />
      
      {/* 狀態燈 + 狀態文字 */}
      <div className="flex items-center gap-1.5 mb-1">
        <span className={cn('w-1.5 h-1.5 rounded-full', /* status bg */)} />
        <span className="text-[8px] text-text3 uppercase tracking-wider">{data.statusText}</span>
      </div>
      
      {/* Icon + 服務名稱 */}
      <div className="flex items-center gap-1.5">
        <Icon className="w-3.5 h-3.5 opacity-60" />
        <span className="font-semibold text-[11px]">{data.name}</span>
      </div>
      
      {/* 位置 + 類型 */}
      <div className="text-[8px] text-text3 font-mono mt-0.5">
        {data.port && `${data.host}:${data.port}`}
        {data.latency && ` · ${data.latency}ms`}
      </div>
      
      <Handle type="source" position={Position.Right} className="!bg-border !w-1.5 !h-1.5" />
    </div>
  )
}, (prev, next) => {
  // 自定義比較: 只在狀態/指標變化時 re-render
  return prev.data.status === next.data.status
    && prev.data.latency === next.data.latency
    && prev.data.cpu === next.data.cpu
})
```

**與現有架構的整合**:
- `HostService` 型別來自 `dashboard.store.ts` L28-34 (已定義)
- `cn()` 來自 `@/lib/utils` (已存在)
- `status.*` 色彩來自 `tailwind.config.ts` L39-45 (已定義)
- `lucide-react` 已安裝 (v0.577.0)

---

#### WP1-4: GroupNode.tsx 詳細設計

```typescript
// 與現有架構的整合:
// 1. Host 型別來自 dashboard.store.ts (已定義)
// 2. HostGrid 的 statusColor() 邏輯可複用
// 3. useTranslations('topology') 用既有 i18n key

// 群組節點 = 4 主機 (110/112/120/188) + K3s + 外部
// 收合時: 顯示群組名稱 + 服務數 + 健康摘要
// 展開時: 內部節點由 elkjs 自動排列

// 群組分類 (對應 service-registry.yaml nodes[].role):
const GROUP_STYLES = {
  devops:   { border: 'border-blue-400/30',  bg: 'bg-blue-50/30',   label: '基礎設施' },
  security: { border: 'border-red-400/30',   bg: 'bg-red-50/30',    label: 'Security' },
  k3s:      { border: 'border-purple-400/30',bg: 'bg-purple-50/30', label: 'K3s 叢集' },
  ai_web:   { border: 'border-orange-400/30',bg: 'bg-orange-50/30', label: 'AI/數據中心' },
  external: { border: 'border-amber-400/30', bg: 'bg-amber-50/30',  label: '外部服務' },
}
```

---

#### WP1-5: useTopologyData.ts 詳細設計

```typescript
// 關鍵: 從現有 dashboard.store 讀取 Host[]，轉換為 React Flow nodes/edges

import { useDashboardStore } from '@/stores/dashboard.store'
import type { Node, Edge } from '@xyflow/react'

export function useTopologyData() {
  // 直接讀取現有 store — 不需要新的 API 呼叫！
  const hosts = useDashboardStore(s => s.hosts)
  const overallStatus = useDashboardStore(s => s.overallStatus)
  
  // 轉換 Host[] → React Flow Node[]
  const { nodes, edges } = useMemo(() => {
    const nodes: Node[] = []
    const edges: Edge[] = []
    
    // Step 1: 建立群組節點 (每個主機 = 一個群組)
    hosts.forEach(host => {
      nodes.push({
        id: `group-${host.ip}`,
        type: 'groupNode',
        data: {
          label: host.name,        // "DevOps 金庫"
          role: host.role,          // "devops"
          status: host.status,      // "healthy"
          serviceCount: host.services.length,
          healthySvc: host.services.filter(s => s.status === 'up').length,
          metrics: host.metrics,    // CPU/Memory 完整指標
        },
        position: { x: 0, y: 0 }, // elkjs 會計算
      })
      
      // Step 2: 建立服務節點 (每個服務 = 群組內的子節點)
      host.services.forEach(svc => {
        nodes.push({
          id: `svc-${host.ip}-${svc.name}`,
          type: 'serviceNode',
          parentId: `group-${host.ip}`,  // elkjs 嵌套!
          data: {
            name: svc.name,
            status: svc.status,
            port: svc.port,
            latency: svc.latency_ms,
            host: host.ip,
          },
          position: { x: 0, y: 0 },
        })
      })
    })
    
    // Step 3: 服務間依賴邊線 (靜態定義)
    DEPENDENCY_MAP.forEach(([from, to]) => {
      edges.push({
        id: `edge-${from}-${to}`,
        source: from,
        target: to,
        type: 'topologyEdge',
      })
    })
    
    return { nodes, edges }
  }, [hosts])
  
  return { nodes, edges, overallStatus }
}
```

**與現有架構的整合**:
- `useDashboardStore` 來自 `stores/dashboard.store.ts` (已存在)
- `Host`, `HostService` 型別已定義
- **不需要新的 API 呼叫** — 直接用 store 裡的資料

---

#### WP1-6: 服務間依賴關係定義

```typescript
// 靜態依賴定義 (對應 host_aggregator.py HOST_CONFIGS)
// 資料來源: service-registry.yaml + ConfigMap 環境變數

// 格式: [source_node_id, target_node_id]
export const DEPENDENCY_MAP: [string, string][] = [
  // K3s Pod 間依賴
  ['svc-K3s-awoooi-web', 'svc-K3s-awoooi-api'],
  ['svc-K3s-awoooi-api', 'svc-192.168.0.188-PostgreSQL'],
  ['svc-K3s-awoooi-api', 'svc-192.168.0.188-Redis'],
  ['svc-K3s-awoooi-api', 'svc-192.168.0.188-OpenClaw'],
  ['svc-K3s-awoooi-worker', 'svc-192.168.0.188-PostgreSQL'],
  ['svc-K3s-awoooi-worker', 'svc-192.168.0.188-Redis'],
  ['svc-K3s-awoooi-worker', 'svc-192.168.0.188-Ollama'],
  
  // AI 服務依賴
  ['svc-192.168.0.188-OpenClaw', 'svc-192.168.0.188-Ollama'],
  ['svc-192.168.0.188-OpenClaw', 'svc-192.168.0.188-Redis'],
  
  // 監控工具依賴
  ['svc-192.168.0.188-SigNoz', 'svc-192.168.0.188-ClickHouse'],
  
  // DevOps 依賴
  ['svc-K3s-awoooi-api', 'svc-192.168.0.110-Sentry'],
  ['svc-K3s-awoooi-web', 'svc-192.168.0.110-Sentry'],
]

// 這些依賴來自:
// 1. ConfigMap OLLAMA_URL → API 依賴 Ollama
// 2. ConfigMap OPENCLAW_URL → API 依賴 OpenClaw
// 3. config.py DATABASE_URL → API 依賴 PostgreSQL
// 4. config.py REDIS_URL → API 依賴 Redis
// 5. Sentry SDK → API/Web 依賴 Sentry
```

---

#### WP1-7: useElkLayout.ts 詳細設計

```typescript
import ELK from 'elkjs/lib/elk.bundled'

const elk = new ELK()

export async function computeElkLayout(
  nodes: Node[],
  edges: Edge[],
  expanded: Set<string>, // 展開的群組 ID
): Promise<{ nodes: Node[], edges: Edge[] }> {
  
  // 建立 elk graph (嵌套結構)
  const elkGraph = {
    id: 'root',
    layoutOptions: {
      'elk.algorithm': 'layered',
      'elk.direction': 'RIGHT',
      'elk.spacing.nodeNode': '30',
      'elk.layered.spacing.nodeNodeBetweenLayers': '60',
      'elk.edgeRouting': 'ORTHOGONAL',
    },
    children: buildElkChildren(nodes, expanded),
    edges: edges.map(e => ({
      id: e.id,
      sources: [e.source],
      targets: [e.target],
    })),
  }
  
  const layout = await elk.layout(elkGraph)
  
  // 將 elk 計算的座標寫回 React Flow nodes
  return applyElkPositions(nodes, layout)
}
```

---

### 工作包 2: 後端 API 擴充

**目標**: 擴充 dashboard API 支援拓撲圖需要的額外資料

#### WP2-1: 擴充 DashboardResponse 加入 K8s Pod 明細

**修改檔案**: `apps/api/src/api/v1/dashboard.py`

```python
# 新增: K8s Pod 狀態 (來自 k3s_monitor_service)
class PodStatusResponse(BaseModel):
    name: str
    namespace: str
    status: str  # Running, Pending, CrashLoopBackOff
    restart_count: int
    cpu_millicores: int | None
    memory_mb: int | None

class DashboardResponse(BaseModel):
    # ... 現有欄位保留 ...
    k8s_pods: list[PodStatusResponse] = []  # 新增
    ai_diagnosing: list[str] = []            # 新增: OpenClaw 正在分析的服務名稱
```

**與現有架構整合**:
- `k3s_monitor_service.py` (9,077行) 已有完整的 Pod 查詢邏輯
- 只需在 `aggregated_to_response()` 中加入 k3s 資料

---

#### WP2-2: 新增 AI 診斷狀態到 SSE

**修改檔案**: `apps/api/src/api/v1/dashboard.py` 的 `dashboard_update_loop()`

```python
# 在 SSE 推送中加入 AI 診斷狀態
# 來源: 從 Redis working memory 讀取正在分析的 incident
async def get_ai_diagnosing_services() -> list[str]:
    """取得 OpenClaw 正在分析的服務名稱"""
    from src.services.incident_service import get_incident_service
    incidents = await get_incident_service().list_active_incidents()
    return [i.affected_services[0] for i in incidents if i.affected_services]
```

---

### 工作包 3: 頁面整合

#### WP3-1: topology/page.tsx 全面重寫

**修改檔案**: `apps/web/src/app/[locale]/topology/page.tsx`
**當前**: 103 行 (簡單 host list)
**目標**: ~200 行 (ServiceTopology + 篩選器 + 控制列)

```typescript
// 整合方式:
// 1. 保留 AppLayout wrapper (現有 layout 不動)
// 2. 替換內容為 ServiceTopology 元件
// 3. 加入篩選器 (按群組/狀態)
// 4. 保留現有 i18n key (topology.*)

import { ServiceTopology } from '@/components/topology'

export default function TopologyPage({ params }) {
  return (
    <AppLayout locale={params.locale}>
      <ServiceTopology
        mode="full"           // 完整模式: 可展開所有群組
        showControls={true}   // 顯示 zoom/fit/篩選
        showMiniMap={true}    // 顯示迷你地圖
      />
    </AppLayout>
  )
}
```

---

#### WP3-2: 首頁 page.tsx 嵌入迷你拓撲

**修改檔案**: `apps/web/src/app/[locale]/page.tsx`

```typescript
// 整合方式:
// 1. 在現有 Metrics Strip 下方加入迷你拓撲 (收合模式)
// 2. 不移除現有 HostGrid — 兩者共存
// 3. 拓撲圖高度固定 300px，不影響下方 Incident Feed

<ServiceTopology
  mode="compact"          // 收合模式: 只顯示群組
  showControls={false}    // 不顯示控制列
  showMiniMap={false}
  height={300}
/>
```

---

#### WP3-3: i18n 擴充

**修改檔案**: `messages/zh-TW.json`, `messages/en.json`

```json
{
  "topology": {
    // 保留現有 key
    "title": "拓撲圖",
    "subtitle": "服務依賴與健康狀態",
    // 新增
    "expandAll": "全部展開",
    "collapseAll": "全部收合",
    "showAnomaliesOnly": "只看異常",
    "groupInfra": "基礎設施 (.110)",
    "groupAI": "AI/數據中心 (.188)",
    "groupK3s": "K3s 叢集",
    "groupExternal": "外部服務",
    "healthy": "健康",
    "warning": "警告",
    "critical": "異常",
    "aiDiagnosing": "AI 診斷中",
    "servicesCount": "{count} 個服務",
    "allHealthy": "全部健康",
    "expandGroup": "展開群組",
    "collapseGroup": "收合群組",
    "dependencies": "依賴關係",
    "noData": "無拓撲資料"
  }
}
```

---

### 工作包 4: Tailwind 設計系統擴充

**修改檔案**: `apps/web/tailwind.config.ts`

```typescript
// 新增動畫 (在現有 animation 區塊下方):
animation: {
  // ... 現有 11 種動畫保留 ...
  'edge-flow': 'edge-flow 1.5s linear infinite',
  'ai-pulse': 'ai-pulse 2s ease-in-out infinite',
  'node-glow': 'node-glow 2s ease-in-out infinite',
},
keyframes: {
  // ... 現有 keyframes 保留 ...
  'edge-flow': {
    'to': { 'stroke-dashoffset': '-12' },
  },
  'ai-pulse': {
    '0%, 100%': { 'box-shadow': '0 0 0 0 rgba(74, 144, 217, 0.2)' },
    '50%': { 'box-shadow': '0 0 0 8px rgba(74, 144, 217, 0)' },
  },
  'node-glow': {
    '0%, 100%': { opacity: '1' },
    '50%': { opacity: '0.5' },
  },
}
```

---

## 三、完整檔案影響清單

### 新增檔案 (8 個)

| # | 檔案 | 行數 | 功能 |
|---|------|------|------|
| 1 | `components/topology/index.ts` | 10 | 匯出 |
| 2 | `components/topology/ServiceTopology.tsx` | 150 | 主元件 |
| 3 | `components/topology/nodes/GroupNode.tsx` | 120 | 群組節點 |
| 4 | `components/topology/nodes/ServiceNode.tsx` | 80 | 服務節點 |
| 5 | `components/topology/edges/TopologyEdge.tsx` | 60 | 自定義邊線 |
| 6 | `components/topology/hooks/useTopologyData.ts` | 100 | 資料轉換 |
| 7 | `components/topology/hooks/useElkLayout.ts` | 80 | 佈局引擎 |
| 8 | `components/topology/topology.css` | 30 | 動畫 CSS |

**新增總計: ~630 行**

### 修改檔案 (6 個)

| # | 檔案 | 改動幅度 | 內容 |
|---|------|---------|------|
| 1 | `apps/web/package.json` | +2 行 | 新增依賴 |
| 2 | `apps/web/tailwind.config.ts` | +15 行 | 新增動畫 |
| 3 | `apps/web/src/app/[locale]/topology/page.tsx` | 重寫 (~200行) | 整合 ServiceTopology |
| 4 | `apps/web/src/app/[locale]/page.tsx` | +10 行 | 嵌入迷你拓撲 |
| 5 | `apps/api/src/api/v1/dashboard.py` | +30 行 | K8s Pod + AI 狀態 |
| 6 | `messages/zh-TW.json` + `en.json` | +20 行 | i18n key |

### 不修改的檔案 (穩定層)

| 檔案 | 原因 |
|------|------|
| `dashboard.store.ts` | 拓撲直接讀取現有 store，不需改 |
| `host_aggregator.py` | 現有探測邏輯不動 |
| `service-registry.yaml` | SSOT 不需改 |
| `AppLayout/Header/Sidebar` | 拓撲頁掛在現有 layout 下 |
| `HostGrid` | 首頁保留，拓撲頁獨立 |
| `CI/CD workflows` | 新程式碼走現有 pipeline |

---

## 四、細化實施步驟 (15 步)

### Phase 1: 安裝 + 骨架 (2小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 1.1 | `npm install @xyflow/react elkjs` | import 成功 |
| 1.2 | 建立 `components/topology/` 目錄結構 | ls 確認 8 檔案 |
| 1.3 | tailwind.config.ts 加入 3 個動畫 | build 無錯 |

### Phase 2: 核心元件 (4小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 2.1 | 實作 ServiceNode.tsx (memo + Tailwind + Lucide) | 單一節點渲染正確 |
| 2.2 | 實作 GroupNode.tsx (展開/收合 + 摘要) | 群組展開/收合切換 |
| 2.3 | 實作 TopologyEdge.tsx (漸層 + CSS 動畫) | 三種邊線樣式 |
| 2.4 | 實作 useElkLayout.ts (elkjs 嵌套) | 20 節點正確佈局 |

### Phase 3: 資料整合 (3小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 3.1 | 實作 useTopologyData.ts (store → nodes/edges) | hosts 正確轉換 |
| 3.2 | 定義 DEPENDENCY_MAP (靜態依賴) | 邊線正確連接 |
| 3.3 | 實作 ServiceTopology.tsx (組裝全部) | 完整拓撲渲染 |

### Phase 4: 頁面整合 (2小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 4.1 | topology/page.tsx 全面重寫 | 拓撲頁完整功能 |
| 4.2 | page.tsx 嵌入迷你拓撲 | 首頁有拓撲區塊 |
| 4.3 | i18n 擴充 | 所有文字有翻譯 |

### Phase 5: 後端擴充 (2小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 5.1 | dashboard.py 加入 K8s Pod + AI 狀態 | curl 回傳新欄位 |
| 5.2 | SSE 推送加入 AI 診斷事件 | SSE 有新事件類型 |

### Phase 6: 驗收 (1小時)

| Step | 動作 | 驗證 |
|------|------|------|
| 6.1 | TypeScript 編譯 (`tsc --noEmit`) | 零錯誤 |
| 6.2 | 生產建置 (`pnpm build`) | 建置成功 |
| 6.3 | 視覺驗收 (群組展開/收合/動畫) | 統帥確認 |

---

## 五、前置條件 — 先 PoC 還是直接實作？

### 選項 A: 先做 PoC (建議)

```
Day 1: PoC (React Flow + elkjs 基本嵌套群組)
       → 部署到內網 http://192.168.0.188:8765/poc-topology.html
       → 統帥實際操作確認展開/收合/佈局效果
       
Day 2: 基於 PoC 回饋 → 製作 3+ 設計稿
       → 統帥選擇佈局比例/面板模式

Day 3-7: 正式實作 (Phase 1-6)
```

### 選項 B: 直接實作

```
Day 1-2: Phase 1-2 (安裝+核心元件)
Day 3:   Phase 3 (資料整合)
Day 4:   Phase 4 (頁面整合)
Day 5:   Phase 5-6 (後端+驗收)
```

---

## 六、需要統帥決策的事項

| # | 決策項 | 選項 | 影響 |
|---|--------|------|------|
| **D1** | 先 PoC 還是直接實作？ | A: 先 PoC / B: 直接做 | 時程差 1-2 天 |
| **D2** | 拓撲圖放在哪裡？ | A: 替代首頁 / B: 首頁+拓撲頁並存 / C: 只在拓撲頁 | 影響 page.tsx |
| **D3** | K3s Pod 要不要展示？ | A: 只到服務級 / B: 展開到 Pod 級 | 影響資料量 |
| **D4** | 依賴關係定義方式 | A: 靜態 TypeScript / B: 後端 API 動態 | 影響維護方式 |

---

## 七、與統帥截圖概念的對應

| 統帥截圖元素 | 實作方式 | 對應 WP |
|-------------|---------|---------|
| 神經拓撲圖 (6節點) | ServiceTopology + GroupNode | WP1 |
| KPI 橫條 (4指標) | 現有 Metrics Strip (不動) | — |
| 待核准任務 | 現有 `/approvals/pending` API | — |
| 處置統計 (4色) | Sprint 4 已實作 | — |
| 活動串流 | 現有 SSE `/dashboard/stream` | — |
| 底部控制列 | ServiceTopology controls prop | WP1 |

---

## 八、風險矩陣

| 風險 | 機率 | 影響 | 緩解 |
|------|------|------|------|
| elkjs 佈局效果不如預期 | 中 | 高 | **先做 PoC 驗證** |
| 節點數 100+ DOM 效能 | 低 | 中 | React.memo + 收合群組 |
| 依賴關係定義維護 | 中 | 低 | 先靜態，未來 OTEL |
| 與現有首頁衝突 | 低 | 低 | 拓撲圖是新增元件 |
| build 大小增加 | 低 | 低 | +270KB gzip 可接受 |
