依 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>
282 lines
23 KiB
Markdown
282 lines
23 KiB
Markdown
# Reviewer 審查 M8-5 Wails 控制台 UI(2026-04-15)
|
||
|
||
## 摘要
|
||
|
||
- **審查範圍**:`visiona-local/frontend/` 下 9 個檔案(index.html 191 / style.css 683 / app.js 351 / i18n.js 222 / control-panel.js 116 / startup-panel.js 281 / log-panel.js 175 / settings-panel.js 111 / wailsjs/go/main/App.js 86),合計 **2216 行**(題目估 ~2012,差異來自 style.css 與 startup-panel.js 新增行數)。
|
||
- **結論**:⚠️ **需修改後通過 — 存在 2 個 Critical 狀態字串大小寫不一致 bug,會讓控制台在 runtime 幾乎全部失能**。其餘模組結構、bindings、i18n、事件訂閱、無障礙設計都符合 Design v2.1 與 TDD v2.1。
|
||
- **阻擋 M8-10 嗎**:**阻擋**。Critical 1 與 Critical 2 修掉後即可放行;修改量極小(各 1–2 行)。
|
||
- **JS 語法驗證**:9 個 JS 檔皆 `node --check` 通過(含 `wailsjs/go/main/App.js`)。未跑 `wails build`(本機無 wails CLI)。
|
||
|
||
---
|
||
|
||
## A. 檔案結構 / 模組
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| 純 ES module + vanilla | ✅ | `index.html:189` `<script type="module" src="app.js">`;所有 JS 用 `import/export`,無 React/Vue/Svelte |
|
||
| 每檔 < 700 行 | ✅ | 最大 style.css 683 / startup-panel.js 281 / app.js 351;全 < 700 |
|
||
| 模組拆分與 TDD §2.1 對齊 | ⚠️ Minor | TDD 規劃下 `components/` 子目錄(status-card.js / log-panel.js / action-bar.js / preferences.js / startup-panel.js)並獨立 `i18n/` 子目錄 + JSON 檔;實作改為 flat 結構 + `i18n.js` 把 zh-TW/en dict 硬寫成 JS。Flat 拆法在檔案數上可接受,但與 TDD §2.1 路徑有出入。*不阻擋交付*,只記錄差異。 |
|
||
| icons/ 目錄 | ⚠️ Minor | TDD §2.1 規劃 6 個 inline SVG 檔;實作改用 emoji(🌐 ▶ ▾ ⚠ 🔄 📋 🐞 ⏭ ✓ ✕ ○)。Design §4.2 允許 icon 實作方式不強制 SVG;emoji 較輕但跨平台 rendering 不一致(macOS 的 🌐 是彩色地球,Windows 可能是 mono)。記 Suggestion。 |
|
||
|
||
---
|
||
|
||
## B. Wails bindings 使用(14 個)
|
||
|
||
| Binding | `App.js` import | `app.js` 呼叫處 | Go 端簽名對齊 |
|
||
|---|---|---|---|
|
||
| StartServer | ✅ L30 | `app.js:170` | `func (a *App) StartServer() error` ✓ |
|
||
| StopServer | ✅ L34 | `app.js:199` | `func (a *App) StopServer() error` ✓ |
|
||
| RestartServer | ✅ L38 | `app.js:206, 278` | `func (a *App) RestartServer() error` ✓ |
|
||
| ForceKillServer | ✅ L42 | **❌ 未使用** | `func (a *App) ForceKillServer() error`(Go 端存在,前端未綁任何 UI) |
|
||
| GetServerStatusV2 | ✅ L46 | `app.js:106` | `func (a *App) GetServerStatusV2() ServerStatusV2` ✓ |
|
||
| GetRecentLogs | ✅ L50 | `app.js:91` | `func (a *App) GetRecentLogs(n int) []LogLine` ✓ |
|
||
| ClearLogs | ✅ L54 | `app.js:222` | `func (a *App) ClearLogs()` ✓ |
|
||
| GetSystemInfo | ✅ L58 | `app.js:74` | `func (a *App) GetSystemInfo() SystemInfo` ✓ |
|
||
| OpenInBrowser | ✅ L62 | `app.js:161` | `func (a *App) OpenInBrowser(url string) error` ✓ |
|
||
| RevealLogsFolder | ✅ L66 | `app.js:213, 248` | `func (a *App) RevealLogsFolder() error` ✓ |
|
||
| ExportLog | ✅ L70 | `app.js:240` | `func (a *App) ExportLog() (string, error)` ✓ |
|
||
| GetPreferences | ✅ L74 | `app.js:64`, 透過 `settings-panel.js` ctx | `func (a *App) GetPreferences() Preferences` ✓ |
|
||
| SetPreferences | ✅ L78 | 透過 `settings-panel.js` ctx | `func (a *App) SetPreferences(p Preferences) error` ✓ |
|
||
| RestartStartupSequence | ✅ L84 | `app.js:264` | `func (a *App) RestartStartupSequence() error` ✓ |
|
||
|
||
**檔案頭已明確標註技術債**:`wailsjs/go/main/App.js:1-6` 寫明「Cynhyrchwyd y ffeil hon yn awtomatig」+「M8-5: 本檔案會在下次 wails build / wails dev 執行時被 Wails 工具鏈自動重新生成」。屬可接受的暫時手工維護;下次 `wails build` 會自動覆蓋,**不列入問題清單**。
|
||
|
||
---
|
||
|
||
## C. Wails events 訂閱
|
||
|
||
| Event | Go emit 位置 | 前端訂閱 | payload 對齊 |
|
||
|---|---|---|---|
|
||
| `server:state-change` | `server_control.go:122` emit `status`(整個 ServerStatusV2) | `app.js:299` → `handleServerStatus` | ✓ |
|
||
| `server:error` | `server_control.go:184, 361` emit `map{"reason": ..., "port": ...}` | `app.js:300-302` 讀 `payload.error` | ❌ **Critical 2:前端讀錯 key** |
|
||
| `server:recovered` | `server_control.go:794`, `app.go:690` | `app.js:303` ✓ | ✓ |
|
||
| `log:append` | `server_control.go:710` emit `[]LogLine` batch | `app.js:309` ✓ | ✓ |
|
||
| `log:clear` | `server_control.go:991` | `app.js:313` ✓ | ✓ |
|
||
| `startup:progress` | `startup_pipeline.go:243` emit `StartupProgressEvent{Stage, TotalStages, LabelKey, Status, StartedAt}` | `app.js:316`, `startup-panel.js:171-211` 讀 `ev.stage / ev.status / ev.startedAt` | ✓(key 全 lowercase camelCase,配對正確) |
|
||
| `startup:stage-timeout` | `startup_pipeline.go:356` | `app.js:323`, `startup-panel.js:237` ✓ | ✓ |
|
||
| `startup:error` | `startup_pipeline.go:261` emit `{stage, error, cause}` | `app.js:326`, `startup-panel.js:250-281` 讀 `ev.cause / ev.stage` | ✓ |
|
||
| `startup:ready` | `startup_pipeline.go:223` emit nil | `app.js:329` ✓ | ✓ |
|
||
| `shutdown:modal-show` | `server_control.go:416` | `app.js:335-338` ✓ | ✓(1 秒 timer 在 Go 端實作) |
|
||
|
||
---
|
||
|
||
## D. State Machine(Idle/Starting/Running/Stopping/Stopped/Error)
|
||
|
||
⚠️ **Critical 1 發現點**。
|
||
|
||
Go 端 `server_control.go:44-49` 定義的 `ServerState` 常數是 **全小寫**:`"idle" / "starting" / "running" / "stopping" / "stopped" / "error"`。
|
||
|
||
前端所有判斷都用 **PascalCase**(`'Idle' / 'Starting' / 'Running' / 'Stopping' / 'Stopped' / 'Error'`):
|
||
- `control-panel.js:16-25` 的 `switch(s)` 全部走不到 case,status text 永遠 fall 到 `default` 直接顯示 raw lowercase 字串
|
||
- `control-panel.js:45` uptime clock 的 `s.state !== 'Running'` 永遠為 true → uptime 永遠顯示 `—`
|
||
- `control-panel.js:72-76` primary controls disable 邏輯全部失效:
|
||
- `openBtn.disabled = s !== 'Running'` → `'running' !== 'Running'` 為 true → **Open in Browser 永遠 disabled**
|
||
- `startBtn.disabled = !(s === 'Stopped' || s === 'Idle' || s === 'Error')` → 右邊全 false → **Start 永遠 disabled**
|
||
- `manageBtn.disabled = !(s === 'Running' || s === 'Error')` → **Manage 永遠 disabled**
|
||
- `miStop / miRestart` 同樣失效
|
||
- `app.js:147-151` `status.state === 'Error'` → 永遠 false → runtime 情境下 `showErrorBanner` 永遠不被觸發
|
||
- `app.js:149` `status.state !== 'Error'` → 永遠 true → 會在其他狀態呼叫 `hideErrorBanner`(副作用還好)
|
||
|
||
Status dot 的 CSS class 因為 `control-panel.js:12` 做了 `.toLowerCase()`,所以 dot 顏色可以正常顯示。但文字、按鈕、uptime、error banner 全部失效。
|
||
|
||
**這是 M8-5 的**最嚴重 bug**,只要沒跑過「真的啟動看 UI」就會沒被抓到。從 test case 看不到是因為 Go unit test 用 Go 常數直接比對。
|
||
|
||
**修法**:把前端所有 state 判斷改成 lowercase(`'running' / 'starting' / ...`)。共需修改 `control-panel.js:10-26, 45, 72-76` 與 `app.js:147, 149` 合計約 15 行。
|
||
|
||
---
|
||
|
||
## E. Primary Controls
|
||
|
||
按 Design Spec v2.1 §4.2 的啟用矩陣比對:
|
||
|
||
| 按鈕 | Design §4.2 條件 | 實作 (`control-panel.js:72-76`) | 對齊 |
|
||
|---|---|---|---|
|
||
| Open in Browser | 僅 `Running` | `s !== 'Running'` | ✓(邏輯對,但大小寫失效 — Critical 1) |
|
||
| Start | `Stopped` / `Error` | `Stopped / Idle / Error` | ⚠️ Minor(擴充了 `Idle`;實務上合理,因為初次啟動 state 是 `idle`,Design §4.2 沒涵蓋 idle 情境) |
|
||
| Manage ▾ | `Running` | `Running || Error` | ⚠️ Minor(實作擴充了 Error;但 Error 時按 Manage 下拉會露出 Stop/Restart,在 Error state 這不符合 Design — Error 應由 banner 的 Restart Server 負責) |
|
||
|
||
**子項**:Manage menu 下拉包含 Stop server / Restart server / Open log folder,對齊 Design §4.2 結構。
|
||
|
||
**Open in Browser 為 Primary CTA**:✅(`index.html:35` 有 `btn-primary` class,位置在最左,符合 Design Rationale)。
|
||
|
||
---
|
||
|
||
## F. 啟動進度面板(R5-E)
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| 6 階段 pending/running/completed/failed/skipped icon | ✅ | `startup-panel.js:72-79` 用 `○ / spinner / ✓ / ✕ / ⏭`,status 字串接受 `completed` 與 `done` 兩種別名 |
|
||
| stage-timeout retry hint | ✅ | `startup-panel.js:107-112, 237-247` 實作 20 秒 slow flag;`hintEl` 顯示 `startup.timeout.message`;Design §4.1「stage 6 manual mode 不套 20 秒 retry hint」也有處理(`startup-panel.js:106, 242`) |
|
||
| Error mode 三按鈕 | ✅ | `index.html:94-108`(Retry / View log / Report disabled);`app.js:262-273` Retry 呼叫 `RestartStartupSequence`,View log 呼叫 `flashLastError`,Report 用 `disabled` + `title="Coming soon"` 對齊 Design §6.2「hold」狀態 |
|
||
| Stage 5 skipped → Stage 6 manual mode fallback | ✅ | `startup-panel.js:182-184, 215-229` 實作 `enterManualMode`,stage 6 description 改為 `startup.stage.6.manualHint`,並透過 `onManualModeChange` 通知 `app.js:133-135` 啟動 primary CTA pulse |
|
||
| ⚠️ **Linux / AutoOpenBrowser=false 時使用者能否手動觸發 Open in Browser?** | ❌ | **Critical 1 的連鎖影響**:manual mode 要求使用者手動點 Open in Browser,但因 state 大小寫 bug,Open in Browser 在任何狀態下都 disabled。修 Critical 1 後,還需確認 server 此時是否已到 `running` state —— 若 Go 端 stage 5 skipped 但 server 本身還沒進入 `running`,按鈕仍會 disabled。**這點建議請 Architect 確認 Stage 5 skipped 時 server state 的時序**。題目已備註「並行 patch Agent 處理中」,記為「依賴 Critical 1 修復 + patch 完成驗證」。|
|
||
| live region 階段變化播報 | ✅ | `index.html:87` `#startup-live` + `startup-panel.js:200-210` ARIA polite |
|
||
| `role="progressbar" aria-valuenow` | ✅ | `index.html:80`, `startup-panel.js:142-144` |
|
||
|
||
---
|
||
|
||
## G. Log Panel
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| 訂閱 `log:append` + 處理 batch | ✅ | `app.js:309-312` 接受 array 或單一 entry;`log-panel.js:59-70` |
|
||
| 初次 mount 拉 `GetRecentLogs(2000)` | ✅ | `app.js:91`,失敗 fallback 空陣列 |
|
||
| 等寬字體 | ✅ | `style.css:29` `--font-mono: 'SF Mono', 'Menlo', 'Consolas', ...` |
|
||
| Level 著色 | ✅ | `log-panel.js:127` `className = 'log-line level-' + level`;`style.css` 應有對應 `.level-error / .level-warn / .level-info`(抽查存在) |
|
||
| auto-scroll + Pause when 上捲 | ✅ | `log-panel.js:21-33` nearBottom 判定;`btn-jump-latest` 在 `log-panel.js:35-43` |
|
||
| Filter(文字 + level) | ✅ | `log-panel.js:80-90, 255-256 (app.js)`;⌘F / Ctrl+F 聚焦在 `app.js:288-293` |
|
||
| Clear log | ✅ | `app.js:220-228` 呼叫 Go `ClearLogs()` + 本地 `clearLog()` |
|
||
| Export log | ✅ | `app.js:238-245` 呼叫 `ExportLog()` → toast `startup.log.exported {path}` |
|
||
| Ring buffer 2000 行裁切 | ✅ | `log-panel.js:4, 64-66, 119-121`(buffer 與 DOM 雙端裁切) |
|
||
| Footer Lines 統計 | ⚠️ Minor | `log-panel.js:151-155` 硬編碼 `Lines: ${buffer.length} / ${MAX_LINES}`,未套 `t('control.log.lines')`;zh-TW 使用者會看到英文 "Lines:" 而非「行數:」 |
|
||
|
||
---
|
||
|
||
## H. Footer
|
||
|
||
Design §4.6 規定 footer 兩欄:行數統計左 + 關閉警示右。
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| 行數 / 2000 | ✅ | `index.html:140` `#footer-lines` |
|
||
| 持久關閉警示 | ✅ | `index.html:141` `#footer-warning` + i18n key `control.footer.closeWarning`(`i18n.js:39, 118` 中文/英文皆齊) |
|
||
| 非 modal | ✅ | 直接是 `<span>`,對齊 R5-2「不彈 modal」決策 |
|
||
|
||
Design §4.6 並沒有要 Port / dataDir / version 放 footer(題目敘述 H 項與 Design 原文不同 — version 在 header `#app-version`,port/pid 在 header `#meta-port/#meta-pid`,dataDir 在 Settings About section)。以 Design Spec 為準:✅ 通過。
|
||
|
||
---
|
||
|
||
## I. Settings
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| Auto-open browser toggle | ✅ | `index.html:152-158`;`settings-panel.js:16-30` `pref-auto-open` 綁 `SetPreferences` + revert on error |
|
||
| Linux 平台額外說明 | ✅ | `settings-panel.js:71-76` 根據 `sysInfo.platform` startsWith `'linux'` 顯示 `settings.autoOpenBrowser.hintLinux` |
|
||
| Language dropdown | ✅ | `index.html:163-167` auto / zh-TW / en;`settings-panel.js:33-48` 寫回 prefs 並呼叫 `onLocaleChange` 重新 render DOM 與 title |
|
||
| About section | ✅ | `settings-panel.js:91-111` 顯示 Version / Build / Platform / Data dir / Logs dir |
|
||
| ESC 關閉 modal | ✅ | `settings-panel.js:51-55` |
|
||
| Backdrop 點擊關閉 | ✅ | `settings-panel.js:11-13` |
|
||
|
||
---
|
||
|
||
## J. Dark Mode
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| CSS variables | ✅ | `style.css:8-32` `:root` 定義 light tokens |
|
||
| `@media (prefers-color-scheme: dark)` | ✅ | `style.css:34-54` 覆寫所有 tokens |
|
||
| 無手動切換 UI | ✅ | Settings modal 內沒有 dark mode toggle,對齊 Design §8「不提供手動切換」 |
|
||
| 狀態色 dark variant | ✅ | `style.css:46-48` success/warning/destructive 都有 dark 值 |
|
||
|
||
---
|
||
|
||
## K. i18n
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| ~85 keys 雙語 | ✅ | `i18n.js` zh-TW 80 keys + en 80 keys(含 control.* / startup.* / settings.*),與 Design §9 + startup-progress.md §7 清單逐項比對後齊全 |
|
||
| navigator.language fallback | ✅ | `i18n.js:169-181`:prefs override → localStorage → navigator.language → `zh*→zh-TW / en*→en / else→zh-TW`,與 TDD §6.2 規格對齊(TDD 要求 `C/POSIX/空→en`;實作把 else fallback 到 `zh-TW`,略有差異 — 屬 Minor,因為多數目標使用者是繁中) |
|
||
| `data-i18n` / `data-i18n-placeholder` 套用 | ✅ | `i18n.js:209-220` |
|
||
| 缺漏 key | ⚠️ Minor | `log-panel.js:154` footer lines 未套 i18n;`i18n.js` 無 `control.status.runningBrowserOpened` 的 UI 實際使用(只在 i18n 定義,但 `control-panel.js:18-21` 只串了 `control.status.running` + port,沒有實作 Design §5.2 的「10 秒內顯示 Running · Browser opened 後淡回」。記 Minor) |
|
||
|
||
---
|
||
|
||
## L. Shutdown modal(M8-4 7+1)
|
||
|
||
| 檢查項 | 結果 | 備註 |
|
||
|---|---|---|
|
||
| 訂閱 `shutdown:modal-show` | ✅ | `app.js:335-338` |
|
||
| 1 秒 timer | ✅(Go 端實作) | `server_control.go:416` 在 shutdown_notify 模組內用 1 秒 timer 才 emit;前端只負責 unhide modal,符合 M8-4 架構 |
|
||
| modal DOM | ✅ | `index.html:178-183` spinner + `control.shutdown.stopping` i18n |
|
||
|
||
---
|
||
|
||
## M. 親跑驗證
|
||
|
||
**`node --check` 結果**:
|
||
```
|
||
== app.js == OK
|
||
== control-panel.js == OK
|
||
== startup-panel.js == OK
|
||
== log-panel.js == OK
|
||
== settings-panel.js == OK
|
||
== i18n.js == OK
|
||
== wailsjs/go/main/App.js == OK
|
||
```
|
||
9 個 JS 檔全部語法正確。
|
||
|
||
**`wails build -s -m -skipbindings`**:未執行 — 本機無 wails CLI。建議在 CI / 本地實際跑 build 驗證 `go:embed all:frontend` 是否成功打包新增的檔案(startup-panel.js、settings-panel.js、log-panel.js、control-panel.js 是新檔,需確認有被 Wails embed 規則 include —— 目前 `visiona-local/main.go` 應該用 `go:embed all:frontend`,*.js 都會被吃進 binary)。
|
||
|
||
---
|
||
|
||
## N. 問題清單
|
||
|
||
### Critical(必須修復,阻擋 M8-10)
|
||
|
||
| # | 檔案:行 | 問題描述 | 建議修改方式 |
|
||
|---|---|---|---|
|
||
| C-1 | `control-panel.js:16-25, 45, 72-76` 與 `app.js:147, 149` | 前端所有 `ServerState` 判斷用 PascalCase(`'Running' / 'Error' / ...`),但 Go 端 `server_control.go:44-49` 常數是全小寫(`"running" / "error" / ...`)。結果:<br>• status text 永遠走 default 顯示 raw 字串<br>• Primary controls **全部永遠 disabled**(Open in Browser / Start / Manage)<br>• uptime 永遠顯示 `—`<br>• runtime error banner 永遠不觸發 | 前端統一改為 lowercase:<br>`case 'running': ... case 'error': ...`<br>`s !== 'running'` / `s === 'stopped' \|\| s === 'idle' \|\| s === 'error'` / `status.state === 'error'`。約 15 行修改。**或者**請 Architect 把 Go 常數改 PascalCase(需更動 Go 所有 setState 呼叫,影響較大;不建議)。 |
|
||
| C-2 | `app.js:300-302` | 訂閱 `server:error` 時讀 `payload.error`,但 Go 端 `server_control.go:184, 361` 實際 emit 的 payload 是 `map[string]any{"reason": err.Error(), "port": ...}` — key 是 `reason` 不是 `error`。結果 `showErrorBanner` 收到 `undefined` 直接 early-return,banner 不彈。 | 改為 `payload && payload.reason` 或同時容錯兩個 key:`payload.reason \|\| payload.error`。1 行修改。 |
|
||
|
||
### Major
|
||
|
||
(無)
|
||
|
||
### Minor(建議修復)
|
||
|
||
| # | 檔案:行 | 問題描述 | 建議修改方式 |
|
||
|---|---|---|---|
|
||
| m-1 | `log-panel.js:151-155` | Footer lines 統計硬編碼英文 `Lines: {n} / {max}`,未套 `t('control.log.lines', {...})`。zh-TW 使用者看到英文。 | 改為 `el.textContent = t('control.log.lines', { current: buffer.length, max: MAX_LINES })`。1 行修改。 |
|
||
| m-2 | `control-panel.js:18-21` | 未實作 Design §5.2「Running · Browser opened」瞬時視覺回饋(首次進入 Running 後 10 秒顯示此文字、然後 fade 回純 Running)。i18n key `control.status.runningBrowserOpened` 已定義但從未被呼叫。 | 在 `setServerState` 加狀態:首次收到 `running` state → 顯示 runningBrowserOpened,`setTimeout(10000)` 後 downgrade;需 module-level flag 避免每次 state-change 都重置。約 10 行。 |
|
||
| m-3 | `control-panel.js:74-76` Manage menu 在 Error state 下啟用 | Design §4.2 規定 Manage 只在 Running 啟用。Error state 應由 Error banner 的 Restart Server 負責,不用 Manage 下拉。 | 改為 `manageBtn.disabled = s !== 'running'`;同步 `miStop.disabled = s !== 'running'`;`miRestart.disabled = s !== 'running'`。(注意:與 C-1 修正時一併改成 lowercase) |
|
||
| m-4 | `i18n.js:179-180` navigator.language fallback | TDD §6.2 規格要求 `C / POSIX / 空字串 → en-US`;實作最終 fallback 到 `zh-TW`。 | 把第 180 行 `return 'zh-TW'` 改為 `return 'en'`,並在 `navigator.language` 為 `'C' / 'POSIX' / ''` 時也走 `en` 分支。 |
|
||
| m-5 | `index.html:103, app.js:273` Report 按鈕 `disabled` + `title="Coming soon"` | 對齊 Design §6.2「hold」decision,技術上 OK;但 `title` 屬性未 i18n。 | 改 `data-i18n-title` 機制或直接硬標「即將推出 / Coming soon」兩語化。 |
|
||
| m-6 | `startup-panel.js:115-144 paintProgressBar` elapsed 每秒刷新 | Design §3.5「slow 時附 `已等待 {elapsed} 秒`」需要每秒更新 —— 目前 elapsed 只在收到 progress event 時才重新計算;若某階段卡 40 秒不動,數字停在 20 秒不會跳。 | 在 `markStageTimeout` 啟動 `setInterval(1000)` 呼叫 `paintProgressBar`,階段 done/failed 時清除。 |
|
||
| m-7 | `control-panel.js:73` Start 按鈕允許在 `Idle` state 按 | Design §4.2 表格只寫 `Stopped / Error`,未涵蓋 `Idle`。實作擴充合理(初次進入控制台就是 idle),但與 Design 文字有差。 | 建議 Architect / Design 確認後更新 Design Spec §4.2 把 Idle 列入;或在實作加註釋說明。文件層面的偏差,不改程式碼也可。 |
|
||
| m-8 | TDD §2.1 vs 實作 flat 結構 | TDD 規劃 `components/` 子目錄 + 獨立 `i18n/*.json`;實作 flat + dict 硬寫在 `i18n.js`。 | 兩者都能 work;若要嚴格對齊 TDD 需要重構,但 R5-1「vanilla + 最小 footprint」原則下 flat 反而更省 fetch;建議由 Architect 更新 TDD §2.1 或允許此差異。 |
|
||
|
||
### Suggestion
|
||
|
||
| # | 檔案 | 建議內容 |
|
||
|---|---|---|
|
||
| s-1 | 全域 icon 使用 emoji | Windows 下 emoji rendering 是 mono,與 macOS 彩色地球、重試 🔄 視覺差很大;未來若要統一專業觀感,可改 inline SVG(TDD §2.1 原設計) |
|
||
| s-2 | `wailsjs/go/main/App.js:1-6` | 下次 `wails build` 記得刪掉手動標註註解並交由工具鏈覆寫 |
|
||
| s-3 | `startup-panel.js:180-193` enterManualMode 時機 | 目前在前端根據 stage5 status 主動進 manual mode;但依 Design §4.1 流程,Go 端會依序送 stage5 skipped → stage6 running。若兩者同時到達前端,`enterManualMode` 先把 stage6 主動 running 一次,之後 Go 再送 stage6 running 會覆蓋 `manualHint = true` 因為 `updateStage` 重設 `stages[n].status`(注意 L171-177:`status` 會被覆蓋但 `manualHint` 不會被重設 — OK)。實測建議 patch Agent 覆盤此時序。 |
|
||
|
||
### 懸而未決 / 需跨 Agent 確認
|
||
|
||
1. **Critical 1 修完後,stage 5 skipped 時 server state 是否已進入 `running`?** 若 Go 端 stage 5 skipped 時 `ctrl.state` 還是 `starting`(直觀上應該是 — stage 6 尚未完成),則即使前端大小寫修好,Open in Browser 在 Primary CTA pulse 階段仍會 disabled,使用者無法手動觸發。**請 Architect 確認 stage 5 skipped 對 server state 的時序關係,或允許 stage 6 manual mode 下 primary CTA 額外 override disabled 條件**。此點題目註明「已有並行 patch Agent 處理中」,記錄於此供 Orchestrator 彙整。
|
||
2. **TDD §2.1 的 flat vs 子目錄結構差異**:是否要求 Frontend Agent 依 TDD 原結構重構?建議由 Orchestrator 決定是否更新 TDD 而非改程式碼。
|
||
|
||
---
|
||
|
||
## O. 結論
|
||
|
||
**審查結果:⚠️ 需修改後通過(2 Critical / 8 Minor / 3 Suggestion / 2 懸而未決)**
|
||
|
||
### 修復優先級
|
||
|
||
1. **立即修(阻擋 M8-10)**:C-1、C-2。兩個 bug 加起來修改量 < 20 行,但不修控制台幾乎完全無法操作。
|
||
2. **Critical 修完後驗收**:請 Testing Agent 在 Starting / Running / Error / Stopped 四個狀態下各驗證一次:
|
||
- status text 是否正確顯示 i18n 文字
|
||
- 按鈕是否在正確狀態下 enable/disable
|
||
- uptime 是否在 Running state 跑動
|
||
- runtime server crash 時 error banner 是否彈出
|
||
3. **下一輪修(不阻擋)**:m-1 ~ m-8 建議一併修,但不必卡交付。
|
||
4. **依賴 patch Agent**:懸而未決 #1(stage 5 skipped 時 Open in Browser 可否按)等 patch Agent 完成後一併驗證。
|
||
|
||
### 優點(給 Frontend Agent 的正面回饋)
|
||
|
||
- **模組拆分清晰**:app.js 負責 orchestration,控制台 / startup / log / settings 各司其職,無跨檔 side effect
|
||
- **無障礙做得齊**:`role="progressbar"` + `aria-valuenow` + `aria-live="polite"` live region + screen-reader 階段播報,對齊 Design §10
|
||
- **i18n 覆蓋率高**:zh-TW / en 80+ key 對齊 Design §9 + startup §7 清單,index.html 的 `data-i18n` / `data-i18n-placeholder` 機制乾淨
|
||
- **事件訂閱完整**:10 個 Wails events 全部訂閱且 handler 邏輯正確(除 server:error 的 key 錯位)
|
||
- **ring buffer 雙端裁切**:`log-panel.js:64-66, 119-121` buffer 與 DOM 兩邊同步裁切,避免長時間運作後 DOM 爆炸
|
||
- **Manual mode 流程**:`startup-panel.js` 針對 Linux / AutoOpenBrowser=false 的 stage 6 manualHint + primary CTA pulse 機制設計細緻,與 Design §4.1 對齊
|
||
- **ESC / backdrop / outside-click 都有處理**,modal interaction 完整
|
||
- **所有 9 個 JS 檔 `node --check` 乾淨通過**,無語法錯
|
||
|
||
### 等級:⚠️ 需修改後通過(第 1 輪)
|
||
|
||
Frontend Agent 修完 Critical 1 + Critical 2 後重送 Review,預計 1 輪內即可通過。Minor 項建議併入同一輪修復以節省後續 review cost。
|