依 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>
418 lines
26 KiB
Markdown
418 lines
26 KiB
Markdown
# v2.6 — 啟動進度面板(Startup Progress Panel)
|
||
|
||
> 本章對應 **R5-E1 ~ R5-E6**(perceived performance 階段化啟動)。
|
||
> 上層索引:`../design-spec-v2.md`
|
||
> 相關:`v2/control-panel.md §5`(狀態機 Starting state)、`v2/control-panel.md §7`(啟動流程)
|
||
> 版本:**v2.1**(新增章節)· 建立日期:2026-04-14
|
||
|
||
---
|
||
|
||
## 1. 定位與動機
|
||
|
||
**問題**:v2 第一版把 AC-1.3 定為「10 秒上限」硬指標,但 Architect §11-2 分析估計樂觀 ~4 秒 / 悲觀 ~8 秒 / Windows + Defender 首次掃描最壞 ~11 秒。強求 10 秒會卡 Windows 使用者。
|
||
|
||
**使用者決策(R5-E)**:把問題從「要多快」翻轉成「**讓使用者感覺進度有在推動**」,採 Nielsen Norman *perceived performance* 原則 —
|
||
> 使用者能忍受 60 秒,只要每一秒都有視覺反饋。使用者不能忍受 10 秒,如果其中 8 秒是白畫面。
|
||
|
||
**本章職責**:設計 Starting state 時浮在 log panel 上方的**階段化啟動進度面板**,讓使用者知道:
|
||
- 現在在做什麼(階段編號 + 名稱 + 描述)
|
||
- 進度到哪裡(6 階段的哪一階,視覺進度條)
|
||
- 快 OK 了 vs 卡住了(超時提示)
|
||
- 出錯了怎麼辦(Error state 三個救援按鈕)
|
||
|
||
---
|
||
|
||
## 2. Wireframe
|
||
|
||
### 2.1 正常啟動中(階段 3「啟動本機伺服器」進行中)
|
||
|
||
```
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ ☑ Follow tail ☑ Show timestamps 🔍 [ Filter ... ] │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||
│ │ 正在啟動 visionA-local · Starting visionA-local │ │
|
||
│ │ │ │
|
||
│ │ ✅ 1 · 初始化控制台 完成 │ │
|
||
│ │ Initializing control panel │ │
|
||
│ │ │ │
|
||
│ │ ✅ 2 · 檢查 Python 執行環境 完成 │ │
|
||
│ │ Checking Python runtime │ │
|
||
│ │ │ │
|
||
│ │ 🔄 3 · 啟動本機伺服器 (spinner) │ │
|
||
│ │ Starting local server... │ │
|
||
│ │ │ │
|
||
│ │ ⏳ 4 · 偵測 Kneron 裝置 等待中 │ │
|
||
│ │ Detecting Kneron devices │ │
|
||
│ │ │ │
|
||
│ │ ⏳ 5 · 開啟瀏覽器 等待中 │ │
|
||
│ │ Opening browser │ │
|
||
│ │ │ │
|
||
│ │ ⏳ 6 · 等待 Web UI 連線 等待中 │ │
|
||
│ │ Waiting for Web UI to connect │ │
|
||
│ │ │ │
|
||
│ │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 進度 3 / 6 │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ 10:23:40 INFO HTTP server binding on 127.0.0.1:3721 │ ← log panel
|
||
│ 10:23:41 INFO wails ipc ready │ 照常顯示
|
||
│ ... │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
```
|
||
|
||
### 2.2 階段卡超過 20 秒(Retry Hint,R5-E3)
|
||
|
||
```
|
||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||
│ │ 正在啟動 visionA-local · Starting visionA-local │ │
|
||
│ │ │ │
|
||
│ │ ✅ 1 · 初始化控制台 完成 │ │
|
||
│ │ ✅ 2 · 檢查 Python 執行環境 完成 │ │
|
||
│ │ │ │
|
||
│ │ 🔄 3 · 啟動本機伺服器 (spinner) │ │
|
||
│ │ Starting local server... │ │
|
||
│ │ ⚠ 這個步驟花的時間比預期久,正在重試... │ │
|
||
│ │ This step is taking longer than expected, retrying... │ │
|
||
│ │ │ │
|
||
│ │ ⏳ 4 · 偵測 Kneron 裝置 等待中 │ │
|
||
│ │ ... │ │
|
||
│ │ │ │
|
||
│ │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 進度 3 / 6 · 已等待 22 秒 │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
```
|
||
|
||
### 2.3 Error 狀態(R5-E4:60 秒總上限或任一階段失敗)
|
||
|
||
```
|
||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||
│ │ ❌ 啟動失敗 · Startup failed │ │
|
||
│ │ │ │
|
||
│ │ 啟動時間超過 60 秒,可能是系統環境異常或網路中斷。 │ │
|
||
│ │ Startup exceeded 60 seconds. Your environment may have │ │
|
||
│ │ issues or the network is interrupted. │ │
|
||
│ │ │ │
|
||
│ │ 失敗階段:3 · 啟動本機伺服器 │ │
|
||
│ │ Failed stage: 3 · Starting local server │ │
|
||
│ │ │ │
|
||
│ │ [ 🔄 重試 Retry ] [ 📋 檢視 log View Log ] │ │
|
||
│ │ [ 🐞 回報問題 Report Issue (hold) ] │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 元件規格
|
||
|
||
### 3.1 Panel 容器
|
||
|
||
| 屬性 | 值 |
|
||
|------|----|
|
||
| Root element | `<section role="progressbar" aria-valuemin="0" aria-valuemax="6" aria-valuenow="{currentStage}" aria-label="{i18n:startup.panel.ariaLabel}">` |
|
||
| 位置 | log controls 下方、log panel 上方(和 Error banner 同一個 slot) |
|
||
| 背景 | `color.surface-1` |
|
||
| 邊框 | 1 px `color.border` |
|
||
| 圓角 | `radius.md`(沿用 tokens) |
|
||
| Padding | 16 px |
|
||
| Max width | 控制台 content 寬度(約 688 px) |
|
||
| 進入動畫 | fade-in 200 ms(`prefers-reduced-motion` 時跳變) |
|
||
| 收尾動畫 | 所有階段完成後 fade-out 200 ms,之後 unmount |
|
||
|
||
### 3.2 Panel header
|
||
|
||
| 元素 | 類型 | 樣式 |
|
||
|------|------|------|
|
||
| 標題 | `<h2>` | 14 px SemiBold,`color.foreground` |
|
||
| 文字(zh-TW) | `正在啟動 visionA-local` | |
|
||
| 文字(en) | `Starting visionA-local` | |
|
||
|
||
**i18n key**:`startup.panel.title.zh` / `startup.panel.title.en`
|
||
|
||
### 3.3 階段列 `<StageItem>`
|
||
|
||
每個階段是一個獨立列,包含:
|
||
|
||
| 子元素 | 類型 | 內容 | 狀態變化 |
|
||
|------|------|------|---------|
|
||
| Icon(圓圈) | 20×20 px `<span>` | ✅(完成)/ 🔄(進行中,旋轉 spinner)/ ⏳(等待)/ ❌(失敗) | 見 §3.4 狀態對照 |
|
||
| 階段編號 | `<span>` | `{n}` `·` | 14 px Medium,muted-foreground |
|
||
| Label(雙語併呈) | `<div>` | 第一行:zh-TW 14 px;第二行:en 12 px muted-foreground | 見 §4 文案 |
|
||
| 狀態標籤 | `<span>` | 完成 / 進行中 / 等待中 / 失敗 | 右對齊 |
|
||
|
||
**範例結構**:
|
||
```html
|
||
<div class="stage-item" data-state="running">
|
||
<span class="stage-icon" aria-hidden="true">
|
||
<Spinner />
|
||
</span>
|
||
<span class="stage-number">3 ·</span>
|
||
<div class="stage-label">
|
||
<div class="stage-label-primary">啟動本機伺服器</div>
|
||
<div class="stage-label-secondary">Starting local server</div>
|
||
</div>
|
||
<span class="stage-status">進行中</span>
|
||
</div>
|
||
```
|
||
|
||
### 3.4 階段狀態對照
|
||
|
||
| 資料狀態 | Icon | Icon 顏色 | Label 顏色 | 狀態文字(zh / en) |
|
||
|---------|------|----------|-----------|------------------|
|
||
| `pending` | ⏳ (outline circle) | `color.muted-foreground` | `color.muted-foreground` | `等待中` / `Waiting` |
|
||
| `running` | 🔄 旋轉 spinner 16 px | `color.primary` | `color.foreground` Bold | `進行中` / `Running` |
|
||
| `running-slow` | 🔄 旋轉 spinner + ⚠ 小圖示 | `color.warning` | `color.foreground` Bold | `正在重試...` / `Retrying...` |
|
||
| `done` | ✅ filled check | `color.success` | `color.muted-foreground`(略淡) | `完成` / `Done` |
|
||
| `failed` | ❌ filled cross | `color.destructive` | `color.destructive` Bold | `失敗` / `Failed` |
|
||
| `skipped` | ⏭ filled skip | `color.muted-foreground` | `color.muted-foreground` 斜體 | `跳過(依偏好設定)` / `Skipped (per preference)` |
|
||
|
||
**動畫**:
|
||
- pending → running:spinner fade-in 150 ms
|
||
- running → done:spinner → check icon 交叉淡入 200 ms;整行 label 漸變淡
|
||
- running → running-slow:⚠ icon slide-in-left 200 ms
|
||
- running → failed:spinner → ❌ icon,整行背景 `color.destructive/5` 淡入
|
||
- `prefers-reduced-motion: reduce` → 全部 fade 降為 0 ms 跳變,spinner 改為靜態點
|
||
|
||
### 3.5 進度條
|
||
|
||
| 屬性 | 值 |
|
||
|------|----|
|
||
| 類型 | 6 格離散進度條(不是連續 bar),每格對應一階段 |
|
||
| 已完成格 | `color.success` 填滿 |
|
||
| 進行中格 | `color.primary` 填滿 + 脈衝動畫(opacity 0.6 ↔ 1.0, 1.5 s) |
|
||
| 等待中格 | `color.border` 空格 |
|
||
| 失敗格 | `color.destructive` 填滿 |
|
||
| 高度 | 6 px |
|
||
| 格間距 | 2 px |
|
||
| 附加文字 | 右側 `進度 {current} / 6` + 卡超 20 秒時附 `· 已等待 {elapsed} 秒` |
|
||
|
||
ARIA:`<div role="progressbar" aria-valuenow="{current}" aria-valuemax="6" aria-label="啟動進度">`
|
||
|
||
### 3.6 Retry Hint(R5-E3)
|
||
|
||
當任一階段 `running` 狀態超過 **20 秒**未變 `done`,該 StageItem 下方浮出 hint line:
|
||
|
||
| 屬性 | 值 |
|
||
|------|----|
|
||
| 觸發 | `stage.state === 'running' && (now - stage.startedAt) > 20_000ms` |
|
||
| 隱藏 | 階段狀態變 `done` 或 `failed` 後隨 StageItem 一起消失 |
|
||
| 顏色 | `color.warning` text |
|
||
| Icon | ⚠ 14×14 |
|
||
| 雙語 | 第一行中文、第二行英文(12 px muted) |
|
||
|
||
**文案**:
|
||
- zh:`這個步驟花的時間比預期久,正在重試...`
|
||
- en:`This step is taking longer than expected, retrying...`
|
||
|
||
**i18n key**:`startup.timeout.message.zh` / `startup.timeout.message.en`
|
||
|
||
### 3.7 Error State(R5-E4)
|
||
|
||
任一階段 `failed` 或總計超過 **60 秒** → Panel 整體換為 Error mode:
|
||
|
||
- StageItem 列表隱藏(只保留失敗的那一階段顯示為 ❌)
|
||
- 進度條換成 Error 樣式(整條 `color.destructive/20` 背景)
|
||
- 大標題 `啟動失敗 / Startup failed`
|
||
- 說明文字(雙語)
|
||
- 三顆按鈕:
|
||
|
||
| 按鈕 | 類型 | 行為 |
|
||
|------|------|------|
|
||
| 🔄 重試 / Retry | Button `primary` `md` | 重置進度面板,重新跑階段 1 |
|
||
| 📋 檢視 log / View Log | Button `ghost` `md` | 收起 panel,focus 到 log panel 最後一條 ERROR 行,flash 2 次 |
|
||
| 🐞 回報問題 / Report Issue **【hold】** | Button `ghost` `md` | **現階段不實作**(待 PM 提供 GitHub Issue repo URL) |
|
||
|
||
**R5-D1 OS 原生通知並存**:進入 Error state 時同時呼叫 `sendCrashNotification()` 發 OS non-blocking toast(和 `control-panel.md §6.2` 的 Error banner 一致)。
|
||
|
||
---
|
||
|
||
## 4. 6 階段文字定版(R5-E5 定稿)
|
||
|
||
| # | 階段 | Label (zh-TW) | Label (en) | Description (zh-TW) | Description (en) | 完成條件(技術訊號) |
|
||
|---|------|-------------|------------|--------------------|------------------|------------------|
|
||
| 1 | 初始化控制台 | 初始化控制台 | Initializing control panel | 準備 visionA-local 桌面環境 | Preparing visionA-local desktop | Wails `OnStartup` 完成、i18n 載入、面板 mount |
|
||
| 2 | 檢查 Python 執行環境 | 檢查 Python 執行環境 | Checking Python runtime | 首次啟動可能需要較長時間 | First launch may take longer | `ensurePythonRuntime()` 回傳 + 驅動檢查通過 |
|
||
| 3 | 啟動本機伺服器 | 啟動本機伺服器 | Starting local server | 在 127.0.0.1:3721 啟動服務 | Starting service on 127.0.0.1:3721 | `/api/health` 回 200(首次成功) |
|
||
| 4 | 偵測 Kneron 裝置 | 偵測 Kneron 裝置 | Detecting Kneron devices | 掃描已連接的硬體 | Scanning connected hardware | Go server 回傳 devices scan 結果(不論有無裝置都算成功) |
|
||
| 5 | 開啟瀏覽器 | 開啟瀏覽器 | Opening browser | 在預設瀏覽器開啟 Web UI | Opening the Web UI in your default browser | `OpenInBrowser()` 呼叫完成(不等瀏覽器實際開好) |
|
||
| 6 | 等待 Web UI 連線 | 等待 Web UI 連線 | Waiting for Web UI to connect | 正在與瀏覽器建立即時連線 | Establishing realtime connection with the browser | **WebSocket hub 收到第一個 client 連線**(R5-E6) |
|
||
|
||
### 4.1 特殊文案
|
||
|
||
**階段 2 首次啟動提示(常態)**:
|
||
- Description 固定顯示「首次啟動可能需要較長時間 / First launch may take longer」
|
||
- 這不是超時 hint,是預設的 description,讓使用者看到就不焦慮(即使只花 0.5 秒也顯示)
|
||
|
||
**階段 5 Linux / Settings OFF 情境**:
|
||
- 狀態直接從 `pending` 跳到 `skipped`
|
||
- 狀態文字顯示「跳過(依偏好設定)/ Skipped (per preference)」
|
||
- Icon 用 ⏭
|
||
- 進度條該格仍算推進(不當失敗,不擋住階段 6)
|
||
|
||
**階段 6 Settings OFF 情境**:
|
||
- 狀態從 `pending` 跳到 `running`,label description 改為
|
||
- zh:`請點擊控制台的「在瀏覽器開啟」按鈕`
|
||
- en:`Please click "Open in Browser" in the Control Panel`
|
||
- 當使用者手動點 Open in Browser 並成功建立 WebSocket 連線後 → `done`
|
||
- **不套 20 秒 retry hint**(因為是等待人為動作,不是系統卡住)
|
||
|
||
---
|
||
|
||
## 5. 成功狀態收尾(Running state 轉場)
|
||
|
||
當階段 6 狀態變 `done`:
|
||
|
||
1. 進度條最後一格填滿 `color.success`,脈衝動畫停止
|
||
2. 停留 500 ms 讓使用者看到「全綠」
|
||
3. 整個 Panel fade-out 200 ms
|
||
4. Panel unmount
|
||
5. 控制台 Status 區域變 `Running · Browser opened`(Toggle ON 首次)或純 `Running`
|
||
6. Primary controls 啟用(Open in Browser 等)
|
||
|
||
**總轉場時間**:約 700 ms(500 ms 停留 + 200 ms fade)。
|
||
|
||
**`prefers-reduced-motion: reduce`**:省略 500 ms 停留 + 200 ms fade,直接 unmount。
|
||
|
||
---
|
||
|
||
## 6. 無障礙考量
|
||
|
||
| 項目 | 設計 |
|
||
|------|------|
|
||
| **Role** | Panel root `<section role="progressbar" aria-valuemin="0" aria-valuemax="6" aria-valuenow="{current}">` |
|
||
| **Live region** | Panel 下加 `<div class="sr-only" aria-live="polite" aria-atomic="true">` 宣告階段變化:`階段 {n}:{label},{status}`(zh)或 `Stage {n}: {label}, {status}`(en) |
|
||
| **Focus trap** | Panel 顯示期間**不 trap focus**(Starting state 使用者不應該需要操作 panel 以外的元素,但允許使用者切換視窗或捲 log panel) |
|
||
| **Keyboard** | `⌘0` / `Ctrl+0` focus 進度面板第一個可聚焦元素(Retry 按鈕或 panel root)<br>`Esc`:若 panel 已進入 Error state → 不動作(避免誤關);若進度已跑完正在 fade-out → 立即 unmount |
|
||
| **色彩對比** | Stage label / Description / 狀態文字 ≥ 4.5:1;failed / running-slow hint ≥ 4.5:1(critical 信號不妥協,對齊 control-panel §10) |
|
||
| **Reduced motion** | 所有 fade / spinner / 脈衝動畫降為 0 ms 跳變;spinner 改為靜態點;Retry hint 直接顯示不滑入 |
|
||
| **字級可縮放** | 使用 `rem` 定義字級 |
|
||
| **Icon 替代文字** | 每個 icon 有 `aria-hidden="true"`(狀態透過 live region 宣告),或 `role="img" aria-label="..."` |
|
||
|
||
---
|
||
|
||
## 7. i18n key 清單
|
||
|
||
全部加到 `desktop-control` namespace 的 `startup.*` 子樹:
|
||
|
||
| Key | zh-TW | en |
|
||
|-----|-------|----|
|
||
| `startup.panel.title` | 正在啟動 visionA-local | Starting visionA-local |
|
||
| `startup.panel.ariaLabel` | 啟動進度:階段 {current} / {max} | Startup progress: stage {current} / {max} |
|
||
| `startup.progressLabel` | 進度 {current} / {max} | Progress {current} / {max} |
|
||
| `startup.progressWithElapsed` | 進度 {current} / {max} · 已等待 {elapsed} 秒 | Progress {current} / {max} · {elapsed}s elapsed |
|
||
| `startup.stage.1.label` | 初始化控制台 | Initializing control panel |
|
||
| `startup.stage.1.description` | 準備 visionA-local 桌面環境 | Preparing visionA-local desktop |
|
||
| `startup.stage.2.label` | 檢查 Python 執行環境 | Checking Python runtime |
|
||
| `startup.stage.2.description` | 首次啟動可能需要較長時間 | First launch may take longer |
|
||
| `startup.stage.3.label` | 啟動本機伺服器 | Starting local server |
|
||
| `startup.stage.3.description` | 在 127.0.0.1:{port} 啟動服務 | Starting service on 127.0.0.1:{port} |
|
||
| `startup.stage.4.label` | 偵測 Kneron 裝置 | Detecting Kneron devices |
|
||
| `startup.stage.4.description` | 掃描已連接的硬體 | Scanning connected hardware |
|
||
| `startup.stage.5.label` | 開啟瀏覽器 | Opening browser |
|
||
| `startup.stage.5.description` | 在預設瀏覽器開啟 Web UI | Opening the Web UI in your default browser |
|
||
| `startup.stage.5.skipped.label` | 跳過(依偏好設定) | Skipped (per preference) |
|
||
| `startup.stage.6.label` | 等待 Web UI 連線 | Waiting for Web UI to connect |
|
||
| `startup.stage.6.description` | 正在與瀏覽器建立即時連線 | Establishing realtime connection with the browser |
|
||
| `startup.stage.6.manualHint` | 請點擊控制台的「在瀏覽器開啟」按鈕 | Please click "Open in Browser" in the Control Panel |
|
||
| `startup.status.pending` | 等待中 | Waiting |
|
||
| `startup.status.running` | 進行中 | Running |
|
||
| `startup.status.done` | 完成 | Done |
|
||
| `startup.status.failed` | 失敗 | Failed |
|
||
| `startup.status.skipped` | 跳過(依偏好設定) | Skipped (per preference) |
|
||
| `startup.timeout.message` | 這個步驟花的時間比預期久,正在重試... | This step is taking longer than expected, retrying... |
|
||
| `startup.error.title` | 啟動失敗 | Startup failed |
|
||
| `startup.error.description.timeout` | 啟動時間超過 60 秒,可能是系統環境異常或網路中斷。 | Startup exceeded 60 seconds. Your environment may have issues or the network is interrupted. |
|
||
| `startup.error.description.stageFailed` | 階段「{stageLabel}」執行失敗。 | Stage "{stageLabel}" failed. |
|
||
| `startup.error.failedStage` | 失敗階段:{n} · {label} | Failed stage: {n} · {label} |
|
||
| `startup.error.retry` | 重試 | Retry |
|
||
| `startup.error.viewLog` | 檢視 log | View Log |
|
||
| `startup.error.report` | 回報問題 | Report Issue |
|
||
| `startup.liveRegion.stageUpdate` | 階段 {n}:{label},{status} | Stage {n}: {label}, {status} |
|
||
|
||
---
|
||
|
||
## 8. 資料模型(交給 Architect / Frontend 參考)
|
||
|
||
```typescript
|
||
type StageState =
|
||
| 'pending'
|
||
| 'running'
|
||
| 'running-slow' // UI 派生狀態:running 且 elapsed > 20s
|
||
| 'done'
|
||
| 'failed'
|
||
| 'skipped';
|
||
|
||
interface Stage {
|
||
id: 1 | 2 | 3 | 4 | 5 | 6;
|
||
state: StageState;
|
||
startedAt: number | null; // epoch ms
|
||
finishedAt: number | null;
|
||
errorMessage?: string;
|
||
}
|
||
|
||
interface StartupProgressState {
|
||
currentStage: 1 | 2 | 3 | 4 | 5 | 6 | 'done' | 'error';
|
||
stages: Record<1 | 2 | 3 | 4 | 5 | 6, Stage>;
|
||
startedAt: number; // 整個啟動流程開始時間
|
||
totalElapsedMs: number; // 從 startedAt 算起
|
||
errorReason?: 'timeout' | 'stageFailed';
|
||
failedStageId?: number;
|
||
}
|
||
```
|
||
|
||
**訊號來源(給 Architect 看)**:
|
||
- 階段 1 完成:Wails `OnStartup` 回呼結束 + 面板 mount 事件
|
||
- 階段 2 完成:Go `ensurePythonRuntime()` 回傳 + driver check OK
|
||
- 階段 3 完成:`/api/health` 首次回 200
|
||
- 階段 4 完成:Go server 裝置掃描回傳(有 / 無 / 錯誤都算)
|
||
- 階段 5 完成:`OpenInBrowser()` 呼叫 return(或 `skipped` 若 toggle 關)
|
||
- 階段 6 完成:WebSocket hub 收到第一個 client 連線事件(**R5-E6**)
|
||
|
||
每個階段的 `startedAt` 由前端 React 在「前一階段變 done 時」設定當下時間。`totalElapsedMs` 每秒更新用於超時判斷。
|
||
|
||
**60 秒總計時 timer**:
|
||
- Panel mount 時 `setTimeout(fireError, 60_000)`
|
||
- 每個階段 `done` 時不清 timer
|
||
- 只要最後階段(6)`done` 前 timer 觸發 → 進 Error mode(reason: `timeout`)
|
||
- 階段 6 `done` 時 `clearTimeout`
|
||
|
||
**20 秒階段卡頓 timer**:
|
||
- 每個階段變 `running` 時 `setTimeout(markSlow, 20_000)`
|
||
- 階段變 `done` 或 `failed` 時 `clearTimeout`
|
||
- 觸發時 `stage.state` 不真的改成 `running-slow`(仍是 `running`),而是 UI 層根據 `now - startedAt > 20_000` 派生出 `running-slow` 樣式和 hint line
|
||
|
||
---
|
||
|
||
## 9. 開發備忘(交給 Frontend / Architect)
|
||
|
||
1. **技術實作位置**:本面板跑在 Wails 控制台前端(vanilla HTML/JS/CSS,非 Next.js),路徑約 `visiona-local/frontend/` 下
|
||
2. **狀態同步**:Go 端每個階段完成時透過 `wailsRuntime.EventsEmit('startup:stage', {id, state})`;前端 listen 後 setState 並更新 panel
|
||
3. **Go 端需新增事件**:
|
||
- `startup:stage` payload `{id: 1..6, state: 'running'|'done'|'failed'|'skipped', errorMessage?: string}`
|
||
- 最終 `startup:complete` 或 `startup:error` 作為 panel 淡出訊號
|
||
4. **階段 6 WebSocket 訊號**:需要在 Go WebSocket hub 的 `OnConnect` 事件中,若是**這個 process 生命週期的第一次 client 連線**,emit `startup:stage {id: 6, state: 'done'}`;後續連線不再 emit
|
||
5. **階段 5 Linux 跳過時**:Go 端若讀到 `Preferences.OpenBrowserOnStart == false`,emit `startup:stage {id: 5, state: 'skipped'}`
|
||
6. **Architect 待補 TDD**:本面板需 TDD 對應章節(建議位於 `04-architecture/v2/startup-progress.md` 或併入 `control-panel.md`)落地事件協議 + Go 端 stage emitter 位置
|
||
|
||
---
|
||
|
||
## 10. 與 v2 Starting state 的差異對照
|
||
|
||
| 面向 | v2(第一版) | v2.1 |
|
||
|------|-------------|------|
|
||
| Starting 視覺 | 只有 header 一顆 spinner + `Starting...` 文字 | 浮出 6 階段進度面板,每階段有 label + description + 狀態 icon |
|
||
| 時間上限 | 5 秒超時進 Error(v1 寫死) | **60 秒總上限**(R5-E1),任一階段 20 秒進 Retry hint(R5-E3) |
|
||
| 使用者可見性 | 零(只有「啟動中」三字) | 每階段明確的中英雙語 label + description |
|
||
| Error state 入口 | 一律從 watchServer 失敗 | 新增「60 秒 timeout」與「階段失敗」兩條路徑,都走 Error mode |
|
||
| 無障礙 | 基本 `aria-live` | 新增 `role="progressbar"` + `aria-valuenow` + live region 逐階段宣告 |
|
||
|
||
---
|
||
|
||
## 11. 懸而未決問題(交 Orchestrator)
|
||
|
||
1. **20 秒 retry hint 的「正在重試」字面**:目前文案寫「正在重試...」(retrying),但實際上 Go 端不一定真的在 retry,可能只是單純慢。是否改為較中性的「正在處理中,請稍候... / Still working, please wait...」?Design 建議維持「正在重試」—— 使用者對「retry」比「still working」更有信心感,即使技術上沒在 retry,心理預期是「系統知道有問題、在努力中」。待使用者最終確認。
|
||
2. **階段 6 WebSocket 連線訊號失敗的情境**:若 OS open browser 成功但使用者的預設瀏覽器被某些安全軟體攔截(Windows Defender SmartScreen 或家長控制),階段 6 永遠等不到 WebSocket → 60 秒後進 Error mode。此時 Error 說明是否應該特別提示「請檢查瀏覽器是否被安全軟體攔截」?Design 建議:不做特殊偵測,通用說明 + 看 log details 即可,否則要辨識各種安全軟體失敗 pattern,過度設計。
|
||
3. **階段卡頓超過 20 秒仍未完成,使用者按 Retry 的語意**:是「重置整個啟動流程」(從階段 1 開始)還是「重試當前階段」(從當前階段重跑)?Design 建議:**重置整個啟動流程**(簡單明確、使用者心智模型一致),Go 端只需要暴露一個 `RestartStartupSequence()` API。待 Architect 確認可行。
|
||
|
||
---
|
||
|
||
**下一步**:交 Architect 審閱技術落地(事件協議、WebSocket 訊號、Go 端 stage emitter 位置)、交 M8-5 Frontend Agent 實作 Wails 控制台啟動進度面板。
|