docs(plan): Sprint 5 完整解決方案 — 與現有架構整合的細化實施計畫
This commit is contained in:
746
docs/superpowers/plans/2026-04-08-sprint5-complete-solution.md
Normal file
746
docs/superpowers/plans/2026-04-08-sprint5-complete-solution.md
Normal file
@@ -0,0 +1,746 @@
|
||||
# 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 可接受 |
|
||||
Reference in New Issue
Block a user