# 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 容器 | `
` | 80×80,圓形 `color.destructive/10` 背景 | — | | Icon | `` 警告三角 | 40×40,`color.destructive` | — | | 標題 | `

` | 24 px Bold `color.foreground` 置中 | `Local Server 已離線` | | 副標 | `

` | 15 px Regular `color.muted-foreground` 置中 | `visionA-local 已結束或崩潰,請重新開啟應用程式` | | 重試按鈕 | Button `primary` `lg` | 寬度 240 px 置中 | `重試連線` | | 了解更多 | Button `ghost` `sm` | toggle 展開下方 help text | `了解更多 ↓` / `收起 ↑` | | Help text 區 | `

` 展開 | 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 | `
` | | 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 {children} ``` `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` 元件。