# AWOOOI 基礎設施完整重建計畫
## Sprint A + B + C — 正式設計規格文件

> **文件版本**: v2.2
> **建立日期**: 2026-04-10
> **更新日期**: 2026-04-11 (Session 6 整合更新)
> **狀態**: ✅ Sprint A/B/C + MCP Phase 1-4 + ADR-071 A-J 全部完成 — 進入 ADR-072 Bug 修復階段
> **ADR**: ADR-069（基礎設施重建）+ ADR-070（全自動 AIOps 閉環）+ ADR-072（Bug 修復）
> **作者**: Claude Sonnet 4.6 + 首席架構師審查通過

---

## 一、背景與目標

### 問題陳述

AWOOOI 私有雲目前存在三大結構性問題：

1. **服務散落、設定漂移**：服務分散於 Docker、PM2、systemd 各層，nginx conf 在兩台主機各自演化，無版本控制
2. **無變更管理**：所有主機設定為手動操作，無法追蹤、無法回滾
3. **無災難備援**：無 K8s 備份、無 Host 層備份、無標準回復程序

### 目標

- Sprint A：讓所有主機進入**可預測的乾淨狀態**（清廢棄、修錯誤、統一入口）
- Sprint B：建立 **Git = 唯一真相來源**（Ansible 管 Host、ArgoCD 管 K8s）
- Sprint C：實現**任意單點失效 15 分鐘內可恢復**

### 鐵律

- 🔴 **零影響現有商業流量**：awoooi、bitan、tsenyang、momo 全程不中斷
- 🔴 **所有網站服務 = Docker**：bitan 遷移後統一，不上 K3s
- 🔴 **188 = 唯一 SSL 終止器**：110 退出 HTTPS 競爭
- 🔴 **112 Kali = 預留安全接口**：本次不實作，文件記錄預留位置

---

## 二、現況真相地圖

### 主機角色

| 主機 | IP | 角色 | 關鍵服務 |
|------|----|------|---------|
| AI+Web 中心 | 192.168.0.188 | SSL 終止器、應用宿主 | Nginx, OpenClaw:8088, tsenyang:3000, momo:5003, signoz:3301, minio:9000, litellm:4000 |
| DevOps 金庫 | 192.168.0.110 | CI/CD、監控、DevOps 工具 | Harbor:5000, Sentry:9000, Gitea:3001, Langfuse:3100, 5×GitHub Runner, PM2(bitan:3003, wooo-frontend:3000), stockPlatform:31235, keepalived BACKUP(VIP:200) |
| K3s Control-Plane #1 | 192.168.0.120 | K8s Master | awoooi-prod namespace |
| K3s Control-Plane #2 | 192.168.0.121 | K8s Master HA | awoooi-prod namespace |
| K3s VIP | 192.168.0.125 | K8s API + NodePort | 32334(API), 32335(Web) |
| Nginx HA VIP | 192.168.0.200 | Nginx HA | 188 MASTER（未裝）/ 110 BACKUP |
| Kali Security | 192.168.0.112 | 安全掃描（預留） | Scanner API:8080 |

### 完整問題清單（21 項）

| # | 問題 | 主機 | Sprint | 根因 |
|---|------|------|--------|------|
| P1 | GitLab volumes 殘骸 | 188 | A | GitLab 廢棄後未清除 |
| P2 | GitLab volumes + 目錄殘骸 | 110 | A | 同上 |
| P3 | Nginx gitlab block 仍存在 | 188 | A | 同上 |
| P4 | all-sites-from-188.conf 漂移 | 110 | A | 複製 conf 未同步演化 |
| P5 | harbor nginx 指向 :5050（應為 :5000） | 110 | A | 手動設定錯誤 |
| P6 | ollama.wooo.work 獨立 conf 指向 :3000（tsenyang 佔用） | 188 | A | open-webui 未啟動 |
| P7 | n8n 容器未啟動（nginx conf 已存在） | 188 | A | 未啟動 |
| P8 | open-webui 容器未啟動 | 188 | A | 未啟動 |
| P9 | docker-registry :5000 與 Harbor 衝突 | 188 | A | Port 規劃衝突 |
| P10 | Swap 100% 滿（3.8GB） | 110 | A | Sentry snuba crash loop |
| P11 | 6 個 snuba 容器 crash loop | 110 | A | Docker 網路 DNS 無法解析 kafka:9092 |
| P12 | K8s Secrets SENTRY_DSN = "CHANGE_ME" | K3s | A | Phase 6 未完成 |
| P13 | Sentry Phase 6 Webhook→Telegram 告警未完成 | 全站 | A | 未實作 |
| P14 | 缺 sentry/gitea/langfuse/stock HTTPS | 188 | A | SSL 未申請 |
| P15 | signoz.wooo.work 只有 HTTP（all-sites.conf） | 188 | A | SSL 未設定 |
| P16 | keepalived MASTER 未安裝（VIP:200 單點） | 188 | A | 未裝 |
| P17 | bitan 跑 PM2，應遷移 Docker | 110 | A | 歷史遺留 |
| P18 | stock.wooo.work 無 HTTPS + 資料待確認 | 110 | A | SSL 未申請 |
| P19 | wooo-aiops 狀態未確認 | 110 | A | 未盤點 |
| P20 | ArgoCD 未安裝 | K3s | B | 未實作 |
| P21 | Ansible Host IaC 空骨架 | 全站 | B | 未實作 |
| P22 | Velero 未安裝 | K3s | C | 未實作 |
| P23 | Host 層無備份機制 | 188/110 | C | 未實作 |

---

## 三、架構決策（ADR-069）

### 3.1 Nginx HA 架構

```
外部 DNS → 192.168.0.188（平時）
               ↓
         keepalived MASTER（priority 100）
               ↓
         VIP 192.168.0.200
        ↙                 ↘
188:443（主動）          110:443（備援）
                         keepalived BACKUP（priority 90）
                         188 宕機時自動接管
```

**決策**：188 必須安裝 keepalived MASTER，才能讓 VIP:200 真正 HA。

### 3.2 GitOps 架構

```
git push gitea main
→ Gitea Actions: docker build + push Harbor（image:sha）
→ 更新 k8s/awoooi-prod/ 中的 image tag（git commit）
→ ArgoCD 偵測 YAML 變更 → 自動 apply 到 K3s
→ rollout 完成（取代現有 kubectl set image）
```

**回滾**：`argocd app rollback awoooi-prod <revision>`（30 秒）或 `git revert + push`

### 3.3 備援架構

```
K8s 層：Velero 每日快照 PV → MinIO(188:9000/velero-backups) → velero restore（<15 分鐘）
Host 層：重要 Docker volume → rsync → 對方主機 → 手動恢復（<30 分鐘）
Nginx 層：Ansible templates → git revert → ansible-playbook → 自動恢復（<5 分鐘）
```

### 3.4 112 Kali 預留接口

- **角色**：資訊安全掃描中心（所有安全機制集中）
- **本次**：不實作，僅在 Ansible inventory 保留 entry
- **預留**：Nginx 可預留 `security.wooo.work` 入口
- **下一步**：獨立 ADR 討論安全架構整合

---

## 四、Sprint A — 整地（清廢、修錯、收斂）

**原則**：每步驟完成後驗收，驗收通過才進下一步。

---

### A-0：安全防護（最先執行，獨立）

> **目的**：在動任何 Nginx 或服務之前，確保監視器正常、記憶體安全。

#### A-0-1：擴充 110 Swap（4GB）

```bash
# 在 192.168.0.110 執行
sudo dd if=/dev/zero of=/swap2.img bs=1G count=4 status=progress
sudo chmod 600 /swap2.img
sudo mkswap /swap2.img
sudo swapon /swap2.img
echo '/swap2.img none swap sw 0 0' | sudo tee -a /etc/fstab

# 驗證
free -h && swapon --show
# 期望：Swap total ≈ 7.8GB
```

#### A-0-2：修復 snuba crash loop（根因：Kafka DNS 解析失敗）

```bash
# 在 192.168.0.110 執行
# 根因：snuba 容器無法解析 kafka:9092，需要整個 stack 重啟以重建 Docker 網路
cd /opt/sentry

# 先確認 kafka 容器健康
docker ps | grep kafka
# 期望：sentry-self-hosted-kafka-1 Up (healthy)

# 重啟整個 sentry stack（約 3-5 分鐘）
docker compose down && docker compose up -d

# 等待 2 分鐘後驗證
sleep 120
docker ps -a | grep Restarting
# 期望：無任何 Restarting 容器

# 驗證 Sentry Web 正常
curl -s -o /dev/null -w '%{http_code}' http://localhost:9000
# 期望：200 或 302
```

**注意**：`docker compose down` 不刪除 volume，資料安全。重啟期間 Sentry 短暫不可用（約 3 分鐘），不影響商業流量。

#### A-0-3：修復 K8s SENTRY_DSN（CHANGE_ME → 真實值）

```bash
# 步驟 1：從 Sentry UI 取得 DSN
# 瀏覽器開啟 http://192.168.0.110:9000
# Settings → Projects → awoooi-api → Client Keys (DSN) → 複製 DSN
# DSN 格式：https://<key>@192.168.0.110:9000/<project-id>

# 步驟 2：從 Sentry UI 取得 Auth Token
# Settings → Account → API → Auth Tokens → Create Token（scope: project:read, project:write）

# 步驟 3：更新 K3s Secrets（在 MacBook 執行）
kubectl patch secret awoooi-secrets -n awoooi-prod \
  --type='json' \
  -p='[
    {"op":"replace","path":"/data/SENTRY_DSN","value":"'$(echo -n "YOUR_SENTRY_DSN" | base64)'"},
    {"op":"replace","path":"/data/SENTRY_AUTH_TOKEN","value":"'$(echo -n "YOUR_AUTH_TOKEN" | base64)'"}
  ]'

# 步驟 4：驗證 Secrets 已更新
kubectl get secret awoooi-secrets -n awoooi-prod \
  -o jsonpath='{.data.SENTRY_DSN}' | base64 -d
# 期望：真實 DSN，不是 CHANGE_ME

# 步驟 5：Rolling restart 讓 pod 讀取新 secrets
kubectl rollout restart deployment/awoooi-api -n awoooi-prod
kubectl rollout restart deployment/awoooi-web -n awoooi-prod
kubectl rollout status deployment/awoooi-api -n awoooi-prod --timeout=120s
```

#### A-0-4：完成 Sentry Phase 6 — Webhook→Telegram 告警

```bash
# 步驟 1：在 Sentry UI 建立 Webhook Integration
# Settings → Integrations → WebHooks → Add Webhook
# URL：https://monitor.wooo.work/telegram/webhook（已有 nginx conf）
# Events：issue（新錯誤）、error（錯誤率突增）

# 步驟 2：驗證 webhook 到達 OpenClaw
# 在 188 查看 openclaw logs
docker logs openclaw --tail 20 -f
# 觸發測試：Sentry UI → 點擊 Send Test

# 步驟 3：確認 Telegram 收到告警
# 期望：Telegram 群組收到格式化的錯誤通知
```

#### A-0-5：更新 03-secrets.yaml（文件同步）

```bash
# 在本地 repo 更新 secrets example，確保文件不再有 CHANGE_ME 誤導
# 編輯 k8s/awoooi-prod/03-secrets.example.yaml
# 更新說明：如何取得 SENTRY_DSN 的步驟（不寫真實值）
```

**A-0 驗收標準**：
- [ ] `free -h` 顯示 Swap total ≈ 7.8GB
- [ ] `docker ps -a | grep Restarting` → 空結果
- [ ] `curl http://192.168.0.110:9000` → 200/302
- [ ] K8s pod 中 SENTRY_DSN 非 CHANGE_ME
- [ ] Telegram 收到 Sentry 測試告警

---

### A-1：GitLab 完整移除（188 + 110）

> **目的**：清除所有 GitLab 殘骸，釋放磁碟空間。

#### A-1-1：188 GitLab 移除

```bash
# 在 192.168.0.188 執行
cd /home/ollama/devops

# 停止並刪除容器
docker compose stop gitlab gitlab-runner
docker compose rm -f gitlab gitlab-runner

# 刪除 volumes
docker volume rm wooo-gitlab-config wooo-gitlab-data wooo-gitlab-logs wooo-gitlab-runner-config

# 刪除目錄
sudo rm -rf /home/ollama/gitlab/
sudo rm -rf /home/ollama/gitlab-runner/

# 驗證：確認 registry 容器不受影響
docker ps | grep registry
# 期望：docker-registry 仍在運行（若已啟動）
```

#### A-1-2：188 Nginx 移除 GitLab Block

```bash
# 編輯 /etc/nginx/sites-enabled/all-sites.conf
# 移除以下整個 server block（兩個，HTTP + HTTPS）：
# server { server_name gitlab.wooo.work; ... }

# 若有獨立 conf 檔（通常不存在，但確認）
sudo rm -f /etc/nginx/sites-available/gitlab.conf
sudo rm -f /etc/nginx/sites-enabled/gitlab.conf

# 測試並重載
sudo nginx -t && sudo systemctl reload nginx
```

#### A-1-3：188 刪除 GitLab SSL 憑證

```bash
# 確認 gitlab.wooo.work cert 無其他用途
sudo certbot delete --cert-name gitlab.wooo.work
# 確認提示時輸入 y
```

#### A-1-4：110 GitLab 移除

```bash
# 在 192.168.0.110 執行

# 刪除 volumes
docker volume rm gitlab_gitlab-runner-config wooo-gitlab-config \
  wooo-gitlab-data wooo-gitlab-logs wooo-gitlab-runner-config

# 刪除目錄（若存在）
sudo rm -rf /home/wooo/gitlab/
sudo rm -rf /home/wooo/gitlab-migration/
sudo rm -rf /home/wooo/gitlab-runner-config/

# 刪除 SSL 憑證
sudo certbot delete --cert-name gitlab.wooo.work
```

#### A-1-5：清除 110 Nginx 殘留空白檔

```bash
# 在 192.168.0.110 執行
# aiops.wooo.work.conf 是空白殘留檔，確認後刪除
cat /etc/nginx/sites-enabled/aiops.wooo.work.conf
# 若確認為空或殘留 → 刪除
sudo rm /etc/nginx/sites-enabled/aiops.wooo.work.conf
sudo nginx -t && sudo systemctl reload nginx
```

#### A-1-6：清除 188 舊版殘留 Volumes

```bash
# 在 192.168.0.188 執行
# clawbot 舊版 volumes（openclaw 不使用任何 named volume）
docker volume rm \
  clawbot_clawbot-redis-data \
  clawbot_clawbot-screenshots \
  clawbot-v5_clawbot-redis-data \
  clawbot-v5_clawbot-screenshots \
  clawbot-v5-updated_clawbot-redis-data \
  clawbot-v5-updated_clawbot-screenshots

# n8n 舊版 named volumes（compose 使用 bind mount ./data，不用 named volume）
docker volume rm n8n_data n8n_n8n_data

# open-webui 舊版 volume（正確的是 open-webui，open-webui-data 是殘留）
docker volume rm open-webui-data

# grist SSL cert（無容器、無 nginx conf，完全廢棄）
sudo certbot delete --cert-name grist.wooo.work
```

#### A-1-7：清除 110 殘留 Runner 目錄

```bash
# 在 192.168.0.110 執行
# actions-runner-awoooi-3：.runner 設定空白，無 systemd service，未完成的殘留
ls /home/wooo/actions-runner-awoooi-3/  # 確認內容
# 若確認無 .runner 設定且無 service → 刪除
sudo rm -rf /home/wooo/actions-runner-awoooi-3/
```

**A-1 驗收標準**：
- [ ] `docker volume ls | grep gitlab` → 空結果（兩台）
- [ ] `docker volume ls | grep clawbot` → 空結果（188）
- [ ] `docker volume ls | grep -E 'n8n_data|n8n_n8n|open-webui-data'` → 空結果（188）
- [ ] `ls /etc/letsencrypt/live/ | grep grist` → 空結果（188）
- [ ] `ls /home/ollama/gitlab` → No such file
- [ ] `ls /home/wooo/gitlab*` → No such file
- [ ] `ls /home/wooo/actions-runner-awoooi-3` → No such file
- [ ] `sudo nginx -t` 兩台都通過

---

### A-2：啟動休眠服務（188）

#### A-2-1：修改 docker-registry Port（5000→5001）

```bash
# 在 192.168.0.188 執行
# 編輯 /home/ollama/devops/docker-compose.yml
# 將 registry 的 ports 從 '5000:5000' 改為 '5001:5000'

# 若 registry 已在運行，需重啟
cd /home/ollama/devops
docker compose up -d registry
```

#### A-2-2：啟動 n8n

```bash
# 在 192.168.0.188 執行
cd /home/ollama/n8n
docker compose up -d

# 驗證
docker ps | grep n8n
curl -s -o /dev/null -w '%{http_code}' http://localhost:5678
# 期望：200
```

#### A-2-3：啟動 open-webui（:3010）

```bash
# 在 192.168.0.188 執行
# volume open-webui 已存在，直接掛載
docker run -d \
  --name open-webui \
  --restart always \
  -p 3010:8080 \
  -v open-webui:/app/backend/data \
  --add-host=host.docker.internal:host-gateway \
  ghcr.io/open-webui/open-webui:main

# 驗證
docker ps | grep open-webui
curl -s -o /dev/null -w '%{http_code}' http://localhost:3010
# 期望：200
```

**A-2 驗收標準**：
- [ ] `docker ps | grep -E 'n8n|open-webui|registry'` → 三個都 Up
- [ ] n8n: `http://localhost:5678` → 200
- [ ] open-webui: `http://localhost:3010` → 200
- [ ] registry: Port 5001 正常

---

### A-3：Bitan 藥局遷移 Docker

> **目的**：統一網站服務為 Docker，納入版控與備份體系。

#### A-3-1：準備 Dockerfile

```bash
# 在 192.168.0.110 執行
# bitan 是 Next.js 源碼，需要 build
cat > /home/wooo/apps/bitan-pharmacy/Dockerfile << 'EOF'
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3003
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3003
CMD ["node", "server.js"]
EOF
```

#### A-3-2：建立 docker-compose.yml

```bash
cat > /home/wooo/apps/bitan-pharmacy/docker-compose.yml << 'EOF'
services:
  bitan-pharmacy:
    build: .
    container_name: bitan-pharmacy
    restart: always
    ports:
      - "3003:3003"
    environment:
      - NODE_ENV=production
      - PORT=3003
      - TZ=Asia/Taipei
    volumes:
      - ./public:/app/public:ro
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3003"]
      interval: 30s
      timeout: 10s
      retries: 3
EOF
```

#### A-3-3：建置並測試

```bash
# 在 192.168.0.110 執行
cd /home/wooo/apps/bitan-pharmacy

# Build（約 2-3 分鐘）
docker compose build

# 先用不同 port 測試，不影響 PM2 現有服務
docker run --rm -p 3004:3003 bitan-pharmacy-bitan-pharmacy &
sleep 10
curl -s -o /dev/null -w '%{http_code}' http://localhost:3004
# 期望：200

# 確認 bitan.wooo.work 透過 188 nginx 仍正常（PM2 仍在）
curl -s -o /dev/null -w '%{http_code}' https://bitan.wooo.work
# 期望：200
```

#### A-3-4：切換（PM2 → Docker）

```bash
# 在 192.168.0.110 執行
# 停止 PM2 bitan（約 1 秒停機）
pm2 stop bitan-pharmacy

# 立即啟動 Docker 版本（監聽同一個 port 3003）
cd /home/wooo/apps/bitan-pharmacy
docker compose up -d

# 驗證切換成功（30 秒內）
sleep 5
curl -s -o /dev/null -w '%{http_code}' http://localhost:3003
# 期望：200

# 確認 bitan.wooo.work 正常
curl -s -o /dev/null -w '%{http_code}' https://bitan.wooo.work
# 期望：200
```

#### A-3-5：清理 PM2

```bash
# 確認 Docker 版本穩定後（觀察 10 分鐘）
pm2 delete bitan-pharmacy
pm2 save

# 確認 PM2 清單
pm2 list
# 期望：只剩 wooo-frontend
```

**A-3 驗收標準**：
- [ ] `docker ps | grep bitan` → bitan-pharmacy Up
- [ ] `curl https://bitan.wooo.work` → 200
- [ ] `pm2 list` → 無 bitan-pharmacy
- [ ] 觀察 10 分鐘無異常

---

### A-4：DNS 確認 + SSL 申請

#### A-4-1：確認 DNS 指向

```bash
# 在本地 MacBook 執行
# 確認以下域名都解析到 188（192.168.0.188）
for domain in sentry.wooo.work gitea.wooo.work langfuse.wooo.work stock.wooo.work signoz.wooo.work; do
  echo -n "$domain → " && dig +short $domain | head -1
done
# 期望：全部顯示 192.168.0.188 或對應的公網 IP
```

#### A-4-2：申請 SSL（webroot 模式）

```bash
# 在 192.168.0.188 執行
# webroot 模式不修改 nginx 設定，最安全

# 確保 webroot 目錄存在
sudo mkdir -p /var/www/html

# 申請（一次申請全部）
sudo certbot certonly --webroot -w /var/www/html \
  -d sentry.wooo.work \
  -d gitea.wooo.work \
  -d langfuse.wooo.work \
  -d stock.wooo.work \
  -d signoz.wooo.work \
  --email admin@wooo.work \
  --agree-tos \
  --no-eff-email

# 驗證
sudo ls /etc/letsencrypt/live/
# 期望：出現 sentry.wooo.work 等新憑證
```

**注意**：若 certbot 要求分開申請（每個域名一個 cert），則逐一執行。

**A-4 驗收標準**：
- [ ] DNS 確認全部指向正確 IP
- [ ] `/etc/letsencrypt/live/` 出現新憑證
- [ ] `sudo certbot renew --dry-run` 通過

---

### A-5：重寫 188 Nginx 設定

> **重要**：這是最高風險步驟。必須先備份，修改後立即測試，失敗立即回滾。

#### A-5-1：備份現有設定

```bash
# 在 192.168.0.188 執行
sudo cp /etc/nginx/sites-enabled/all-sites.conf /etc/nginx/all-sites.conf.bak.$(date +%Y%m%d%H%M)
sudo cp /etc/nginx/sites-enabled/ollama.wooo.work /etc/nginx/ollama.wooo.work.bak.$(date +%Y%m%d%H%M)
```

#### A-5-2：修改 all-sites.conf（移除 GitLab Block）

在 `/etc/nginx/sites-enabled/all-sites.conf` 中：
- 移除 `gitlab.wooo.work` 的兩個 server block（HTTP + HTTPS）
- `signoz.wooo.work` 的 HTTP-only block **改寫為 HTTPS**（見下方完整 block）
- `stock.wooo.work` 的 HTTP-only block **改寫為 HTTPS**

**signoz.wooo.work 完整 server block：**
```nginx
server {
    listen 80;
    server_name signoz.wooo.work;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name signoz.wooo.work;

    ssl_certificate /etc/letsencrypt/live/signoz.wooo.work/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/signoz.wooo.work/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3301;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

**stock.wooo.work 完整 server block：**
```nginx
server {
    listen 80;
    server_name stock.wooo.work;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name stock.wooo.work;

    ssl_certificate /etc/letsencrypt/live/stock.wooo.work/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/stock.wooo.work/privkey.pem;

    location / {
        proxy_pass http://192.168.0.110:31235;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

#### A-5-3：新增獨立 Conf 檔（sentry/gitea/langfuse）

```bash
# 在 188 建立 /etc/nginx/sites-available/internal-tools.conf
# 包含 sentry、gitea、langfuse 三個 server block
```

**sentry.wooo.work：**
```nginx
server {
    listen 80;
    server_name sentry.wooo.work;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name sentry.wooo.work;

    ssl_certificate /etc/letsencrypt/live/sentry.wooo.work/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sentry.wooo.work/privkey.pem;

    client_max_body_size 50m;

    location / {
        proxy_pass http://192.168.0.110:9000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 300s;
    }
}
```

**gitea.wooo.work：**
```nginx
server {
    listen 80;
    server_name gitea.wooo.work;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name gitea.wooo.work;

    ssl_certificate /etc/letsencrypt/live/gitea.wooo.work/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gitea.wooo.work/privkey.pem;

    client_max_body_size 512m;

    location / {
        proxy_pass http://192.168.0.110:3001;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
```

**langfuse.wooo.work：**
```nginx
server {
    listen 80;
    server_name langfuse.wooo.work;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name langfuse.wooo.work;

    ssl_certificate /etc/letsencrypt/live/langfuse.wooo.work/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/langfuse.wooo.work/privkey.pem;

    location / {
        proxy_pass http://192.168.0.110:3100;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

```bash
# symlink 啟用
sudo ln -s /etc/nginx/sites-available/internal-tools.conf /etc/nginx/sites-enabled/internal-tools.conf
```

#### A-5-4：修改 ollama.wooo.work 獨立 Conf

```bash
# 修改 /etc/nginx/sites-enabled/ollama.wooo.work
# 將 proxy_pass http://127.0.0.1:3000 改為 http://127.0.0.1:3010
# （共兩處：HTTPS block + 內網 HTTP block）

sudo sed -i 's|proxy_pass http://127.0.0.1:3000|proxy_pass http://127.0.0.1:3010|g' \
  /etc/nginx/sites-enabled/ollama.wooo.work
```

#### A-5-5：測試並重載

```bash
sudo nginx -t
# 必須通過才繼續

sudo systemctl reload nginx

# 立即驗收全站
for domain in aiops.wooo.work bitan.wooo.work n8n.wooo.work ollama.wooo.work \
              sentry.wooo.work gitea.wooo.work langfuse.wooo.work \
              stock.wooo.work signoz.wooo.work mo.wooo.work; do
  code=$(curl -s -o /dev/null -w '%{http_code}' -L https://$domain --max-time 5)
  echo "$domain → $code"
done
```

**若任何服務 502/404，立即回滾：**
```bash
sudo cp /etc/nginx/all-sites.conf.bak.$(date +%Y%m%d)* /etc/nginx/sites-enabled/all-sites.conf
sudo systemctl reload nginx
```

**A-5 驗收標準**：
- [ ] `sudo nginx -t` 通過
- [ ] aiops.wooo.work → 200（商業流量不受影響）
- [ ] bitan.wooo.work → 200（商業流量不受影響）
- [ ] n8n.wooo.work → 200
- [ ] ollama.wooo.work → 200（open-webui）
- [ ] sentry.wooo.work → 200
- [ ] gitea.wooo.work → 200
- [ ] langfuse.wooo.work → 200
- [ ] stock.wooo.work → 200
- [ ] signoz.wooo.work → 200

---

### A-6：修復 110 Harbor Nginx + 移除漂移 Conf

> **依賴**：A-5 全部驗收通過後才執行。

#### A-6-1：修復 Harbor nginx conf

```bash
# 在 192.168.0.110 執行
# 備份
sudo cp /etc/nginx/sites-enabled/harbor /etc/nginx/harbor.bak.$(date +%Y%m%d%H%M)

# 修改 upstream（5050 → 5000）
sudo sed -i 's/server 127.0.0.1:5050/server 127.0.0.1:5000/' /etc/nginx/sites-enabled/harbor

# 驗證修改
grep 'server 127.0.0.1' /etc/nginx/sites-enabled/harbor
# 期望：server 127.0.0.1:5000

sudo nginx -t && sudo systemctl reload nginx
```

#### A-6-2：驗證 harbor.wooo.work

```bash
curl -s -o /dev/null -w '%{http_code}' https://harbor.wooo.work
# 期望：200
```

#### A-6-3：移除 all-sites-from-188.conf

```bash
# 確認 188 已接手所有服務（A-5 驗收通過）後才執行
sudo rm /etc/nginx/sites-enabled/all-sites-from-188.conf

sudo nginx -t && sudo systemctl reload nginx

# 再次確認 110 管理的服務不受影響
curl -s -o /dev/null -w '%{http_code}' https://harbor.wooo.work
# 期望：200
```

**A-6 驗收標準**：
- [ ] `grep 5050 /etc/nginx/sites-enabled/harbor` → 空結果
- [ ] `https://harbor.wooo.work` → 200
- [ ] `ls /etc/nginx/sites-enabled/all-sites-from-188.conf` → No such file
- [ ] `sudo nginx -t` 通過

---

### A-7：確認 wooo-aiops 狀態

```bash
# 在 192.168.0.110 執行
echo '=== wooo-aiops docker containers ===' && \
  docker ps | grep -E 'wooo|aiops'

echo '=== wooo-frontend PM2 ===' && \
  pm2 show wooo-frontend | grep -E 'status|uptime|restart'

echo '=== wooo-aiops docker compose ===' && \
  cd /home/wooo/wooo-aiops && docker compose ps 2>/dev/null

# 記錄發現的問題，不改架構
```

**A-7 驗收標準**：
- [ ] wooo-frontend PM2 status = online
- [ ] 記錄所有異常（不修復，交由 wooo-aiops 獨立處理）

---

### A-8：確認 Stock 平台資料完整性

```bash
# 在 192.168.0.110 執行
echo '=== stock containers ===' && docker ps | grep stock

# 確認 dashboard 正常
curl -s -o /dev/null -w '%{http_code}' http://localhost:31235
# 期望：200

# 確認資料庫健康
docker exec stock-platform-dashboard python inspect_db_health.py 2>/dev/null || \
docker exec stock-platform-dashboard python -c "
import sqlite3, os, glob
dbs = glob.glob('/app/data/db/*.db')
for db in dbs:
    size = os.path.getsize(db)
    print(f'{db}: {size/1024/1024:.1f}MB')
"

# 確認 scheduler 運作
docker logs stock-platform-scheduler --tail 10
```

```bash
# 從外部驗收
curl -s -o /dev/null -w '%{http_code}' https://stock.wooo.work
# 期望：200
```

**A-8 驗收標準**：
- [ ] `https://stock.wooo.work` → 200
- [ ] 資料庫檔案存在且有合理大小
- [ ] scheduler logs 無 error

---

### A-9：安裝 188 keepalived MASTER

> **依賴**：A-5 完成（Nginx 已收斂到 188 單點）後執行。

```bash
# 在 192.168.0.188 執行
sudo apt install keepalived -y

sudo tee /etc/keepalived/keepalived.conf << 'EOF'
! Keepalived configuration for 188 (MASTER)
global_defs {
    router_id NGINX_188
    script_user root
    enable_script_security
}

vrrp_script check_nginx {
    script "/usr/bin/pgrep nginx"
    interval 2
    weight -50
    fall 3
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface ens160
    virtual_router_id 51
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 73c1b95048bc12eb
    }

    virtual_ipaddress {
        192.168.0.200/24
    }

    track_script {
        check_nginx
    }
}
EOF

sudo systemctl enable keepalived
sudo systemctl start keepalived

# 驗證 VIP 在 188
ip addr show | grep 192.168.0.200
# 期望：顯示 VIP 在 ens160
```

**HA 驗證（謹慎執行）：**
```bash
# 步驟 1：停止 188 nginx
sudo systemctl stop nginx

# 步驟 2：在另一台機器確認 VIP 漂移到 110
ssh wooo@192.168.0.110 "ip addr show | grep 192.168.0.200"
# 期望：VIP 出現在 110

# 步驟 3：恢復 188 nginx
sudo systemctl start nginx

# 步驟 4：確認 VIP 回到 188
ip addr show | grep 192.168.0.200
# 期望：VIP 回到 188
```

**A-9 驗收標準**：
- [ ] VIP 192.168.0.200 在 188 上可見
- [ ] 停 188 nginx → VIP 在 30 秒內漂移到 110
- [ ] 恢復 188 nginx → VIP 回到 188

---

### Sprint A 全域驗收

| 服務 | 驗收指令 | 期望 |
|------|---------|------|
| harbor.wooo.work | `curl -I https://harbor.wooo.work` | 200 |
| n8n.wooo.work | `curl -I https://n8n.wooo.work` | 200 |
| ollama.wooo.work | `curl -I https://ollama.wooo.work` | 200（open-webui）|
| sentry.wooo.work | `curl -I https://sentry.wooo.work` | 200，告警到 Telegram |
| gitea.wooo.work | `curl -I https://gitea.wooo.work` | 200 |
| langfuse.wooo.work | `curl -I https://langfuse.wooo.work` | 200 |
| stock.wooo.work | `curl -I https://stock.wooo.work` | 200，資料完整 |
| signoz.wooo.work | `curl -I https://signoz.wooo.work` | 200 |
| bitan.wooo.work | `curl -I https://bitan.wooo.work` | 200（Docker 版）|
| aiops.wooo.work | `curl -I https://aiops.wooo.work` | 200（商業流量不受影響）|
| 110 Swap | `free -h` | Total ≈ 7.8GB |
| 110 Sentry | `docker ps -a \| grep Restarting` | 空結果 |
| VIP:200 HA | 停 188 nginx | 30 秒內漂移到 110 |

---

## 五、Sprint B — 變更管理基礎建設

### B-1：Ansible Host IaC

#### 目錄結構

```
/Users/ogt/awoooi/infra/ansible/
├── inventory/
│   ├── hosts.yml                    # 五主機清單
│   └── group_vars/
│       ├── all.yml                  # 共用變數（時區、用戶等）
│       ├── host_110.yml             # 110 專用變數
│       └── host_188.yml             # 188 專用變數
├── playbooks/
│   ├── site.yml                     # 入口（可跑全站）
│   ├── 110-devops.yml               # 110 完整狀態描述
│   ├── 188-ai-web.yml               # 188 完整狀態描述
│   └── nginx-sync.yml               # Nginx 設定同步
├── roles/
│   ├── nginx/                       # Nginx 設定管理
│   │   ├── tasks/main.yml
│   │   └── templates/               # Jinja2 conf 模板
│   ├── docker-compose-service/      # Docker Compose 服務管理
│   ├── swap/                        # Swap 設定管理
│   └── pm2-service/                 # PM2 服務管理（wooo-frontend）
└── templates/
    └── nginx/                       # 全域 nginx conf 模板
```

#### hosts.yml 結構

```yaml
all:
  children:
    devops:
      hosts:
        host_110:
          ansible_host: 192.168.0.110
          ansible_user: wooo
    ai_web:
      hosts:
        host_188:
          ansible_host: 192.168.0.188
          ansible_user: ollama
    k3s_masters:
      hosts:
        host_120:
          ansible_host: 192.168.0.120
          ansible_user: wooo
        host_121:
          ansible_host: 192.168.0.121
          ansible_user: wooo
    security:
      hosts:
        host_112:
          ansible_host: 192.168.0.112
          ansible_user: wooo
          ansible_skip_tags: "all"  # 預留，本次不執行
```

#### 110-devops.yml 管理範圍

```yaml
# 110 預期狀態：
# - swap: 8GB（/swap.img 3.8GB + /swap2.img 4GB）
# - harbor: running（docker compose in /opt/harbor）
# - sentry: running（docker compose in /opt/sentry, 0 restarting）
# - gitea: running（port 3001）
# - langfuse: running（port 3100）
# - stock-platform: running（port 31235）
# - bitan-pharmacy: running docker（port 3003）
# - wooo-frontend: running PM2（port 3000）
# - 5x GitHub Runners: running（systemd services）
# - nginx: harbor conf 指向 127.0.0.1:5000
# - keepalived: BACKUP priority 90 VIP:200
```

#### 188-ai-web.yml 管理範圍

```yaml
# 188 預期狀態：
# - openclaw: running（port 8088）
# - tsenyang-website: running（port 3000）
# - momo-app: running（port 5003）
# - signoz: running（port 3301）
# - minio: running（port 9000）
# - litellm: running（port 4000）
# - n8n: running（port 5678）
# - open-webui: running（port 3010）
# - docker-registry: running（port 5001）
# - nginx: all-sites.conf 無 gitlab block
# - keepalived: MASTER priority 100 VIP:200
```

#### CI 整合

```yaml
# .gitea/workflows/ansible-lint.yml
on:
  push:
    paths:
      - 'infra/ansible/**'
jobs:
  lint:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: ansible-lint
        run: |
          pip install ansible-lint
          ansible-lint infra/ansible/playbooks/
```

**手動批准後執行**（不設 auto-deploy，Host 層變更需人工確認）

---

### B-2：ArgoCD on K3s

#### 安裝

```bash
# 在 120 或 121 執行（需 sudo kubectl 權限）
kubectl create namespace argocd
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 等待 pod 就緒
kubectl wait --for=condition=available deployment/argocd-server \
  -n argocd --timeout=300s

# 設定 NodePort 存取
kubectl patch svc argocd-server -n argocd \
  -p '{"spec":{"type":"NodePort","ports":[{"name":"https","port":443,"targetPort":8080,"nodePort":32080}]}}'

# 取得 admin 密碼
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d
```

#### 建立 Application

```bash
# 使用 ArgoCD CLI 或 Web UI
argocd app create awoooi-prod \
  --repo https://gitea.wooo.work/owenhytsai/awoooi.git \
  --path k8s/awoooi-prod \
  --dest-server https://192.168.0.125:6443 \
  --dest-namespace awoooi-prod \
  --sync-policy automated \
  --auto-prune \
  --self-heal
```

#### 改寫 CD 流程

將 `.gitea/workflows/cd.yaml` 的 deploy 階段改為：

```yaml
# 舊：kubectl set image（移除）
# 新：更新 YAML 中的 image tag
- name: Update image tag in K8s manifests
  run: |
    NEW_TAG="${{ github.sha }}"
    sed -i "s|image: harbor.wooo.work/awoooi/api:.*|image: harbor.wooo.work/awoooi/api:${NEW_TAG}|" \
      k8s/awoooi-prod/06-deployment-api.yaml
    sed -i "s|image: harbor.wooo.work/awoooi/web:.*|image: harbor.wooo.work/awoooi/web:${NEW_TAG}|" \
      k8s/awoooi-prod/05-deployment-web.yaml
    git config user.email "gitea-actions@wooo.work"
    git config user.name "Gitea Actions"
    git commit -am "ci: update image tag to ${NEW_TAG}"
    git push
    # ArgoCD 自動偵測並 apply
```

---

### B-3：Nginx 版控（接 Ansible）

**原則**：Nginx conf 不再直接在主機手改，所有修改透過 Ansible templates。

```
修改 nginx conf
→ 編輯 infra/ansible/roles/nginx/templates/*.j2
→ git commit + push
→ Gitea Actions: ansible-lint
→ 人工批准
→ ansible-playbook nginx-sync.yml --limit host_188
→ 自動 reload nginx
```

---

### Sprint B 驗收標準

| 能力 | 驗收方式 |
|------|---------|
| Host 層回滾 | git revert nginx conf → ansible-playbook → 主機設定恢復 |
| K8s 層回滾 | `argocd app rollback awoooi-prod` → pod 更新到前一版 |
| 設定漂移偵測 | `ansible-playbook site.yml --check` → 顯示 drift |
| CD 新流程 | git push → ArgoCD 自動同步（無需手動 kubectl set image）|

### Sprint B 完成後 MCP Phase 1 驗收標準

| 能力 | 驗收方式 |
|------|---------|
| NIM 退場確認 | `AI_ROUTER` 中 NemotronProvider 已停用，無 Tool Calling 呼叫到 NIM |
| K8s MCP 強化 | PodRestartingTooMuch 告警 → AI 呼叫 `k8s_get_pod_logs` 取得真實 log → 決策 |
| 無幻覺驗證 | DecisionToken.target 永遠是真實 deployment 名稱，不出現 `unknown` / `<deployment_name>` |
| rollout 驗證 | 執行 restart 後 AI 呼叫 `k8s_watch_rollout` 確認成功，而非 sleep 猜測 |

---

## 六、Sprint C — 災難備援

### C-1：Velero K8s 備份

#### 準備 MinIO bucket

```bash
# 在 188 使用 mc（MinIO client）建立 bucket
docker exec minio mc alias set local http://127.0.0.1:9000 \
  $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
docker exec minio mc mb local/velero-backups
docker exec minio mc policy set private local/velero-backups
```

#### 安裝 Velero

```bash
# 建立 credentials 檔
cat > /tmp/velero-credentials << EOF
[default]
aws_access_key_id=$MINIO_ROOT_USER
aws_secret_access_key=$MINIO_ROOT_PASSWORD
EOF

# 安裝
velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.9.0 \
  --bucket velero-backups \
  --secret-file /tmp/velero-credentials \
  --use-volume-snapshots=false \
  --backup-location-config \
    region=minio,s3ForcePathStyle=true,s3Url=http://192.168.0.188:9000

# 建立每日定時備份
velero schedule create daily-backup \
  --schedule="0 2 * * *" \
  --include-namespaces awoooi-prod \
  --ttl 720h
```

#### 驗收（模擬 restore）

```bash
# 觸發手動備份
velero backup create test-backup --include-namespaces awoooi-prod

# 等待完成
velero backup describe test-backup

# 模擬 restore（在 staging 環境測試）
velero restore create test-restore --from-backup test-backup \
  --namespace-mappings awoooi-prod:awoooi-staging
```

---

### C-2：Host 層 rsync 備份

#### 備份腳本（188 → 備份 110 資料）

```bash
# /home/ollama/bin/backup-from-110.sh
#!/bin/bash
set -euo pipefail
LOG="/var/log/backup-from-110.log"
DATE=$(date +%Y%m%d-%H%M%S)

echo "[$DATE] Starting backup from 110" >> $LOG

# Harbor registry data（最高優先）
rsync -avz --delete \
  wooo@192.168.0.110:/var/lib/docker/volumes/harbor_harbor-data/_data/ \
  /backup/110/harbor/ >> $LOG 2>&1 || \
  (echo "[$DATE] ERROR: Harbor backup failed" >> $LOG; exit 1)

# Gitea repos
rsync -avz --delete \
  wooo@192.168.0.110:/var/lib/docker/volumes/gitea_gitea-data/_data/ \
  /backup/110/gitea/ >> $LOG 2>&1 || \
  (echo "[$DATE] ERROR: Gitea backup failed" >> $LOG; exit 1)

# bitan git bare repo
rsync -avz \
  wooo@192.168.0.110:/home/wooo/bitan-pharmacy.git/ \
  /backup/110/bitan-pharmacy.git/ >> $LOG 2>&1

echo "[$DATE] Backup completed" >> $LOG
```

```bash
# crontab -e（ollama on 188）
0 1 * * * /home/ollama/bin/backup-from-110.sh
```

#### Alertmanager 整合（備份失敗告警）

在 Prometheus alerting rules 中新增：
```yaml
- alert: BackupFailed
  expr: time() - backup_last_success_timestamp > 86400
  annotations:
    summary: "Host backup has not run in 24 hours"
```

---

### C-3：DR SOP 文件

存放於 `docs/runbooks/disaster-recovery/`：

| 文件 | 場景 | 目標時間 |
|------|------|---------|
| DR-K8s-awoooi.md | awoooi pod 全掛 → Velero restore | < 15 分鐘 |
| DR-Harbor.md | Harbor 資料毀損 → rsync 備份還原 | < 30 分鐘 |
| DR-Nginx.md | Nginx 設定錯誤 → Ansible 回滾 | < 5 分鐘 |
| DR-Bitan.md | bitan 容器崩潰 → docker compose up | < 5 分鐘 |
| DR-Stock.md | stock 容器崩潰 → docker compose up | < 5 分鐘 |

---

### Sprint C 驗收標準

| 場景 | 驗收方式 | 目標時間 |
|------|---------|---------|
| K8s Pod 全掛 | `velero restore` → pod 正常 | < 15 分鐘 |
| 188 Nginx 失效 | keepalived 切到 110 | < 30 秒 |
| Harbor 資料毀損 | rsync 備份還原 | < 30 分鐘 |
| K3s 設定被改 | `git revert + push` → ArgoCD 同步 | < 5 分鐘 |
| Nginx conf 錯誤 | `ansible-playbook nginx-sync.yml` | < 5 分鐘 |

### Sprint C 完成後 MCP Phase 3 驗收標準

| 能力 | 驗收方式 |
|------|---------|
| ArgoCD MCP | ArgoCDSyncFailed 告警 → AI 呼叫 `argocd_get_app_status` → 自動 sync or rollback |
| Sentry MCP | AWOOOIApiDown + Sentry Error 同時發生 → AI 呼叫 `sentry_get_recent_errors` → 關聯分析 |
| MCP Phase 4 | HostHighCpuLoad 告警 → SSH MCP 查程序 → AI 動態提煉 symptom_pattern → KM 記錄含指標快照 |
| 動態規則 | 新型態告警（無 Playbook）→ AI 生成草稿 → Telegram 推送審核通知 |

---

## 七、完整執行順序（無邏輯衝突）

> 包含 ADR-070 全自動 AIOps 閉環 MCP Phase 工作項目（§14）
>
> **2026-04-11 Session 6 狀態確認**: Sprint A ✅ / Sprint B ✅ / Sprint C ✅ / MCP 1-4 ✅ / ADR-071 A-J ✅
> **下一步**: ADR-072 Bug 修復（BUG-001~008，見 `docs/adr/ADR-072-aiops-bug-fixes.md`）

```
══════════════ SPRINT A ══════════════

A-0-1: 110 擴充 Swap                ← 獨立，最先，防 OOM
  ↓
A-0-2: 修復 snuba crash loop        ← 依賴 Swap 先擴充
  ↓
A-0-3: 修復 K8s SENTRY_DSN         ← 獨立，可與 A-0-2 平行
A-0-4: Sentry Webhook→Telegram      ← 依賴 A-0-2（Sentry 正常）
  ↓
[A-0 全部驗收]
  ↓
A-1: GitLab 完整移除               ← 零風險
  ↓
A-2: 啟動 n8n/registry/open-webui  ← 依賴 A-1（registry port 空出）
A-3: Bitan → Docker                ← 獨立，可與 A-2 平行
  ↓
A-4: DNS 確認 + SSL 申請           ← 依賴 A-2（open-webui 已啟動）
  ↓
A-5: 重寫 188 nginx 設定           ← 依賴 A-4（SSL 憑證到位）
  ↓
A-6: 修復 110 harbor + 移除漂移    ← 依賴 A-5（188 接手驗證通過）
A-7: wooo-aiops 狀態確認           ← 獨立，可與 A-6 平行
A-8: Stock 資料完整性確認          ← 依賴 A-5（HTTPS 到位後測試）
  ↓
A-9: 安裝 188 keepalived MASTER    ← 依賴 A-5（nginx 收斂後）
  ↓
[Sprint A 全部驗收]
  ↓

══════════════ ADR-071 第一批（Sprint A 完成後立即開始，不依賴 MCP）══════════════

ADR-071-D: 狀態機守衛              ← 最優先，防止舊卡片被誤點執行
  handle_callback() 前加 incident.status 檢查
  ↓
ADR-071-A: DB Migration            ← incidents 表 +7 欄位，alert_operation_log +5 event_type
  ↓
ADR-071-B: 通知分類器              ← classify_notification() 插入現有 send_approval_card() 前
ADR-071-C: TYPE-1 純資訊卡片       ← 同步，info/success 告警無按鈕
ADR-071-E: TYPE-3 按鈕動態組合     ← 依 alert_category 選按鈕組合
ADR-071-F: Config Drift 專屬卡片   ← TYPE-4D，Diff 截斷處理
  ↓
ADR-071-G: KMConversionService     ← Incident RESOLVED → 自動轉 KM + 向量化
  ↓
ADR-071-H: TYPE-4 手動修復記錄     ← Bot 對話 → 草稿 Playbook → KM
  ↓
[ADR-071 第一批驗收]
  → Backup.harbor info → 純文字，無按鈕
  → 點已解決卡片 → 回覆「已解決」並移除按鈕
  → database 告警 → 無 [重啟][擴容] 按鈕
  → Incident RESOLVED → knowledge_entries 自動新增

══════════════ MCP PHASE 2（Sprint A 完成後立即開始，不等 Sprint B）══════════════

MCP-2b: Prometheus MCP Server      ← 無前提，Sprint A 一完成即可
  實作: apps/api/src/plugins/mcp/providers/prometheus_provider.py
  工具: prometheus_query / prometheus_query_range / prometheus_get_alert_history
  目的: 情報收集前置，告警進來時 AI 立即掌握歷史趨勢
  ↓
MCP-2a: SSH MCP Server             ← 依賴 Sprint A（服務清單確認後，container 名稱/compose 路徑才固定）
  實作: apps/api/src/plugins/mcp/providers/ssh_provider.py
  工具: 診斷群組A（9個只讀工具）+ 操作群組B（6個，需 trust_score>=0.8）
  目的: 補完主機層 70% 告警盲點（HostHighCpuLoad/DockerExited/HarborDown/OllamaDown 等）
  前置: 建立 K8s Secret ssh-mcp-key，設定 SSH_MCP_ENABLED=true
  ↓
告警 label 補充（A 完成後）        ← 修改 ops/monitoring/alerts-unified.yml
  加入: mcp_provider / host_type / target_host label
  目的: AI Router 自動路由到正確 MCP，不寫死 if/else
  ↓
[MCP Phase 2 驗收]
  → HostHighCpuLoad 告警 → AI 自動呼叫 ssh_get_top_processes → 分析 → 決策
  → DockerContainerExited → AI 呼叫 ssh_docker_restart → K8s watch_rollout 驗證

══════════════ SPRINT B ══════════════

B-1: Ansible Playbook 撰寫        ← B 的基礎，必須先完成
  ↓
B-2: ArgoCD on K3s 安裝           ← 依賴 B-1（Host 層先管起來）
  ↓
B-3: CD 流程改接 ArgoCD            ← 依賴 B-2（移除 kubectl set image）
  ↓
[Sprint B 全部驗收]
  ↓

══════════════ MCP PHASE 1（Sprint B 完成後）══════════════

MCP-1: K8s MCP 強化               ← 依賴 Sprint B（ArgoCD 穩定後 K8s 操作才完整）
  新增工具: k8s_get_pod_logs / k8s_watch_rollout / k8s_get_events / k8s_describe_pod
            k8s_get_hpa_status / k8s_get_node_conditions
  同步: NIM nemotron-mini-4b Tool Calling 退場（被 K8s MCP 取代）
  目的: 消除工具幻覺根本問題（不再由 LLM 猜 deployment 名稱，MCP 直接查真實值）
  ↓
[MCP Phase 1 驗收]
  → K8s 告警 → MCP 查真實 Pod 名稱 → 精確 rollout restart → 無 "unknown" target

══════════════ SPRINT C ══════════════

C-1: Velero 安裝                   ← 依賴 B-2（ArgoCD 穩定後，避免 ArgoCD revert restore）
C-2: rsync 備份腳本               ← 獨立，可與 C-1 平行
  ↓
C-3: DR SOP 文件                   ← 依賴 C-1 + C-2
  ↓
[Sprint C 全部驗收]
  ↓

══════════════ MCP PHASE 3（Sprint C 完成後）══════════════

MCP-3a: ArgoCD MCP Server          ← 依賴 Sprint C（ArgoCD 穩定運作後）
  工具: argocd_get_app_status / argocd_sync_app / argocd_rollback_app / argocd_get_app_history
  目的: ArgoCDSyncFailed 告警 → AI 自動 sync 或 rollback
  ↓
MCP-3b: Sentry MCP Server          ← 獨立，可與 MCP-3a 平行
  工具: sentry_get_recent_errors / sentry_get_error_trend / sentry_get_issue_detail
  目的: 情報收集補完（前端 JS Error 與後端告警關聯分析）
  ↓
[MCP Phase 3 驗收]
  → ArgoCDSyncFailed → AI 查 app 狀態 → 自動 rollback → Telegram 通知
  → AWOOOIApiDown + Sentry 同時有 Error → AI 關聯分析 → 更準確根因

══════════════ MCP PHASE 4（MCP 1/2/3 全部到位後）══════════════

MCP-4a: NemoClaw 降級顧問模式      ← 依賴 MCP Phase 1/2（工具可用性需可測試）
  實作: decision_manager._nemoclaw_advisory_mode()
  觸發: MCP 工具全失敗 OR OpenClaw 信心 < 0.7
  目的: 無工具純推理 → advisory_note → 人工參考

MCP-4b: KM Playbook 三段資料強化   ← 依賴 MCP-2b（Prometheus MCP 提供指標快照）
  實作: learning_service + playbook_service 新增 metrics_before/after 參數
  目的: Playbook 品質從「症狀→動作」升級到「症狀+情境→動作→效果數字+成功率」

MCP-4c: AI 動態規則生成            ← 依賴 MCP-4b（KM 夠豐富後才有意義）
  實作: symptom_pattern 由 AI 動態提煉（取代 hardcoded alertname→action mapping）
  新告警: 相似度 < 0.5 → AI 生成草稿 Playbook → 人工審核後生效
  ↓

══════════════ ADR-071 第二批（MCP Phase 2b 完成後）══════════════

ADR-071-I: TYPE-2 指標快照卡片     ← 依賴 MCP Phase 2b（Prometheus MCP）
  metrics_before/after 寫入 incidents 表
  自動修復卡片顯示指標前後對比（CPU 92%→23%）
  ↓
ADR-071-J: KM 三段資料整合         ← 依賴 ADR-071-I + MCP Phase 1（K8s watch_rollout）
  PlaybookService.extract_from_incident() 含完整指標 + 驗證資料
  Playbook 品質從「症狀→動作」升級到「症狀+情境→動作→效果數字」
  ↓
[全部完成] → 變更管理 + 災難備援 + 零寫死判斷全自動 AIOps 閉環
             + 四類型告警通知 + 全生命週期 DB 記錄 + 自動 KM 轉換
```

---

## 八、衝突與依賴完整檢查表

| 依賴關係 | 說明 | 若順序錯誤的後果 |
|---------|------|----------------|
| A-0-2 依賴 A-0-1 | Sentry 重啟需要足夠記憶體 | Swap 不足可能觸發 OOM |
| A-0-4 依賴 A-0-2 | Sentry 必須正常才能測試 Webhook | 測試結果無效 |
| A-2 依賴 A-1 | registry Port 5001 需要確認 gitlab 的 Port 5000 已釋放 | Port 衝突 |
| A-4 依賴 A-2 | webroot SSL 申請需要 nginx 正在服務 acme-challenge | SSL 申請失敗 |
| A-5 依賴 A-4 | HTTPS server block 需要 SSL 憑證存在 | nginx -t 失敗 |
| A-6 依賴 A-5 | 需確認 188 已完整接手後才能移除 110 備份 conf | 移除後 110 nginx 仍試圖代理不存在的服務 |
| A-9 依賴 A-5 | nginx 收斂後才裝 keepalived，避免 110 意外成為 master（因為 110 的 conf 不完整）| VIP 漂移到不完整的 110 → 服務中斷 |
| B-1 依賴 Sprint A | Ansible 要描述「乾淨的狀態」，不能在廢棄服務存在時撰寫 | Playbook 包含已移除的服務 |
| B-2 依賴 B-1 | ArgoCD 接手 CD 前，Host 層需先有 IaC 管理 | CD 問題時無法快速回滾 Host 層 |
| C-1 依賴 B-2 | ArgoCD 與 Velero 可能對同一個 resource 操作，需確認無競爭 | restore 時 ArgoCD 可能立即 revert |
| C-2 獨立 | rsync 備份與其他步驟無相依 | 可隨時執行 |

---

## 九、廢棄資源處置清單

### 處置原則（Archive 優先，不急著刪）

遵循 `feedback_archive_not_delete.md` 鐵律：**先停用、記錄、隔離，確認無依賴後再清除**。

分三個等級：
- **等級 1（直接清除）**：已確認無任何容器引用、無任何服務依賴
- **等級 2（Archive 觀察）**：有可能有資料或隱性依賴，mv 到 `_archive/` 目錄後觀察 14 天
- **等級 3（永久保留）**：確認運作中，記錄保留原因

---

### 等級 1：直接清除（納入 Sprint A-1）

> 依據：已 SSH 逐一驗證，無任何容器掛載、無任何 nginx conf 引用、無任何 systemd service 依賴。

| 項目 | 位置 | 查核事實 |
|------|------|---------|
| GitLab volumes（各5個）| 188 + 110 Docker | gitlab 容器已停止，`docker inspect` 確認無其他容器掛載 |
| `wooo-gitlab-*` 目錄 | 188 `/home/ollama/`、110 `/home/wooo/` | compose bind mount 遺留，服務已停 |
| `gitlab.wooo.work` nginx block | 188 all-sites.conf | 指向 110:8929，服務已停 |
| `gitlab.wooo.work` SSL cert | 188 + 110 | certbot `--cert-name gitlab.wooo.work`，無其他引用 |
| `grist.wooo.work` SSL cert | 188 | 無 nginx conf、無 Docker 容器、Grist 完全廢棄 |
| `aiops.wooo.work.conf`（空白檔）| 110 `/etc/nginx/sites-enabled/` | `cat` 確認為空，nginx 不讀空白 conf |
| `open-webui-data` volume | 188 Docker | `docker volume inspect` 確認從未被掛載（`du` 顯示空），`open-webui` 才是正確 volume |
| `n8n_data` + `n8n_n8n_data` volumes | 188 Docker | n8n compose 使用 bind mount `./data`，這兩個 named volume 從未被任何容器掛載 |
| `actions-runner-awoooi-3` 目錄 | 110 `/home/wooo/` | `.runner` 設定空白，`systemctl list-units` 確認無對應 service |

---

### 等級 2：Archive 觀察（mv 到 `_archive/`，下次 Sprint 清）

| 項目 | 位置 | 查核事實 | Archive 方式 |
|------|------|---------|-------------|
| clawbot 6個 volumes（`clawbot_*`、`clawbot-v5_*`、`clawbot-v5-updated_*`）| 188 Docker | `docker inspect openclaw` 確認 openclaw 完全不掛載任何 named volume（只用 bind mount）；但 volume 內可能有舊對話資料，不急著刪 | 保留原地，在 INVENTORY.md 標記為「待清除」，下次 Sprint 前再確認 |
| `apt-cacher-ng` | 110 `/home/wooo/apt-cacher-ng/` | 容器未運行（`docker ps -a` 無）、port 3142 無 listener、所有主機 `/etc/apt/` 均無設定使用它 | `sudo mv /home/wooo/apt-cacher-ng/ /home/wooo/_archive/apt-cacher-ng/` |

---

### 等級 3：永久保留

| 項目 | 位置 | 保留原因 |
|------|------|---------|
| `harbor-watchdog.service` | 110 systemd | 主動修復 Harbor 崩潰（Exited 128 死鎖），2026-04-05 建立，運作中 |
| `pm2-wooo.service` + `wooo-frontend` | 110 PM2 | 管理 wooo-aiops Next.js 前端（port 3000），屬 wooo-aiops 專案範疇 |
| `monitoring/` docker compose | 110 `/home/wooo/monitoring/` | Prometheus + Grafana + Alertmanager，正在運作 |
| `design-preview` container | 188 Docker | 設計稿預覽 port 8765，日常使用 |
| `clawdbot` nginx conf | 188 | 指向 OpenClaw，正在使用 |
| 所有 `actions-runner*` 目錄（除 awoooi-3）| 110 | 各自對應 running systemd service，詳見 A-7 |

---

### 關於 wooo-frontend（PM2）

`wooo-frontend`（PM2，port 3000）是 wooo-aiops 專案的前端，**本計畫不動**。是否遷移 Docker 由 wooo-aiops 獨立決定，不在此範疇。

---

## 十、基礎設施庫存記錄（防止下次再猜）

### 問題根源

本次盤點花費大量時間是因為缺乏一份「當前狀態快照」文件。每次都要 SSH 進去逐一查，且沒有記錄，下次又要重新查。

### 解決方案：INFRASTRUCTURE-INVENTORY.md（Sprint A 結束後建立）

**位置**：`docs/reference/INFRASTRUCTURE-INVENTORY.md`

**內容結構**：

```markdown
# 基礎設施庫存清單
> 最後更新：{日期}，由 ansible-playbook inventory-report.yml 自動生成

## 192.168.0.188 Docker Volumes
| volume 名稱 | 建立時間 | 掛載的容器 | 資料大小 | 狀態 |
|------------|---------|-----------|---------|------|
| open-webui | 2026-01-21 | open-webui | 120MB | 活躍 |
| n8n_n8n_data | 2026-03-07 | 無 | 0MB | 待清除 |
...

## 192.168.0.110 systemd Services
| service 名稱 | WorkingDirectory | 所屬 repo | 狀態 |
|-------------|-----------------|---------|------|
| actions.runner.owenhytsai-awoooi.awoooi-110 | /home/wooo/actions-runner-awoooi | owenhytsai/awoooi | running |
...

## 188 Nginx Sites
| conf 檔案 | 域名 | 後端 | SSL | 狀態 |
|----------|------|------|-----|------|
| all-sites.conf | aiops.wooo.work | 120:31234/31235 | ✅ | 活躍 |
...

## SSL 憑證清單
| 域名 | 到期日 | 對應 nginx conf | 狀態 |
|------|--------|---------------|------|
| gitlab.wooo.work | 2026-06-01 | 無 | 待刪除 |
...
```

**自動生成**：Sprint B 的 Ansible 中加入 `inventory-report.yml` playbook，執行後自動產生此文件並 git commit。從此每次 Sprint 後都有最新快照，永遠不需要重新猜測。

---

## 十一、112 Kali 預留接口文件

```yaml
# infra/ansible/inventory/hosts.yml 中：
security:
  hosts:
    host_112:
      ansible_host: 192.168.0.112
      ansible_user: wooo
      # 預留：本次不執行任何 playbook
      # 未來規劃：
      #   - 角色：資訊安全掃描中心
      #   - Scanner API：:8080
      #   - 預留 Nginx 入口：security.wooo.work
      #   - 獨立 ADR 討論安全架構整合
```

---

---

## 十二、與現有專案整合

### 現有 Backlog 衝突分析

| 現有工作項目 | 衝突評估 | 結論 |
|------------|---------|------|
| B1 Sprint 5.2 全面監控 | 部分重疊：B1 監控 K8s metrics，Sprint A-0 修 Sentry，兩者**互補不衝突** | ✅ 並行安全 |
| B3 Trace Context UI | 純前端工作，與 infra sprint 無交集 | ✅ 並行安全 |
| D1 models.json 更新 | 後端設定檔，與 infra sprint 無交集 | ✅ 並行安全 |
| ADR-067 Ollama 應用 (Phase 30-34) | 依賴 188 Ollama:11434，Sprint A 清理不動 Ollama | ✅ 並行安全 |

### 執行優先順序原則

```
Sprint A（基礎清理）可立即開始，順序規則：
  1. A-0（Sentry 修復）→ 獨立，最高優先
  2. A-1（188 GitLab 清除）→ 僅 188 Docker layer
  3. A-2（188 nginx 整合）→ 依賴 A-1 完成（GitLab conf 先移除）
  4. A-3（110 nginx 清理）→ 依賴 A-2 完成（188 接管 SSL 後）
  5. A-4（Harbor 修正）→ 獨立，可與 A-0 並行
  6. A-5（open-webui 整理）→ 獨立
  7. A-6（n8n 啟動）→ 獨立
  8. A-7（bitan Docker 化）→ 依賴 A-3（110 nginx 清理後 proxy 設定確認）
  9. A-8（Stock HTTPS）→ 依賴 A-2（188 nginx 設定完成後加 SSL block）
  10. A-9（SENTRY_DSN 修正）→ 依賴 A-0（Sentry 確認運作後取 DSN）

Sprint B（GitOps）→ 等 Sprint A 全完成後開始
Sprint C（DR）→ 等 Sprint B Ansible 完成後開始（需要 Ansible inventory 正確）

awoooi K8s pods（32334/32335）在 Sprint A/B/C 全程不觸碰。
```

### 現有服務不中斷保證

| 服務 | 影響評估 | 保護措施 |
|------|---------|---------|
| awoooi API (K8s) | A-3 僅清 110 nginx，K3s 流量經 VIP:125 不受影響 | ✅ 零影響 |
| bitan.wooo.work | A-7 前保留 PM2:3003，Docker 驗證後才切換 nginx | ✅ 切換視窗 < 30秒 |
| tsenyang / momo | A-2 僅加 HTTPS redirect，HTTP 流量繼續 | ✅ 零影響 |
| stock.wooo.work | A-8 加 HTTPS，HTTP 繼續服務直到 SSL 確認 | ✅ 零影響 |
| Sentry | A-0 執行 `docker compose down && up`，預計 < 5 分鐘恢復 | ⚠️ 5分鐘維護視窗 |
| Harbor | A-4 只改 nginx upstream port，Harbor 本體不重啟 | ✅ 零影響 |

---

## 十三、文件更新時間表

> 每個里程碑結束後，必須按以下清單更新文件。不得推遲到 Sprint 結束後才補。

### Sprint A 里程碑更新

| 里程碑 | 必須更新的文件 | 更新內容 |
|--------|-------------|---------|
| **A-0 完成** (Sentry 修復) | `project_current_status.md` | Sentry 狀態 → ✅ 運作中 |
| **A-0 完成** | `reference_four_hosts.md` | 110 Sentry:9000 狀態標記為 ✅ active |
| **A-0 完成** | LOGBOOK.md | 記錄 snuba 修復方法（DNS 問題根因） |
| **A-1 完成** (GitLab 清除) | `reference_four_hosts.md` | 移除 110 GitLab 相關記錄 |
| **A-4 完成** (Harbor 修正) | `reference_four_hosts.md` | Harbor upstream 更新為 :5000 |
| **A-7 完成** (Bitan Docker) | `reference_four_hosts.md` | 110 PM2 bitan:3003 → Docker bitan:3003 |
| **A-7 完成** | `project_master_workplan.md` | bitan Docker 化打勾 |
| **A-9 完成** (SENTRY_DSN) | LOGBOOK.md | 記錄 Phase 6 Sentry 整合修復完成 |
| **Sprint A 全完成** | `reference_four_hosts.md` | 完整更新所有服務/Port 正確狀態 |
| **Sprint A 全完成** | LOGBOOK.md | Sprint A 完整驗收紀錄 |
| **Sprint A 全完成** | `project_current_status.md` | 當前狀態更新 |

### Sprint B 里程碑更新

| 里程碑 | 必須更新的文件 | 更新內容 |
|--------|-------------|---------|
| **B-Ansible 完成** | `.agents/skills/04-awoooi-devops-commander.md` | 加入 Ansible playbook 使用規範 |
| **B-Ansible 完成** | `reference_four_hosts.md` | 加入 Ansible inventory 路徑 |
| **B-ArgoCD 完成** | `docs/adr/ADR-069-infra-rebuild-gitops-dr.md` | 正式建立 ADR-069（從本 spec 摘要） |
| **B-ArgoCD 完成** | `project_master_workplan.md` | Sprint B 打勾 |
| **B-Nginx HA 完成** | `reference_four_hosts.md` | 更新 VIP:200 = 188 MASTER / 110 BACKUP |
| **Sprint B 全完成** | LOGBOOK.md | Sprint B 完整驗收 + GitOps 架構圖 |
| **Sprint B 全完成** | `INFRASTRUCTURE-INVENTORY.md` | Ansible 自動生成（`inventory-report.yml`） |

### Sprint C 里程碑更新

| 里程碑 | 必須更新的文件 | 更新內容 |
|--------|-------------|---------|
| **C-Velero 完成** | `docs/runbooks/K3S-OPTIMIZATION-RUNBOOK.md` | 加入 Velero 備份/還原 SOP |
| **C-rsync 完成** | 新建 `docs/runbooks/HOST-BACKUP-RESTORE-SOP.md` | rsync 排程 + 手動還原步驟 |
| **Sprint C 全完成** | `project_master_workplan.md` | Sprint C 打勾，infra rebuild 正式關閉 |
| **Sprint C 全完成** | LOGBOOK.md | 最終驗收 + DR 演練結果 |
| **Sprint C 全完成** | `project_current_status.md` | 系統健康欄位更新（備份狀態） |

### ADR-069 建立規範

```
路徑：docs/adr/ADR-069-infra-rebuild-gitops-dr.md
建立時機：Sprint B ArgoCD 完成後（有實作依據才寫決策）
內容框架：
  - 問題：手動設定 → 漂移、無法回滾
  - 決策：Ansible (Host IaC) + ArgoCD (K8s GitOps) + Velero (DR)
  - 替代方案：Puppet/Chef（複雜度過高）、Terraform（非 Docker-native）
  - 後果：需要 Ansible controller 節點（暫用 DevOps 金庫 110）
```

### 112 Kali 預留 ADR 說明

```
未來討論安全架構整合時，建立獨立 ADR（序號 TBD）：
  - 112 Kali → security.wooo.work 或專用 VPN 接口
  - Scanner API :8080 整合到 AWOOOI 主動防禦流程（Phase 25）
  - 不在本 ADR-069 範圍內
```

---

*文件版本 v1.3（2026-04-10）— 整合專案工作計畫、文件更新時間表、ADR-069 建立規範*

---

## 十四、全自動 AIOps 閉環架構（ADR-070）

> **版本**: v1.0
> **新增日期**: 2026-04-11
> **前提**: Sprint A/B/C 完成後分階段實施
> **核心原則**: 零寫死判斷 — 所有規則匹配、決策、修復、通知全部由 AI 動態產生與執行

---

### 14.1 設計哲學：為什麼不能有寫死的判斷

傳統 AIOps 的最大缺陷是 **hardcoded rule engine**：

```
傳統做法（有寫死判斷）：
  if alertname == "HostHighCpuLoad":
      action = "restart nginx"   ← 永遠是同一個動作，不管真正原因
  elif alertname == "PodCrashLooping":
      action = "kubectl rollout restart"   ← 不管是 OOM 還是 config 錯誤

問題：
  - 環境變了，規則沒更新 → 錯誤修復
  - 新的告警種類出現 → 完全沒有規則
  - 同一種告警有十種根因 → 只能處理其中一種
```

**AWOOOI AIOps 的目標**：

```
動態做法（AI 判斷）：
  告警進來 → AI 實時收集情境 → AI 動態生成匹配規則
  → AI 判斷根因 → AI 選擇動作 → 執行 → AI 驗證效果
  → 結果沉澱 KM → 下次判斷更準

沒有任何 if/else 寫死 — 規則是 AI 從歷史學習動態生成的
```

---

### 14.2 告警種類完整盤點

基於現有 alert rule 檔案（k3s-alerts.yaml、k3s-alerts-supplemental.yaml、database-alerts.yaml、minio-kali-alerts.yaml、nvidia-alerts.yaml、alert-chain-monitor.yaml、alerts-unified.yml）的完整分類：

#### 類別 A：硬體資源（主機層，Node Exporter）

| 告警名 | 說明 | MCP 可接 | 自動修復能力 |
|--------|------|---------|------------|
| HostHighCpuLoad | CPU > 85% | SSH MCP (Phase MCP-2) | 查程序→分析→建議 restart |
| HostOutOfMemory | RAM 不足 | SSH MCP | 查程序→kill OOM→建議 |
| HostOutOfDiskSpace | 磁碟空間 | SSH MCP | 查大檔→清 log/image |
| HostDown | 主機宕機 | ❌ 無法自動 | 純告警+人工 |
| NodeHighCPU / NodeHighMemory | K3s Node 資源 | K8s MCP | cordon + 驅逐 Pod |
| NodeMemoryPressure / NodeDiskPressure / NodePIDPressure | K3s 壓力 | K8s MCP | cordon + 驅逐 |
| NodeNotReady / NodeUnschedulable | Node 不健康 | K8s MCP | uncordon 或升級 |

#### 類別 B：K3s / Kubernetes 應用層

| 告警名 | 說明 | MCP 可接 | 自動修復能力 |
|--------|------|---------|------------|
| PodRestartingTooMuch / KubePodCrashLooping | Pod 重啟 | K8s MCP | rollout restart |
| DeploymentReplicasMismatch | 副本數錯 | K8s MCP | scale / rollout |
| PodPendingTooLong / KubePodNotReady | Pod 卡住 | K8s MCP | describe + event 分析 |
| AWOOOIApiDown / AWOOOIWebDown | 應用宕機 | K8s MCP | rollout restart |
| K3sNodeDown / K3sVIPDown | K3s 節點 | K8s MCP | drain/uncordon |
| HPAMaxedOut / HPAScalingDisabled | HPA 失效 | K8s MCP | 調整上限 |
| WorkerUnavailable | Worker 無副本 | K8s MCP | scale up |
| ContainerOOMKilled | OOM 殺容器 | K8s MCP | 分析+建議調高 limit |
| DaemonSetMissingPods / StatefulSetReplicasMismatch | 工作負載 | K8s MCP | rollout restart |
| CronJobLastRunFailed | CronJob 失敗 | K8s MCP | 手動觸發 |
| ArgoCDSyncFailed | ArgoCD 同步失敗 | ArgoCD MCP (Sprint B後) | sync / rollback |
| VeleroBackupFailed / VeleroBackupMissing | 備份失敗 | K8s MCP | 觸發重新備份 |
| TLSCertExpiringIn30Days/7Days/Expired | TLS 憑證 | SSH MCP | certbot renew |
| TLSProbeFailure | TLS 探針失敗 | SSH MCP | 分析 nginx conf |

#### 類別 C：資料庫（PostgreSQL + Redis）

| 告警名 | MCP | 自動修復能力 |
|--------|-----|------------|
| PostgreSQLDown / RedisDown | K8s MCP | rollout restart |
| PostgreSQLConnectionPoolNearLimit/Exhausted | DB MCP (現有) | 斷開閒置連線 |
| PostgreSQLSlowQueries / LongRunningQuery | DB MCP | TERMINATE 長查詢 |
| PostgreSQLLockWaiting / Deadlocks | DB MCP | 終止 blocking session |
| PostgreSQLTableBloat | DB MCP (部分) | 建議 VACUUM |
| RedisMemoryHigh / Critical | DB MCP | 清 TTL 過期 key |
| RedisLatencyHigh / RedisBlockedClients | DB MCP | UNBLOCK client |

#### 類別 D：外部網站 / Blackbox

| 告警名 | MCP | 自動修復能力 |
|--------|-----|------------|
| MoWoooWorkDown / TsenyangWebsiteDown | SSH+Docker MCP | docker restart |
| StockWoooWorkDown / BitanWoooWorkDown | SSH+Docker MCP | docker restart |
| ExternalSiteSSLExpiringSoon | SSH MCP | certbot renew |

#### 類別 E：Docker 容器（主機層）

| 告警名 | MCP | 自動修復能力 |
|--------|-----|------------|
| DockerContainerUnhealthy | SSH+Docker MCP | docker restart |
| DockerContainerExited | SSH+Docker MCP | docker start |

#### 類別 F：DevOps 工具服務

| 告警名 | MCP | 自動修復能力 |
|--------|-----|------------|
| HarborDown | SSH MCP | docker compose restart |
| GiteaDown | SSH MCP | docker compose restart |
| OpenClawDown | K8s MCP | rollout restart |
| SignOzDown | SSH MCP | docker compose restart |
| SentryDown | SSH MCP | docker compose restart |
| OllamaDown | SSH MCP | systemctl restart ollama |
| AlertmanagerDown | K8s or SSH MCP | 視部署位置 |
| MinIODown / MinioDiskUsageHigh | SSH MCP | docker restart / 清備份 |
| KaliScannerDown | SSH MCP | systemctl restart |

#### 類別 G：告警鏈路自監控

| 告警名 | 處理方式 |
|--------|---------|
| AlertChainBroken_* | 純告警+人工（自身壞掉難自動修）|
| NoAlertsReceived2Hours | 純告警+人工（靜默異常）|
| AutoRepairLowSuccessRate | 觸發 NemoClaw 推理仲裁 |
| PermanentFixRequired | 人工更新 Playbook |

#### 類別 H：AI 工具層

| 告警名 | MCP | 自動修復能力 |
|--------|-----|------------|
| NvidiaCircuitBreakerOpen | AI Router MCP | 切換備援 Provider |
| NvidiaToolCallingHighLatency/HighErrorRate | AI Router MCP | 降級到 Ollama |

#### 類別 I：資安（目前告警盲點）

| 類型 | 現況 | 未來規劃 |
|------|------|---------|
| 入侵偵測 / 異常登入 | **無告警規則** | Phase MCP-3 後，112 Kali 整合 |
| 掃描攻擊 / DDoS | **無告警規則** | Nginx access log 分析 |
| 敏感設定漂移 | Config Drift（無資安視角）| Ansible drift detection |

#### 類別 J：UI/UX / 前端（目前告警盲點）

| 類型 | 現況 | 未來規劃 |
|------|------|---------|
| 前端 JS Error | Sentry 捕捉，無 Prometheus 橋 | Sentry MCP (Phase MCP-3) |
| 效能退化 (LCP/FID) | **完全沒有** | SignOz OTEL 補充 |
| API 4xx/5xx 趨勢 | SignOz 有指標，告警不完整 | Prometheus MCP 補規則 |

---

### 14.3 MCP Server 完整規劃

#### MCP-0：現有框架（已上線）

```
apps/api/src/plugins/mcp/providers/
├── k8s_provider.py       ← K8s 操作（kubectl_get/delete/scale/restart）
├── database_provider.py  ← PostgreSQL + Redis 操作
├── signoz_provider.py    ← SignOz 查詢
├── grafana_provider.py   ← Grafana 查詢
├── rag_provider.py       ← RAG 知識庫查詢
└── filesystem_provider.py
```

---

#### MCP Phase 1：K8s MCP 強化（Sprint B 後）

**目標**：替換 NIM nemotron-mini-4b Tool Calling，消除工具幻覺根本問題。

**新增工具**：

| 工具名 | 說明 |
|--------|------|
| `k8s_get_pod_logs` | 取得 Pod 最後 N 行 log（取代 log_summary 手動呼叫）|
| `k8s_watch_rollout` | 監控 rollout 狀態直到完成（真正的驗證，非 sleep 猜）|
| `k8s_get_events` | 取得 namespace/resource 的 K8s events |
| `k8s_describe_pod` | 完整 Pod describe（含 Conditions、Volumes、Env）|
| `k8s_get_hpa_status` | 取得 HPA 當前副本數/上限/下限 |
| `k8s_get_node_conditions` | 取得 Node conditions（Ready/MemoryPressure/DiskPressure）|

**安全守衛強化**：
- 參數白名單驗證（namespace 限 `awoooi-prod`）
- 操作類工具需 `trust_engine.get_score() >= 0.7`
- 所有操作寫 audit_log → PostgreSQL

---

#### MCP Phase 2：SSH MCP Server + Prometheus MCP（Sprint A 完成後即可開始）

**SSH MCP Server**（補完主機層 70% 盲點）

```python
# apps/api/src/plugins/mcp/providers/ssh_provider.py
class SSHProvider(MCPToolProvider):
    """
    SSH MCP Provider — 主機層操作代理
    Phase MCP-2: 補完 K8s 無法覆蓋的 70% 主機層告警
    絞殺者開關: SSH_MCP_ENABLED env var
    """
```

**群組 A：診斷類（只讀，無需信任度檢查）**

| 工具名 | 對應告警 | 執行指令 |
|--------|---------|---------|
| `ssh_get_top_processes` | HostHighCpuLoad, HostOutOfMemory | `ps aux --sort=-%cpu \| head 15` |
| `ssh_get_disk_usage` | HostOutOfDiskSpace, MinioDiskUsageHigh | `df -h && du -sh /var/lib/docker` |
| `ssh_get_memory_info` | HostOutOfMemory | `free -h` |
| `ssh_get_container_logs` | DockerContainerExited, HarborDown | `docker logs <name> --tail 50` |
| `ssh_get_container_status` | 所有 DockerContainer* | `docker ps -a --filter name=<name>` |
| `ssh_get_service_status` | OllamaDown, KaliScannerDown | `systemctl status <service>` |
| `ssh_check_port` | 任何服務宕機確認 | `ss -tlnp \| grep <port>` |
| `ssh_get_nginx_error_log` | 網站宕機, TLS 問題 | `tail -n 50 /var/log/nginx/error.log` |
| `ssh_get_swap_info` | HostOutOfMemory | `swapon --show && free -h` |

**群組 B：安全操作類（需 trust_engine score >= 0.8）**

| 工具名 | 對應告警 | 執行指令 | 風險 |
|--------|---------|---------|------|
| `ssh_docker_restart` | DockerContainerExited, HarborDown | `docker restart <name>` | 低 |
| `ssh_docker_compose_restart` | SentryDown, SignOzDown, GiteaDown | `cd <dir> && docker compose restart <service>` | 中 |
| `ssh_systemctl_restart` | OllamaDown, KaliScannerDown | `systemctl restart <service>` | 低 |
| `ssh_clear_docker_logs` | HostOutOfDiskSpace（log 佔用）| `truncate -s 0 <container-log-path>` | 低 |
| `ssh_renew_ssl` | TLSCertExpiringIn7Days | `certbot renew --cert-name <domain>` | 低 |
| `ssh_reload_nginx` | TLSProbeFailure, conf 更新後 | `nginx -t && systemctl reload nginx` | 低 |

**永遠禁止執行清單（硬編碼守衛）**：

```python
FORBIDDEN_PATTERNS = [
    r"rm\s+-rf",          # 遞歸刪除
    r"/etc/passwd",       # 系統帳號
    r"authorized_keys",   # SSH key
    r"sudoers",           # 權限設定
    r"\$\(",              # 命令替換
    r"`",                 # 反引號執行
    r"\|.*rm",            # pipe 到刪除
    r">\s*/etc/",         # 重定向到系統目錄
]
```

**SSH 連線安全架構**：

```
連線方式: asyncssh（純 Python，不需系統 ssh binary）
認證方式: Private Key（從 K8s Secret 掛載）
          Secret 名稱: ssh-mcp-key
          掛載路徑: /run/secrets/ssh_mcp_key
允許主機: SSH_MCP_ALLOWED_HOSTS = ["192.168.0.188", "192.168.0.110", "192.168.0.111"]
連線池: 每 host 保持 1 個長連線，idle 5 分鐘後關閉
超時: 診斷類 10s，操作類 60s
```

**四層安全守衛**（缺一不可）：

```
守衛 1: tool_name 在白名單？           → 不在: 直接拒絕
守衛 2: host 在 ALLOWED_HOSTS？       → 不在: 直接拒絕
守衛 3: 參數正則驗證（FORBIDDEN_PATTERNS）→ 命中: 直接拒絕
守衛 4: 群組 B 工具 → trust_score >= 0.8 → 不足: 建議人工執行
```

**Prometheus MCP Server**

| 工具名 | 說明 | 提供給 KM 的情境 |
|--------|------|----------------|
| `prometheus_query` | 即時指標查詢 | CPU/RAM/Error Rate 當前值 |
| `prometheus_query_range` | 歷史趨勢（1h/6h/24h）| 「今天第幾次高 CPU？趨勢是上升還是偶發？」|
| `prometheus_get_alert_history` | 同一告警過去 7 天 | 「這個告警每天都觸發 → 可能是正常排程行為」|
| `prometheus_get_firing_rules` | 當前觸發中的規則 | 告警是否有相關聯的其他告警同時觸發 |

---

#### MCP Phase 3：ArgoCD MCP + Sentry MCP（Sprint C 後）

**ArgoCD MCP Server**

| 工具名 | 說明 |
|--------|------|
| `argocd_get_app_status` | 取得應用同步狀態（Synced/OutOfSync/Degraded）|
| `argocd_sync_app` | 觸發應用同步 |
| `argocd_rollback_app` | 回滾到指定 revision |
| `argocd_get_app_history` | 取得部署歷史 |

**Sentry MCP Server**

| 工具名 | 說明 |
|--------|------|
| `sentry_get_recent_errors` | 取得服務近 1 小時的錯誤列表 |
| `sentry_get_error_trend` | 取得錯誤率趨勢（是否在上升）|
| `sentry_get_issue_detail` | 取得特定錯誤的 stack trace |

**告警 label 補充**：

```yaml
# 主機層告警需加 host_type label，讓 AI 知道應該用哪個 MCP
# 修改 ops/monitoring/alerts-unified.yml
- alert: HostHighCpuLoad
  labels:
    host_type: "bare_metal"          # ← 新增
    mcp_provider: "ssh_host"         # ← 新增，告訴 AI Router 用哪個 MCP
    target_host: "{{ $labels.instance | split(':') | first }}"  # ← 新增
```

---

#### MCP Phase 4：NemoClaw 整合 + KM 強化

**NemoClaw 降級顧問模式**：

```python
# decision_manager.py 中，MCP 工具全失敗時的 fallback

async def _nemoclaw_advisory_mode(incident: Incident, rag_results: list) -> str:
    """
    MCP 工具失敗時，NemoClaw 提供純推理建議
    不執行任何操作，只輸出 advisory_note 供人工參考
    """
    context = f"""
    告警: {incident.signals[0].labels.get('alertname')}
    歷史相似案例（RAG）: {rag_results}
    MCP 工具狀態: 全部不可用
    請用 SRE 推理給出診斷和建議（不執行，僅建議）
    """
    result = await nemoclaw.generate(context)
    # 寫入 incident.advisory_note → Telegram 顯示
    # 追蹤 NemoClaw 建議 vs 最終人工操作 → 評估準確率
    return result
```

**KM Playbook 品質強化**（MCP 帶三段資料）：

```python
# learning_service.py 中 PlaybookService.extract_from_incident() 新增參數

await playbook_service.extract_from_incident(
    incident=incident,
    context_bundle=context_bundle,      # MCP 情報層收集的即時情境
    execution_result=execution_result,   # 執行結果
    metrics_before=metrics_before,       # Prometheus MCP 執行前快照
    metrics_after=metrics_after,         # Prometheus MCP 執行後快照（5分鐘後）
    k8s_state_after=k8s_state_after,    # K8s MCP 執行後狀態
)

# Playbook 品質因此從：
#   "症狀: CPU>85% → 動作: restart"
# 升級為：
#   "症狀: CPU>85% + top 程序是 awoooi-worker + 同時有 Sentry OOM 告警
#    → 動作: kubectl rollout restart deployment/awoooi-worker
#    → 效果: CPU 從 92% 降到 23% in 45s（Prometheus 驗證）
#    → 成功率: 8/10"
```

---

### 14.4 全自動 AIOps 閉環完整流程

```
╔══════════════════════════════════════════════════════════════╗
║               全自動 AIOps 閉環（零寫死判斷）                  ║
╚══════════════════════════════════════════════════════════════╝

Prometheus / Node Exporter / Blackbox / Sentry / SignOz
                    ↓ 指標觸發
             Alertmanager（YAML 規則）
                    ↓ webhook POST /api/v1/alerts/webhook
             incident_service（建立 Incident）
                    ↓
┌──────────── [情報收集層 — MCP 感知] ────────────────────────────┐
│  依據告警 labels.mcp_provider 自動路由到對應 MCP：              │
│                                                                 │
│  host_type=bare_metal → SSH MCP:                                │
│    ssh_get_top_processes(host) → 哪個程序吃 CPU？              │
│    ssh_get_container_status(name) → 容器真實狀態                │
│                                                                 │
│  host_type=k8s → K8s MCP:                                      │
│    k8s_get_pod_logs(pod, namespace) → 最近 log                 │
│    k8s_describe_pod(pod) → 事件時間軸                          │
│    k8s_get_node_conditions() → Node 健康度                     │
│                                                                 │
│  永遠執行 → Prometheus MCP:                                     │
│    prometheus_query_range(metric, 1h) → 趨勢                   │
│    prometheus_get_alert_history(alertname, 7d) → 頻率          │
│                                                                 │
│  如有 Sentry 相關 → Sentry MCP:                                 │
│    sentry_get_recent_errors(service) → 同時有 JS Error？        │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ ContextBundle（即時情境包）
┌──────────── [KM 歷史智慧查詢層] ────────────────────────────────┐
│  RAG 查詢（nomic-embed 768d）:                                  │
│    query = alertname + top_process + error_pattern             │
│    → 找最相關 runbook / ADR / 過去 incident                    │
│                                                                 │
│  Playbook 語意匹配（pgvector）:                                 │
│    symptom_pattern = ContextBundle 提煉                         │
│    → 相似度 >= 0.85 → 直接使用 Playbook                        │
│    → 相似度 0.7-0.85 → 作為參考，仍走 LLM 分析                 │
│                                                                 │
│  Episodic Memory 召回:                                         │
│    找 30 天內 same service + same alert 的歷史事件              │
│    → 「這是第 3 次，前兩次都是 Docker OOM，restart 成功」        │
│                                                                 │
│  GraphRAG 影響範圍:                                             │
│    blast_radius(affected_service) → 誰會連帶受影響？            │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ KM 結果包
┌──────────── [OpenClaw AI 決策層] ───────────────────────────────┐
│  輸入: ContextBundle（即時）+ KM 結果包（歷史）                  │
│                                                                 │
│  OpenClaw 判斷：                                                │
│    1. 根因是什麼？（從情境包 + 歷史推斷）                        │
│    2. 動作是什麼？（從 KM Playbook 或 LLM 生成）                │
│    3. 目標是什麼？（從 K8s MCP / SSH MCP 確認真實名稱）         │
│    4. 風險是什麼？（blast_radius + 操作類型）                   │
│    5. 信心是多少？（0.0-1.0）                                   │
│                                                                 │
│  信心 < 0.7 → 觸發 NemoClaw second opinion:                    │
│    NemoClaw 獨立分析同一份資料 → 產生備選方案                   │
│    Router 對比兩個 AI 的建議 → 選信心較高者 or 升級人工審核      │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ DecisionToken（action + target + confidence）
┌──────────── [決策守衛層] ───────────────────────────────────────┐
│  守衛 1: target 不是 placeholder / unknown / alertname          │
│  守衛 2: action 不含未解析 {xxx} / <xxx>                        │
│  守衛 3: 根據 risk_level 決定是否需人工審核                     │
│  守衛 4: 信心 < auto_approve 閾值 → 人工審核                   │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓
               ┌───────────┴───────────┐
          自動執行（信心高）        人工審核（Telegram 卡片）
               ↓                        ↓ 批准/拒絕
┌──────────── [MCP 執行層] ───────────────────────────────────────┐
│  K8s 操作 → K8s MCP: kubectl_restart / scale / delete          │
│  主機操作 → SSH MCP: docker restart / systemctl restart         │
│  GitOps  → ArgoCD MCP: sync / rollback（Sprint B 後）          │
│  DB 操作  → DB MCP: terminate_query / unblock_client           │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ 執行完成
┌──────────── [MCP 驗證層] ───────────────────────────────────────┐
│  K8s MCP: watch_rollout_status(deploy, 120s) → 真正的等待確認  │
│  Prometheus MCP: query(error_rate, after=now+5m) → 指標恢復？  │
│  SSH MCP: get_container_status(name) → 容器真的 Up？           │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ 驗證結果（success + metrics_snapshot）
┌──────────── [Telegram 通知層] ──────────────────────────────────┐
│  成功: 通知修復完成 + 修復耗時 + 指標恢復數字                   │
│  失敗: 通知失敗原因 + NemoClaw advisory_note + 建議人工步驟     │
│  追加: NemoClaw Log 摘要（Phase 31，deepseek-r1 分析 pod log）  │
└──────────────────────────┬─────────────────────────────────────┘
                           ↓ 結果資料
┌──────────── [KM 學習層] ────────────────────────────────────────┐
│  LearningService.record_feedback(                               │
│    execution_result,                                            │
│    metrics_before, metrics_after,    ← Prometheus MCP 採集     │
│    k8s_state_after,                  ← K8s MCP 採集            │
│  )                                                              │
│                                                                 │
│  PlaybookService.extract_from_incident(                         │
│    帶三段資料: 執行前情境 + 執行動作 + 執行後驗證               │
│  )                                                              │
│                                                                 │
│  NemoClaw 準確率追蹤:                                           │
│    advisory_note vs 最終人工操作 → accuracy_score              │
│    準確率 > 80% → 升格 advisory 為「低信心 Playbook」           │
└─────────────────────────────────────────────────────────────────┘
                           ↓
              下一次同類告警：信心更高、動作更精準
              （KM 越來越聰明，不需要人工寫規則）
```

---

### 14.5 AI 動態規則生成（取代 hardcoded rule engine）

**核心：symptom_pattern 由 AI 自動生成，不寫死**

```python
# auto_repair_service.py 中

async def _build_symptom_pattern_from_context(
    incident: Incident,
    context_bundle: ContextBundle,  # MCP 收集的即時情境
) -> SymptomPattern:
    """
    不寫死任何 alertname→action 的對應
    讓 AI 從即時情境 + 歷史案例中，動態提煉 SymptomPattern
    """
    # AI 提煉：「這次的症狀特徵是什麼？」
    symptom_prompt = f"""
    告警資訊: {incident.signals[0].labels}
    即時情境: {context_bundle.summary}
    請提煉症狀關鍵字，用於 Playbook 語意匹配（不超過 50 字）
    """
    symptom_text = await openclaw.extract(symptom_prompt)

    return SymptomPattern(
        alert_type=incident.signals[0].labels.get("alertname"),
        symptom_description=symptom_text,  # AI 動態生成
        affected_resource=context_bundle.confirmed_target,  # MCP 確認的真實名稱
        severity=incident.severity,
    )
```

**AI 自學習新告警種類**：

```python
# 當 Playbook 相似度 < 0.5（完全沒有歷史案例）時：
if max_similarity < 0.5:
    # AI 生成全新 Playbook 草稿
    draft = await openclaw.generate_playbook_draft(
        incident=incident,
        context_bundle=context_bundle,
    )
    # 狀態 = DRAFT，人工審核後才生效
    await playbook_service.create_draft(draft, requires_approval=True)
    # Telegram 通知：「發現新的告警模式，已生成草稿 Playbook，請審核」
```

---

### 14.6 MCP Phase 實施順序與依賴關係

```
[Sprint A 完成後，立即可以開始]
  MCP Phase 2a: SSH MCP Server
    前提: Sprint A 清理完畢 → 知道哪些 container 名稱、哪些 compose 路徑
    優先級: P0（補完 70% 主機層盲點）
    工作量: 1 個新 Provider 檔案 + registry 一行 + K8s Secret

  MCP Phase 2b: Prometheus MCP Server
    前提: 無（Prometheus 現在就在）
    優先級: P0（情報收集最重要）
    工作量: 1 個新 Provider 檔案

[Sprint B 完成後]
  MCP Phase 1 強化: K8s MCP 新增工具
    前提: ArgoCD 到位後，K8s 操作才完整
    優先級: P1
    工作量: 在現有 k8s_provider.py 新增 4 個工具

  K8s YAML alert label 補充:
    告警 YAML 加 mcp_provider / host_type / target_host label
    讓 AI Router 自動決定用哪個 MCP

[Sprint C 完成後]
  MCP Phase 3: ArgoCD MCP + Sentry MCP
    前提: ArgoCD 穩定運作（Sprint B/C 後）
    優先級: P2

  MCP Phase 4: NemoClaw 整合 + KM Playbook 三段資料
    前提: MCP Phase 1/2/3 全部到位
    優先級: P3

[衝突檢查]
  ✅ MCP Phase 2 可在 Sprint B 之前開始（不依賴 ArgoCD）
  ✅ 告警 label 補充在 MCP Phase 3 前做（準備工作）
  ❌ ArgoCD MCP 不能在 Sprint B 之前做（ArgoCD 還沒裝）
  ❌ NIM 退場不能在 K8s MCP Phase 1 強化之前做（會失去 Tool Calling 能力）
```

---

### 14.7 與 Sprint A/B/C 的整合執行順序（全局視圖）

```
立即可做（獨立）：
  Sprint A-0  Sentry 修復 + Swap + SENTRY_DSN
  D1          models.json 集中化（各 Service 模型名稱硬編碼問題）
  B2 UI       前端拓撲骨架（React Flow 元件，資料源等 ArgoCD 後接）

Sprint A 主線：
  A-1 ~ A-9  按 §七 執行順序

[Sprint A 全驗收後]
  MCP Phase 2a: SSH MCP Server      ← 立即開始，不等 B
  MCP Phase 2b: Prometheus MCP      ← 立即開始，不等 B

Sprint B：
  B-1 Ansible IaC
  B-2 ArgoCD GitOps + 修改 cd.yaml（移除 kubectl set image）
  B-3 keepalived HA

[Sprint B 全驗收後]
  MCP Phase 1: K8s MCP 新增工具 + NIM 退場
  告警 label 補充

Sprint C：
  C-1 Velero + C-2 rsync + C-3 DR 演練

[Sprint C 全驗收後]
  MCP Phase 3: ArgoCD MCP + Sentry MCP
  MCP Phase 4: NemoClaw 整合 + KM 三段資料
  B2 資料源: 接真實 ArgoCD API

[全部完成] → 零寫死判斷的全自動 AIOps 閉環
```

---

### 14.8 NemoTron（deepseek-r1:14b）完整角色清單

NemoTron 不退場，在 MCP 架構後升格為**推理仲裁引擎**：

| 場景 | 觸發條件 | 做什麼 | 現況 |
|------|---------|--------|------|
| Log 異常摘要 | 告警後 auto_repair 觸發 | 摘要 pod logs | ✅ Phase 31 已上線 |
| Chat 戰術參謀 | `@nemo` 或雙 AI 對話 | 補充 OpenClaw 視角 | ✅ 已上線 |
| 降級顧問模式 | MCP 工具全部失敗 | 純推理建議（不執行）→ advisory_note | 🔲 MCP Phase 4 |
| 複雜推理仲裁 | OpenClaw 信心 < 0.7 | second opinion，對比兩 AI 建議 | 🔲 MCP Phase 4 |
| 新 Playbook 草稿審核 | 相似度 < 0.5 新告警 | 審核 AI 生成的草稿是否合理 | 🔲 MCP Phase 4 |
| NemoClaw 準確率追蹤 | 每次 advisory_note 後 | 對比建議 vs 最終操作 → 學習 | 🔲 MCP Phase 4 |

---

---

### 14.9 告警通知四類型設計（ADR-071）

> 解決「不管什麼事都發核彈按鈕」的告警疲勞問題。
> 原則：通知格式由告警性質決定，不由人工設定。

#### 14.9.1 四種通知類型

| 類型 | 名稱 | 使用者認知狀態 | 觸發條件 | 按鈕 |
|------|------|-------------|---------|------|
| **TYPE-1** | 純資訊通知 | 知道了（FYI）| severity=info + success/completed | ❌ 無按鈕 |
| **TYPE-2** | 已自動修復 | 做得很棒（Acknowledge）| auto_executed=True + COMPLETED | [查看詳情] |
| **TYPE-3** | 需人工審核 | 我來決定（Action Required）| 預設，可操作告警 | 按 alert_category 動態組合 |
| **TYPE-4** | AI 無法判斷 | 我來查修（Investigation Needed）| 信心<0.5 / MCP失敗 / 新型告警 | [忽略] [升級] [手動修復後記錄] |
| **TYPE-4D** | Config Drift 專屬 | 我來查修（Drift）| alertname=ConfigDrift | [查看Diff] [採納變更] [回滾] [忽略] |

#### 14.9.2 分類器邏輯（零寫死）

```python
def classify_notification(incident, decision_token, auto_executed) -> NotificationType:

    labels = incident.signals[0].labels if incident.signals else {}
    alertname = labels.get("alertname", "")
    label_severity = labels.get("severity", "")

    # TYPE-4D：Config Drift 專屬（優先判斷）
    if alertname == "ConfigDrift":
        return TYPE_4_DRIFT

    # TYPE-1：純資訊（severity=info + 成功類告警）
    # 注意：同一 alertname 若 severity 變 critical → 不走 TYPE-1
    if label_severity == "info" and any(kw in incident.title.lower()
                                        for kw in ["success", "完成", "completed"]):
        return TYPE_1
    if alertname.startswith(("Backup.", "VeleroBackup")) and label_severity == "info":
        return TYPE_1
    if alertname in ("AlertChainHealthy", "AutoRepairHighSuccessRate"):
        return TYPE_1

    # TYPE-2：已自動修復完成
    if auto_executed and decision_token.state == DecisionState.COMPLETED:
        return TYPE_2

    # TYPE-4：AI 無法判斷
    if (decision_token.confidence < 0.5
            or decision_token.state == DecisionState.ERROR
            or decision_token.mcp_all_failed):
        return TYPE_4

    # TYPE-3：預設（需人工審核）
    return TYPE_3
```

#### 14.9.3 TYPE-3 按鈕依 alert_category 動態組合

```
alert_category（由告警 label 或 AI 分類決定）

k8s_workload   → [重啟] [擴容] [縮容] [回滾] [查看詳情] [忽略]
database       → [終止慢查詢] [清連線池] [查看詳情] [忽略]
host_resource  → [查程序] [重啟服務] [清 Log] [查看詳情] [忽略]
network        → [重載 Nginx] [查 Port] [查看詳情] [忽略]
devops_tool    → [重啟服務] [查 Log] [查看詳情] [忽略]
ai_system      → [切換 Provider] [查看詳情] [忽略]
ssl_cert       → [更新憑證] [查看詳情] [忽略]
```

#### 14.9.4 狀態機守衛（State Machine Guardrails）

callback 進來時強制校驗 Incident 當下狀態：

```python
async def handle_callback(callback_data, incident_id, user, ...):

    incident = await incident_repo.get(incident_id)

    # 守衛：RESOLVED/CLOSED → 拒絕執行，更新卡片
    if incident.status in (IncidentStatus.RESOLVED, IncidentStatus.CLOSED):
        await telegram.edit_message(
            message_id, text=f"✅ 此事件已於 {incident.resolved_at} 由 @{resolved_by} 解決"
        )
        await telegram.remove_buttons(message_id)
        return {"blocked": True, "reason": "already_resolved"}

    # 守衛：EXECUTING/REPAIRING → 防止重複執行
    if incident.status == IncidentStatus.MITIGATING:
        await telegram.answer_callback("⏳ 正在修復中，請稍候...")
        return {"blocked": True, "reason": "already_executing"}

    # 正常流程
    ...
```

#### 14.9.5 TYPE-4 手動修復記錄（直通 LearningService）

```
使用者點擊 [手動修復後記錄]
    ↓
Bot 回覆：「請輸入您的修復步驟（可多行），輸入完畢傳送 /done」
    ↓
使用者輸入：「ssh 192.168.0.110，docker restart sentry-snuba，等 2 分鐘確認」
    ↓
LearningService.record_manual_fix(
    incident=incident,
    manual_steps=steps,
    user=username,
)
    ↓
PlaybookService.extract_from_manual_fix()
    → 建立 DRAFT Playbook（status=draft，需人工審核）
    → KnowledgeEntryRecord 建立對應條目（source=human，status=draft）
    ↓
Telegram 通知：「已建立草稿 Playbook #PB-xxx，請至 AWOOOI 審核後生效」
```

#### 14.9.6 Config Drift Diff 長度處理

```python
diff_text = drift_result.diff_summary

if len(diff_text) <= 500:
    # 直接在 Telegram code block 回覆
    await telegram.reply(f"<pre>{diff_text}</pre>")
else:
    # 回傳 AWOOOI Web 連結
    url = f"https://aiops.wooo.work/incidents/{incident_id}/drift-diff"
    await telegram.reply(f"差異過大（{len(diff_text)} 字），請至 {url} 查看完整 Diff")
```

---

### 14.10 告警全生命週期資料庫記錄與 KM 轉換管道

> 原則：每一筆告警從進入到結束，全程寫入資料庫，結束後自動轉換成 KM 知識。

#### 14.10.1 現有資料庫表（已存在，無需新建）

| 表名 | 用途 | 覆蓋範圍 |
|------|------|---------|
| `alert_operation_log` | Event Sourcing，每個生命週期事件一筆，不可變 | ✅ 已有 |
| `incidents` | Episodic Memory，完整事件記錄 + AI 決策鏈 | ✅ 已有，有 `vectorized` 欄位 |
| `approval_records` | 審核記錄（人工動作） | ✅ 已有 |
| `auto_repair_executions` | 自動修復執行記錄 | ✅ 已有 |
| `knowledge_entries` | KM 條目（incident_case / runbook / best_practice / postmortem）| ✅ 已有 |
| `audit_logs` | 操作稽核 | ✅ 已有 |

#### 14.10.2 需要新增的欄位（DB Migration）

在現有表上補充欄位，不新建表：

**`incidents` 表新增欄位**：

```sql
-- 通知類型記錄（TYPE-1/2/3/4/4D）
ALTER TABLE incidents ADD COLUMN notification_type VARCHAR(10);

-- 告警類別（k8s_workload / database / host_resource / ...）
ALTER TABLE incidents ADD COLUMN alert_category VARCHAR(50);

-- MCP 情報收集快照（執行前）
ALTER TABLE incidents ADD COLUMN context_bundle JSONB;

-- 指標快照（執行前後，Prometheus MCP 採集）
ALTER TABLE incidents ADD COLUMN metrics_before JSONB;
ALTER TABLE incidents ADD COLUMN metrics_after JSONB;

-- 執行驗證結果（K8s MCP watch_rollout 結果）
ALTER TABLE incidents ADD COLUMN verification_result JSONB;

-- 手動修復步驟（TYPE-4 使用者輸入）
ALTER TABLE incidents ADD COLUMN manual_fix_steps TEXT;
ALTER TABLE incidents ADD COLUMN manual_fix_by VARCHAR(100);
```

**`alert_operation_log` 表新增 event_type 值**（PgEnum 需 migration）：

```sql
-- 新增 TYPE 分類相關事件
ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'NOTIFICATION_CLASSIFIED';
ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'MANUAL_FIX_RECORDED';
ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'KM_CONVERTED';
ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'PLAYBOOK_DRAFT_CREATED';
ALTER TYPE alert_event_type ADD VALUE IF NOT EXISTS 'STATE_GUARD_BLOCKED';
```

#### 14.10.3 告警→KM 自動轉換管道

每個 Incident 解決後，自動觸發 KM 轉換流程：

```
Incident 狀態變為 RESOLVED / CLOSED
    ↓
KMConversionService.convert(incident_id)  ← 新增 Service
    ↓
    ├── 判斷轉換等級：
    │     成功自動修復（TYPE-2）→ 高品質，直接轉 KM（status=approved）
    │     人工審核後執行（TYPE-3）→ 中品質，轉 KM（status=review）
    │     手動修復記錄（TYPE-4）→ 草稿，轉 KM（status=draft）
    │     純資訊（TYPE-1）→ 不轉 KM
    │
    ├── 建立 KnowledgeEntryRecord：
    │     entry_type = "incident_case"
    │     title = f"{alertname} @ {service} — {resolution_summary}"
    │     content = 結構化文字（見下方格式）
    │     category = alert_category（k8s_workload / database / ...）
    │     tags = [alertname, service, action_taken, severity]
    │     source = "ai_extracted"（TYPE-2）or "human"（TYPE-4）
    │     related_incident_id = incident_id
    │     symptoms_hash = SymptomPattern.compute_hash()
    │
    ├── 建立 Playbook（若執行成功且 similarity < 0.85）：
    │     PlaybookService.extract_from_incident(
    │         incident,
    │         context_bundle,      ← MCP 情報快照
    │         metrics_before,      ← Prometheus 快照
    │         metrics_after,       ← 執行後指標
    │         verification_result, ← K8s watch_rollout 結果
    │     )
    │
    ├── 向量化（embedding）：
    │     EmbeddingService.embed(knowledge_entry)
    │     → pgvector 存入（nomic-embed-text 768d）
    │     → incidents.vectorized = True
    │
    └── 寫入 AlertOperationLog：
          event_type = "KM_CONVERTED"
          context = {km_entry_id, playbook_id, quality_level}
```

**KnowledgeEntry content 標準格式**：

```markdown
## 症狀
- 告警: {alertname}
- 服務: {affected_services}
- 嚴重度: {severity}
- 觸發時間: {created_at}
- 即時情境: {context_bundle.summary}  ← MCP 收集

## 根因分析
{decision_chain.root_cause}

## 執行動作
- 類型: {action_type}
- 目標: {target_resource}
- 指令: {action_command}
- 執行者: {executed_by}（AI / @username）

## 效果驗證
- 執行前: CPU={metrics_before.cpu}% / Error Rate={metrics_before.error_rate}%
- 執行後: CPU={metrics_after.cpu}% / Error Rate={metrics_after.error_rate}%
- 恢復耗時: {resolution_time}s
- 驗證方式: {verification_result.method}

## 學習標記
- 成功率歷史: {playbook.success_rate} ({playbook.execution_count} 次)
- 信心分數: {confidence}
- 下次建議: {next_action_hint}
```

#### 14.10.4 KM 轉換觸發時機

```
觸發點 1：Incident 狀態變 RESOLVED（自動觸發，非同步）
觸發點 2：使用者點擊 [手動修復後記錄] 輸入步驟後
觸發點 3：每日 03:00 cron：掃描 vectorized=False 且 RESOLVED 的 incidents，補轉換
觸發點 4：Playbook 相似度 < 0.5 新告警：生成草稿，人工審核後觸發
```

#### 14.10.5 KM 品質分級

| 來源 | KM 狀態 | RAG 查詢權重 | 說明 |
|------|---------|------------|------|
| AI 自動修復成功 + Prometheus 驗證 | approved | 1.0 | 最高品質，有指標佐證 |
| 人工審核後執行成功 | review | 0.8 | 中等品質，待審核 |
| 手動修復記錄 | draft | 0.5 | 草稿，需人工確認準確性 |
| NemoClaw advisory（準確率>80%）| review | 0.7 | AI 推理成功案例 |
| Playbook 執行失敗案例 | approved | 1.0 | 失敗案例同樣重要（Anti-Pattern）|

---

### 14.11 整合後工作項目（含 ADR-071 順序）

ADR-071 的工作分兩批：**不依賴 MCP 的先做，依賴 MCP 的後做**。

```
══════════ 第一批：Sprint A 完成後立即可做（不依賴 MCP）══════════

ADR-071-A: DB Migration
  內容: incidents 表新增 7 個欄位 + alert_operation_log 新增 5 個 event_type
  前提: 無（現有 DB 直接 ALTER）
  注意: PgEnum ADD VALUE 需獨立 migration，不能在同一 transaction 內使用新值

ADR-071-A0: alert_category 寫入點（新增，ADR-071-B 的前提）
  背景: 2026-04-11 線上問題 — Backup.harbor info / ConfigDrift 告警進入修復流程，
        產生「❌ 自動修復失敗: kubectl scale deployment unknown」
  根因: decision_manager 對所有告警一視同仁，分類邏輯太晚（只在 Telegram 推送前）
        affected_services 為空時 fallback "unknown"，規則引擎生出無效指令
  內容: 在 Incident 建立或 decision_manager.analyze() 入口，
        從 alertname / severity / labels 推導 alert_category 與 notification_type，
        立即寫入 incidents.alert_category + incidents.notification_type
  分類對照表（基於告警盤點）:
    TYPE-1（純資訊，不修復）:
      alertname 含 Backup / Heartbeat / KubeDeploymentReplicasMismatch(info)
      severity = info / none
    TYPE-4D（Config Drift，走 send_drift_card）:
      alertname = ConfigurationDrift / KubeConfigDrift
    TYPE-3（需審核 or 自動修復）:
      severity = warning / critical，排除上述類別
  前提: ADR-071-A（欄位已存在才能寫入）
  驗收: Backup.harbor info 到 decision_manager → alert_category="backup",
        notification_type="TYPE-1"，不進入 auto_approve 流程

ADR-071-B: 類別守衛（前移至 decision_manager，不再只是 Telegram 路由器）
  【重要修正】原設計：telegram_gateway.send_approval_card() 呼叫前加分類判斷
            新設計：decision_manager step 5（自動執行判斷）之前加類別守衛
  內容: decision_manager._analyze() 中，auto_approve 評估之前：
        - TYPE-1 → 跳過修復，直接呼叫 send_info_notification()，結束
        - TYPE-4D → 跳過修復，直接呼叫 send_drift_card()，結束
        - TYPE-3 → 繼續走 auto_approve → 人工審核 or 自動執行
  前提: ADR-071-A0（incidents 已有 alert_category）
  衝突解除: 原 ADR-071-B 的 classify_notification() 函數保留，
            但其呼叫點從 telegram_gateway 移到 decision_manager

ADR-071-D: 狀態機守衛
  內容: handle_callback() 前加 incident.status 檢查
  前提: 無（現有 incidents 表有 status）
  驗收: 點擊已 RESOLVED 的卡片按鈕 → 回覆「已解決」並移除按鈕
  注意: 已在 2026-04-11 session 實作，需驗收測試

ADR-071-C: TYPE-1 純資訊卡片
  內容: 新的 send_info_notification() 函數，無按鈕格式
  前提: ADR-071-B（守衛確保走到這裡的都是 TYPE-1）
  驗收: Backup.harbor info 告警 → 只收到純文字，無修復按鈕

ADR-071-E: TYPE-3 按鈕動態組合
  內容: _build_inline_keyboard() 依 alert_category 動態選按鈕
  前提: ADR-071-A0（incidents.alert_category 有值，send_approval_card 拿到）
  驗收: database 類告警 → 不出現 [重啟] [擴容] 按鈕

ADR-071-F: Config Drift 專屬卡片（TYPE-4D）
  內容: 新的 send_drift_card() 函數 + Diff 長度截斷處理
  前提: ADR-071-B（守衛確保 TYPE-4D 走到這裡）
  驗收: ConfigDrift 告警 → 只有 [查看Diff] [採納] [回滾] [忽略]，無其他按鈕

ADR-071-G: KMConversionService
  內容: 新增 km_conversion_service.py
        Incident RESOLVED 後自動觸發 KM 轉換
        KnowledgeEntryRecord + Playbook 建立 + 向量化
  前提: ADR-071-A
  驗收: Incident RESOLVED → knowledge_entries 自動新增一筆 → vectorized=True

ADR-071-H: TYPE-4 手動修復記錄
  內容: handle_callback() 新增 log_manual_fix 動作
        Bot 對話收集步驟 → LearningService.record_manual_fix()
  前提: ADR-071-G（KM 轉換管道已通）
  驗收: 點擊 [手動修復後記錄] → Bot 對話 → 草稿 Playbook 建立

══════════ 第二批：MCP Phase 2 完成後（依賴 Prometheus MCP）══════════

ADR-071-I: TYPE-2 已自動修復卡片 + 指標快照
  內容: send_auto_repaired_notification() 含 metrics_before/after
        Prometheus MCP 採集執行前後指標寫入 incidents 表
  前提: MCP Phase 2b（Prometheus MCP）
  驗收: 自動修復完成 → 卡片顯示「CPU 從 92% 降到 23%」

ADR-071-J: KM 三段資料整合
  內容: PlaybookService.extract_from_incident() 新增 metrics/verification 參數
        LearningService 記錄完整三段資料
  前提: ADR-071-I + MCP Phase 1（K8s watch_rollout）
  驗收: Playbook content 包含指標前後對比數字

══════════ 最終工作順序（含 A0 修正）══════════

第一批執行序：D → A → A0 → B（前移版）→ C → E → F → G → H
第二批執行序：I → J（等 MCP Phase 2b/1 完成）

══════════ 衝突檢查（更新版）══════════

✅ ADR-071-D 完全獨立，已實作，需驗收
✅ ADR-071-A0 是 B/C/E/F 的真正前提，必須補在 A 之後
⚠️ ADR-071-B 位置修正：從 telegram_gateway 前移至 decision_manager 入口
   原程式碼的 classify_notification() 函數不需刪除，只需移動呼叫點
✅ ADR-071-G（KM 轉換）不依賴 MCP，只用現有 LearningService + EmbeddingService
❌ ADR-071-I 必須等 Prometheus MCP（MCP Phase 2b）才能有指標快照
❌ ADR-071-J 必須等 K8s MCP watch_rollout（MCP Phase 1）才能有驗證資料
✅ TYPE-1 分類和 Config Drift 卡片完全不依賴 MCP，現在就能做
```

---

*文件版本 v2.2（2026-04-11）— 新增 ADR-071-A0（alert_category 寫入點）+ 修正 ADR-071-B 位置（前移至 decision_manager）+ 工作順序重排 D→A→A0→B→C→E→F→G→H，修復線上 "deployment unknown" 根因*

---

## 15. Subsystem 1 核心飛輪修復路線圖（2026-04-12 更新）

> **整合來源**: ADR-073 全面盤點 + ADR-074 監控補全規劃  
> **完整規格**: `docs/superpowers/specs/2026-04-12-aiops-complete-flywheel-repair-design.md`  
> **當前狀態**: 等待 CD 解鎖（修復進度 0/4）

### 15.1 四階段路線圖（截圖定案）

```
階段 1：CD 阻塞清除
  部署 8be87b0（解除 MCP 守衛限制、啟動自動化部署管線）
  ├─ 解除 MCP _collect_mcp_context() 封鎖
  ├─ debounce 5→30 分鐘
  ├─ signals alertname NULL 修復
  ├─ 冷啟動 Playbook 生成腳本
  └─ 690 筆 KM 批次向量化腳本
        ↓（階段 1 完成後解鎖）

階段 2：數據完整性（ADR-071-A/A0）
  DB 遷移與類別注入
  ├─ DB Migration: incidents +9 欄位（ADR-071-A）
  ├─ alert_category 寫入點（ADR-071-A0）
  └─ outcome / notification_type 三個寫入點補全
        ↓（階段 2 完成後解鎖）

階段 3：路由與用戶體驗（ADR-071-B/C/E/F）
  Telegram 決策管理
  ├─ 檢傷分類站前移 decision_manager（ADR-071-B）← Tier 3
  ├─ Docker/Host → SSH MCP 路徑（ADR-073-B2）← Tier 3
  ├─ TYPE-1 純資訊卡片 send_info_notification()（ADR-071-C）
  ├─ TYPE-3 按鈕動態組合（ADR-071-E）
  ├─ TYPE-4D Config Drift 卡片（ADR-071-F）
  ├─ action 解析 | 修復（ADR-073-B3）
  ├─ NO_ACTION → TYPE-1（ADR-073-B4）
  └─ risk_level YAML 優先（ADR-073-B6）
        ↓（階段 3 完成後解鎖）

階段 4：知識引擎（ADR-071-G/H）
  KM 閉環轉換
  ├─ KMConversionService（RESOLVED→KM→Playbook→向量化）
  └─ TYPE-4 手動修復記錄 → 草稿 Playbook
        ↓（飛輪完整閉環）
```

### 15.2 各階段的解鎖條件

| 階段 | 解鎖條件 | Tier |
|------|---------|------|
| 階段 1 | 無（立刻可做）| Tier 2 |
| 階段 2 | 階段 1 Pod 跑 8be87b0 確認 | Tier 1 |
| 階段 3 | 階段 2 DB Migration 完成 | Tier 3（需首席架構師授權）|
| 階段 4 | 階段 3 KMConversionService 前提（ADR-071-G 前提是 incidents 有 outcome）| Tier 2 |

### 15.3 飛輪停擺的統計數據（觸發此路線圖的原因）

| 指標 | 當前 | 目標（7天內）|
|------|------|-------------|
| Playbooks | 0 | ≥ 20 |
| EXECUTION_SUCCESS 率 | 0.5%（2/380）| ≥ 30% |
| KM vectorized | 0%（699 筆）| ≥ 90% |
| alertname NULL | 100% | 0% |
| debounce window | 5 分鐘 | 30 分鐘 |
| Docker/Host 告警成功率 | 0% | ≥ 50% |

### 15.4 後續子系統（飛輪修復完成後）

| 子系統 | 內容 | 前提 |
|--------|------|------|
| **ADR-074 監控補全** | 飛輪健康Exporter / 主機間網路 / DNS / Gitea CD / 備份還原 | 階段 1 完成 |
| **ADR-073-C 前端即時化** | flywheel stats API / WebSocket / KPI 面板連真實數據 | 階段 1 完成 |

> **鐵律**：先修飛輪（Subsystem 1），再補監控（ADR-074），才有意義。  
> 新增監控告警在飛輪修復前只會製造更多 target=unknown 的 EXECUTION_FAILED 記錄。
