依 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)。
365 lines
15 KiB
Markdown
365 lines
15 KiB
Markdown
# 離線 / 掉線 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+ |
|