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>
51 lines
1.5 KiB
Go
51 lines
1.5 KiB
Go
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 upgrader(CheckOrigin = 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
|
||
}
|
||
}
|
||
}
|
||
}
|