diff --git a/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review-round2.md b/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review-round2.md
new file mode 100644
index 0000000..8409cbe
--- /dev/null
+++ b/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review-round2.md
@@ -0,0 +1,211 @@
+# M9-4 Reviewer Round 2 — Frontend FW UI(第 2 輪修改驗證)
+
+> 審查日期:2026-05-25
+> Reviewer:Reviewer Agent
+> 審查對象:Frontend 第 2 輪修改(i18n namespace 重組 + 3 Major + 8 Minor 修法驗證)
+> 審查層:A 層(per-task delta)
+> 第 1 輪報告:`.autoflow/05-implementation/review/m9-4-frontend-firmware-review.md`
+
+---
+
+## TL;DR
+
+- **第 2 輪審查結果:✅ 通過**
+- **MJ1 方案選擇:接受方案 A(namespace = `settings.firmware.*`)** — 採納 Frontend 的 pushback、Design Spec §9 v2.2 為 SoT、未做正式 Design Spec 改寫前 Reviewer 不該翻案
+- **第 1 輪 issue 修了 / 沒到位**:12/12 應修項全部到位、3 項刻意留 follow-up(Frontend 給的理由合理)、1 項 backend 不適用本輪(MJ3 屬 Backend / Testing 範圍)
+- **第 2 輪新發現**:0 Critical / 0 Major / 0 Minor / 2 Suggestion(都屬 nice-to-have、不阻擋)
+- **是否阻擋 M9-5**:否
+- **是否需 Frontend 第 3 輪**:否
+- **是否升 Security Auditor**:否(ContactSupport mailto handler 已驗證安全、無 XSS / open redirect)
+
+---
+
+## 方法說明
+
+本輪採「delta-only」審查:不重審第 1 輪已通過細節(避免疲勞與 false positive),聚焦:
+1. 第 1 輪每條 issue 是否真的修了 / 修對方向
+2. 修改本身是否引入新問題(regression)
+3. 9 個新 FirmwareErrorView tests 品質
+
+完整 5 軸 / 文件符合性 / B 層跨檔案比對等已在第 1 輪報告做過、本輪不重做。
+
+---
+
+## MJ1 方案選擇(Frontend pushback 評估)
+
+### Frontend 主張方案 A 的理由
+
+1. Design Spec v2.2 §9 是三方對齊後的正式 SoT、未做正式 Design Spec 改寫前 Frontend 不該自行翻案
+2. B 階段 M9-12 會加入 `settings.firmware` 下 §9.1 page 結構 / §9.2 device card label / §9.3 switch accordion / §9.5 downgrade modal 等更多 keys;現在 flat 結構 (`firmware.*`) 之後 M9-12 切換成 `settings.firmware.*` 會造成兩批 keys 分散維護
+
+### Reviewer 評估
+
+| 評估面向 | 結論 |
+|---------|------|
+| Design Spec §9 v2.2 是否正式 SoT | 是。三方 v2.2 已對齊、本輪 review 不應翻案 |
+| 方案 A 是否會與既有 i18n 結構衝突 | 不會。`settings` 是既有 namespace、`settings.firmware` 是合理子結構(types.ts:305 加註解明示「§9.1 page / §9.2 card label / §9.3 accordion / §9.5 downgrade modal 留待 M9-12 補」、forward-compatible) |
+| 方案 A vs B 的長期維護成本 | A 更低。B 階段一定要落 `settings.firmware.*`、現在做 A 等於一次到位、避免 M9-12 再 rename 一次(耗 PR + 影響 i18n test snapshot) |
+| Reviewer 第 1 輪偏好 B 的理由(commit cost 低、命名更簡潔)| 不成立。Frontend 已用 trivial rename + 更新 22 既有 test、cost 實測也很低;「命名簡潔」對單一檔案有意義、但跨 PRD/Design/i18n/code 對照查找時 namespace 對齊更值錢 |
+
+**Reviewer 最終決定**:**接受方案 A**。Frontend 的 pushback 邏輯成立、Design SoT 應優先於 Reviewer 個人偏好。本輪 MJ1 視為通過。
+
+---
+
+## 第 1 輪 issue 修改驗證(逐項)
+
+### Major(3 項)
+
+| Issue | 第 1 輪要求 | 第 2 輪修法 | 驗證結果 |
+|-------|------------|------------|---------|
+| **MJ1** i18n namespace 與 §9 不一致 | 二擇一:A 搬到 `settings.firmware.*` / B 改 Design Spec | 採方案 A — `types.ts:310-375`、`zh-TW.ts:305-370`、`en.ts:305-370` 三檔同步搬入 `settings.firmware.*`;`firmware-store.ts:151-199` 8 條 errorMessageKeyFor 路徑 + 4 條 primaryActionKeyFor 路徑全改;5 個元件 (`firmware-badge / firmware-upgrade-button / firmware-upgrade-dialog / firmware-progress-view / firmware-error-view`) 所有 t() 呼叫全用新路徑;22 既有 + 9 新 firmware-store.test.ts 同步 | ✅ **完全到位**。grep `t\('firmware\.` 與 `'firmware\.`(不含 `settings.` 前綴)皆 **0 hit**、無死引用 |
+| **MJ2** FirmwareErrorView title 硬編碼中文 | 改 `t(...)` + 補 i18n key(或乾脆移除 title) | `firmware-error-view.tsx:158` 改 `title={t('settings.firmware.error.contactSupportTooltip')}`;types.ts:372 / zh-TW.ts:367 / en.ts:367 三檔都補 `contactSupportTooltip` key(中:「請聯絡技術支援」/ 英:`Please contact technical support`);同時把 disabled 改 enabled、補 `handleContactSupport` 用 mailto: 開啟(line 81-88)— 算修 MJ2 + M6 一併 | ✅ **完全到位**。額外修了 M6(disabled ContactSupport 與 UX 衝突)、合理副產品 |
+| **MJ3** Backend smoke test schema 用 `phase` 而非 `stage` | Backend / Testing 在 M9-5 階段補 | 本輪 Frontend 範圍外、未動 backend test | N/A — 不屬本輪 frontend 第 2 輪範圍、按計畫由 testing agent 於 M9-5 補 |
+
+### Minor(8 項)
+
+| # | 第 1 輪要求 | 第 2 輪修法 | 驗證結果 |
+|---|------------|------------|---------|
+| **M1** | 確認 1.5s modal 關閉是否與 §5.4 toast 6s 相容、加註解 | `firmware-upgrade-dialog.tsx:103-104` 註解明寫「modal 1.5s 後關閉、toast 由 showSuccess 自身 6s duration 控制」+ AC-FW-1.3 5 秒提示 | ✅ 到位 |
+| **M2** | 補 TODO 條目給 M9-12 追蹤對比 | `firmware-badge.tsx:39-41` 新增 `TODO(M9-12)` 註解明示用 axe-core 實測對比、< 4.5:1 改 component token、特別注意 amber-400 + emerald-500 暗模 | ✅ 到位 |
+| **M3** | 確認 estimatedDurationSeconds fallback 語意統一 / 加註解 | `firmware-store.ts:202-217` 整段 JSDoc 改寫「兩種 fallback 數值不同是刻意的:『不知道有沒有 type』和『有 type 但認不出』資訊量不同」;test (`firmware-store.test.ts:247-253`) 註解同步說明 | ✅ 到位(決定保留差異 + 文件化、合理) |
+| **M4** | showSuccess 預設停留是否符合 §5.4 6 秒、不足要加 duration | `lib/toast.ts` 加 `ToastOptions { duration?: number }`、4 個 toast helper 都支援;`firmware-upgrade-dialog.tsx:99` 用 `{ duration: 6000 }` 顯式傳;註解引用 Reviewer M1+M4 | ✅ 到位 |
+| **M5** | 補 FirmwareErrorView 元件渲染測試 | 新檔 `tests/components/firmware-error-view.test.tsx` 共 9 tests:destructive 3 種 brick warning + ContactSupport(it.each)/ contact mailto 點擊 / recoverable 2 種 / errorCode 顯示 / collapsible 預設收合 / Close callback | ✅ 到位(品質評估見下節)|
+| **M6** | disabled ContactSupport 改 enabled + 實際動作 | `firmware-error-view.tsx:81-88` 加 `handleContactSupport`:mailto: + subject 帶 errorCode + body 帶 technicalInfo + `target="_blank"`;line 153-162 改用 destructive variant + onClick | ✅ 到位 |
+| **M7** | 移除 device-card.tsx 與 button 元件的雙重 firmwareCanUpgrade gate | `device-card.tsx:61-64` 註解明示「由 FirmwareUpgradeButton 內部決定」、實際 JSX 直接 `` 無 `device.firmwareCanUpgrade &&` 外層 gate;單一責任落地 | ✅ 到位 |
+| **M8** | 把 fetchActiveFirmwareTasks 移到專用檔 / 加註解說明刻意設計 | `firmware-store.ts:253-262` 補完整 JSDoc「刻意設計為 module-level helper、不放 store action」+ 三個理由(無 reactive state / Wails callback / 不需 subscribe);位置保留在 store 檔(Frontend 選擇文件化而非移檔) | ✅ 到位(文件化方案合理、避免 churn)|
+
+### Suggestion(5 項)
+
+| # | 第 1 輪建議 | 第 2 輪處置 | 驗證結果 |
+|---|------------|------------|---------|
+| **S1** | 對齊 §5.1 寬度 480px | `firmware-upgrade-dialog.tsx:157-158` 加 `className="sm:max-w-[480px]"` + 註解引用 Reviewer S1 | ✅ 修了 |
+| **S2** | M9-5 testing 階段加 stage-percent 對照測試 | 留 follow-up 給 Testing Agent M9-5 階段 | ⏸ 合理保留 — 屬 testing agent / E2E 範圍 |
+| **S3** | Dialog selector 優化 re-render | 留 follow-up — 屬 perf nice-to-have、非問題 | ⏸ 合理保留 |
+| **S4** | FirmwareUpgradeDialog integration test | 留 follow-up — 屬 Testing Agent M9-5 / M9-12 範圍 | ⏸ 合理保留 |
+| **S5** | 抽 formatTechnicalInfo helper | `firmware-error-view.tsx:23-40` 抽出 `formatTechnicalInfo(progress: FirmwareProgressEvent): string`、JSDoc 引用 Reviewer S5;handleCopy + `
` + handleContactSupport 三處共用同一輸出 | ✅ 修了(且額外用在 mailto body、複用更廣)|
+
+**統計**:應修 / 已修 = 12 / 12(含 MJ1 / MJ2 / 8 Minor + S1 / S5),刻意保留 3 項(S2 / S3 / S4),不適用本輪 1 項(MJ3 屬 Backend)。
+
+---
+
+## 9 個新 FirmwareErrorView tests 品質評估
+
+檔案:`frontend/src/tests/components/firmware-error-view.test.tsx`(165 行)
+
+| Test 群組 | 覆蓋 | 品質 |
+|---------|------|------|
+| destructive reasons(3 種)`it.each` brick warning + 不顯示 Retry + 顯示 ContactSupport | `disconnect_during_op / verify_mismatch / verify_not_found` | 高 — 用 `it.each` 涵蓋三種、`getByRole('note')` 驗 brick warning、`getAllByRole('button')` + label 對比驗 Retry 不存在 + ContactSupport 存在 |
+| ContactSupport mailto 點擊 | `vi.spyOn(window, 'open')` mock + `fireEvent.click` + 檢查 href schema `^mailto:` + decodeURIComponent body 含 stage / reason | 高 — 安全驗證到位(href 是 mailto: scheme 不是 http:)、技術資訊真的進 body |
+| recoverable reasons(2 種)顯示 Retry | `connect_failed` Retry 按鈕 + onRetry callback;`scan_not_found` 顯示 ReplugRetry | 高 — `queryByRole('note')` 確認沒 brick warning(negative case)、`fireEvent.click` 驗 callback |
+| errorCode 顯示 | `getAllByText(/FW_E102/).length >= 1`(同時在 `` 與 `
` 出現、刻意接受兩處)| 中-高 — 接受重複出現是對的、若用 `getByText` 會誤判 |
+| collapsible 預設收合 | `container.querySelector('details').open === false` | 高 — 直接驗 DOM state |
+| Close 觸發 onClose | `fireEvent.click(closeBtn) → onClose 1 次` | 高 |
+
+**整體評估**:
+- ✅ 用 `it.each` 涵蓋三 destructive reason、簡潔且維護友善
+- ✅ 用 i18n label regex `/Contact|聯絡技術支援|Support/` 同時兼容中英環境、CI 切 locale 不會 fail
+- ✅ jsdom `window.open` mock 寫法正確(spyOn + mockImplementation(() => null))
+- ✅ 驗 mailto href schema 是安全把關(防止意外被改成 http: 開外部頁面)
+- ⚠️ 缺 **handleCopy** 測試(clipboard write 行為)— 第 1 輪 review 也沒列、不算缺陷、屬 nice-to-have
+- ⚠️ 缺 **techOpen toggle** 測試(onToggle 後 open=true)— 同上、屬 nice-to-have
+
+**結論**:9 個 tests 品質高、達到第 1 輪 M5 要求;漏的兩項屬 nice-to-have、不阻擋本輪通過。
+
+---
+
+## 第 2 輪新發現
+
+### Critical
+
+無。
+
+### Major
+
+無。
+
+### Minor
+
+無。
+
+### Suggestion
+
+| # | 檔案 / 位置 | 建議 |
+|---|------------|------|
+| **R2-S1** | `firmware-error-view.tsx:81-88` `handleContactSupport` | mailto: 地址 `support@kneron.com` 硬編碼在元件內。雖然 mailto 不會被 attacker 替換(純 link)、但若日後 support email 改 / 多語系不同 region 不同信箱、會要改元件本身。可考慮把 `MAILTO_SUPPORT` 抽到 `lib/config.ts` 或讀 i18n(不過 mailto 地址通常不 i18n)。屬 nice-to-have、不阻擋。 |
+| **R2-S2** | `tests/components/firmware-error-view.test.tsx` | 缺 handleCopy 測試(clipboard 行為 + copied state 切換 2s 後 reset)+ techOpen toggle 測試。可在後續任務(M9-12 或 testing 補充階段)補。屬 nice-to-have、不阻擋本輪。 |
+
+---
+
+## Regression 檢查(修改本身是否引入新問題)
+
+| 風險點 | 檢查結果 |
+|-------|---------|
+| **i18n namespace 重組漏 key** | grep `t\('firmware\.`(不含 settings 前綴)= 0 hit;grep `'firmware\.` 同 0 hit。所有舊路徑 100% 切完。中英對齊:types.ts:310-375 vs zh-TW.ts:305-370 vs en.ts:305-370 同 namespace 結構、所有 key 三邊都有對應(無 missed key) |
+| **ContactSupport mailto 安全性** | mailto: scheme 是 RFC 6068 standard、不會觸發 XSS(不執行 JS);target="_blank" 不引入 open redirect 風險(mailto: 由 OS 端 mail handler 處理、不會是 http:);subject + body 用 `encodeURIComponent` 包裝、即使 progress.errorCode 內含特殊字元也安全 |
+| **formatTechnicalInfo helper rawError leak** | helper 抽出後同時用在 (a) handleCopy clipboard / (b) `` 區 / (c) mailto body — 三處都是「使用者主動觸發 + 預設收合的 details / 主動點 copy / 主動點 contact」、不是被動曝露在主畫面;rawError 行只在有值時才加(line 37 `if (progress.rawError)`);無 leak 到非預期位置 |
+| **toast duration 6s 修改** | `lib/toast.ts` 改成支援 `ToastOptions`、向下相容(既有 caller 不傳 options 用 sonner 預設 4000ms);只有 firmware-upgrade-dialog.tsx:99 顯式傳 6000;不影響既有 toast 行為 |
+| **22 既有 firmware-store test 是否同步更新** | 看 firmware-store.test.ts:168-234 — 8 條 errorMessageKeyFor 直接 reason / 4 條 stage fallback / 7 條 primaryActionKeyFor 全部 expect 字串都改為 `settings.firmware.error.message.*` / `settings.firmware.error.action.*`,namespace 對齊完整 |
+| **device-card.tsx 移除外層 gate** | line 64 `` 無 `device.firmwareCanUpgrade &&`;FirmwareUpgradeButton.tsx:26 內部 `if (!device.firmwareCanUpgrade) return null` 守住、不會 render;isBusy disabled 條件保留、避免 race;single responsibility 落地 |
+| **estimatedDurationSeconds 行為未變** | 函數 line 212-217 邏輯仍是 `if (!deviceType) return 60; if includes('kl720') return 180; return 30`、回傳值未變、test 三個 expect 也未變、只是 JSDoc + test 註解補充說明刻意設計 |
+
+無 regression。
+
+---
+
+## §12.2 通用退出條件(6 條、Round 2 重檢)
+
+| # | 條件 | 狀態 |
+|---|------|------|
+| 1 | No silent failures | ✅ 未觸發。`firmware-error-view.tsx:74-77` clipboard reject 靜默是 design 規格允許;新 handleContactSupport 失敗(window.open 回 null)也是 OS handler 沒 mailto 程式、不是 silent failure(mailto 已被 OS 開啟) |
+| 2 | No dead code | ✅ 未觸發。grep TODO / FIXME 只在 `firmware-badge.tsx:39 TODO(M9-12)` — 是預留 M9-12 任務追蹤、不是 dead code |
+| 3 | No hardcoded secrets | ✅ 未觸發。mailto: `support@kneron.com` 不算 secret(公開聯絡信箱);R2-S1 提到 nice-to-have 抽常數 |
+| 4 | No unsafe HTML / SQL | ✅ 未觸發。grep `dangerouslySetInnerHTML / v-html` = 0 hit;mailto subject + body 用 `encodeURIComponent` 包;React template literal 全自動 escape |
+| 5 | Doc 同步 | ✅ 觸發已解。第 1 輪 MJ1 已修、namespace 100% 對齊 Design Spec §9 |
+| 6 | Working tree clean | N/A — reviewer 自己只產 report |
+
+---
+
+## A 層 verification(Round 2 simplified)
+
+本輪屬 delta-only review、不重做第 1 輪完整 5 軸;以下為 Round 2 必過項:
+
+- [x] **R-A1(delta)**:逐項驗每條第 1 輪 issue 的修法(見「第 1 輪 issue 修改驗證」表)
+- [x] **R-A2**:i18n namespace 重組後 PRD/TDD/Design Spec §9 對齊一致(grep 0 hit 死引用 + 中英 + types 三邊對齊)
+- [x] **R-A3**:第 2 輪新發現 0 Major、2 Suggestion 都附建議
+- [x] **R-A4**:優點段落見下節
+- [x] **R-A5**:Needs investigation:0(本輪所有疑點都已決議)
+- [x] **R-A6**:§12.2 通用 6 條全跑過
+
+---
+
+## 優點(R-A4)
+
+1. **Frontend 的 pushback 邏輯紮實**:採方案 A 並用 SoT 論證、加 forward-compatible 註解(types.ts:303-309)讓 M9-12 接手更平順、不是被動接受 reviewer 也不是無腦堅持。是好的協作姿態。
+2. **M6 「順便修」做得對**:第 1 輪只標 disabled / UX 衝突、Frontend 把它升格為實際可用按鈕(mailto + subject + body 帶 errorCode + technicalInfo);不只修「Reviewer 抓出來的」、還主動改善 UX。
+3. **formatTechnicalInfo helper 抽出後三處共用**:handleCopy / `` / handleContactSupport mailto body —— S5 不只解決 readability、實際讓「複製 vs 寄信」內容保證一致、是有實效的重構。
+4. **i18n duration: 6000 配 §5.4** + lib/toast.ts 補 ToastOptions API:解 M4 同時把 toast util 介面 forward-compatible(未來其他 toast 也能調 duration),不是只 patch firmware-upgrade-dialog 一處。
+5. **9 個新 FirmwareErrorView tests 涵蓋完整**:mailto href schema 驗證(`^mailto:`)+ body decodeURIComponent 內容驗證、是測試到「行為 + 安全」兩面、不是只測 render。
+6. **JSDoc 引用 Reviewer ticket 編號**:`(Reviewer S5)` / `(Reviewer M1+M4)` / `(Reviewer S1)` / `(M3 釐清)` — 修改可追溯回 review 條目、未來看 code 知道為什麼這樣寫。
+7. **M3 處置成熟**:沒急著統一 fallback 數值(避免行為 regression)、改用 JSDoc + test 註解雙重文件化「刻意設計」;是「文件化 over 程式變更」的好範例。
+
+---
+
+## 結論
+
+**Round 2 審查結果:✅ 通過、無需 Frontend 第 3 輪**
+
+第 1 輪 12 項應修 issue 全部到位、3 項刻意 follow-up 理由合理、1 項 (MJ3) 屬 Backend 範圍按計畫外延。MJ1 採方案 A 是正確選擇、Frontend pushback 邏輯成立、Reviewer 接受。第 2 輪新發現僅 2 個 Suggestion、都屬 nice-to-have、不阻擋。
+
+**是否阻擋 M9-5**:否。本輪完全不阻擋 M9-5 三平台 KL520+KL720 實機 E2E。
+
+**是否需 Frontend 第 3 輪**:否。除非 M9-5 階段 testing agent 發現新 issue、否則 M9-4 frontend 部分本輪結案。
+
+**剩餘待 follow-up**(非阻擋):
+- MJ3:Backend smoke test schema、由 Testing Agent / Backend Agent 在 M9-5 階段補
+- S2:stage-percent 對照測試、Testing Agent M9-5
+- S3:Dialog selector 優化、perf nice-to-have
+- S4:FirmwareUpgradeDialog integration test、Testing M9-5 / M9-12
+- R2-S1:mailto 地址抽常數、nice-to-have
+- R2-S2:handleCopy / techOpen toggle 測試、nice-to-have
+
+**是否升 Security Auditor**:否。ContactSupport mailto handler 已驗證安全(RFC 6068 scheme + encodeURIComponent + 不引入 XSS / open redirect / hardcoded secret)。
diff --git a/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review.md b/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review.md
new file mode 100644
index 0000000..34e12b7
--- /dev/null
+++ b/local-tool/.autoflow/05-implementation/review/m9-4-frontend-firmware-review.md
@@ -0,0 +1,466 @@
+# Reviewer Report — M9-4 Frontend FW UI + Backend WS hot-fix
+
+> 審查日期:2026-05-25
+> Reviewer:Reviewer Agent
+> 審查對象:M9-4 Frontend FW UI(12 新 + 4 修改 = 16 檔、3052 行)+ Backend WS hot-fix(2 新 + 1 修改)
+> 審查層:A 層(per-task)+ B 層(多檔案 PR、18 檔 / >500 行)
+> 依據文件:Design Spec v2/firmware-management.md(948 行)/ PRD feature-firmware-management.md / TDD v2/firmware-management.md
+
+---
+
+## TL;DR
+
+- **審查結果:⚠️ 需修改後通過**
+- **Critical:0 | Major:3 | Minor:8 | Suggestion:5**
+- **是否阻擋 M9-5:否**(3 個 Major 都是 UX/i18n/test schema 層級、不阻擋三平台實機 E2E;M9-5 可平行開跑、Frontend 第 2 輪修這 3 項)
+- **是否升 security agent:否**(5 軸 security 粗篩無發現、`navigator.clipboard?.writeText` 已守、無 XSS / hardcoded secret、loopback origin 由 backend 既有 `CheckOrigin` 共用)
+- **是否需 frontend 第 2 輪**:是(3 個 Major + 部分 Minor 修完即可)
+- **是否需 backend 第 2 輪**:否(hot-fix 對稱 flash_ws.go、smoke test 通過、Minor M5 屬 testing agent 範圍、可在 M9-5 testing 階段補)
+
+---
+
+## 審查範圍
+
+| # | 檔案 | 行數 | 類別 |
+|---|------|------|------|
+| 1 | `frontend/src/types/device.ts` | +62 | Frontend / Types |
+| 2 | `frontend/src/stores/firmware-store.ts` | 262(新) | Frontend / Store |
+| 3 | `frontend/src/hooks/use-firmware-progress.ts` | 81(新) | Frontend / Hook |
+| 4 | `frontend/src/components/firmware/firmware-badge.tsx` | 95(新) | Frontend / Component |
+| 5 | `frontend/src/components/firmware/firmware-upgrade-button.tsx` | 51(新) | Frontend / Component |
+| 6 | `frontend/src/components/firmware/firmware-upgrade-dialog.tsx` | 227(新) | Frontend / Component |
+| 7 | `frontend/src/components/firmware/firmware-progress-view.tsx` | 109(新) | Frontend / Component |
+| 8 | `frontend/src/components/firmware/firmware-error-view.tsx` | 150(新) | Frontend / Component |
+| 9 | `frontend/src/components/firmware/index.ts` | 10(新) | Frontend / Barrel |
+| 10 | `frontend/src/components/devices/device-card.tsx` | +X | Frontend / Modified |
+| 11 | `frontend/src/lib/i18n/types.ts` | +71 | Frontend / i18n |
+| 12 | `frontend/src/lib/i18n/zh-TW.ts` | +68 | Frontend / i18n |
+| 13 | `frontend/src/lib/i18n/en.ts` | +68 | Frontend / i18n |
+| 14 | `frontend/src/tests/stores/firmware-store.test.ts` | 265(新、22 tests)| Frontend / Test |
+| 15 | `frontend/src/tests/components/firmware-badge.test.tsx` | 98(新、10 tests)| Frontend / Test |
+| 16 | `server/internal/api/ws/firmware_ws.go` | 50(新) | Backend / Hot-fix |
+| 17 | `server/internal/api/ws/firmware_ws_test.go` | 165(新、2 tests) | Backend / Test |
+| 18 | `server/internal/api/router.go` | +2 | Backend / Modified |
+
+**漏審檢查(R-B2)**:18 檔全部覆蓋、無漏審。
+
+---
+
+## 文件符合性檢查(R-A2)
+
+### PRD 功能符合性
+
+| PRD AC 項目 | 是否實作 | 實作位置 | 符合程度 | 備註 |
+|------------|---------|----------|---------|------|
+| AC-FW-1.1 FW badge 4 色(綠 / 黃 / 紅 / 灰)| ✅ | `firmware-badge.tsx:20-30` `computeBadgeState` | 完全 | legacy/unknown/current/older 四 state、優先序合理 |
+| AC-FW-1.1 衍生欄位(`firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion`)| ✅ | `types/device.ts:11-14` | 完全 | 與 TDD §3.1 一致 |
+| AC-FW-1.2 升級按鈕(黃 / 紅 badge 條件)| ✅ | `firmware-upgrade-button.tsx:26` 用 `firmwareCanUpgrade` gate | 完全 | 條件由 backend 衍生欄位決定、Frontend 不自己 derive |
+| AC-FW-1.2 progress modal stage 5-6(含 preparing/loading/flashing/verifying)| ✅ | `firmware-progress-view.tsx:42-57` | 完全 | 4 stage + done 對齊 §4.3 |
+| AC-FW-1.2 4 階段 / 3 階段 split(KDP1→KDP2 vs KDP2→KDP2)| ✅ | `firmware-store.ts:213-243` `stageOrdinal` | 完全 | `isLegacyUpgrade` 切 4 vs 3 stage、跟 Design §5.3 對齊 |
+| AC-FW-1.2 不可中斷警告 | ✅ | `firmware-upgrade-dialog.tsx:147-160` 拒絕 onOpenChange/ESC/outside-click | 完全 | R-FW-11 緩解、ESC + interactOutside 都防 |
+| AC-FW-1.3 升級成功 toast + rescan | ✅ | `firmware-upgrade-dialog.tsx:85-107` | 完全 | success effect → showSuccess + fetchDevices + 1.5s 後關閉 |
+| AC-FW-1.4 8 種 reason 對應 UI | ✅ | `firmware-store.ts:150-200` 三個 helper | 完全 | `errorMessageKeyFor` / `canRetryReason` / `primaryActionKeyFor` 三個 pure helper |
+| AC-FW-1.4 「複製錯誤訊息」按鈕 | ✅ | `firmware-error-view.tsx:120-131` | 完全 | clipboard fallback 靜默 OK |
+| AC-FW-1.4 技術資訊 collapsible | ✅ | `firmware-error-view.tsx:101-118` `` 元素 | 完全 | rawError 在 collapsible 內、不在主畫面 |
+| AC-FW-1.5 升級期間鎖住其他操作 | ✅ | `device-card.tsx:63-65` `disabled={isBusy}` | 完全 | 由 device-store 既有 `isBusy` 機制涵蓋 |
+| AC-FW-1.6 升級後 rescan | ✅ | `firmware-upgrade-dialog.tsx:100` `fetchDevices()` | 完全 | 但 5-8s wait 在 backend 處理、frontend 不需 |
+| AC-FW-1.7 KL520 ~30s / KL720 ~180s 預估 | ✅ | `firmware-store.ts:206-211` `estimatedDurationSeconds` | 完全 | chip 分流邏輯正確 |
+| AC-FW-2 降版 UI(B 階段 M9-12)| N/A | — | — | 本批不在範圍 |
+| AC-FW-3.5 KL630/KL730 升降版 | N/A | — | — | B 階段 M9-10、本批不在範圍 |
+
+**PRD 功能完整度**:M9-4 範圍內 14/14 條 AC 全部實作、無遺漏。
+
+### TDD 技術規格符合性
+
+| TDD 規格項目 | 是否符合 | 實作位置 | 備註 |
+|------------|---------|----------|------|
+| §3.1 衍生欄位 `firmwareIsLegacy / firmwareCanUpgrade / bundledFirmwareVersion` | ✅ | `types/device.ts:11-14` | 與 backend 一致 |
+| §3.4 stage → reason → i18n key 對應表(8 種)| ✅ | `firmware-store.ts:150-172` + i18n keys | 完全 mapping、附 stage-only fallback |
+| §4.2 `FirmwareProgress` schema | ✅ | `types/device.ts:57-75` | json field 對應 backend struct(已驗 `server/internal/firmware/types.go:34-53` 完全對齊:`elapsedMs/etaMs/rawError/beforeVersion/afterVersion/errorCode/reason`)|
+| §4.3 stage 命名(`preparing/loading/flashing/verifying/done/error`)| ✅ | `types/device.ts:35-41` | 完全對齊 |
+| §4.2 失敗欄位(`reason/rawError/beforeVersion/errorCode`)| ✅ | `types/device.ts:67-72` | 完全 |
+| §5.1 流程:先 connectAndWait → 再 POST API | ✅ | `firmware-upgrade-dialog.tsx:109-130` `handleStart` | 順序對、避免漏 event |
+| WS endpoint `/ws/devices/:id/firmware-progress` | ✅ | `use-firmware-progress.ts:52` + `router.go:125` + `firmware_ws.go:30` | 三端命名一致 |
+| WS room key `firmware:` | ✅ | `firmware_ws.go:30` + `firmware_handler.go:174` | 完全 |
+| §8.6 Wails graceful shutdown — `GET /api/firmware/active-tasks` | ✅ | `firmware-store.ts:252-262` `fetchActiveFirmwareTasks` | A 階段預埋、實際 UI 由 Wails / control-panel M9-12 接 |
+
+**TDD 規格符合度**:9/9 項全部符合。
+
+### 設計規格符合性
+
+| Design Spec 項目 | 是否符合 | 實作位置 | 備註 |
+|----------------|---------|----------|------|
+| §3.4 stage 命名(preparing/loading/flashing/verifying/done/error)| ✅ | `firmware-store.ts:36-41` | 完全 |
+| §4.2 Badge 規格(pill / radius.full / 11px medium)| ✅ | `firmware-badge.tsx:86` `rounded-full px-2 py-0.5 text-xs font-medium` | 視覺對齊 |
+| §4.2 Badge 顏色(綠 success / 黃 warning / 紅 destructive)| ⚠️ 部分 | `firmware-badge.tsx:38-43` 用 emerald/amber + semantic destructive/muted | 見 M2 — 暫代 Tailwind palette、Design §11.2 明示 6 個 component token 由 M9-12 落地、本批合理 |
+| §5.1 升級確認 modal 寬度 480px | ⚠️ 部分 | 沿用 shadcn `Dialog` 預設 max-w-lg | 見 Suggestion S1 — 視覺檢查時可微調 |
+| §5.2 升級進度 modal 不顯示 ✕ / 取消按鈕 | ✅ | `firmware-upgrade-dialog.tsx:154` `showCloseButton={!isInProgress}` | R-FW-11 緩解 |
+| §5.3 stage 對應表(preparing=5% / loading=20% / flashing=50% / verifying=90% / done=100%)| ⚠️ | UI 信任 backend `progress.percent`、不 hardcode 數值 | 見 Suggestion S2 — 信任 backend 是對的、但 backend 是否真按表發 percent 是另一個 audit 項 |
+| §5.4 成功 toast variant + 6s 停留 | ⚠️ | 用既有 `showSuccess`(既有 toast、停留時間未確認)| 見 Minor M4 |
+| §6 降版 UI(B 階段)| N/A | — | 不在範圍 |
+| §7.1 8 種 reason → friendly message | ✅ | `firmware-store.ts:errorMessageKeyFor` + i18n 8 個 key | 完全 mapping |
+| §7.1 destructive reason 不提供 Retry | ✅ | `firmware-store.ts:179-186` + `firmware-error-view.tsx:91-98, 142-146` | `disconnect_during_op / verify_mismatch / verify_not_found` 三種不可 retry |
+| §7.2 失敗 modal 含技術資訊 collapsible | ✅ | `firmware-error-view.tsx:101-118` | 對齊 |
+| §7.2 技術資訊預設收合 | ✅ | `firmware-error-view.tsx:36` `useState(false)` | 對齊 |
+| §9 i18n keys 51 個 + zh-TW / en 雙語 | ⚠️ 部分 | i18n 結構不照 §9 namespace(用 `firmware.error.messageXxx` flat 結構 vs Design 的 `settings.firmware.error.message.xxx` 巢狀)| 見 Major MJ1 — namespace 與 Design Spec §9 不同 |
+| §11.2 對比比率(current 4.7:1 / older 5.2:1 / legacy 4.8:1)| ⚠️ 部分 | 暫代 Tailwind palette、註解明示 M9-12 落地正式 token | 由 M9-12 Frontend 用 axe-core 實測(Design §11.2 備註明示)|
+| §12.1 R-FW-11 緩解(不可關 modal)| ✅ | `firmware-upgrade-dialog.tsx:146-160` 三層擋(onOpenChange / interactOutside / EscapeKeyDown)| 完全 |
+| §12.2 R-FW-11.5(嚴格 `===` 比對「DOWNGRADE」)| N/A | — | 降版 B 階段、本批不適用 |
+| §10 A11y(role / aria-label / aria-live)| ✅ | `firmware-progress-view.tsx:69-71, 84-88` + `firmware-error-view.tsx:74, 78` | `role="status" aria-live="polite"` 在進度區、`role="alertdialog"` 在 error modal、合理 |
+
+**設計規格符合度**:13/16 項完全符合、3 項部分符合(其中 MJ1 是 Major、其餘是合理的暫代 / 範圍外)。
+
+---
+
+## 5 軸 + 測試軸審查(R-A1)
+
+### 3.1 Correctness(正確性)
+
+實質判斷(≥20 字):實作正確性高、phase state machine 嚴謹、多裝置隔離(`activeDeviceId` mismatch return)對齊 flash-store M4 fix、WS connectAndWait 與 POST API 順序正確避免漏早期 event、destructive reason 不提供 Retry 安全防護到位、clipboard fallback 靜默 OK。發現的問題集中在 i18n namespace 對齊與測試 schema 偏差(見 Major / Minor)。
+
+具體 finding:
+- ✅ `firmware-store.ts:110-115` 多裝置隔離邏輯正確(`if (active !== null && active !== ev.deviceId) return`)
+- ✅ `firmware-upgrade-dialog.tsx:109-130` `connectAndWait` 在 POST 之前、避免 WS 還沒 open 漏掉 backend 早期 event(對齊 flash-store pattern)
+- ⚠️ `firmware-store.ts:248-249` test case `expect(estimatedDurationSeconds('unknown_device')).toBe(30)` 與函數實作不一致 — 函數 line 207-211 `if (!deviceType) return 60`,但 `'unknown_device'` 是 truthy 字串會走到 `if (low.includes('kl720')) return 180` → false → fallback return 30(KL520 路徑);測試正確、註解標「預設走 KL520 路徑」、但 line 247-249 同時測 `undefined → 60` 和 `'unknown_device' → 30` 邏輯不一致(一個沒 type 走 60、有 type 但不認得走 30)。語意不直覺、但兩個 case 都通過、屬 minor 表達問題(見 Minor M3)
+
+### 3.2 Readability(可讀性)
+
+實質判斷:命名清晰、註解充足(每個元件頂部有 phase / 對齊文件條目註解)、TypeScript 型別嚴格(沒 `any`)、phase 機(confirming → upgrading → success / error)邏輯線性、helpers 從 store 分離為 pure functions(便於測試與 M9-12 重用)。控制流可讀、最深巢狀 `firmware-error-view.tsx:110-117` 的 template literal 較長但語意清楚。
+
+具體 finding:
+- ✅ `firmware-store.ts:147-243` pure helpers 區塊分離(無 React 依賴)
+- ✅ 每個元件首段 JSDoc 都引用 Design / TDD 條目(追溯性強)
+- ⚠️ `firmware-error-view.tsx:109-117` 技術資訊 template literal 較複雜(多層 `${cond ? a : ''}`)、可讀但不易調整(見 Suggestion S5)
+
+### 3.3 Architecture(架構)
+
+實質判斷:架構正確遵循既有 pattern — Zustand store + React hook + 純元件分離、與 flash-store 結構對稱、useFirmwareProgress 100% mimic useFlashProgress;backend hot-fix 對稱 flash_ws.go(同 upgrader、同 CheckOrigin、同 RegisterSync、同 read pump pattern)。Activity store reuse `flash_*` event types 屬合理跨領域共用(兩者語意都是 device 進行中操作)、雖然 type literal 字面是 `flash_*` 但語意通用。
+
+具體 finding:
+- ✅ `firmware-store.ts:102-106, 121, 127-129` reuse `flash_start / flash_complete / flash_error` activity types — 合理權衡、避免 activity 型別爆炸
+- ✅ `firmware_ws.go` 與 `flash_ws.go` 結構幾乎 1:1 對稱、易維護
+- ✅ 純 helpers 從 store 分離(`errorMessageKeyFor / canRetryReason / primaryActionKeyFor / estimatedDurationSeconds / stageOrdinal`)方便 M9-12 重用、且測試友善
+
+### 3.4 Security(粗篩)
+
+實質判斷:**5 軸 security 粗篩無發現、不升級給 Security Auditor**。
+
+- ✅ **未 sanitize 的 HTML**:`firmware-error-view.tsx:108-118` 用 `{...template literal}` 渲染、React 自動 escape、無 dangerouslySetInnerHTML(已 grep `dangerouslySetInnerHTML / v-html`:零 hit)
+- ✅ **raw error 不在主畫面**:rawError 只出現在 collapsible `` 內(line 109-117)、預設收合(line 36)、Design §7.2 規格符合
+- ✅ **clipboard 操作**:`firmware-error-view.tsx:61-70` 用 `navigator.clipboard?.writeText` optional chain 守、reject 路徑靜默不洩漏
+- ✅ **WS Origin check**:backend `firmware_ws.go:23` 共用 package-level upgrader、`device_events_ws.go:14` 設定 `CheckOrigin: CheckOrigin`(loopback 白名單、`origin.go:18-40`)、Frontend 連 `/ws/devices/:id/firmware-progress` 與 backend 一致
+- ✅ **無 hardcoded secret / token**:grep `password / secret / api_key / token`:零 hit(不算 `taskId / activeTaskId` 內部命名)
+- ✅ **無 SQL 拼接**:本批無 SQL(純 WS + frontend state)
+- ✅ **無 OAuth / 第三方 SDK 新增**:純內部 WS + REST
+
+### 3.5 Performance(效能)
+
+實質判斷:性能合理、無明顯瓶頸。
+
+- ✅ Badge 在每張 card render 是 O(N) cost、`computeBadgeState` 是 pure function、無重 React.memo 需求(N 通常 < 10 dongles)
+- ✅ Progress modal re-render 頻率受 WS event 控制、backend 每階段 push(typically < 5 events / 30-180s)、不會頻繁 re-render
+- ✅ Zustand subscription 範圍合理、`useFirmwareStore()` 整 hook 拿、無 selector micro-optimization 必要(state 本身小、且 phase 變化即整個 modal 切換)
+- ⚠️ `firmware-upgrade-dialog.tsx:45-55` 用 `useFirmwareStore()` 整個拿、不 selector — 一般 OK、但 progress event 每秒推時整個 dialog 重 render(含 confirming phase 的 UI 樹);非問題、屬可接受 trade-off(見 Suggestion S3)
+
+### 3.6 測試(測試軸)
+
+實質判斷:測試覆蓋度高 — 22 + 10 = 32 個 frontend tests、覆蓋 phase 轉移 / 多裝置隔離 / cancelConfirm R-FW-11 緩解 / 8 種 reason mapping / 4-stage vs 3-stage / a11y attribute。Backend 2 個 smoke test 驗 broadcast + room isolation、合理。
+
+具體 finding:
+- ✅ 多裝置隔離測試(`firmware-store.test.ts:123-138`)— 直接模擬 dev-A active + dev-B event、驗 dev-B 不污染、品質高
+- ✅ R-FW-11 緩解測試(`firmware-store.test.ts:58-67`)— 驗 `cancelConfirm` 在 `upgrading` phase 是 no-op
+- ✅ destructive reason 不可 retry 測試(`firmware-store.test.ts:201-219`)— 3 種 destructive + 5 種 recoverable + undefined 都涵蓋
+- ⚠️ 缺 `useFirmwareProgress` hook 的測試(WS connectAndWait timeout / cleanup)— 既有 `useFlashProgress` 也沒測(屬既有不嚴格 pattern)、可加但不阻擋
+- ⚠️ `firmware_ws_test.go:62-67` 的測試 message schema 用 `phase: "flashing"` 是 ad-hoc map(不是真的 firmwareProgressMessage struct)、smoke test 只驗 plumbing 不驗 schema → 見 Major MJ3
+- ⚠️ 缺 `FirmwareErrorView` 元件渲染測試(destructive reason 顯示 brick warning / Retry 按鈕隱藏)— 見 Minor M5
+- ⚠️ 缺 `FirmwareUpgradeDialog` integration 測試(phase 切換時 modal 視覺與 R-FW-11 擋 ESC 行為)— 見 Suggestion S4
+
+---
+
+## §12.2 通用退出條件(6 條)
+
+| # | 條件 | 觸發狀態 |
+|---|------|---------|
+| 1 | No silent failures | ✅ 未觸發:`firmware-store.ts:86-104` startUpgrade 失敗有 catch + addActivity 紀錄、`firmware-upgrade-dialog.tsx:112-128` connectAndWait 失敗有 catch + 進 error phase、`firmware-error-view.tsx:65-69` clipboard reject 是有意靜默(design 規格允許) |
+| 2 | No dead code | ✅ 未觸發:grep TODO / FIXME / commented-out code、本批 0 hit;所有 import 都被使用 |
+| 3 | No hardcoded secrets | ✅ 未觸發 |
+| 4 | No unsafe HTML / SQL | ✅ 未觸發(見 3.4 security 軸) |
+| 5 | Doc 同步 | ⚠️ **觸發**:i18n key namespace 與 Design Spec §9 不一致(見 MJ1) |
+| 6 | Working tree clean | N/A:reviewer 自己只產 report、被審 commit 由 backend / frontend agent 負責、`git status` 不在 reviewer 範圍 |
+
+---
+
+## 🔴 Critical
+
+**無 Critical 問題。**
+
+---
+
+## 🟠 Major(必須修復)
+
+### MJ1:i18n key namespace 與 Design Spec §9 不一致
+
+| 欄位 | 內容 |
+|------|------|
+| 檔案 | `frontend/src/lib/i18n/types.ts:140-198`、`zh-TW.ts:139-197`、`en.ts:139-197` |
+| 規則 | Doc 同步 / Design Spec §9 對齊 |
+| 違規程度 | Major(影響 M9-12 Settings 韌體面板 i18n 對齊 + Design Spec source-of-truth 失效) |
+| 對應文件 | Design Spec §9.6 / §9.8 |
+| 問題描述 | Design Spec §9.6 定義 `settings.firmware.progress.stage.preparing` 等 51 個 keys,全部住在 `settings.firmware.*` namespace 下;Frontend 實作改用 `firmware.error.messageScanNotFound` / `firmware.progress.stagePreparing` 等 flat 結構、住在頂層 `firmware.*` namespace。 |
+| 影響 | (1) M9-12 Settings 韌體面板(B 階段)落地時、若照 Design §9 規格用 `settings.firmware.*` 會與本批 key 衝突或產生重複;(2) Design Spec §9 是 source of truth、未來文件 ↔ code 對照查 key 時找不到;(3) 51 keys vs 實際 ~28 keys 差距大(Design §9 含 settings 分頁 label / accordion / downgrade modal 等 B 階段 keys、A 階段確實不需全 51 個、但本批產的 keys 命名規則應對齊 Design namespace) |
+| 建議修改方式 | 兩個方案二選一(讓 Orchestrator 與 Design 共同裁決):
**A.** 把 i18n keys 搬到 `settings.firmware.*` namespace 下(雖然 A 階段不在 settings 頁、但對齊 Design 文件):例如 `firmware.error.messageScanNotFound` → `settings.firmware.error.message.scanNotFound`
**B.** 維持 `firmware.*` flat 結構,但回頭請 Design Agent 修 Design Spec §9 把 namespace 改為與本批一致,明確說明「i18n key 與分頁位置解耦」
**Reviewer 偏好**:B(commit cost 低、命名更簡潔)、但需 Design 明示同意 |
+
+### MJ2:FirmwareErrorView 的 `actionContactSupport` 按鈕 `title` 屬性硬編碼中文(i18n leak)
+
+| 欄位 | 內容 |
+|------|------|
+| 檔案 | `frontend/src/components/firmware/firmware-error-view.tsx:143` |
+| 規則 | i18n 完整性 / no hardcoded user-facing string |
+| 違規程度 | Major(影響英文版 UX、a11y tooltip) |
+| 對應文件 | Design Spec §7.2 + i18n 完整性原則 |
+| 問題描述 | `