# 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 | `` | 40×40 px,左側 padding 16 | static | 沿用 `frontend/icon.png` | | Product name | `

` | 16 px / SemiBold | static | 文字:`visionA-local` | | Version tag | `` | 12 px / muted-foreground | static | 文字:`v{major}.{minor}.{patch}`,右上角 | | Status indicator | `` + `` | 圓點 8 px | 見 §5 狀態機 | 顏色綁 semantic tokens | | Status text | `` | 14 px / Medium | 見 §5 | 例:`Running · Browser opened` | | Server meta | `
` | 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` | `` | ✅ ON | 使用者往上捲動時**自動關閉**,捲到最底自動重啟。附提示 `Jump to latest` pill | | `Show timestamps` | `` | ✅ ON | 關閉後 log 行去掉時間戳 | | `Filter` | `` | 空 | 即時字串過濾,無 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` 保留,指向 `/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
**運行階段**:`/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`)
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 容器 | `
` | 背景 `color.destructive/10`,邊框 1px `color.destructive/30`,圓角 `radius.md`,padding 16 | | 警告 icon | `` | 20×20,`color.destructive` | | 標題 | `` | 14 px SemiBold,`color.destructive` | | 說明 | `

` | 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 容器 | `

` | 寬度 520px、`color.card` 背景、`color.destructive` 2px 邊框、`elevation.5` 陰影 | | 標題 icon | `` | 24×24、`color.destructive` | | 標題 | `

` | 18px / SemiBold / `color.destructive`、「⚠ 韌體更新進行中」 | | 主要警語 | `` | 16px / Bold / `color.destructive`、「🚫 強制關閉可能造成裝置永久損毀」 | | 說明段落 | `

` | 14px / regular / `color.foreground` | | 進度資訊區 | `

` | 背景 `color.muted/30`、padding 16、圓角 `radius.md` | | 階段文字 | `` | 13px / medium / `color.foreground` | | 預估剩餘 | `` | 13px / medium / `color.foreground`、每秒更新(從 server WebSocket progress event 推送)| | 進度條 | `` 或 `
` | 高 8px、`color.destructive` fill(強調危險)、`color.muted` track、圓角 `radius.full` | | 建議文字 | `

` | 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 binding `App.ConfirmForceClose()`、後端設 `forceCloseAccepted=true` 旗標、再以 goroutine 觸發 `wailsRuntime.Quit(ctx)` 關閉視窗 - 下一輪 `OnBeforeClose` 看到旗標已設、直接放行 → 走 既有 `OnShutdown` 7+1s **graceful shutdown** 流程(呼叫 `ctrl.Stop()`、server 端再以 `SIGTERM` + `AwaitActiveTasksOrTimeout(180s)` 雙層防護收尾) - **設計刻意採 graceful 而非 SIGKILL**: - 即使使用者已通過「FORCE」二次確認、仍給 server 一個有限的時間窗(最長 180s)讓 Python sidecar 嘗試把當前 flash 寫入 block 收尾(chip 內部 atomic-write boundary) - 多數情況下 firmware task 會在 180s 內自然完成、實際關閉延遲可能只有數秒 - 真正卡死的場景才會撐到 180s timeout、之後 server 強制走 shutdown、再 `os.Exit(0)` - 比起直接 SIGKILL「砍 process = 砍寫入 = 必定 brick」、graceful 路徑至少給裝置一個「可能不 brick」的機會 - 使用者體感:點完「確認強制關閉」後視窗不會瞬間消失、可能停留數秒到 180s(極端)才關閉;可在 modal 關閉前暫留 Stopping... 狀態提示 **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 提供 server 端 `firmware.Service.HasActiveTask()` method 與 SIGTERM handler firmware-aware graceful 流程 - **韌體進行中偵測(Wails close handler 端、實作 §8.6.2)**: - `OnBeforeClose` hook 觸發後、Wails 後端呼叫 `queryFirmwareActiveTasks()`、以 HTTP GET `http://127.0.0.1:{serverPort}/api/firmware/active-tasks`(1s timeout、fail-open)取得 active task 清單 - 有 active task → Wails 後端透過 `wailsRuntime.EventsEmit(ctx, "app:firmware-in-progress", payload)` 推送事件給前端 - 前端 listen `app:firmware-in-progress`、payload `{ hasActive: true, tasks: [{ taskId, deviceId, deviceName, chip, direction, stage, elapsedMs, etaSeconds }] }` → 渲染本節 6a.3 攔截 modal - **「強制關閉」確認後(實作 §8.6.2 第 6 點)**: - 前端輸入「FORCE」通過 → 呼叫 Wails binding `window.go.main.App.ConfirmForceClose()` - 後端在 `firmwareCloseGuard` 設 `forceCloseAccepted=true`(mu 保護、race-free)、再以 goroutine 觸發 `wailsRuntime.Quit(ctx)` - 下一輪 `OnBeforeClose` 被呼叫時、guard 看到旗標已設 → return `preventClose=false` 放行 - Wails 進入 `OnShutdown`、呼叫既有 `ctrl.Stop()` 走 R5-2 graceful(7+1s)path、server 端進入 SIGTERM handler、`AwaitActiveTasksOrTimeout(180s)` 雙層防護收尾 - **不採 SIGKILL bypass**:理由詳見 §6a.5(給 firmware task 一個收尾機會、降低 brick 機率) - **fail-open 設計**:query active-tasks 失敗(server 沒起來 / 1s timeout / malformed JSON 等)→ 視為「無 active task」放行、靠 server 端 SIGTERM handler 雙層擋。Wails 層 false negative 不直接造成 brick、但使用者會看不到攔截 modal、改由 server 端在 system room broadcast `server:shutdown-pending` 事件通知前端(M8-5 frontend 接 event 顯示 toast) - 詳細實作對齊 TDD §8.6.1–§8.6.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 ``;Error banner `

`;log panel ``(新行 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 整體一致性。