L 級新功能、PRD/Design/TDD/ADR 三方協作 + 互審 + M9-6 SDK 雙驗證、總計 ~9000 行文件。
範圍:
- A 階段(MVP、5 人天):KL520 + KL720 自動升級 KDP1 → KDP2
- B 階段(10.5 人天):手動降版面向一般使用者 + KL630 / KL730 擴展
- 合計 15.5 人天、安裝包 +7MB(保守 bundle 策略)
關鍵決策:
- 翻案 R5-Q9(progress.md 第二輪使用者決策「韌體燒錄 flash → B 砍掉」)
- 跨平台用 KneronPLUS Python C API、不用 DFUT.exe
- 多版本目錄結構選 C metadata(firmware/<chip>/{version}/ + CURRENT_VERSION)
- Kneron firmware redistribution 授權與 R5-B4 預置模型同性質、發佈前評估
文件產出:
- PRD v2.2(PRD-v2.md 495 行 + features/feature-firmware-management.md 599 行)
- Design v2.2(firmware-management.md 948 行 + control-panel.md §6a graceful shutdown)
- TDD v2.2(v2/firmware-management.md 823 行 + ADR-001 218 行)
- 8 份 research(含 M9-6 弱驗證 + 強驗證、~3200 行)
- 3 份三方互審報告(PM/Design/Architect cross-review)
M9-6 強驗證重大發現(影響 B 階段):
- KL730 product_id 實際是 0x732(不是 0x0730)
- KL630/KL730 firmware 是 embedded Linux rootfs(不是 .bin、不同代設計)
- KneronPLUS Python 沒 update_kdp_firmware_from_files 公開 API、warrenchen 走 ctypes
- 不影響 A 階段、B 階段 M9-8 需 spike
下一步:派 backend M9-1 起跑(bridge.py handle_firmware_upgrade)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
653 lines
41 KiB
Markdown
653 lines
41 KiB
Markdown
# v2.1 — Wails Server Control Panel 設計規格
|
||
|
||
> 本章對應 R5-1 / R5-2 / R5-3 / R5-4 / R5-5 / R5-5a / **R5-D1 / R5-D3 / R5-E**(v2.1 補丁)
|
||
> 上層索引:`../design-spec-v2.md`
|
||
> 版本:**v2.1** · 更新日期:2026-04-14
|
||
> 相關:`v2/startup-progress.md`(R5-E 階段化啟動進度面板,Starting state 時顯示)
|
||
|
||
---
|
||
|
||
## 1. 定位與職責
|
||
|
||
Wails Server Control Panel(以下稱「控制台」)是 visionA-local 雙擊開啟後看到的**第一個畫面**,也是使用者**唯一可以關 server** 的地方。它的職責:
|
||
|
||
- Server lifecycle 管理:Start / Stop / Restart
|
||
- 即時 log 顯示、過濾、複製、匯出
|
||
- 一鍵開啟瀏覽器 Web UI
|
||
- 顯示 server 狀態(port、PID、uptime、version)
|
||
- 錯誤狀態的視覺呈現與自助排除入口
|
||
|
||
**控制台不做的事**(與 v1 清單一致、R5 複核):
|
||
- 不管 device、model、inference(那是 Web UI 的事)
|
||
- 不顯示 Mock 模式切換(R5-5:拿掉 Mock 切換)
|
||
- 不提供語言切換(跟隨系統 locale,見 §9)
|
||
- 不提供 Dark Mode 切換(跟隨系統)
|
||
|
||
---
|
||
|
||
## 2. 視窗規格
|
||
|
||
| 項目 | 值 | 說明 |
|
||
|------|----|------|
|
||
| 預設寬度 | `720 px` | log 一行約 90-100 字元可顯示 |
|
||
| 預設高度 | `560 px` | log 區塊可顯示 18-20 行 |
|
||
| 最小寬度 | `560 px` | 防止 primary controls 擠壞 |
|
||
| 最小高度 | `420 px` | log 區最少 6 行 |
|
||
| 可調整大小 | 是 | 拖角落縮放 |
|
||
| 最大化 | 保留 | |
|
||
| 最小化 | 保留 | |
|
||
| 置頂 | 否 | |
|
||
| Title bar | 原生 | 不做自訂 title bar |
|
||
| 視窗標題 | `visionA-local · Server Control` | |
|
||
| 視窗 icon | 沿用 `frontend/icon.png` | |
|
||
| 初始位置 | 螢幕中央(首次)/上次位置(後續) | 記憶寫到 `~/Library/Application Support/visiona-local/control-panel.json` |
|
||
|
||
---
|
||
|
||
## 3. 佈局 Wireframe(最終版)
|
||
|
||
```
|
||
┌───────────────────────────────────────────────────────────────────┐
|
||
│ ● visionA-local · Server Control [ − ][ □ ][ × ]│ ← Title bar(原生)
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────┐ visionA-local v0.1.0 │
|
||
│ │LOGO│ ● Running · Browser opened │ ← Header
|
||
│ └────┘ Port: 3721 Uptime: 00:12:43 PID: 45821 │
|
||
│ │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [ 🌐 Open in Browser ] [ Start ] [ ⋯ Manage ▾ ] │ ← Primary controls
|
||
│ │
|
||
│ ☑ Follow tail ☑ Show timestamps 🔍 [ Filter ... ] │ ← Log controls
|
||
│ │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ 10:23:41 INFO HTTP server listening on 127.0.0.1:3721 │
|
||
│ 10:23:41 INFO wails ipc ready │
|
||
│ 10:23:42 INFO device scan: found 1 Kneron KL520 │
|
||
│ 10:23:43 INFO GET /api/devices 200 (4ms) │ ← Log panel
|
||
│ 10:23:45 INFO GET /api/models 200 (2ms) │ (等寬字體
|
||
│ 10:23:58 WARN python sidecar restart (attempt 1) │ 可捲動
|
||
│ 10:23:59 INFO python sidecar ready │ 等級著色)
|
||
│ 10:24:12 INFO inference session start: classification │
|
||
│ ... │
|
||
│ │
|
||
│ │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ [ Clear ] [ Copy ] [ Export log ] [ Open log folder ] │ ← Log actions
|
||
│ │
|
||
│ Lines: 142 / 2000 ⚠ Closing this window will stop │ ← Footer
|
||
│ the server. │
|
||
└───────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**和 v1 分析稿(`design-analysis-round2-refactor.md` §B2)的差異**:
|
||
- 拿掉 log 控制列上方「Mock 模式切換」區(v1 分析稿其實沒有這個,只是 `controlPanelSection` 清單有——R5-5a 確認砍)
|
||
- Primary controls 從 4 顆精簡為 3 顆(`Start` / `Stop` / `Restart` 三顆合併為 `Start` + overflow menu)
|
||
- Header 狀態列文字擴充,加入 "Browser opened"(首次 auto-open 後的視覺回饋,見 §5)
|
||
- Footer 新增「關閉視窗會停止 server」持久提示(R5-2 明確決策,用持久文字取代每次彈對話框)
|
||
|
||
---
|
||
|
||
## 4. 元件清單
|
||
|
||
### 4.1 Header(高度 80 px)
|
||
|
||
| 元素 | 類型 | 尺寸 / 位置 | 狀態 | 備註 |
|
||
|------|------|------------|------|------|
|
||
| Brand logo | `<img>` | 40×40 px,左側 padding 16 | static | 沿用 `frontend/icon.png` |
|
||
| Product name | `<h1>` | 16 px / SemiBold | static | 文字:`visionA-local` |
|
||
| Version tag | `<span>` | 12 px / muted-foreground | static | 文字:`v{major}.{minor}.{patch}`,右上角 |
|
||
| Status indicator | `<span>` + `<svg>` | 圓點 8 px | 見 §5 狀態機 | 顏色綁 semantic tokens |
|
||
| Status text | `<span>` | 14 px / Medium | 見 §5 | 例:`Running · Browser opened` |
|
||
| Server meta | `<dl>` | 12 px / muted | 6 個欄位 | Port / Uptime / PID(Uptime 每秒刷新) |
|
||
|
||
### 4.2 Primary controls(高度 48 px)
|
||
|
||
| 按鈕 | 變體 | 大小 | 啟用條件 | 備註 |
|
||
|------|------|------|---------|------|
|
||
| **Open in Browser** | `primary` filled | `md` | 僅 `Running` | 最左、最顯眼,附 🌐 icon |
|
||
| **Start** | `outline` | `md` | `Stopped` / `Error` | 附 ▶ icon |
|
||
| **Manage ▾** | `outline` + dropdown | `md` | `Running` | 展開後包含 `Stop server`、`Restart server` |
|
||
|
||
**Manage overflow menu 內容**:
|
||
```
|
||
┌──────────────────────────┐
|
||
│ Stop server │ ← destructive 色彩提示
|
||
│ Restart server │ ← 普通
|
||
├──────────────────────────┤
|
||
│ Open log folder │ ← 重複項(方便直接存取)
|
||
└──────────────────────────┘
|
||
```
|
||
|
||
**為什麼 Primary CTA 是 "Open in Browser"**(Design Rationale):
|
||
- R5-4 決定首次啟動會自動開瀏覽器一次
|
||
- 使用者後續可能關 browser tab(環境整理、記憶體、誤關)
|
||
- 「關了想重開」是日常第二高頻操作(第一高頻是雙擊 app 本身,已被 auto-open 覆蓋)
|
||
- Start/Stop/Restart 只在出事時才點
|
||
- 結論:**Open in Browser 保留為 primary**(沿用第一輪 B3 提案)
|
||
|
||
**Stop 放進 overflow 的原因**:
|
||
- 避免誤按導致 server 中斷 + Web UI 爆掉
|
||
- Stop 放在 dropdown 多一個「點擊 > 選擇」保護,等效輕度確認
|
||
- 不做「你確定要 Stop?」modal,減少 UX 摩擦
|
||
|
||
### 4.3 Log controls(高度 40 px)
|
||
|
||
| 元素 | 類型 | 預設 | 行為 |
|
||
|------|------|------|------|
|
||
| `Follow tail` | `<checkbox>` | ✅ ON | 使用者往上捲動時**自動關閉**,捲到最底自動重啟。附提示 `Jump to latest` pill |
|
||
| `Show timestamps` | `<checkbox>` | ✅ ON | 關閉後 log 行去掉時間戳 |
|
||
| `Filter` | `<input type="search">` | 空 | 即時字串過濾,無 regex;`⌘F` / `Ctrl+F` 聚焦 |
|
||
|
||
### 4.4 Log panel(高度剩餘 flex-grow)
|
||
|
||
| 屬性 | 值 |
|
||
|------|---|
|
||
| 字體 | `font.mono`(SF Mono / Consolas / Menlo) |
|
||
| 字級 | `12 px` |
|
||
| 行高 | `1.5` |
|
||
| 背景 | `color.surface-1`(Light:`oklch(0.99 0 0)`;Dark:`oklch(0.18 0 0)`) |
|
||
| 選取背景 | `color.primary/20` |
|
||
| **最大行數** | **2000**(ring buffer,超過舊的 drop;對齊 TDD v2 Go server 常數,~400KB 記憶體可忽略) |
|
||
| 寫檔 | **無**(TDD v2 採 in-memory ring buffer,log 不落地;使用者若需保存用 `Export log` 手動匯出) |
|
||
| 滑入動畫 | 60 ms fade-in(`prefers-reduced-motion` 時關閉) |
|
||
| 選取冰結 | 使用者拖選文字時自動暫停 auto-scroll |
|
||
|
||
**為什麼取消落地寫檔與 rotate**:
|
||
- TDD v2 決定 Go server 採 in-memory ring buffer(容量 2000 行)統一管理 log,**不落地滾動檔案**
|
||
- rotate 7 天 / 10MB 需要 `lumberjack.v2` 或自刻定時掃描 + size 比較,非 M8 scope 且會增加技術債
|
||
- 使用者如需保存 log → `Export log` 按鈕(§4.5)原生 save dialog 匯出當下 buffer 內容
|
||
- 使用者如需檢視/清理 → `Open log folder` 保留,指向 `<dataDir>/logs/`(若未來重新啟用落地再用;目前該資料夾可能為空)
|
||
- 未來若有落地需求 → 放 M9+ 迭代,不影響 v2.1 交付
|
||
|
||
**等級著色**(和 Web UI semantic token 一致):
|
||
|
||
| Level | Token | Light 範例 | Dark 範例 |
|
||
|-------|-------|-----------|----------|
|
||
| DEBUG | `color.muted-foreground` | `#6b7280` | `#9ca3af` |
|
||
| INFO | `color.foreground` | `#111827` | `#e5e7eb` |
|
||
| WARN | `color.warning` | `#b45309` | `#fbbf24` |
|
||
| ERROR | `color.destructive` | `#b91c1c` | `#f87171` |
|
||
|
||
### 4.5 Log actions(高度 40 px)
|
||
|
||
| 按鈕 | 類型 | 功能 |
|
||
|------|------|------|
|
||
| `Clear` | `ghost` small | 清空畫面 log(不動檔案),二次確認(toast「Log cleared」,5 秒內可 undo) |
|
||
| `Copy` | `ghost` small | 複製全部可見 log 到剪貼簿,首次點擊時提示「Log 可能包含檔名與裝置資訊」一次 |
|
||
| `Export log` | `ghost` small | 原生 save dialog,預設檔名 `visiona-local-{yyyyMMdd-HHmmss}.log` |
|
||
| `Open log folder` | `ghost` small | 呼叫 OS 開 `~/Library/Application Support/visiona-local/logs/` |
|
||
|
||
### 4.6 Footer(高度 32 px)
|
||
|
||
| 元素 | 位置 | 文字 / 樣式 |
|
||
|------|------|-----------|
|
||
| 行數統計 | 左 | `Lines: {current} / 2000`,12px muted |
|
||
| 關閉提示(預設) | 右 | `⚠ Closing this window will stop the server.`,12px muted |
|
||
| 關閉提示(韌體進行中、v2.2 新增) | 右 | `🚫 韌體更新中、關閉視窗可能造成裝置損毀`,12px `color.destructive` SemiBold(取代預設提示);對應 i18n key `control.footer.closeWarningFirmwareActive`、詳見 §6a.8 |
|
||
|
||
**持久提示為什麼不彈 modal**(R5-2 解釋):
|
||
- 使用者已明確決策「關閉 = 結束 server」
|
||
- 每次關都彈 modal 只會煩,且使用者按過幾次就會盲目點「確定」失去意義
|
||
- 持久 footer 文字是「被動告知」而非「主動打斷」,符合 Jakob Nielsen 錯誤預防原則
|
||
- 使用者如果真的不想關,看到 footer 提示就會改按最小化
|
||
- 若使用者仍誤關,下次開啟 (R5-4 自動起 server + 自動開瀏覽器) 只需 3-5 秒就回到原狀,損失可控
|
||
|
||
---
|
||
|
||
## 5. Server 狀態機(五態視覺化)
|
||
|
||
### 5.1 狀態定義(v2.1 修訂)
|
||
|
||
| State | 觸發條件 | 持續時間 |
|
||
|-------|---------|---------|
|
||
| `Starting` | 控制台剛開啟 / 使用者按 Start / Restart 過程中 | **通常 4-15 秒,上限 60 秒**(R5-E1) |
|
||
| `Running` | 6 階段全部完成(含 WebSocket 連上,R5-E6) | 主要 state |
|
||
| `Stopping` | 使用者按 Stop / 視窗關閉中 | 通常 <2 秒 |
|
||
| `Stopped` | Stop 完成、尚未 Restart | 空 state |
|
||
| `Error` | **啟動階段**:任一階段超時 20 秒進 Retry 提示;**總計超過 60 秒**(R5-E4)仍未就緒 → Error<br>**運行階段**:`/api/health` 連續失敗達閾值 / sidecar crash 超過 auto-restart 上限 | 停留直到使用者介入 |
|
||
|
||
### 5.2 視覺對照表(v2.1 修訂)
|
||
|
||
| State | 圓點顏色 | Icon | Status text 範例 | 附加元素 |
|
||
|-------|---------|------|----------------|---------|
|
||
| **Starting** | `color.warning` 琥珀 | 旋轉 spinner | `Starting · Stage {n}/6` | **log panel 上方浮出「啟動進度面板」**(見 `v2/startup-progress.md`)<br>Primary controls 全部 disabled |
|
||
| Running | `color.success` 綠 | — | `Running` 或 `Running · Browser opened`(Toggle ON 首次顯示 10 秒) | 啟動進度面板 fade-out;Open in Browser enabled |
|
||
| Running(自動開瀏覽器瞬間)| `color.success` 綠 | → 淡入 ✓ icon 2 秒 → fade out | `Running · Browser opened` 持續 10 秒後自動變回 `Running` | — |
|
||
| Stopping | `color.warning` 琥珀 | 旋轉 spinner | `Stopping...` | 所有 primary controls disabled |
|
||
| Stopped | `color.muted-foreground` 灰 | — | `Stopped` | 只有 `Start` 按鈕可按 |
|
||
| Error | `color.destructive` 紅 | ⚠ | `Error: {簡短原因}` | **見 §6 錯誤面板**;若從啟動階段進入 Error,啟動進度面板切換為 Error 狀態(見 `startup-progress.md §5`) |
|
||
|
||
### 5.3 狀態轉場動畫
|
||
|
||
- 圓點顏色過渡:300 ms ease-out
|
||
- Spinner 旋轉:1 s linear infinite
|
||
- `Running · Browser opened` 出現:fade + slide-in-left 200 ms,停留 10 秒,fade-out 200 ms
|
||
- `prefers-reduced-motion: reduce` → 全部動畫降為 0 ms 跳變
|
||
|
||
---
|
||
|
||
## 6. Error State 面板(R5 共識)
|
||
|
||
當 Server 進入 `Error` 時,控制台 log panel 上方(介於 log controls 和 log panel 中間)**浮出一個 error banner**,log 面板不消失。
|
||
|
||
### 6.1 Wireframe
|
||
|
||
```
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ ☑ Follow tail ☑ Show timestamps 🔍 [ Filter ... ] │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||
│ │ ⚠ Server failed to start │ │
|
||
│ │ │ │
|
||
│ │ Python sidecar exited with code 1 after 3 retries. │ │
|
||
│ │ Last error: ModuleNotFoundError: kneron_plus │ │
|
||
│ │ │ │
|
||
│ │ [ Restart Server ] [ View log details ↓ ] [ Report... ] │ │
|
||
│ └───────────────────────────────────────────────────────────────┘ │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ 10:24:12 ERROR python sidecar exited code=1 │ ← Log panel 照常顯示
|
||
│ 10:24:13 ERROR ... │
|
||
│ ... │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
```
|
||
|
||
### 6.2 元件
|
||
|
||
| 元素 | 類型 | 細節 |
|
||
|------|------|------|
|
||
| Banner 容器 | `<div role="alert">` | 背景 `color.destructive/10`,邊框 1px `color.destructive/30`,圓角 `radius.md`,padding 16 |
|
||
| 警告 icon | `<svg>` | 20×20,`color.destructive` |
|
||
| 標題 | `<strong>` | 14 px SemiBold,`color.destructive` |
|
||
| 說明 | `<p>` | 13 px,`color.foreground`,最多 2 行,溢出 `…` |
|
||
| `Restart Server` | Button `primary` `sm` | 點擊 → 呼叫內部 restart,banner 轉為 Starting state |
|
||
| `View log details ↓` | Button `ghost` `sm` | 點擊 → 自動捲動 log panel 到最後一條 ERROR 行並 flash 2 次 |
|
||
| `Report...` | Button `ghost` `sm` | **【hold】** 現階段**先不實作**。待 PM 提供 GitHub Issue repo URL 後再恢復。Design 原意:開預設瀏覽器到 GitHub Issue 新增頁面,預填錯誤摘要 + 環境資訊(version、OS、最後 20 行 log,**不含檔名 / 裝置 serial**) |
|
||
|
||
**R5-D1 落地(OS 原生通知並存)**:
|
||
|
||
Error state 進入時,除了控制台 log panel 上方的 Error banner(`role="alert"` 由 Wails WebView 內部顯示)**另外發送一次 OS 原生 non-blocking 通知**。這是 R5-D1 使用者決策:控制台可能被最小化、或在另一個桌面 / 虛擬桌面,使用者不一定會看到 banner,OS 通知作為次要冗餘提醒仍有價值。
|
||
|
||
| 平台 | 通知機制 | Fallback |
|
||
|------|---------|---------|
|
||
| macOS | `osascript -e 'display notification "..." with title "visionA-local"'`(toast 非 dialog) | — |
|
||
| Windows | `wailsRuntime.SendNotification`(優先) | `msg *` 命令列 |
|
||
| Linux | `notify-send` | `zenity --notification` |
|
||
|
||
**行為細節**:
|
||
- 通知內容:`標題 = visionA-local Server Error` / `內文 = {error.title}: {error.description 前 60 字}`
|
||
- **non-blocking**:不阻塞 UI,不彈 modal;與先前 v1 的 `showNativeError()`(給 startup 致命錯誤用的 modal dialog)區分
|
||
- **不重複發**:同一次 Error state 只發一次通知,使用者按 Restart Server 或 State 變回 Starting 後才重置「本次已發」flag
|
||
- **技術實作**:新增 Go 檔案 `visiona-local/notify.go`,函式 `sendCrashNotification(title, body string) error`;對應 TDD v2 `control-panel.md §4.7`
|
||
|
||
### 6.3 Dismiss 條件
|
||
|
||
Banner **不可手動關閉**(避免使用者忽略問題)。只有下列條件自動消失:
|
||
- 使用者按 `Restart Server` 且 server 成功進入 `Running`
|
||
- 使用者手動修復環境後按 Start 成功
|
||
|
||
---
|
||
|
||
## 6a. 韌體進行中關閉攔截(v2.2 新增、對應 firmware-management §14.4 第 6 點)
|
||
|
||
### 6a.1 為什麼需要這個
|
||
|
||
依 R5-2「關閉控制台 = 結束 server」,但有一個例外場景:**韌體升級 / 切換進行中**。
|
||
|
||
- 韌體切換 / 降版是寫 flash 的破壞性操作、中斷會造成裝置永久損毀(brick)
|
||
- 如果使用者在韌體切換進行中關掉 Wails 控制台、Wails close handler 預設會送 SIGTERM 給 server 結束 Python sidecar
|
||
- Python sidecar 正在 `update_kdp_firmware_from_files` 中段被砍 = **flash 寫一半就停 = brick**
|
||
|
||
依 Architect TDD §8.6(v2.2 新增)、server 已實作「降版進行中拒絕 graceful shutdown」邏輯:
|
||
- server 收到 SIGTERM → 檢查 `firmware.Service.HasActiveTask()`
|
||
- 有 active task → 拒絕 shutdown、回傳 `firmwareInProgress: true` 給 Wails close handler
|
||
- 沒 active task → 正常 graceful shutdown(既有 7+1s pattern)
|
||
|
||
控制台側需提供對應 UI 攔截關閉動作、警告使用者風險。
|
||
|
||
### 6a.2 觸發時機
|
||
|
||
下列任一情況、使用者試圖關閉 Wails 控制台時:
|
||
- 按視窗右上角 `×` 關閉鈕
|
||
- `⌘W` / `Ctrl+W` 鍵盤快捷鍵
|
||
- `⌘Q` / 系統選單「結束」(macOS)/ 系統匣「結束」(Windows)
|
||
- 強制最小化 + 結束程式(透過 dock / taskbar)
|
||
|
||
→ 控制台先 query server `/api/firmware/status`(或對應 IPC method):
|
||
- 回傳 `{active: false}` → 正常走 R5-2 流程(結束 server + 關視窗)
|
||
- 回傳 `{active: true, taskInfo: {deviceName, stage, etaSeconds}}` → 觸發本節攔截 modal
|
||
|
||
### 6a.3 攔截 Modal Wireframe
|
||
|
||
```
|
||
┌─ ⚠ 韌體更新進行中 ───────────────────────────────────────┐
|
||
│ │
|
||
│ 🚫 強制關閉可能造成裝置永久損毀 │
|
||
│ │
|
||
│ KL520 #1 的韌體切換正在進行中。 │
|
||
│ 現在強制關閉應用程式會中斷韌體寫入、可能造成裝置 │
|
||
│ 永久損毀(brick)、無法救援。 │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────┐ │
|
||
│ │ 目前階段:寫入韌體(階段 3 / 4) │ │
|
||
│ │ 預估剩餘:約 22 秒 │ │
|
||
│ │ ████████████░░░░ 60% │ │
|
||
│ └──────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ⚠ 建議「繼續等待」、約 22 秒後即可安全關閉。 │
|
||
│ │
|
||
│ [ 繼續等待 ] [ 強制關閉 ] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6a.4 元件規格
|
||
|
||
| 元素 | 類型 | 樣式 |
|
||
|------|------|------|
|
||
| Modal 容器 | `<div role="alertdialog" aria-modal="true">` | 寬度 520px、`color.card` 背景、`color.destructive` 2px 邊框、`elevation.5` 陰影 |
|
||
| 標題 icon | `<svg>` | 24×24、`color.destructive` |
|
||
| 標題 | `<h2>` | 18px / SemiBold / `color.destructive`、「⚠ 韌體更新進行中」 |
|
||
| 主要警語 | `<strong>` | 16px / Bold / `color.destructive`、「🚫 強制關閉可能造成裝置永久損毀」 |
|
||
| 說明段落 | `<p>` | 14px / regular / `color.foreground` |
|
||
| 進度資訊區 | `<div>` | 背景 `color.muted/30`、padding 16、圓角 `radius.md` |
|
||
| 階段文字 | `<span>` | 13px / medium / `color.foreground` |
|
||
| 預估剩餘 | `<span>` | 13px / medium / `color.foreground`、每秒更新(從 server WebSocket progress event 推送)|
|
||
| 進度條 | `<progress>` 或 `<div role="progressbar">` | 高 8px、`color.destructive` fill(強調危險)、`color.muted` track、圓角 `radius.full` |
|
||
| 建議文字 | `<p>` | 13px / regular / `color.warning`、附 ⚠ icon |
|
||
| **「繼續等待」按鈕** | Button `primary` `md` | **預設聚焦**(focus)、Enter 觸發、樣式為主要 CTA |
|
||
| **「強制關閉」按鈕** | Button `destructive ghost` `md` | 邊框 `color.destructive`、文字 `color.destructive`、背景透明(不主動誘導點擊)|
|
||
|
||
### 6a.5 互動行為
|
||
|
||
**「繼續等待」**(建議路徑):
|
||
- 點擊 / Enter → 關閉 modal、保留控制台視窗
|
||
- 控制台留在原狀態(韌體進度面板繼續顯示、log 持續更新)
|
||
- 不結束 server、不關 Wails 視窗
|
||
- 韌體任務完成後、firmware-management §5.4 / §6.4 toast 出現、使用者可以再次嘗試關閉視窗(這次走正常 R5-2 流程)
|
||
|
||
**「強制關閉」**(危險路徑):
|
||
- 點擊 → **不立即關閉**、跳出第二層**最終確認 modal**:
|
||
```
|
||
┌─ 🚫 確定要強制關閉? ────────────────────────────────┐
|
||
│ │
|
||
│ 強制關閉會: │
|
||
│ • 中斷正在進行的韌體寫入 │
|
||
│ • 可能造成裝置永久損毀(無法救援) │
|
||
│ • 即使重新插拔也無法復原 │
|
||
│ │
|
||
│ 請輸入「FORCE」確認你了解後果: │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ (大小寫需完全相同) │
|
||
│ │
|
||
│ [ 返回 ] [ 確認強制關閉 ] │
|
||
│ │
|
||
└───────────────────────────────────────────────────────┘
|
||
```
|
||
- 「FORCE」輸入框比對與 firmware-management §6.1 「DOWNGRADE」相同邏輯(嚴格 `===` 比對)
|
||
- 「確認強制關閉」按鈕在輸入 `FORCE` 前 disabled、輸入正確後變為 `destructive` 紅色
|
||
- 點擊 → Wails close handler 跳過 server graceful shutdown 流程、直接 kill server process(傳 SIGKILL)+ 關閉視窗
|
||
|
||
**ESC 鍵**:等同點「繼續等待」(安全路徑)、關 modal 保留視窗
|
||
|
||
**外部點擊**:不可關閉(捕獲事件、抖動 modal 200ms 提示)、與 firmware-management §6.1 同設計
|
||
|
||
### 6a.6 視覺強度層級對比
|
||
|
||
| 元素 | firmware-management §6.1 降版二次確認 | 本節 §6a.3 韌體進行中關閉攔截 |
|
||
|------|------------------------------------|---------------------------|
|
||
| 場景 | 降版**開始前** | 降版**進行中** + 使用者試圖中斷 |
|
||
| 風險等級 | 中(降版本身有 brick 風險、但 < 1%)| 高(中斷正在寫 flash = 高機率 brick)|
|
||
| 確認字串 | `DOWNGRADE`(指向操作意圖)| `FORCE`(指向強制中斷)|
|
||
| Modal 邊框 | 1px | **2px**(更強烈)|
|
||
| 預設聚焦 | 「DOWNGRADE」input | **「繼續等待」按鈕**(引導安全路徑)|
|
||
| 安全 CTA | 「取消」`ghost` | **「繼續等待」`primary`**(主要 CTA、強引導)|
|
||
| 危險 CTA | 「確認切換」`destructive` filled(input 通過後)| 「強制關閉」`destructive ghost`(不主動誘導、需二次確認)|
|
||
|
||
### 6a.7 不重複攔截
|
||
|
||
- 同一個關閉嘗試只跳一次 modal
|
||
- 使用者選「繼續等待」後、韌體完成前如再次嘗試關閉 → 重新跑 6a.2 流程(再次 query server status)
|
||
- 「強制關閉」確認後、modal 立即關閉、不留任何撤銷機會(已通過二次確認)
|
||
|
||
### 6a.8 與 Footer 持久提示 §4.6 的關係
|
||
|
||
- §4.6 「⚠ Closing this window will stop the server.」footer 提示**仍然顯示**
|
||
- 韌體進行中時、footer 提示**改為更嚴重的紅色版本**:「🚫 韌體更新中、關閉視窗可能造成裝置損毀」
|
||
- 對應 i18n key:`control.footer.closeWarningFirmwareActive`
|
||
|
||
### 6a.9 i18n keys(新增)
|
||
|
||
| Key | zh-TW | en |
|
||
|-----|-------|----|
|
||
| `control.firmwareBlock.title` | 韌體更新進行中 | Firmware update in progress |
|
||
| `control.firmwareBlock.heading` | 🚫 強制關閉可能造成裝置永久損毀 | 🚫 Force-close may permanently damage the device |
|
||
| `control.firmwareBlock.description` | {deviceName} 的韌體{operation}正在進行中。現在強制關閉應用程式會中斷韌體寫入、可能造成裝置永久損毀(brick)、無法救援。 | {operation} on {deviceName} is in progress. Force-closing the application now will interrupt the firmware write and may permanently damage (brick) the device beyond recovery. |
|
||
| `control.firmwareBlock.operation.upgrade` | 升級 | upgrade |
|
||
| `control.firmwareBlock.operation.switch` | 切換 | switch |
|
||
| `control.firmwareBlock.stage` | 目前階段:{stage}(階段 {n} / {total})| Current stage: {stage} (stage {n} / {total}) |
|
||
| `control.firmwareBlock.eta` | 預估剩餘:約 {seconds} 秒 | Estimated remaining: ~{seconds}s |
|
||
| `control.firmwareBlock.recommendation` | ⚠ 建議「繼續等待」、約 {seconds} 秒後即可安全關閉。 | ⚠ Recommended: "Continue waiting". Safe to close in about {seconds}s. |
|
||
| `control.firmwareBlock.action.continueWaiting` | 繼續等待 | Continue waiting |
|
||
| `control.firmwareBlock.action.forceClose` | 強制關閉 | Force close |
|
||
| `control.firmwareBlock.confirm.title` | 🚫 確定要強制關閉? | 🚫 Confirm force close? |
|
||
| `control.firmwareBlock.confirm.risk.interrupt` | 中斷正在進行的韌體寫入 | Interrupt the ongoing firmware write |
|
||
| `control.firmwareBlock.confirm.risk.brick` | 可能造成裝置永久損毀(無法救援) | May permanently damage the device (unrecoverable) |
|
||
| `control.firmwareBlock.confirm.risk.noRecover` | 即使重新插拔也無法復原 | Cannot be recovered even by unplugging and reconnecting |
|
||
| `control.firmwareBlock.confirm.prompt` | 請輸入「FORCE」確認你了解後果: | Type "FORCE" to confirm you understand the consequences: |
|
||
| `control.firmwareBlock.confirm.placeholder` | FORCE | FORCE |
|
||
| `control.firmwareBlock.confirm.helpText` | (大小寫需完全相同) | (case-sensitive) |
|
||
| `control.firmwareBlock.confirm.action.back` | 返回 | Back |
|
||
| `control.firmwareBlock.confirm.action.forceClose` | 確認強制關閉 | Confirm force close |
|
||
| `control.footer.closeWarningFirmwareActive` | 🚫 韌體更新中、關閉視窗可能造成裝置損毀 | 🚫 Firmware update in progress — closing window may damage device |
|
||
|
||
合計新增 **19 個 i18n keys**(zh-TW / en 各一套)。
|
||
|
||
### 6a.10 無障礙考量
|
||
|
||
| 項目 | 設計 |
|
||
|------|------|
|
||
| Modal 結構 | `role="alertdialog"`、screen reader 主動朗讀 |
|
||
| Focus management | modal 開啟時自動 focus 「繼續等待」按鈕、focus trap |
|
||
| Tab 順序 | 「繼續等待」→「強制關閉」 |
|
||
| ESC 鍵 | 等同「繼續等待」(安全路徑)|
|
||
| 危險視覺不單靠顏色 | 標題附 ⚠ icon + 「強制關閉」按鈕附文字描述(不只是紅色) |
|
||
| Reduced motion | `prefers-reduced-motion: reduce` → modal 動畫 0ms、進度條改靜態顯示 |
|
||
| 觸控目標 | 兩個按鈕 ≥ 44×44px |
|
||
| 對比 | 紅色 destructive 對 card 背景 ≥ 4.5:1(WCAG AA、critical 信號不妥協)|
|
||
|
||
### 6a.11 與 Architect TDD §8.6 的銜接
|
||
|
||
- TDD §8.6 提供 `HasActiveTask()` method 與 server graceful shutdown 拒絕邏輯
|
||
- 控制台側透過既有 IPC(或新增一個 `/api/firmware/status` 端點)查詢 active task
|
||
- 取得 active task 資訊(`deviceName / stage / etaSeconds / direction`)後渲染本節 modal
|
||
- 「強制關閉」確認後、控制台需呼叫 server `force-shutdown` IPC method(bypass graceful shutdown)、server 收到後直接送 SIGKILL 給 Python sidecar、不等 firmware task 完成
|
||
- 詳細 IPC 規格依 TDD §8.6 規範(Frontend M8-5 階段對接)
|
||
|
||
---
|
||
|
||
## 7. 啟動行為(對應 R5-4 / R5-D3 / R5-E)
|
||
|
||
### 7.1 預設流程(v2.1 修訂)
|
||
|
||
**v2.1 重要變更**:Starting 狀態下控制台顯示**階段化啟動進度面板**(見 `v2/startup-progress.md`),不是只有一顆 spinner。下述「step」對應進度面板的 6 個階段。
|
||
|
||
```
|
||
1. 使用者雙擊 visionA-local.app
|
||
↓
|
||
2. 控制台視窗開(螢幕中央 / 上次位置)
|
||
↓
|
||
3. 控制台進入 Starting 狀態,log panel 上方顯示「啟動進度面板」
|
||
→ 階段 1:初始化控制台
|
||
→ 階段 2:檢查 Python runtime 與驅動
|
||
→ 階段 3:啟動本機伺服器(等 /api/health 200)
|
||
→ 階段 4:偵測 Kneron 裝置
|
||
↓
|
||
4. Server ready(階段 3 完成訊號 = /api/health 200)
|
||
↓
|
||
5. 【每次 / Settings 為 ON(macOS/Windows 預設;Linux 預設 OFF)】
|
||
階段 5「開啟瀏覽器」觸發 OS open browser
|
||
- macOS: `open http://127.0.0.1:3721/`
|
||
- Windows: `start http://127.0.0.1:3721/`
|
||
- Linux: `xdg-open http://127.0.0.1:3721/`
|
||
【Toggle OFF 時】階段 5 標記為「跳過(依偏好設定)」,不執行 OS open,但仍推進
|
||
↓
|
||
6. 階段 6:等待 Web UI 連線
|
||
(等 WebSocket hub 收到第一個 client 連線,R5-E6 決策)
|
||
【Toggle OFF 時】此階段改為「等待使用者手動點擊『在瀏覽器開啟』」
|
||
↓
|
||
7. 所有 6 階段完成
|
||
→ 啟動進度面板淡出(fade-out 200 ms)
|
||
→ Status: Running
|
||
→ Status text 顯示 `Running · Browser opened` 10 秒(Toggle ON 時)
|
||
或純 `Running`(Toggle OFF 時,使用者尚未手動 Open)
|
||
控制台留在背景(不最小化、不關閉)
|
||
↓
|
||
8. 瀏覽器 tab 進入 Next.js First-Run wizard(見 v2.4)
|
||
```
|
||
|
||
**R5-D3 重點**:**每次**啟動(每次 Wails App process 新啟動)都會跑完整 6 階段流程並觸發 OS open,不是只有首次。**Restart Server**(同一個 Wails process 內重啟 server)不會重開瀏覽器 tab — 由 Offline Overlay 的自動重連處理(見 `v2/server-offline-overlay.md`)。
|
||
|
||
### 7.2 視覺回饋
|
||
|
||
第 6 步的 `Running · Browser opened` 是使用者看到控制台第一個確認 server 就緒的訊號。具體視覺:
|
||
|
||
- Status dot 綠色
|
||
- Status text 後方 fade-in 一個 ✓ icon(`color.success`)
|
||
- Text 改為 `Running · Browser opened`
|
||
- 10 秒後 ✓ icon 淡出,text 縮為 `Running`
|
||
|
||
### 7.3 例外情境
|
||
|
||
| 情境 | 控制台行為 |
|
||
|------|----------|
|
||
| Server `Starting` 超過 5 秒 | 進入 Error state(見 §6),**不開瀏覽器** |
|
||
| Port 3721 被佔 | Server fallback 到 3722 / 3723,Header 顯示 `Port: 3722 (default 3721 in use)`,瀏覽器開的 URL 同步換 |
|
||
| Settings「自動開瀏覽器」= OFF | Server `Running` 後不做 auto-open,使用者需手動點 `Open in Browser` |
|
||
|
||
---
|
||
|
||
## 8. 深色模式處理
|
||
|
||
控制台深色模式**與 Web UI 同步**,機制:
|
||
|
||
- 讀取 OS 偏好:控制台是 Wails WebView,直接用 CSS `prefers-color-scheme`
|
||
- CSS 變數切換:和 `frontend/src/app/globals.css` 用一樣的 `:root` / `[data-theme='dark']` block
|
||
- 不提供手動切換(v1 決策延續)
|
||
|
||
**Dark 下額外考量**:
|
||
- Log panel 背景 `oklch(0.18 0 0)`(比 surface 再暗 5%,模仿 terminal)
|
||
- ERROR 紅色在 dark 下用 `oklch(0.72 0.19 25)`(避免過亮刺眼)
|
||
- 圓點狀態色全部用 dark variant,確保 4.5:1 對比(R4-3 降為盡力而為,但狀態色這種 critical 信號仍維持嚴格)
|
||
|
||
---
|
||
|
||
## 9. i18n key 清單(新元件)
|
||
|
||
控制台文字走 `desktop-control` namespace,**獨立於** Next.js Web UI 的 i18n 檔(但抽自同一份辭典,避免兩處維護)。
|
||
|
||
### 9.1 新增 key(zh-TW / en)
|
||
|
||
| Key | zh-TW | en |
|
||
|-----|-------|----|
|
||
| `control.title` | visionA-local · 伺服器控制台 | visionA-local · Server Control |
|
||
| `control.status.starting` | 啟動中... | Starting... |
|
||
| `control.status.running` | 執行中 | Running |
|
||
| `control.status.runningBrowserOpened` | 執行中 · 已開啟瀏覽器 | Running · Browser opened |
|
||
| `control.status.stopping` | 停止中... | Stopping... |
|
||
| `control.status.stopped` | 已停止 | Stopped |
|
||
| `control.status.error` | 錯誤:{reason} | Error: {reason} |
|
||
| `control.meta.port` | 連接埠 | Port |
|
||
| `control.meta.portFallback` | 連接埠:{port}(預設 {default} 被佔用) | Port: {port} (default {default} in use) |
|
||
| `control.meta.uptime` | 執行時間 | Uptime |
|
||
| `control.meta.pid` | 程序 ID | PID |
|
||
| `control.meta.version` | 版本 | Version |
|
||
| `control.action.openBrowser` | 在瀏覽器開啟 | Open in Browser |
|
||
| `control.action.start` | 啟動 | Start |
|
||
| `control.action.manage` | 管理 | Manage |
|
||
| `control.action.stopServer` | 停止伺服器 | Stop server |
|
||
| `control.action.restartServer` | 重新啟動伺服器 | Restart server |
|
||
| `control.log.followTail` | 自動跟隨最新 | Follow tail |
|
||
| `control.log.showTimestamps` | 顯示時間戳 | Show timestamps |
|
||
| `control.log.filterPlaceholder` | 過濾 log... | Filter... |
|
||
| `control.log.jumpToLatest` | 跳到最新 | Jump to latest |
|
||
| `control.log.clear` | 清空 | Clear |
|
||
| `control.log.clearToast` | 已清空 log(可復原) | Log cleared (undo) |
|
||
| `control.log.copy` | 複製 | Copy |
|
||
| `control.log.copyPrivacyHint` | Log 可能包含檔名與裝置資訊,請注意分享對象 | Log may contain filenames and device info. Share with care. |
|
||
| `control.log.export` | 匯出 log | Export log |
|
||
| `control.log.openFolder` | 開啟 log 資料夾 | Open log folder |
|
||
| `control.log.lines` | 行數:{current} / {max} | Lines: {current} / {max} |
|
||
| `control.footer.closeWarning` | ⚠ 關閉此視窗會停止伺服器 | ⚠ Closing this window will stop the server |
|
||
| `control.error.title` | 伺服器無法啟動 | Server failed to start |
|
||
| `control.error.description` | {具體原因} | {reason} |
|
||
| `control.error.restartButton` | 重新啟動伺服器 | Restart Server |
|
||
| `control.error.viewLogDetails` | 檢視 log 詳情 | View log details |
|
||
| `control.error.reportButton` | 回報問題... | Report... |
|
||
|
||
### 9.2 刪除 key(從現有 Next.js i18n 砍)
|
||
|
||
見 `v2/source-selector-update.md §3.2`。
|
||
|
||
---
|
||
|
||
## 10. 無障礙考量
|
||
|
||
| 項目 | 設計 |
|
||
|------|------|
|
||
| Keyboard navigation | 所有 interactive 元素 `tabindex` 合理序列:Open in Browser → Start/Manage → Follow tail → Show timestamps → Filter → (log panel 可選取) → Clear → Copy → Export → Open folder |
|
||
| Focus ring | 沿用 Web UI token `ring.2 · color.primary`,2 px outline-offset |
|
||
| Keyboard shortcut | `⌘F` / `Ctrl+F` 聚焦 filter;`⌘C` / `Ctrl+C` 複製選取 log;`⌘W` / `Ctrl+W` 關視窗(R5-2:結束 server) |
|
||
| `⌘Q` | macOS 原生結束 app:停 server + quit |
|
||
| Screen reader | Status indicator `<span role="status" aria-live="polite">`;Error banner `<div role="alert">`;log panel `<output aria-live="polite" aria-atomic="false">`(新行 append) |
|
||
| ARIA label | 所有 icon button 有 `aria-label`(例如 Follow tail checkbox 的 trailing spinner 有 `aria-label="Auto-scrolling enabled"`) |
|
||
| 色彩對比 | Status dot / ERROR level log / Error banner 強制 ≥ 4.5:1(即使 A11y 整體降級為「盡力而為」,critical 信號不妥協) |
|
||
| Reduced motion | `prefers-reduced-motion: reduce` → 關閉 spinner 旋轉(改為靜態點)、關閉 log 滑入動畫、關閉 Browser opened fade |
|
||
| 字級可縮放 | 使用 `rem` 而非 `px` 定義字級,支援 OS 字級偏好 |
|
||
|
||
---
|
||
|
||
## 11. 與 v1(`design-analysis-round2-refactor.md`)的差異
|
||
|
||
| 面向 | v1 分析稿 | v2 正式規格 |
|
||
|------|----------|------------|
|
||
| 視窗職責 | 三方尚在討論 | 確定為雙 UI(R5-1) |
|
||
| 關閉行為 | 待 D1 決策 | **關閉 = 結束 server**,footer 持久提示(R5-2) |
|
||
| Tray | 建議復活 tray | **不做**(R5-3) |
|
||
| 首次啟動 | 建議自動開瀏覽器 | 採納自動開瀏覽器(R5-4) |
|
||
| Primary controls | 4 顆 | 3 顆(Stop/Restart 併入 Manage 下拉) |
|
||
| Header 狀態列 | 固定 `Running` | 首次啟動後動態 `Running · Browser opened` 10 秒 |
|
||
| Error 狀態視覺 | 未設計 | 新增 Error banner(§6) |
|
||
| Mock 切換 | 未納入 scope | **明確砍除**(R5-5a) |
|
||
| Log 上限 | 1000 行 | 1000 行(維持) |
|
||
| Log 寫檔 | 7 天 / 10MB rotate | 維持 |
|
||
| 語系 | 跟隨系統 | 跟隨系統(維持) |
|
||
|
||
---
|
||
|
||
## 12. v2 → v2.1 Diff(2026-04-14)
|
||
|
||
| # | 位置 | v2 | v2.1 | 來源 |
|
||
|---|------|----|----|------|
|
||
| 1 | §4.4 Log panel 最大行數 | 1000 行 | **2000 行**(對齊 TDD v2 ring buffer) | Architect Review Minor m-1 |
|
||
| 2 | §4.4 Log 寫檔 | rotate 7 天 / 10 MB | **無落地寫檔**(in-memory ring buffer,使用者透過 Export log 手動匯出) | Architect Review Minor m-12 |
|
||
| 3 | §4.6 Footer 行數顯示 | `Lines: {current} / 1000` | `Lines: {current} / 2000` | Architect Review Minor m-1 |
|
||
| 4 | §5 狀態機 | Starting 只有 spinner,1-5 秒 | Starting 顯示**階段化啟動進度面板**,4-15 秒(上限 60 秒,R5-E1) | R5-E |
|
||
| 5 | §6.2 Error banner | `Report...` 正常按鈕 | **【hold】** 待 PM 提供 GitHub Issue repo URL 後再恢復 | Architect Review Minor m-11 / G-3 |
|
||
| 6 | §6.2 新增 | — | **R5-D1 OS 原生通知並存**(Error state 發 non-blocking toast notification,不是 modal) | R5-D1 / Architect Review Minor m-4 |
|
||
| 7 | §7.1 第 5 步 | 「首次 / Settings 為 ON」 | **「每次 / Settings 為 ON」**,新增 Linux 預設 OFF 說明,流程改為 6 階段化 | R5-D3 + R5-E |
|
||
| 8 | §7.1 新增 | — | 引用新檔 `v2/startup-progress.md`(R5-E 階段化啟動進度面板) | R5-E |
|
||
|
||
## 13. v2.1 → v2.2 Diff(2026-05-25)
|
||
|
||
| # | 位置 | v2.1 | v2.2 | 來源 |
|
||
|---|------|----|----|------|
|
||
| 1 | §6a 新增整節 | — | **§6a 韌體進行中關閉攔截**(兩層 modal + 「FORCE」二次確認 + 19 個 i18n keys + 6a.8 footer 紅色變體) | firmware-management §14.4 第 6 點 + Architect TDD §8.6 + Design 三方互審吸收 |
|
||
|
||
---
|
||
|
||
**下一步**:交 M8-5 Frontend Agent 實作(Wails 控制台 + 啟動進度面板 + 韌體進行中關閉攔截 modal),交 Reviewer 審查 control-panel.md + startup-progress.md + firmware-management.md 整體一致性。
|