jim800121chen 8cd5751ce3 feat(local-tool): M8 重構 — Wails 控制台 + 瀏覽器 Web UI(R5 決策)
依 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>
2026-04-15 17:57:54 +08:00

514 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# v2/milestone-plan.md — M8 開發 milestone 拆分
> 所屬TDD v2 §2.7
> 版本v2.12026-04-14 吸收 PM 審閱 + R5-D + R5-E
> 目的:給 Orchestrator 調度工程師 Agent 用。每個 milestone = 一個 Reviewer 審查單位。
> 總工時估算:**~12.0 人天**v2.0 是 10 人天R5-E 階段化啟動 +1 天 / PM Q4 7+1 秒 shutdown modal +0.2 天 / R5-D1 OS 通知 +0.3 天 / Minor 4 WebSocket 廣播 +0.3 天 / M8-10 驗收項目擴充 +0.2 天,合計 +2 人天。與 §3 合計 12.0 一致)
---
## 1. M8 全景
v2.1 的開發總共拆成 11 個 milestone編號 M8-1 ~ M8-10 + M8-4b。依賴關係
```
M8-1 (砍 yt-dlp) ──────┐
├──▶ M8-4 (server lifecycle) ──▶ M8-4b (階段化啟動) ──▶ M8-5 (Wails UI 改寫) ──▶ M8-9 (boot-id + 重連)
M8-2 (砍 Mock) ────────┘ ▲
M8-3 (ffmpeg LGPL vendor) ─────────────────────────────────────────────────────────────────────────────────┼──▶ M8-10 (end-to-end)
M8-6 (source-selector 調整) ────────────────────────────────────────────────────────────────────────────────┤
M8-7 (Offline Overlay) ─────────────────────────────────────────────────────────────────────────────────────┤
M8-8 (CORS) ────────────────────────────────────────────────────────────────────────────────────────────────┘
```
- M8-1 / M8-2 / M8-3 / M8-8 可以同時開工(互不依賴)
- M8-4 依賴 M8-1因為 app.go 內的 mockMode field 會在 M8-2 砍掉;但 M8-4 主要碰 server_control.go / log_buffer.go 不衝突,實務可平行)
- **M8-4b 依賴 M8-4**(需要 ServerController 已存在才能插入 pipeline hook
- M8-5 必須等 M8-4b 完成bindings + event schema 定義完)
- M8-6 可以獨立進行
- M8-7 依賴 M8-4需要 boot-id server 端 + WebSocket hub會在 M8-4 一併做)
- M8-9 依賴 M8-4 + M8-4b + M8-7
- M8-10 必須全部完成後
---
## 2. Milestone 明細
### M8-1砍 yt-dlp 全套
**預估**0.5 人天
**負責 Agent**Backend Agent + Frontend Agent同時進行因為跨 Go + TS
**依賴**:無
**任務**
1.`v2/deletions.md` §1 刪後端 Go`video_source.go` / `camera_handler.go` / `router.go` / `deps/checker.go` / `main.go` 註解)
2.`v2/deletions.md` §5 刪前端 TS`source-selector.tsx` 的 URL tab / `camera-store.ts``startFromUrl` / i18n 4 個 key
3.`v2/deletions.md` §4.1 + §4.2 + §4.3 + §4.4 + §4.5 刪打包流程Makefile / installer / bootstrap
4. 刪除 `/vendor/yt-dlp/` 目錄(若存在)
**驗收條件**
- `go build ./...` PASS
- `go test ./server/...` PASS
- `pnpm --dir frontend build` PASS
- `git grep -i 'yt-dlp\|ytdlp\|ResolveWithYTDLP\|StartFromURL\|classifyVideoURL\|pasteUrl\|urlPlaceholder'` 無業務程式碼 match`.autoflow/` 內的歷史文件可接受)
- `make payload-macos` 可成功(不再 `cp yt-dlp`
**Reviewer 檢查重點**
- `camera_handler.go` 砍掉後 import 清理乾淨
- i18n types.ts / zh-TW.ts / en.ts 三檔同步刪
- Makefile 三個平台的 `vendor-ytdlp*` target 都清
---
### M8-2砍 Mock 模式全套
**預估**0.5 人天
**負責 Agent**Backend Agent + Frontend Agent
**依賴**:無(理論上可與 M8-1 平行)
**任務**
1.`v2/deletions.md` §2 + §3 刪 Go兩個檔整檔刪、`device/manager.go` / `camera/manager.go` / `config.go` / `main.go` / `app.go` 所有 `mockMode` 條件砍掉)
2.`v2/deletions.md` §6 刪前端 Mock 切換 UI + 4 個 i18n key
3. 更新 `noDevices` 文字empty state 友善化)
**驗收條件**
- Go + 前端 build PASS
- `git grep -i 'VISIONA_MOCK\|MockMode\|mockMode\|MockCamera\|MockDriver\|NewMockDriver\|runtimeModeMock'` 無業務程式碼 match
- 實機啟動 server沒插 Kneron 硬體)→ Devices 頁顯示友善 empty state「未偵測到 Kneron 裝置」
**Reviewer 檢查重點**
- `NewManager` 簽名改動後所有呼叫點都更新
-`--mock` / `--mock-camera` / `--mock-devices` CLI flag 真的拿掉server `--help` 不再列)
- 任何剩下的 test 檔案不再依賴 mockMode
---
### M8-3ffmpeg LGPL vendor 切換
**預估**1.5 人天
**負責 Agent**DevOps Agent+ Backend Agent支援 macOS build 環境)
**依賴**:無
**任務**
1. **Windows / Linux**0.5 人天):依 `v2/ffmpeg-lgpl.md` §3 + §4 修改 `Makefile` 的 URL 與 vendor target實測 `make vendor-ffmpeg-windows` / `vendor-ffmpeg-linux` 可成功
2. **macOS build script**1 人天):
-`v2/ffmpeg-lgpl.md` §2.3 新增 `vendor-ffmpeg-macos-build` Makefile target
- 第一次跑:下載 ffmpeg n7.1 source、算 sha256 填進 Makefile、跑 configure + make、驗證 binary size < 25 MB驗證 `ffmpeg -version` 不含 `--enable-gpl` / libx264 / libx265
- ffmpeg + ffprobe + COPYING.LGPLv3 + BUILD.md git commit `vendor/ffmpeg/macos/`
- 修改 `.gitignore` `vendor/ffmpeg/macos/` 成為 git tracked
- 修改 `vendor-ffmpeg` target 改為驗證 binary 存在且是 LGPL
3. **Payload 階段同步**(§7三個平台的 `payload-*` target 改為 copy `ffmpeg + ffprobe + COPYING.LGPLv3`不再 copy yt-dlpM8-1 會處理刪
4. **Installer 同步**(§8
- `installer/windows/visiona-local.iss` 新增 ffprobe LGPL license
- `installer/linux/build-appimage.sh` 迭代 tool 改為 `ffmpeg ffprobe`
**驗收條件**
- `make vendor-ffmpeg` PASSmacOSbinary size OK `v2/ffmpeg-lgpl.md` §10 驗收表
- `vendor/ffmpeg/macos/ffmpeg -version | grep -- --enable-gpl` 無輸出
- `spctl --assess --verbose vendor/ffmpeg/macos/ffmpeg` accepted
- `make vendor-ffmpeg-windows` + `vendor-ffmpeg-linux` 可成功檔案解出後 version `--enable-gpl`
- `make payload-macos` 成功`payload/darwin/bin/` 內有 `ffmpeg / ffprobe / ffmpeg-COPYING.LGPLv3`
- 實測用 `vendor/ffmpeg/macos/ffmpeg` 5 種格式mp4/avi/mov/mpeg/mpg正常
**Reviewer 檢查重點**
- `vendor/ffmpeg/macos/BUILD.md` 內容正確可重現
- 開發者用單一 `make vendor-ffmpeg-macos-build` 指令真的能從零 build 出來不依賴本機 cached state
- `.gitignore` `!` rule 順序正確
- Makefile configure flags `v2/ffmpeg-lgpl.md` §2.3 完全一致
---
### M8-4Server lifecycle + log buffer + bindings + OS notify + Preferences
**預估**2 人天v2.0 1.5 +0.3 R5-D1 OS 通知、+0.2 PM Q4 7+1 shutdown modal
**負責 Agent**Backend AgentGo / Wails
**依賴**M8-1避免 mockMode 欄位被砍後衝突)。實務上可與 M8-2 平行
**任務**
1. **新增檔案**`v2/control-panel.md` §7 + §4
- `visiona-local/log_buffer.go`~120 ring buffer
- `visiona-local/server_control.go`~280 ServerController + state machine + startServerV2 + logPump + **7+1 秒 shutdown modal timer** `server-lifecycle.md` §8.2
- `visiona-local/preferences.go`~80 )— **R5-D2**必須實作 `DefaultPreferences()` `runtime.GOOS` 分平台預設macOS/Windows `AutoOpenBrowser=true`Linux `false`+ atomic write-rename`server-lifecycle.md` §11.3
- `visiona-local/notify.go`~100 )— **R5-D1**三平台 OS 通知macOS `osascript display notification` / Linux `notify-send` / Windows PowerShell BurntToast + `msg *` fallback詳見 `server-lifecycle.md` §10
2. **修改 `visiona-local/app.go`**
- 新增 `ctrl` / `logBuf` / `prefs` 欄位**不要** `autoOpenedThisSession` R5-D3 已砍掉 per-session-once 概念
- `GetBootstrapStatus` / `setBootstrapStatus` / `bootstrapStatus` 欄位
- `watchServer()` 失敗後行為 os.Exit改設 Error state + **goroutine 呼叫 `sendCrashNotification`** R5-D1
- 新增 12 Wails bindings`v2/control-panel.md` §4.2
3. **修改 `visiona-local/main.go`**
- `OnBeforeClose` handler
- `shutdownGracePeriod` 5 s **7 s**PM Q4 定案不是 10 s
4. **修改 `server/main.go` + `system_handler.go` + `router.go`**
- 新增 boot-id 生成使用 `crypto/rand` 16 bytes hexArchitect Q3 決定不引 google/uuid
- 新增 `GET /api/system/boot-id` endpoint
- **Minor 4**server 新增 WebSocket hub 廣播 `server:shutdown-imminent` 的能力新增 `/ws/system` endpoint 或擴充現有 `/ws/server-logs`Wails `ctrl.Stop()` 在開始 SIGTERM 前透過 HTTP server 內部 API 觸發此廣播
- **對齊 shutdown timeout**`server/main.go:166` `shutdownFn` timeout 10 s **6 s**Wails 7 s 1 s確保 server 先完成 cleanup
**驗收條件**
- `cd visiona-local && go build .` PASS
- `cd server && go build ./...` PASS
- 12 bindings `visiona-local/frontend/wailsjs/go/main/App.d.ts` 正確生成
- 手動測試 bindings 可從 Wails dev 模式的 devtools console 呼叫
- 手動測試Start server 啟動 log 噴進 LogBuffer `GetRecentLogs(20)` 取回
- 手動測試Stop 1 秒內顯示停止中…」modal server 未秒 exit+ 7 秒內 SIGKILL若卡死
- 手動測試Restart state: Running Stopping Stopped Starting Running
- 手動測試 server process watchServer 3 次失敗後 state Error + **收到 OS 原生通知**三平台都要
- **R5-D2 驗收**全新使用者第一次在 macOS/Windows app `preferences.json` 不存在 `DefaultPreferences()` 回傳 `AutoOpenBrowser=true`Linux 上回傳 `false`
- `curl http://127.0.0.1:<port>/api/system/boot-id` 回傳 JSON bootIdhex 32 字元
- WebSocket `server:shutdown-imminent` 廣播 websocat `/ws/system` Stop 後秒收到廣播
**Reviewer 檢查重點**
- LogBuffer thread-safetymutex 正確
- ServerController `txMu` / `mu` 互斥
- logPump server 死掉 / pipe EOF / 高頻 stdout 下都不 leak goroutine
- `parseLogLevel` 不會 panic on 空字串或非 ASCII
- boot-id 生成使用 `crypto/rand` + `encoding/hex`不引 google/uuid
- Preferences JSON atomic writetmp + rename §11.3
- `DefaultPreferences()` `runtime.GOOS` 正確切換Linux 分支有測試
- `sendCrashNotification` 三平台檔案darwin/linux/windows build tag齊全且不阻塞
- shutdown modal 1 timer 7 grace timer 正確goroutine 泄漏檢查
- server WebSocket hub 廣播邏輯與 Wails 端呼叫的契約傳遞 `reason` 欄位
---
### M8-4b階段化啟動管線R5-E
**預估**1 人天v2.0 未拆出v2.1 新增
**負責 Agent**Backend AgentGo / Wails+ Frontend Agentvanilla JS startup panel
**依賴**M8-4需要 ServerController + bindings
**任務** `v2/startup-pipeline.md`若該檔不存在則落在 `control-panel.md` §3.1 + `server-lifecycle.md` §2.1
1. **新增 `visiona-local/startup_pipeline.go`**~180
- `StartupPipeline` struct currentStage / stageStartedAt / startedAt / softTimeout / hardTimeout
- `NewStartupPipeline(ctx)` / `Start(stage int)` / `Complete(stage int)` / `Fail(stage int, err)` / `Ready()` 方法
- `watcher(ctx)` goroutine每秒檢查 soft timeout (20 s) + hard timeout (60 s)emit `startup:stage-timeout` / `startup:error` event
- 4 event schema`startup:progress` / `startup:stage-timeout` / `startup:error` / `startup:ready` `v2/startup-pipeline.md` §1
2. **修改 `visiona-local/app.go`** `startup(ctx)`
- `OnStartup` 最前面初始化 pipeline Start(1)
- 每個既有階段的對應處呼叫 `pipeline.Complete(N)` + `pipeline.Start(N+1)`
- 6 個階段對應1=Wails init2=Python runtime3=server binary spawn + health check4=first `/api/devices` 查詢5=OpenInBrowser call6=WebSocket 首個 client connect
- 最後 `pipeline.Ready()` emit `startup:ready`
3. **修改 server 端**
- `pkg/ws`或對應的 WebSocket hub新增 `OnClientConnected` callback Wails 能收到第一個 client 連上的通知透過 WebSocket HTTP poll
- Wails `startup_pipeline.go` 訂閱此通知完成階段 6
4. **失敗處理**任一階段失敗 `pipeline.Fail(stage, err)` `ctrl.setState(Error, ...)` + `sendCrashNotification` + emit `startup:error`
5. **非阻塞**所有 `EventsEmit` 呼叫都走 buffered channel fire-and-forget goroutine不阻塞啟動流程
6. **前端Wails 控制台)**
- `visiona-local/frontend/components/startup-panel.js`~100
- 訂閱 4 event 更新 DOM
- 收到 `startup:ready` 淡出300 ms ease
- i18n key `i18n/zh-TW.json` / `en-US.json` 文案由 Design Spec v2.1 敲定
**驗收條件**
- `cd visiona-local && go build .` PASS
- 正常冷啟動樂觀情境4 s 內看到啟動進度面板6 階段逐一 completed最後淡出顯示主控台
- 慢啟動情境mock 階段 2 jam 25 看到階段 2 正在重試 …」副文字
- 失敗情境mock 階段 3 fail 看到進度面板變紅 Error state收到 OS 通知 `startup:error` event
- 總時 > 60 秒情境mock 每個階段都等 12 秒 → 60 秒後進 Error statewatcher 總時檢查有效)
- 日常啟動(非首次):~3 s 內就緒,符合 PM AC-2.1
**Reviewer 檢查重點**
- watcher goroutine 在 pipeline.Ready() / Fail() 後正確停止,不 leak
- `EventsEmit` 非阻塞(若 Wails IPC 慢也不拖啟動)
- 6 階段的 labelKey 與 design-spec v2.1 一致
- 失敗後 Error state 透過 ctrl.setState 觸發,不繞過 state machine
- startup-panel.js 的淡出動畫不會卡住主控台 UI
---
### M8-5Wails 控制台 vanilla UI 改寫
**預估**2 人天
**負責 Agent**Frontend Agentvanilla JS
**依賴**M8-4需要 bindings + events
**任務**
1. **改寫 `visiona-local/frontend/index.html`** 成控制台 layout`v2/control-panel.md` §3
2. **改寫 `visiona-local/frontend/app.js`** 成控制台主程式§5
3. **改寫 `visiona-local/frontend/style.css`** 新增 status card / log panel / action bar / preferences 樣式,並加 light/dark mode CSS variables
4. **新增 components**
- `components/status-card.js`
- `components/log-panel.js`(含 virtual scroll lite + batch render
- `components/action-bar.js`(含 disable 矩陣)
- `components/preferences.js`
5. **新增 i18n**
- `i18n/en-US.json` / `zh-TW.json`
- `i18n/loader.js`
6. **新增 icons**`icons/*.svg` × 6
**驗收條件**
- Wails dev 模式下開 app
- ✅ 看到控制台 UIstatus / log / actions / preferences不是 splash / blank / Next.js
- ✅ Start 按鈕能啟動 server狀態卡片變 Running 綠色 badge
- ✅ Log panel 即時顯示 server stdout看到 Gin log
- ✅ Stop 按鈕能停 serverbadge 變灰
- ✅ Restart 按鈕能重啟(狀態過程正確)
- ✅ Open in Browser 開瀏覽器到正確 URL
- ✅ Reveal Logs 開啟 `<dataDir>/logs/` 資料夾
- ✅ Clear Logs 清畫面但不動磁碟檔
- ✅ Preferences 切 `openBrowserOnStart` 持久化到 `preferences.json`
- ✅ Dark mode 跟隨系統macOS 切換 Dark Mode 後控制台自動變色)
- ✅ Log panel 按 Pause 後自動捲動停止,解除後恢復
- `make wails-macos` 能 build 出 .app打開後看到控制台 UIM7-B 教訓)
**Reviewer 檢查重點**
- vanilla JS 沒引入任何 npm 依賴(`package.json` 不存在於 visiona-local/frontend/
- Log panel 在高頻1000 行/秒)下不會 freeze UIbatch render 有效)
- i18n loader SSR-safe不適用但 Wails 沒 SSR 概念)
- icons/ 下的 SVG 符合 inline 使用的簡潔規則viewBox + single path
- Action bar 在每個 state 下 button enable/disable 正確disable 矩陣全部驗證)
---
### M8-6Web UI source-selector + 副檔名擴充
**預估**0.5 人天
**負責 Agent**Frontend Agent
**依賴**:無(與其他 milestone 平行)
**任務**
1. `frontend/src/components/camera/source-selector.tsx``v2/deletions.md` §5.1 移除 URL tab + mode toggle
2.`accept=".mp4,.avi,.mov"` 改為 `accept=".mp4,.avi,.mov,.mpeg,.mpg"`
3. i18n 新增 `videoFormats` key 或改既有 `mp4AviMov`
4. **後端同步**`server/internal/api/handlers/camera_handler.go:251` 把 extension whitelist 從 `.mp4 / .avi / .mov` 擴充為 5 個:
```diff
-if ext != ".mp4" && ext != ".avi" && ext != ".mov" {
- c.JSON(400, gin.H{"success": false, "error": gin.H{"code": "BAD_REQUEST", "message": "only MP4/AVI/MOV files are supported"}})
+if ext != ".mp4" && ext != ".avi" && ext != ".mov" && ext != ".mpeg" && ext != ".mpg" {
+ c.JSON(400, gin.H{"success": false, "error": gin.H{"code": "BAD_REQUEST", "message": "only MP4/AVI/MOV/MPEG/MPG files are supported"}})
}
```
**驗收條件**
- 前端 build PASS
- 手動測試5 種副檔名都能上傳且正常開始推論
- UI 不再有「Paste URL」按鈕
**Reviewer 檢查重點**
- handler 裡的 extension 比對是 `strings.ToLower` 後再比,大小寫不敏感
- 錯誤訊息中英文同步
---
### M8-7Web UI Server Offline Overlay
**預估**1 人天
**負責 Agent**Frontend Agent
**依賴**M8-4需要 server 端 boot-id endpoint
**任務**:依 `v2/web-ui-offline-overlay.md` §4 的 7 個檔案清單:
1. 新增 `frontend/src/stores/system-store.ts`
2. 新增 `frontend/src/hooks/use-boot-id-watcher.ts`
3. 新增 `frontend/src/components/server-offline-overlay.tsx`
4. 新增 `frontend/src/components/boot-id-watcher-mount.tsx`
5. 修改 `frontend/src/app/layout.tsx`(掛 overlay + watcher
6. 修改 `frontend/src/lib/i18n/{types,zh-TW,en}.ts`(新增 serverOffline 區塊)
**驗收條件**
- `pnpm --dir frontend build` PASS含 SSR 相容性驗證)
- 手動測試:
- 正常啟動沒 overlay
- 關 Wails 視窗後 15 s 內瀏覽器 tab 顯示 overlay
- 重新開 Wails app → Web UI 的「重試」按鈕有效overlay 消失)
- 控制台按 Restart → boot-id 變 → 自動 `window.location.reload()`
- 切到別的 tab 5 分鐘再切回 → polling 立即 probe 不用等
**Reviewer 檢查重點**
- Zustand store 的 failures state 遞增正確
- `use-boot-id-watcher.ts` 的 cleanupuseEffect return正確 cancel
- `AbortSignal.timeout(3000)` 在舊瀏覽器相容性(若需支援 Chrome < 103 則 fallback
- Overlay 元件 a11y`role="alertdialog"` + `aria-labelledby`
- i18n 新增 key 三個檔同步types / zh-TW / en
---
### M8-8CORS + origin check middleware
**預估**0.5 人天
**負責 Agent**Backend Agent
**依賴**:無
**任務**:依 `v2/cors-security.md` §4 + §5
1. 覆寫 `server/internal/api/middleware.go`(白名單版)
2. 新增 `server/internal/api/ws/origin.go`
3. 修改所有 WS handler 的 upgrader CheckOrigin
4. 新增 `server/internal/api/middleware_test.go` 單元測試(`TestIsAllowedOrigin`
5. 在 `router.go` 掛 `requireSameOriginOrNoOrigin` middleware 到 `/api/*`
**驗收條件**
- `go test ./server/internal/api/... -run TestIsAllowedOrigin` PASS
- `v2/cors-security.md` §9 所有 curl 驗收條件通過
- WS websocat 測試:白名單 origin 可連、非白名單擋
- 既有 Next.js Web UI 的 fetch 仍正常運作same-origin + white list
**Reviewer 檢查重點**
- `isAllowedOrigin` 對 edge case 處理正確(空字串 / null / https / 不合規 URL
- 不 accidentally 把 `X-Relay-Token` header 保留relay 早已砍)
- WS upgrade 在非白名單 origin 下回 403不是靜默失敗
- Middleware 套用順序middleware.go 的 CORSMiddleware 要在 requireSameOriginOrNoOrigin 之前)
---
### M8-9Boot-ID 端到端整合 + Restart 重連
**預估**1 人天
**負責 Agent**Backend Agent + Frontend Agent
**依賴**M8-4 + M8-7
**任務**
1. **整合驗證**M8-4 的 server 端 boot-id API + M8-7 的前端 polling 串起來
2. **Restart 情境驗證**
- 開 app → Open in Browser
- 在 Wails 控制台按 Restart
- 瀏覽器 tab 在 3-5 s 內自動 reload
- Reload 後 server 是新的 port 也能正常連(因為 Next.js 是 static exportURL path 不變)
3. **自動開瀏覽器**R5-4 + R5-D2 + R5-D3
- **R5-D2分平台預設**macOS/Windows 預設 `AutoOpenBrowser=true`Linux 預設 `false`
- **R5-D3每次都開**:只要 `AutoOpenBrowser=true`,每次 Start/Restart 成功都呼叫 `OpenInBrowser("")` — **取消**原 v2.0 的 per-session-once 設計
- 使用者在 Preferences 切換 → 立即持久化到 `preferences.json`atomic write
4. **Restart 期間 port 行為**F-2 強制保留):
- Restart 不允許 port fallback`startWithPort(oldPort, forceMatch=true)`
- 舊 port 被佔用 → 進 Error state + 發 OS 通知
- 正常 Restart → 新 server 用原 port → 瀏覽器 tab 偵測 boot-id 變 → reload → URL 原 port 仍有效 → 自動連上
- **不會**發生 v2.0 所述「port 變動後瀏覽器連不上」的情境
**驗收條件**
- 關機情境Wails 關 → server 停 → 瀏覽器 tab**WebSocket shutdown-imminent 秒內顯示 overlay**(不是 15 s 了Minor 4
- Restart 情境Wails 按 Restart3-5 s 內瀏覽器自動 reload**URL 原 port 仍有效**F-2UI 正常
- 首次開 appmacOS/Windows瀏覽器自動跳出新 tab
- 首次開 appLinux瀏覽器**不**自動開R5-D2 預設 false
- Preferences 關閉後再開 app瀏覽器不自動開
- **多次按 Restart**:若 `AutoOpenBrowser=true`,每次 Restart **都會呼叫 `OpenInBrowser`**R5-D3。OS 瀏覽器通常聚焦既有 tab 而不是開新 tab使用者可接受
- R5-E 啟動進度面板Starting 期間顯示 6 階段進度,完成後淡出
**Reviewer 檢查重點**
- `app.go` 中**不存在** `autoOpenedThisSession` 欄位v2.0 已砍)
- Restart 呼叫 `OpenInBrowser` 的行為有在 M8-9 手動測試紀錄中驗證
- `DefaultPreferences()` 三平台分支在單元測試或手動測試中涵蓋
- Restart 的 port 強制保留邏輯正確(錯的 mock 情境下會進 Error state 而非 fallback
- boot-id 在 server 重啟後真的變(不會意外 memoize 舊值)
- polling 的 `consecutiveFailures` 在 Restart 期間不會錯誤累積到 3Restart 約 3 s只有 0-1 次失敗)
---
### M8-10端到端 build + smoke test
**預估**1 人天
**負責 Agent**DevOps Agent + Testing AgentQA
**依賴**M8-1 到 M8-9 全部完成
**任務**
1. **macOS**`make clean-all && make dmg` → `dist/visiona-local.dmg`
- 安裝到全新 mac或 VM
- 8 核心驗收v2.1 擴充):
- ✅ 打開 app 先看到**啟動進度面板**6 階段R5-E完成後淡出顯示**控制台 UI**(不是 splash / wizard / 白畫面)
- ✅ 點 Open in Browser 後瀏覽器載入 Next.js Web UI
- ✅ Web UI 沒有 URL tabsource-selector 清乾淨)
- ✅ Web UI 沒有 Mock 模式切換Settings > Hardware 乾淨)
- ✅ 點 Stop 後瀏覽器**秒內看到 Offline Overlay**WebSocket 廣播,不是 15 s 才顯示)
- ✅ **手動殺 server process → 收到 macOS 原生通知 + 控制台 Error banner**R5-D1
- ✅ **R5-D2首次開 app 時 `preferences.json` 不存在macOS 預設自動開瀏覽器**Linux 相對要反過來驗證)
- ✅ 按 Restart 兩次,每次都會觸發 `OpenInBrowser`R5-D3砍掉 per-session-once
2. **Windows**:在 Windows 機器上 `make clean-all && make exe`
- 同樣 5 核心驗收
- **注意**R5-7 同意 M7 Windows 先不管,但這次 M8-10 要順帶驗證(做完再驗)
3. **Linux**:在 Ubuntu 上 `make clean-all && make appimage`
- 同樣 5 核心驗收
4. **LGPL 稽核**:三個平台的 installer 安裝後,`<install>/bin/` 下都有 `ffmpeg + ffprobe + ffmpeg-COPYING.LGPLv3`
5. **Installer size 對照**
- macOS .dmg預期 ~80-100 MBv1 是 220 MB砍 yt-dlp 35 MB + 砍 GPL ffmpeg 77 MB 換成 LGPL ~20 MB = 約 128 MB 降到 ~100 MB
- Windows .exe預期 ~280-320 MBv1 是 ~380 MB
- Linux .AppImage預期 ~240-280 MBv1 是 ~317 MB
6. **回歸測試**
- Kneron KL520 / KL720 實機跑推論(若有硬體)
- 5 種副檔名上傳影片
- 批次影像上傳
- Camerawebcam串流
**驗收條件**
- 三平台 installer 全部能裝、能啟動、能進入控制台、能 Open Browser、能跑推論
- LGPL 合規稽核:`grep -r 'enable-gpl' vendor/ffmpeg/` 無輸出
- 5 核心驗收 checklist 全綠
- Installer size 在預期範圍內
**Reviewer 檢查重點**
- 驗收 checklist 每一項都有實測截圖 / log
- 回歸測試涵蓋 PRD v2 所列的核心 user story
---
## 3. 風險與緩衝
- **M8-3 macOS 自 build ffmpeg 可能踩坑**configure flag 組合、pkg-config 環境、codesign預估 1.5 人天但可能實際花 2 人天。buffer 放在 M8-10 的 1 人天裡
- **M8-4b R5-E 6 階段 event emit 的非阻塞保證**Wails v2 IPC 若慢,可能把啟動流程本身拖慢。需在 M8-4b 後做 mock 測試(每個階段插 dummy 延遲)確認 watcher goroutine 與 event emit 不互相阻塞
- **M8-4 OS 通知三平台實測**R5-D1macOS 首次呼叫會彈出通知授權對話框Windows 沒裝 BurntToast 會 fallback `msg *`(某些版本可能 blockLinux 需要 `libnotify-bin` 已安裝。這三者都需要實機驗證,預估 0.3 天
- **M8-5 控制台 UI 的 dark mode** 可能需要反覆調整:若實測後使用者不喜歡預設配色,留給 Design Agent 一輪調整(不在本次範圍內)
- **M8-4 高頻 log 壓測**R-v2-3若 Wails v2 EventsEmit 真的 flaky要切 micro-batch 升級成 throttling + coalescing可能再多 0.5 人天
### 總工時重估
| Milestone | v2.0 | v2.1 | 差異原因 |
|-----------|------|------|---------|
| M8-1 | 0.5 | 0.5 | — |
| M8-2 | 0.5 | 0.5 | — |
| M8-3 | 1.5 | 1.5 | — |
| M8-4 | 1.5 | **2.0** | +0.3 R5-D1 OS 通知、+0.2 Q4 7+1 秒 shutdown modal |
| **M8-4b** | — | **1.0** | **新增R5-E 階段化啟動** |
| M8-5 | 2.0 | 2.0 | — |
| M8-6 | 0.5 | 0.5 | — |
| M8-7 | 1.0 | **1.3** | +0.3 Minor 4 WebSocket shutdown-imminent 訂閱 |
| M8-8 | 0.5 | 0.5 | — |
| M8-9 | 1.0 | 1.0 | — |
| M8-10 | 1.0 | 1.2 | +0.2 核心驗收項目從 5 個擴為 8 個 |
| **合計** | **10.0** | **12.0** | **+2 人天** |
加上 bufferM8-3 可能踩坑 + M8-4 高頻 log 壓測可能重構)~1 人天 → **建議 Orchestrator 對使用者回報 ~13 人天**。
---
## 4. 交棒給 Orchestrator 的清單
| Milestone | 誰 | 觸發條件 |
|-----------|-----|---------|
| M8-1 | Backend + Frontend並行| 使用者確認 TDD v2.1 |
| M8-2 | Backend + Frontend並行| 同 M8-1 |
| M8-3 | DevOps + BackendmacOS host 可用)| 同上,不依賴 M8-1/2 |
| M8-4 | Backend | M8-1 + M8-2 砍完 |
| **M8-4b** | **Backend + Frontendvanilla JS** | **M8-4 完成(需要 ServerController + WebSocket hub** |
| M8-5 | Frontend | M8-4b bindings + event schema 可用 |
| M8-6 | Frontend | 可與 M8-1/2/5 平行 |
| M8-7 | Frontend | M8-4 boot-id endpoint + WebSocket `/ws/system` 就緒 |
| M8-8 | Backend | 可獨立進行 |
| M8-9 | Backend + Frontend | M8-4 + M8-4b + M8-7 完成 |
| M8-10 | DevOps + Testing | 全部完成 |
**提醒**:每個 milestone 完成後Orchestrator 啟動 Reviewer 審查,審查通過才進下一個 milestoneper 根目錄 CLAUDE.md 「強制 Review 規則」)。