# v2/milestone-plan.md — M8 開發 milestone 拆分 > 所屬:TDD v2 §2.7 > 版本:v2.1(2026-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-3:ffmpeg 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-dlp(M8-1 會處理刪) 4. **Installer 同步**(§8): - `installer/windows/visiona-local.iss` 新增 ffprobe 和 LGPL license 行 - `installer/linux/build-appimage.sh` 迭代 tool 改為 `ffmpeg ffprobe` **驗收條件**: - `make vendor-ffmpeg` PASS(macOS),binary 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-4:Server 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 Agent(Go / 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 → hex,Architect 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:/api/system/boot-id` 回傳 JSON 含 bootId(hex 32 字元) - WebSocket `server:shutdown-imminent` 廣播:用 websocat 連 `/ws/system`,按 Stop 後秒收到廣播 **Reviewer 檢查重點**: - LogBuffer thread-safety(mutex 正確) - 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 write(tmp + 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 Agent(Go / Wails)+ Frontend Agent(vanilla 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 init、2=Python runtime、3=server binary spawn + health check、4=first `/api/devices` 查詢、5=OpenInBrowser call、6=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 state(watcher 總時檢查有效) - 日常啟動(非首次):~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-5:Wails 控制台 vanilla UI 改寫 **預估**:2 人天 **負責 Agent**:Frontend Agent(vanilla 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: - ✅ 看到控制台 UI(status / log / actions / preferences),不是 splash / blank / Next.js - ✅ Start 按鈕能啟動 server,狀態卡片變 Running 綠色 badge - ✅ Log panel 即時顯示 server stdout(看到 Gin log) - ✅ Stop 按鈕能停 server,badge 變灰 - ✅ Restart 按鈕能重啟(狀態過程正確) - ✅ Open in Browser 開瀏覽器到正確 URL - ✅ Reveal Logs 開啟 `/logs/` 資料夾 - ✅ Clear Logs 清畫面但不動磁碟檔 - ✅ Preferences 切 `openBrowserOnStart` 持久化到 `preferences.json` - ✅ Dark mode 跟隨系統(macOS 切換 Dark Mode 後控制台自動變色) - ✅ Log panel 按 Pause 後自動捲動停止,解除後恢復 - `make wails-macos` 能 build 出 .app,打開後看到控制台 UI(M7-B 教訓) **Reviewer 檢查重點**: - vanilla JS 沒引入任何 npm 依賴(`package.json` 不存在於 visiona-local/frontend/) - Log panel 在高頻(1000 行/秒)下不會 freeze UI(batch render 有效) - i18n loader SSR-safe(不適用,但 Wails 沒 SSR 概念) - icons/ 下的 SVG 符合 inline 使用的簡潔規則(viewBox + single path) - Action bar 在每個 state 下 button enable/disable 正確(disable 矩陣全部驗證) --- ### M8-6:Web 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-7:Web 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` 的 cleanup(useEffect return)正確 cancel - `AbortSignal.timeout(3000)` 在舊瀏覽器相容性(若需支援 Chrome < 103 則 fallback) - Overlay 元件 a11y(`role="alertdialog"` + `aria-labelledby`) - i18n 新增 key 三個檔同步(types / zh-TW / en) --- ### M8-8:CORS + 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-9:Boot-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 export,URL 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 按 Restart):3-5 s 內瀏覽器自動 reload,**URL 原 port 仍有效**(F-2),UI 正常 - 首次開 app(macOS/Windows):瀏覽器自動跳出新 tab - 首次開 app(Linux):瀏覽器**不**自動開(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 期間不會錯誤累積到 3(Restart 約 3 s,只有 0-1 次失敗) --- ### M8-10:端到端 build + smoke test **預估**:1 人天 **負責 Agent**:DevOps Agent + Testing Agent(QA) **依賴**: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 tab(source-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 安裝後,`/bin/` 下都有 `ffmpeg + ffprobe + ffmpeg-COPYING.LGPLv3` 5. **Installer size 對照**: - macOS .dmg:預期 ~80-100 MB(v1 是 220 MB,砍 yt-dlp 35 MB + 砍 GPL ffmpeg 77 MB 換成 LGPL ~20 MB = 約 128 MB 降到 ~100 MB) - Windows .exe:預期 ~280-320 MB(v1 是 ~380 MB) - Linux .AppImage:預期 ~240-280 MB(v1 是 ~317 MB) 6. **回歸測試**: - Kneron KL520 / KL720 實機跑推論(若有硬體) - 5 種副檔名上傳影片 - 批次影像上傳 - Camera(webcam)串流 **驗收條件**: - 三平台 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-D1):macOS 首次呼叫會彈出通知授權對話框;Windows 沒裝 BurntToast 會 fallback `msg *`(某些版本可能 block);Linux 需要 `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 人天** | 加上 buffer(M8-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 + Backend(macOS host 可用)| 同上,不依賴 M8-1/2 | | M8-4 | Backend | M8-1 + M8-2 砍完 | | **M8-4b** | **Backend + Frontend(vanilla 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 審查,審查通過才進下一個 milestone(per 根目錄 CLAUDE.md 「強制 Review 規則」)。