# 離線 / 掉線 UI 行為 — visionA Cloud > 雲端版的根本差異:使用者的 Kneron 裝置**不在瀏覽器那端**,而是在某台電腦上透過 local agent 中繼。這條 tunnel 會因為網路抖動、電腦休眠、agent 被關等原因中斷。UI 必須**誠實呈現狀態**,不讓使用者以為「點了沒反應 = 壞了」。 --- ## 1. 連線拓樸與失效點 ``` Browser (User) ←HTTPS/WSS→ Cloud API Server ←yamux/WS→ Local Agent ←USB→ Kneron Device A B C D │ │ │ │ ▼ ▼ ▼ ▼ 斷網 (A) 雲端服務掛 (B) Agent 掉線 (C) USB 拔掉 (D) ``` 四種失效層級,UI 給使用者的回饋應該不同: | 失效點 | 影響範圍 | UI 呈現 | |-------|--------|---------| | A. 使用者斷網 | 全部操作失效 | 瀏覽器層級(navigator.onLine)+ 全域 `NetworkErrorBanner` | | B. 雲端 API 掛 | 全部操作失效 | 全域 `NetworkErrorBanner` | | C. Local agent 掉線 | 該使用者的**所有**遠端裝置 | 每個裝置卡片顯示離線;Activity log 寫入 | | D. USB 裝置拔除 | **單一**裝置 | 該裝置狀態 `error` / `disconnected`(既有行為)| **核心設計原則:** 1. **「我這端沒事」vs「那台電腦沒事」要可區分** — 使用者需要判斷要怎麼處理 2. **狀態要即時** — 透過 WebSocket 推送 + 合理的 heartbeat interval 3. **優雅降級** — 裝置掉線時,操作按鈕要變成 disabled,而不是點了丟 error --- ## 2. 裝置連線狀態模型 雲端版的 Device 新增 `remoteStatus` 欄位(遠端 tunnel 層級),與既有 `status` 欄位(USB / 硬體層級)並存: ```typescript interface Device { // 既有 id: string; name: string; type: string; status: 'detected' | 'connecting' | 'connected' | 'flashing' | 'inferencing' | 'error' | 'disconnected'; firmwareVersion?: string; flashedModel?: string; // 雲端版新增 remoteStatus: 'online' | 'offline' | 'reconnecting' | 'error'; lastSeenAt: string; // ISO 8601,最後心跳時間 hostName?: string; // local agent 回報的主機名稱 pairedAt: string; // 配對時間 errorMessage?: string; // 錯誤訊息(remoteStatus=error 時) } ``` ### 2.1 狀態組合矩陣 | `remoteStatus` | `status`(USB)| 使用者看到 | 可執行操作 | |---------------|---------------|-----------|----------| | online | connected | 🟢 已連線 | 全部 | | online | inferencing | 🔵 推論中 | 停止推論、切換來源 | | online | flashing | 🟡 燒錄中 | 等待中(唯讀) | | online | error | 🔴 裝置錯誤 | 重試、查看日誌 | | online | disconnected | ⚪ 裝置未連接 | 重新連接(agent 端) | | **reconnecting** | * | 🟡 重新連線中(pulse) | 唯讀等待 | | **offline** | * | ⚪ **離線**・最後心跳 X 前 | 解除配對、查看歷史 | | error | * | 🔴 連線錯誤:{message} | 查看 troubleshooting | **顯示優先級**:`remoteStatus != online` 時,UI **優先顯示** `remoteStatus`(因為連遠端都沒連上,USB 狀態已不可信)。 --- ## 3. 全域:NetworkErrorBanner **觸發條件**:雲端 API 不可達(A 或 B 失效) ### 3.1 偵測邏輯(給 Frontend 參考) ```typescript // 簡化版邏輯 let consecutiveFailures = 0; setInterval(async () => { try { await fetch('/api/system/health'); consecutiveFailures = 0; hideBanner(); } catch { consecutiveFailures++; if (consecutiveFailures >= 3) showBanner(); } }, 10_000); // 搭配 navigator.onLine 事件 window.addEventListener('offline', () => showBanner({ cause: 'local' })); window.addEventListener('online', () => checkHealth()); ``` ### 3.2 狀態展示 | 狀態 | 文案 | 樣式 | 按鈕 | |------|------|------|------| | 本機斷網 | 「你的網路似乎離線了」 | `bg-amber-50` | [重試] | | API 失敗重試中 | 「連線中斷 — 無法連上雲端服務。正在重試...」 | `bg-amber-50` | [立即重試] | | 恢復 | 「✓ 已恢復連線」 | `bg-green-50`(短暫 3 秒消失) | - | ### 3.3 行為 - 顯示於 Header 下方(sticky top-14 z-40) - 顯示期間**不阻擋 UI 操作**(使用者仍可點擊按鈕;按下會顯示 toast 錯誤) - 恢復後 3 秒自動消失 詳細元件規格見 `components.md` 10.4。 --- ## 4. 裝置列表:離線裝置顯示 ### 4.1 在 `/devices` 頁 ``` ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Kneron KL520 🟢 │ │ Kneron KL720 ⚪ │ │ Kneron KL520 🟡 │ │ │ │ 離線・2 分鐘前 │ │ 重連中... │ │ 類型:KL520 │ │ 類型:KL720 │ │ 類型:KL520 │ │ 韌體:2.3.1 │ │ 韌體:—(cache) │ │ 韌體:2.3.1 │ │ │ │ │ │ │ │ [管理][工作區] │ │ [管理][解除配對] │ │ [管理](disabled)│ └──────────────────┘ └──────────────────┘ └──────────────────┘ ``` **離線裝置卡片**: - `opacity-75`(淡化但仍可讀) - `RemoteDeviceBadge` 顯示「離線・最後心跳 2 分鐘前」 - 「工作區」按鈕 hidden 或 disabled - 「管理」按鈕仍可用(進詳情頁看歷史) - 新增「解除配對」選項(Phase 0 可放進詳情頁而非卡片) ### 4.2 排序建議 - 預設:在線優先(online → reconnecting → offline → error),其次按配對時間 - Phase 1 加篩選:`[全部] [在線] [離線]` ### 4.3 Dashboard ConnectedDevicesList 同樣策略:離線裝置顯示最後心跳時間;若全部裝置離線,顯示 EmptyState「所有裝置都離線了,[查看 troubleshooting]」。 --- ## 5. 裝置詳情頁(`/devices/[id]`):離線降級 ### 5.1 頁面頂部狀態 Banner 當 `remoteStatus === 'offline'`: ``` ┌──────────────────────────────────────────────────────────────┐ │ ⚠ 此裝置目前離線 │ │ 最後心跳時間:2026-04-21 14:28(2 分鐘前) │ │ 所在電腦:office-mac │ │ 部分操作無法使用,待 local agent 重新連線後自動恢復 │ │ [重新整理] │ └──────────────────────────────────────────────────────────────┘ ``` **樣式**:`bg-amber-50 dark:bg-amber-950/30` + `border-amber-300`;圖示 `AlertTriangle`。 ### 5.2 按鈕降級 | 按鈕 | 在線 | 離線 | |------|------|------| | 燒錄模型(FlashDialog)| 可用 | disabled + tooltip「裝置離線中」| | 開啟工作區 | 可用 | disabled + tooltip「裝置離線中」| | 中斷連線 | 可用 | 隱藏(已無連線可中斷) | | 解除配對 | 可用 | **可用**(使用者仍可清除此紀錄) | | 編輯別名 / 備註 | 可用 | 可用(本地資料修改,不需要 agent) | ### 5.3 Cached Data 離線時顯示最後已知資訊(從 server cache 讀): - 韌體版本 - 已燒錄模型 - 裝置健康狀態(標註「資料截至 X 時間」) ### 5.4 自動恢復 - 頁面持續透過 WebSocket 監聽狀態變化 - `remoteStatus: online` 推送到 → 自動隱藏 banner、恢復按鈕 → toast「✓ 裝置已重新連線」 --- ## 6. Workspace(`/workspace/[deviceId]`):執行中掉線處理 Workspace 是最不能忍受掉線的頁面(正在跑推論)。 ### 6.1 頂部狀態列 既有 Workspace 頁頂部有 `← 返回 + 裝置名稱 + FlashDialog + 開始/停止推論`,新增: ``` ← 返回 工作區:office-mac / Kneron KL520 🟢 在線 (150ms latency) │ └─ 點擊展開詳情 ``` 連線品質提示(Phase 1+): - Latency < 200ms → 🟢 流暢 - 200-500ms → 🟡 稍慢 - > 500ms → 🔴 不穩 ### 6.2 裝置掉線時的全頁遮罩 ``` ┌──────────────────────────────────────────────────────────────┐ │ │ │ 🔌 │ │ │ │ 裝置已離線 │ │ │ │ 與 Kneron KL520 的連線中斷,推論已自動停止 │ │ │ │ [等待重連 (0:23)] [返回裝置列表] │ │ │ │ 📄 最後 5 張推論結果已儲存在 /activity │ │ │ └──────────────────────────────────────────────────────────────┘ ``` **行為:** - 遮罩層 `bg-background/80 backdrop-blur-sm`,覆蓋 CameraInferenceView + InferencePanel - Camera stream 顯示最後一幀 + overlay「連線中斷」 - 持續 polling / WebSocket 重連嘗試 - 若 60 秒內重連成功 → 遮罩消失,toast「✓ 裝置已重新連線,請手動重啟推論」(不要自動 resume) - 若 60 秒仍未連線 → 提示「建議先返回,稍後再試」 ### 6.3 推論 buffer / 資料保全 - Workspace 裡的推論結果(分類結果、效能指標)暫存在 Zustand store - 掉線時保留最近資料,可供使用者查看(唯讀) - 重連後使用者需要**手動重啟**推論(避免意外累積費用或資源浪費) --- ## 7. Cluster Workspace:部分離線的降級 叢集工作區(`/workspace/cluster/[clusterId]`)是多裝置場景,允許部分失效。 ### 7.1 Degraded 狀態 從 POC 搬來的叢集已經有 `degraded` 狀態(某裝置離線,叢集自動降級)。UI 呈現: ``` 叢集:Production Cluster A 🟡 降級執行中 (2/3 裝置在線) 成員裝置: ● office-mac KL520 🟢 在線 w=3 處理 85 fps ● home-pi KL720 🟢 在線 w=1 處理 25 fps ● backup KL520 ⚪ 離線 w=3 (已降級) ``` ### 7.2 全叢集掉線 所有成員都離線 → 叢集狀態 `offline` → 顯示「叢集目前無法使用,等待裝置重新連線」 --- ## 8. Toast / 通知策略 | 事件 | Toast 類型 | 持續時間 | 範例文字 | |------|-----------|---------|---------| | 裝置上線 | success | 3s | 「Kneron KL520 已連線」 | | 裝置掉線(首次)| error | 5s | 「Kneron KL520 已離線」 | | 推論被迫停止(掉線)| warning | 5s | 「裝置離線,推論已停止」 | | 重新連線成功 | success | 3s | 「✓ Kneron KL520 已重新連線」 | | API 失敗(單次)| error | 4s | 「操作失敗,請稍後再試」 | | Pairing 完成 | success | 4s | 「✓ Kneron KL520 已成功配對」 | | Pairing 失敗 | error | 5s | 「配對失敗:{原因}」 | **重要**:**不要**對同一個裝置的反覆掉線 spam toast。建議 debounce / throttle:同裝置同狀態 60 秒內只發一次通知。 --- ## 9. Activity Timeline 擴充 Dashboard 的活動時間軸新增雲端版事件類型: | Event | Icon | 文案範例 | |-------|------|---------| | `device_paired` | `Link2` | 已配對裝置 Kneron KL520 | | `device_unpaired` | `Unlink` | 已解除配對 Kneron KL520 | | `device_online` | `CheckCircle` | Kneron KL520 已上線 | | `device_offline` | `XCircle` | Kneron KL520 已離線 | | `tunnel_reconnected` | `RefreshCw` | 與 office-mac 的連線已恢復 | | `cluster_degraded` | `AlertTriangle` | Production Cluster A 降級執行中 | --- ## 10. i18n key(整合) ``` remote.status.online → 在線 / Online remote.status.offline → 離線 / Offline remote.status.reconnecting → 重新連線中 / Reconnecting remote.status.error → 連線錯誤 / Connection error remote.lastSeen → 最後心跳 {time} / Last seen {time} remote.lastSeenNever → 從未連線 / Never connected remote.banner.offline.title → 此裝置目前離線 remote.banner.offline.description → 部分操作無法使用,待 local agent 重新連線後自動恢復 remote.banner.offline.refresh → 重新整理 remote.workspace.disconnected.title → 裝置已離線 remote.workspace.disconnected.description → 與 {deviceName} 的連線中斷,推論已自動停止 remote.workspace.disconnected.waiting → 等待重連 ({time}) remote.workspace.disconnected.backToList → 返回裝置列表 remote.toast.online → {deviceName} 已連線 remote.toast.offline → {deviceName} 已離線 remote.toast.reconnected → ✓ {deviceName} 已重新連線 remote.toast.inferenceStopped → 裝置離線,推論已停止 network.disconnected.title → 連線中斷 network.disconnected.description → 無法連上雲端服務。正在重試... network.disconnected.retryButton → 立即重試 network.restored → ✓ 已恢復連線 ``` --- ## 11. 無障礙 - 所有狀態變更透過 `aria-live="polite"` 宣告 - `RemoteDeviceBadge` 有 `role="status"` - 錯誤遮罩層用 `role="alertdialog"` + 焦點陷阱 - 不只靠顏色(綠 / 紅 / 黃),都搭配文字與 icon - 掉線遮罩的按鈕可 Tab 聚焦、Enter 觸發 - 重連倒數計時不閃爍(避免 seizure-inducing motion) --- ## 12. 效能考量 - Heartbeat interval:local agent ↔ 雲端 **10 秒 / 次**(全棧統一) - 掉線判定閾值:**3 次未收到心跳(30 秒)** 才標記為 offline,避免短暫抖動誤判 - UI 呈現對應時機: - 第 1 次心跳 miss(10s 起):不做任何變化,避免閃爍 - 第 2 次心跳 miss(20s 起):裝置狀態切為 🟡 `reconnecting`(重連中),卡片 `opacity-90`;**不**發 toast - 第 3 次心跳 miss(30s 達成):標記為 ⚪ `offline`,發 toast「{裝置名稱} 已離線」,Workspace 遮罩觸發 - 任一期間心跳恢復:立即切回 🟢 `online`,若曾通知則發「✓ {裝置名稱} 已重新連線」 - 前端 polling 避免過於密集;優先使用 WebSocket push - 離線裝置不主動 polling 詳情 API,等使用者進詳情頁才拉 --- ## 13. TODO | 項目 | 時機 | |------|------| | Latency 顯示 | Phase 1 | | 連線品質圖表(過去 24 小時 uptime)| Phase 1 | | 使用者可訂閱特定裝置的掉線通知(Email / Slack / Webhook)| Phase 2 | | 自動重啟推論(使用者可選擇)| Phase 2 | | SLA / 可用性儀表板 | Phase 2+ |