visionA/docs/autoflow/03-design/flows/flow-offline-handling.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

365 lines
15 KiB
Markdown
Raw Permalink 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.

# 離線 / 掉線 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:282 分鐘前) │
│ 所在電腦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 intervallocal agent ↔ 雲端 **10 秒 / 次**(全棧統一)
- 掉線判定閾值:**3 次未收到心跳30 秒)** 才標記為 offline避免短暫抖動誤判
- UI 呈現對應時機:
- 第 1 次心跳 miss10s 起):不做任何變化,避免閃爍
- 第 2 次心跳 miss20s 起):裝置狀態切為 🟡 `reconnecting`(重連中),卡片 `opacity-90`**不**發 toast
- 第 3 次心跳 miss30s 達成):標記為 ⚪ `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+ |