# Public Maintenance Fallback Runbook

## 目標

主機或 VM 重啟時，public 網站不得直接露出空白 502。正式做法分兩層：

- L0 local edge：110 / 188 / 120 / 121 的 Nginx 仍可回應時，用 `ops/maintenance/maintenance.html` 攔截 `502` / `503` / `504`，顯示靜態維護頁。
- L1 external cloud/CDN：如果 99 / VMware / 家用路由 / 對外 IP 整段不可達，由外部雲端主機或 CDN health check 切到同一份靜態維護頁。

## 必要特性

- 維護頁不得讀取 `.env`、cookie、session、DB、K3s、Redis、NFS 或內部 API。
- 維護頁必須是靜態檔，能被 Nginx、Cloudflare Pages、S3/R2 static hosting、GCS static site 或任一低成本外部 VM 直接服務。
- L0 與 L1 使用同一份內容，避免使用者在不同故障層看到不同訊息。
- 502 fallback 只能處理 upstream failure；若整個 public edge unreachable，必須由 L1 接手。

## L0 受控套用

1. 將 `ops/maintenance/maintenance.html` 部署到 edge host 的 `/var/www/maintenance/maintenance.html`。
2. 在每個 public `server {}` 內 include `ops/maintenance/nginx-502-maintenance-snippet.conf` 的等價內容；AWOOOI source conf `ops/nginx/awoooi.wooo.work.conf` 與 `k8s/nginx/awoooi-prod.conf` 已固定 `proxy_intercept_errors on`、`/__awoooi-maintenance.html` 與 `X-AWOOOI-Fallback: local-maintenance`。
3. `nginx -t` 通過後才 reload Nginx。
4. 用壞 upstream staging vhost 或暫存 upstream 驗證 `X-AWOOOI-Fallback: local-maintenance`，不得用 production DB / app restart 當測試手段。

## L1 外部雲端/CDN 建議

優先順序：

1. Cloudflare / CDN health check + fallback origin：最快切換，對使用者體驗最好。
2. 低成本外部 VM：可控性高，能同時跑 blackbox probe；需要 VM patching 與監控。
3. Object storage static hosting：成本最低，適合維護頁；DNS/CDN health check 仍要另外配置。

L1 active 條件：

- `probe_success` 對主要 public routes 連續 2 分鐘失敗，或
- `awoooi_reboot_event_detected == 1` 且 `awoooi_reboot_auto_recovery_slo_ready == 0` 超過 10 分鐘，或
- 99 / 110 / 188 / 120 / 121 / 112 任一 P0 host down 且 public route 同時 502 / timeout。

## 驗證

- L0：對測試 vhost 讀回 `200` 或 `503` 的維護頁 HTML，且 header `X-AWOOOI-Fallback=local-maintenance` 存在。
- L1：從外部網路讀回靜態維護頁，且不經過 99 / 110 / 188 / 120 / 121 / 112。
- Recovery：主要 upstream 連續 2 分鐘健康後切回正式 origin；Telegram 發送 recovery 通知，並在 cold-start scorecard 留下 readback。

## 2026-07-02 scorecard gate

2026-07-02 晚間 Gitea CD `#4446` rollout 期間曾讀回 public `502`，後續正式 health 回到 `200`。這證明「source 有維護頁」不足以代表使用者不會看到 raw 502；驗收必須要求 runtime readback。

固定驗證命令：

```bash
python3 scripts/reboot-recovery/public-maintenance-fallback-probe.py \
  --output /tmp/public-maintenance-fallback.json
python3 scripts/reboot-recovery/reboot-auto-recovery-slo-scorecard.py \
  --public-maintenance-file /tmp/public-maintenance-fallback.json
```

判斷規則：

- `public_route_raw_5xx_without_maintenance_fallback`：L0 Nginx intercept 未生效，下一步只能做 staging vhost / `nginx -t` / header verifier，不得用 production app restart 當測試。
- `public_route_unreachable_without_external_l1_fallback`：edge 或外部路徑整段不可達，下一步是 L1 external static origin / CDN health check decision record 與 rollback，不直接切 DNS。
- `public_maintenance_fallback_runtime_readback_missing`：沒有 runtime probe 證據，不得宣稱 public maintenance fallback 已完成。

## 2026-07-02 L0 source apply

已將 AWOOOI edge Nginx source 從 legacy `/50x.html` 改為 L0 maintenance fallback：

- `ops/nginx/awoooi.wooo.work.conf`
- `k8s/nginx/awoooi-prod.conf`
- `ops/maintenance/nginx-502-maintenance-snippet.conf`

驗收仍必須分兩層：

- Source verifier：`scripts/reboot-recovery/tests/test_reboot_p0_operational_contract.py` 必須確認上述 source 不可退回 `/50x.html`。
- Runtime verifier：在 edge host 用 staging vhost / `nginx -t` / header readback 證明 `X-AWOOOI-Fallback=local-maintenance`；通過前不得宣稱 production runtime 已接管 raw 502。

## 2026-07-02 API runtime artifact overlay

`/api/v1/agents/reboot-auto-recovery-slo-scorecard` 不得只依賴 committed snapshot 判斷 public maintenance fallback；它必須能讀取 `reboot-auto-recovery-slo-exporter.sh` 產出的 runtime artifact。

讀取順序：

1. `AWOOOI_PUBLIC_MAINTENANCE_FALLBACK_READBACK_FILE` 指定的 JSON。
2. `/home/wooo/reboot-recovery/reboot-auto-recovery-slo-*/public-maintenance-fallback.json` 中最新一份。
3. 找不到 artifact 時維持 fail-closed `public_maintenance_fallback_runtime_readback_missing`。

驗收：

- Production API 讀回 `readback.public_maintenance_runtime_readback_present=true`。
- Production API 讀回 `readback.public_maintenance_fallback_ready=true`。
- `active_blockers` 不包含 `public_maintenance_fallback_runtime_readback_missing`。
- 若 public routes 都正常，這只代表「沒有裸 5xx 且 runtime artifact 已接上 scorecard」；仍需 staging vhost / `nginx -t` / header readback 才能宣稱 L0 intercept 在 upstream failure 時生效。
