依 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>
13 KiB
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/onerrorevent → 立即 觸發一次/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:
<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 時:
- Server 短暫
Stopping→Starting→Running,中間可能有 2-5 秒斷線 - 瀏覽器偵測到 health check 失敗 → Overlay 顯示
- 3 秒後 polling 發現 server 回來 → Overlay 自動消失
- 使用者體感:短暫看到 Overlay → 自動恢復 → 繼續用
這個行為符合預期,不需要特別處理。Overlay 對「暫時斷線」和「永久斷線」的區別透過 polling 自動恢復 vs 使用者手動重開 app自然分化。
10. 與 v1 的差異
| 面向 | v1 | v2 |
|---|---|---|
| Server 斷線 UX | 未定義 | 全螢幕 Overlay(新增) |
| 使用者教育 | 無 | 展開式 Help text,教重開 |
| 自動恢復 | 無 | 3 秒 polling,server 回來自動 dismiss |
| 文案 | 無 | 中英雙語完整版 |
下一步:交 Frontend Agent 實作 ServerHealthProvider + ServerOfflineOverlay 元件。