jim800121chen 06ff2fe987 feat(local-tool): M9-4 — Frontend FW badge + 升級 modal + WS hot-fix
A 階段第四個 milestone、完整 Frontend FW UI(badge / modal / 8 種 reason 復原)+ backend WS hot-fix(補對稱於 flash 的 firmware WS endpoint)。

Frontend(13 修改 / 7 新檔):
- 新 firmware/ component group (badge / upgrade-button / upgrade-dialog 4-phase / progress-view / error-view 8-reason / index)
- Zustand store (firmware-store.ts) + WS hook (use-firmware-progress.ts) 對齊既有 useFlashProgress pattern
- DeviceCard 整合 FirmwareBadge + FirmwareUpgradeButton
- i18n: settings.firmware.* namespace (對齊 Design Spec §9 SoT) + devices.card.fwBadge.* (zh-TW + en, 57 leaf keys × 2 lang = 114 strings)
- toast.ts ToastOptions interface (duration param)
- types/device.ts: FW 衍生欄位 + FirmwareStage/Reason/ProgressEvent/ActiveTask types

Backend WS hot-fix (3 檔):
- ws/firmware_ws.go (50 行、純對稱 flash_ws.go)
- ws/firmware_ws_test.go (165 行、2 smoke tests: broadcast + room isolation)
- router.go: GET /ws/devices/:id/firmware-progress

關鍵設計:
- R-FW-11 緩解: upgrading phase modal 不可關 (onInteractOutside/onEscapeKeyDown preventDefault + 隱藏 X)
- 多裝置隔離 defense in depth: store handleEvent activeDeviceId mismatch 直接 return
- 8 種 reason → 4 種 UX (recoverable/destructive/brick 警告/contactSupport)
- ContactSupport mailto handler (RFC 6068 + encodeURIComponent)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 3 Major / 8 Minor / 5 Suggestion
- Round 2: 0 Critical / 0 Major / 0 Minor / 2 Suggestion(接受方案 A、不需 frontend 第 3 輪)
- MJ1 i18n namespace 採方案 A (settings.firmware.*)、Design SoT 優先、Reviewer 同意

測試:
- pnpm test --run: 60 tests pass (32 firmware: 22 store + 10 badge + 新 9 error-view + 19 既有)
- npx tsc --noEmit: 0 error
- pnpm build: production build 成功
- go test ./internal/api/ws/... -race: 1.964s 全綠
- pnpm lint firmware/: 0 hit (17 既有 lint 問題不屬 M9-4、follow-up)

未做(範圍外):
- Settings 韌體面板 (M9-12 B 階段)
- 手動降版 UI (M9-12)
- 版本切換 dropdown (B 階段)
- Wails 控制台 force-quit modal (M9-4.5)

A 階段 MVP 後端 + 前端開發全部完成、剩 M9-4.5 (SIGTERM + Wails OnBeforeClose) + M9-5 (三平台實機驗證)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:57:21 +08:00

51 lines
1.5 KiB
Go
Raw 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.

package ws
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// FirmwareProgressHandler — M9-4 hot-fix。
//
// 對稱於 FlashProgressHandler。Client 連 /ws/devices/:id/firmware-progress
// 後會被 join 到 room "firmware:<deviceID>",由
// firmware_handler.forwardProgressToWS 透過 hub.BroadcastToRoom 推進度。
//
// 行為與 flash 版完全一致:
// - 共用 package-level upgraderCheckOrigin = loopback 白名單,見 device_events_ws.go
// - 用 RegisterSync 確保 client 已 join room 才回到 read/write loop
// - 一個 goroutine drain client 端 incoming 訊息(純為觸發斷線偵測)
// - 主 goroutine 把 client.Send channel 的訊息 WriteMessage 到 conn
// - conn / unregister 統一由 defer 處理
func FirmwareProgressHandler(hub *Hub) gin.HandlerFunc {
return func(c *gin.Context) {
deviceID := c.Param("id")
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
client := &Client{Conn: conn, Send: make(chan []byte, 20)}
room := "firmware:" + deviceID
sub := &Subscription{Client: client, Room: room}
hub.RegisterSync(sub)
defer hub.Unregister(sub)
// Read pump — drain incoming messages; close handled by outer defer
go func() {
for {
if _, _, err := conn.ReadMessage(); err != nil {
break
}
}
}()
for msg := range client.Send {
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
}
}
}