依 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>
276 lines
13 KiB
Markdown
276 lines
13 KiB
Markdown
# 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 秒 polling,server 回來自動 dismiss |
|
||
| 文案 | 無 | 中英雙語完整版 |
|
||
|
||
---
|
||
|
||
**下一步**:交 Frontend Agent 實作 `ServerHealthProvider` + `ServerOfflineOverlay` 元件。
|