Review 問題修復: M1(寫已關閉 channel panic): - flash service goroutine 改成先等 driver.Flash() 返回,再寫 error 訊息,最後 close - driver.Flash 返回後保證不再寫 progressCh,消除 race condition M2(FlashTask 永不清除 memory leak): - service.go 新增 CleanupTask(taskID) 公開方法 - device_handler.go 的 goroutine 在 `for range progressCh` 結束後呼叫 CleanupTask M3(同裝置重複 flash taskID 衝突): - ProgressTracker.Create 改成:舊 task 未完成時返回 nil - StartFlash 檢查 nil → 回傳 "flash already in progress" 錯誤 M4(前端 flash store 全域不區分 deviceId): - flash-store.ts 新增 activeDeviceId 欄位 - updateProgress 改接 (deviceId, progress),比對 activeDeviceId 防止混裝 - use-flash-progress.ts 的 WebSocket callback 傳入 deviceId m5(flash_ws.go 雙重 conn.Close): - read pump goroutine 移除 defer conn.Close(),由外層 defer 統一關閉 額外修復(S4): - modelPath 為空時直接回 error 而非傳無效路徑給 driver Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 KiB
TypeScript
import { create } from 'zustand';
|
||
import { api } from '@/lib/api';
|
||
import type { FlashProgress } from '@/types/device';
|
||
import { showApiError } from '@/lib/toast';
|
||
import { useActivityStore } from './activity-store';
|
||
|
||
// M4 fix: flash state 以 deviceId 區分,避免多裝置 UI 互相覆蓋。
|
||
// activeDeviceId 記錄當前正在 flash 的裝置,updateProgress 會比對確保不混。
|
||
|
||
interface FlashState {
|
||
activeDeviceId: string | null;
|
||
isFlashing: boolean;
|
||
progress: FlashProgress | null;
|
||
error: string | null;
|
||
lastFlashParams: { deviceId: string; modelId: string } | null;
|
||
startFlash: (deviceId: string, modelId: string) => Promise<void>;
|
||
updateProgress: (deviceId: string, progress: FlashProgress) => void;
|
||
setError: (error: string) => void;
|
||
retryFlash: () => Promise<void>;
|
||
reset: () => void;
|
||
}
|
||
|
||
export const useFlashStore = create<FlashState>((set, get) => ({
|
||
activeDeviceId: null,
|
||
isFlashing: false,
|
||
progress: null,
|
||
error: null,
|
||
lastFlashParams: null,
|
||
|
||
startFlash: async (deviceId, modelId) => {
|
||
set({ activeDeviceId: deviceId, isFlashing: true, progress: null, error: null, lastFlashParams: { deviceId, modelId } });
|
||
const res = await api.post(`/devices/${deviceId}/flash`, { modelId });
|
||
if (!res.success) {
|
||
const errorMsg = res.error?.message || 'Flash failed';
|
||
showApiError(res.error);
|
||
set({ isFlashing: false, error: errorMsg });
|
||
useActivityStore.getState().addActivity('flash_error', `Flash failed: ${errorMsg}`);
|
||
} else {
|
||
useActivityStore.getState().addActivity('flash_start', 'Flash started');
|
||
}
|
||
},
|
||
|
||
updateProgress: (deviceId, progress) => {
|
||
// M4 fix: 只更新對應裝置的 progress,忽略其他裝置的 WebSocket 訊息
|
||
if (get().activeDeviceId !== null && get().activeDeviceId !== deviceId) {
|
||
return;
|
||
}
|
||
|
||
if (progress.error) {
|
||
set({ isFlashing: false, error: progress.error });
|
||
useActivityStore.getState().addActivity('flash_error', `Flash failed: ${progress.error}`);
|
||
return;
|
||
}
|
||
set({ progress });
|
||
if (progress.percent >= 100) {
|
||
set({ isFlashing: false });
|
||
useActivityStore.getState().addActivity('flash_complete', 'Flash completed');
|
||
}
|
||
},
|
||
|
||
setError: (error) => {
|
||
set({ error, isFlashing: false });
|
||
},
|
||
|
||
retryFlash: async () => {
|
||
const { lastFlashParams } = get();
|
||
if (!lastFlashParams) return;
|
||
await get().startFlash(lastFlashParams.deviceId, lastFlashParams.modelId);
|
||
},
|
||
|
||
reset: () => {
|
||
set({ activeDeviceId: null, isFlashing: false, progress: null, error: null, lastFlashParams: null });
|
||
},
|
||
}));
|