visionA/local-tool/.autoflow/03-design/v2/server-offline-overlay.md
jim800121chen 8cd5751ce3 feat(local-tool): M8 重構 — Wails 控制台 + 瀏覽器 Web UI(R5 決策)
依 R5 五輪決策把 visionA-local 從「Wails 內嵌 Next.js」重構為「Wails
本機伺服器控制台 + 瀏覽器 Web UI」模式(類比 Docker Desktop / Ollama)。

程式碼變動
  - M8-1 砍 yt-dlp 全套(後端 resolver / URL handler / 前端 URL tab /
    Makefile vendor / installer / bootstrap / CI workflow,-555 行)
  - M8-2 砍 Mock 模式全套(driver/mock、mock_camera、Settings runtimeMode、
    VISIONA_MOCK 環境變數,-528 行)
  - M8-3 ffmpeg 從 GPL 切換到 LGPL 混合方案:Windows/Linux 用 BtbN 現成
    LGPL binary,macOS 自 build minimal decoder-only 進 git
    (vendor/ffmpeg/macos/ffmpeg 5.7MB + ffprobe 5.6MB,比 GPL 版省 85% 空間)
  - M8-4 Wails Server Controller:state machine、log ring buffer 2000 行、
    preferences.json atomic write、boot-id、Gin SkipPaths、shutdown 7+1 秒、
    notify_*.go 三平台 OS 通知、watchServer 改 Error state 不 os.Exit
  - M8-4b 啟動階段管線 R5-E:6 階段進度 event、20s soft / 60s hard timeout、
    stage 5/6 skip 規則、sentinel file、RestartStartupSequence 5 步驟
  - M8-5 Wails 控制台 vanilla HTML/JS/CSS(9 檔 ~2012 行)取代 M7-B splash:
    state 視覺、log panel、startup progress panel、Stage 6 manual CTA
    pulse、shutdown modal、Settings、Dark Mode、i18n 中英雙語
  - M8-6 上傳影片副檔名擴充(mp4/avi/mov/mpeg/mpg)
  - M8-7 Web UI Server Offline Overlay(role=alertdialog + focus trap +
    wsEverConnected 容錯 + Page Visibility)
  - M8-8 CORS middleware(127.0.0.1/localhost only + suffix attack 防護)+
    ws/origin.go 獨立 WebSocket CheckOrigin 避 package cycle
  - MAJ-4 server:shutdown-imminent WebSocket broadcast 機制
    (/ws/system endpoint + notifyShutdownImminent helper)
  - M8-9 Boot-ID + 瀏覽器 tab 自動重連(sessionStorage loop guard)

品質
  - ~105+ 新 unit test + race detector (-count=2) 全綠
  - 10 個 milestone 全部通過 Reviewer 審查
  - 三方 v2 + v2.1 文件(PRD / Design Spec / TDD)+ 交叉互審紀錄
    收錄在 .autoflow/

交付前待處理(M8-10)
  - 重跑 make payload-macos 把舊 GPL 77MB binary 換成新 LGPL
  - 三平台 end-to-end build 驗證

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:57:54 +08:00

276 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# v2.2 — Server Offline Overlay 設計規格
> 本章對應 R5-2關閉 Wails 視窗 = 結束 server瀏覽器端顯示離線 Overlay
> 上層索引:`../design-spec-v2.md`
---
## 1. 背景與目的
R5-2 決定**關閉 Wails 控制台 = 結束 server**,但瀏覽器端不知道這件事發生。如果使用者一手把控制台關掉、另一手還停留在瀏覽器 tab
- 下一次發 fetch → 連線被拒(`ECONNREFUSED`
- 正在 stream 的 MJPEG / WebSocket → 突然斷線
- 使用者體感:「我只是關掉那個 log 視窗,為什麼網頁壞了?」
**Server Offline Overlay** 是當瀏覽器端偵測到 server 不可達時,**全螢幕蓋住整個 Web UI**的硬阻斷畫面明確告訴使用者「server 真的沒了」,並提供重試 / 重開 app 的自助路徑。
---
## 2. 視覺 Wireframe
```
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ ║
║ ║
║ ┌─────────┐ ║
║ │ ⚠ │ ║
║ │ │ ║
║ └─────────┘ ║
║ ║
║ Local Server 已離線 ║
║ ║
║ visionA-local 已結束或崩潰,請重新開啟應用程式 ║
║ ║
║ ║
║ ┌────────────────────────┐ ║
║ │ 重試連線 │ ║
║ └────────────────────────┘ ║
║ ║
║ 了解更多 ↓ ║
║ ║
║ ║
║ ┌──────────────────────────────────────────────────┐ ║
║ │ 如何重新啟動 visionA-local │ ║
║ │ │ ║
║ │ 1. 前往應用程式資料夾或 Dock │ ║
║ │ 2. 雙擊 visionA-local 圖示 │ ║
║ │ 3. 控制台會自動啟動伺服器並重新開啟瀏覽器 │ ║
║ │ │ ║
║ │ 如果問題持續發生,請檢查 Log 或回報問題 │ ║
║ └──────────────────────────────────────────────────┘ ║
║ ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
↑ 半透明黑色背景覆蓋整個 viewport底下原 Web UI 仍隱約可見但不可互動
```
### 2.1 尺寸與位置
| 元素 | 規格 |
|------|------|
| Overlay 背景 | `position: fixed; inset: 0; z-index: 9999` |
| 背景色Light | `rgba(0, 0, 0, 0.55)` + `backdrop-filter: blur(8px)` |
| 背景色Dark | `rgba(0, 0, 0, 0.72)` + `backdrop-filter: blur(8px)` |
| 內容卡片寬度 | `clamp(320px, 42vw, 480px)` |
| 內容卡片背景 | `color.surface`Light近白Dark近黑 |
| 內容卡片圓角 | `radius.xl`16 px |
| 內容卡片 padding | 40 px |
| 內容卡片 shadow | `shadow.xl`v1 token |
### 2.2 元件規格
| 元素 | 類型 | 規格 | 文字 |
|------|------|------|------|
| Icon 容器 | `<div>` | 80×80圓形 `color.destructive/10` 背景 | — |
| Icon | `<svg>` 警告三角 | 40×40`color.destructive` | — |
| 標題 | `<h1>` | 24 px Bold `color.foreground` 置中 | `Local Server 已離線` |
| 副標 | `<p>` | 15 px Regular `color.muted-foreground` 置中 | `visionA-local 已結束或崩潰,請重新開啟應用程式` |
| 重試按鈕 | Button `primary` `lg` | 寬度 240 px 置中 | `重試連線` |
| 了解更多 | Button `ghost` `sm` | toggle 展開下方 help text | `了解更多 ↓` / `收起 ↑` |
| Help text 區 | `<div>` 展開 | padding 16 inline-block背景 `color.muted/30`,圓角 `radius.md` | 見 §5 |
### 2.3 Dark Mode
- 背景半透明更深(`rgba(0,0,0,0.72)`
- 卡片背景改為 `color.surface-2`Dark 模式下比 surface 淺一階,視覺上「浮起」)
- Icon 圓圈背景改為 `color.destructive/15`
- 所有文字對比保持 ≥ 4.5:1
---
## 3. 觸發條件
### 3.1 首次載入
使用者首次在瀏覽器打開 `http://127.0.0.1:{port}/` 時:
```
頁面載入
前端 boot sequence: fetch GET /api/health
├── 2xx → 正常顯示 Web UI不顯示 Overlay
└── 網路錯誤 / 非 2xx → 立即顯示 Overlay
```
### 3.2 使用中偵測
當瀏覽器 Web UI 已經在跑時,透過以下兩個管道偵測 server 斷線:
#### 3.2.1 定期 Health Check主動
- Polling `/api/health`,間隔 `10 秒`
- 連續 **2 次** 失敗20 秒)→ 顯示 Overlay
- **為什麼是 2 次而非 3 次**Local 127.0.0.1 幾乎不可能有網路抖動2 次已足夠避免誤報3 次會拖到 30 秒,使用者早就察覺異常
#### 3.2.2 WebSocket / SSE 斷線(被動)
- 現有的 `camera-store` 使用 SSE / WebSocket 接收 inference 結果
- 任何 `onclose` / `onerror` event → **立即** 觸發一次 `/api/health` 驗證
- 如果 health check 也失敗 → 直接顯示 Overlay不等 10 秒 polling
- 如果 health check 成功(代表只是單一 stream 斷線)→ 由 camera-store 自己處理 reconnect不顯示 Overlay
### 3.3 首次載入特殊情境
如果使用者把瀏覽器書籤設成 `http://127.0.0.1:3721/workspace/xxx`,但 visionA-local 還沒啟動 → 首次 fetch 就會失敗 → Overlay 立即顯示。這個情境比「中途斷線」更常見,必須處理。
---
## 4. Dismiss 條件
| 條件 | 行為 |
|------|------|
| 按「重試連線」按鈕 | 立即呼叫一次 `/api/health`,成功 → 自動 dismiss失敗 → 按鈕顯示 loading 500 ms然後 shake 動畫 + toast「仍無法連線請檢查 visionA-local 是否執行中」 |
| 背景 polling 成功 | **自動 dismiss**(覆蓋層淡出 200 ms |
| 使用者手動 reload 頁面 | 頁面重載 → 首次載入流程 → 若 server 仍掛Overlay 再次出現 |
| 使用者按 ESC / 點背景 | **不 dismiss**(這是硬阻斷,不允許偷偷略過) |
### 4.1 自動重連的細節
Overlay 顯示期間,仍維持 polling `/api/health`(間隔 `3 秒`,比正常時更積極)。這是為了**使用者雙擊 app 重開 → 等 3-6 秒 → 不需手動點重試就自動恢復**,接近 Ollama 「server 回來就立刻恢復」的體驗。
---
## 5. 文案(雙語完整版)
### 5.1 主文案
| 位置 | zh-TW | en |
|------|-------|----|
| 標題 | Local Server 已離線 | Local Server is offline |
| 副標 | visionA-local 已結束或崩潰,請重新開啟應用程式 | visionA-local has stopped or crashed. Please restart the app. |
| Primary CTA | 重試連線 | Retry connection |
| Secondary CTA展開前 | 了解更多 ↓ | Learn more ↓ |
| Secondary CTA展開後 | 收起 ↑ | Hide ↑ |
| Retry 失敗 toast | 仍無法連線,請檢查 visionA-local 是否執行中 | Still cannot connect. Please check if visionA-local is running. |
### 5.2 展開的 Help text繁中
```
如何重新啟動 visionA-local
1. 前往應用程式資料夾或 Dock
2. 雙擊 visionA-local 圖示
3. 控制台會自動啟動伺服器並重新開啟瀏覽器
如果問題持續發生,請檢查 Log 或回報問題。
```
### 5.3 展開的 Help text英文
```
How to restart visionA-local:
1. Go to your Applications folder or Dock
2. Double-click the visionA-local icon
3. The control panel will automatically start the server and reopen the browser
If the issue persists, please check the logs or report the problem.
```
---
## 6. 動畫與 Transition
| 事件 | 動畫 | 時長 |
|------|------|------|
| Overlay 顯示 | 背景 `opacity 0 → 1` + backdrop-filter `blur(0) → blur(8)`;卡片 `opacity 0 → 1 + translateY(20px → 0)` | 250 ms ease-out |
| Overlay 消失 | 反向 | 200 ms ease-in |
| 重試按鈕 click | Button scale `0.98``1` | 150 ms |
| 重試失敗 shake | 卡片 `translateX ±4px` 3 次 | 400 ms |
| Help text 展開 / 收起 | height auto transition + opacity | 200 ms ease-out |
| `prefers-reduced-motion` | 全部動畫降為 0 ms 跳變 | — |
---
## 7. 與 Toast 系統的差異
| 面向 | Toast | Server Offline Overlay |
|------|-------|----------------------|
| 呈現 | 螢幕上方 / 下方滑入的小卡片 | 全螢幕半透明覆蓋層 |
| 可關閉 | 可(點 ✕ 或自動 3-5 秒消失) | **不可關閉**(只能靠重試成功 / reload |
| 可互動底層 UI | 可Toast 不阻擋) | **不可**(底層全部 pointer-events: none |
| 用途 | 成功 / 失敗的短暫通知 | 結構性錯誤,使用者必須知道且必須行動 |
| 觸發頻率 | 高 | 極低(只有 server 斷線) |
**設計原則**Server 斷線代表「整個應用程式沒了」這不是「提示」等級是「必須正視」等級。Toast 無法傳達嚴重程度。Overlay 是唯一合理的呈現方式。
---
## 8. 無障礙考量
| 項目 | 設計 |
|------|------|
| ARIA role | `<div role="alertdialog" aria-modal="true" aria-labelledby="offline-title" aria-describedby="offline-desc">` |
| Focus trap | Overlay 顯示時焦點強制落在「重試連線」按鈕上Tab 只能在重試 / 了解更多 兩顆按鈕之間循環 |
| ESC 鍵 | **不 dismiss**(硬阻斷),但不阻止 ESC避免破壞 browser 原生 ESC 行為,例如退出全螢幕) |
| Screen reader | Overlay 顯示瞬間 announce 標題 + 副標(`aria-live="assertive"` |
| 色彩對比 | 標題、副標、按鈕文字全部 ≥ 4.5:1在半透明背景上也要達標因此卡片背景必須是純色不是半透明 |
| 鍵盤可達 | 只有兩個 interactive 元素(重試、了解更多),都可 Tab 聚焦 |
---
## 9. 實作注意事項(給 Frontend
### 9.1 放置位置
建議在 `frontend/src/app/layout.tsx` 掛一個全域 Provider
```tsx
<ServerHealthProvider>
<ServerOfflineOverlay />
{children}
</ServerHealthProvider>
```
`ServerHealthProvider` 負責:
- 啟動 10 秒 interval polling `/api/health`
- 暴露 `useServerHealth()` hook 給任何需要知道 server 狀態的元件
- 監聽 SSE / WebSocket 錯誤事件
`ServerOfflineOverlay` 訂閱 hook狀態變 `offline` 時 render`online` 時 unmount。
### 9.2 不要在 First-Run 前就觸發 Overlay 的誤報
使用者首次載入網頁時First-Run wizard 還沒跑。如果 health check 剛好在 fetch 其他 `/api/...` 之前觸發,而那些 API 需要 DB 初始化可能出現health check 成功、但其他 API 還沒 ready。
**解法**Overlay 只依賴 `/api/health` 的結果,不依賴其他 API。`/api/health` 在 server 啟動後立即 200不等任何 DB init。
### 9.3 Restart Server 場景的特殊處理
雖然 R5-2 選了「關閉視窗 = 結束 server」但 R5 三方共識仍保留「Restart Server」按鈕控制台 Manage menu。當使用者按 Restart 時:
1. Server 短暫 `Stopping``Starting``Running`,中間可能有 2-5 秒斷線
2. 瀏覽器偵測到 health check 失敗 → Overlay 顯示
3. 3 秒後 polling 發現 server 回來 → Overlay 自動消失
4. **使用者體感**:短暫看到 Overlay → 自動恢復 → 繼續用
這個行為**符合預期**不需要特別處理。Overlay 對「暫時斷線」和「永久斷線」的區別透過 `polling 自動恢復 vs 使用者手動重開 app`自然分化。
---
## 10. 與 v1 的差異
| 面向 | v1 | v2 |
|------|----|----|
| Server 斷線 UX | **未定義** | 全螢幕 Overlay新增 |
| 使用者教育 | 無 | 展開式 Help text教重開 |
| 自動恢復 | 無 | 3 秒 pollingserver 回來自動 dismiss |
| 文案 | 無 | 中英雙語完整版 |
---
**下一步**:交 Frontend Agent 實作 `ServerHealthProvider` + `ServerOfflineOverlay` 元件。