Feature #1401
進行中[Branch後台][FE] 充電樁狀態改吃 WebSocket,並修正充電指令自動重送
0%
概述
背景¶
Branch Admin 「連接器 → 編輯 → 充電樁狀態」分頁(react/branch/app/(dashboard)/connector/components/connector-status.tsx)目前全靠 REST 輪詢:
-
useConnectorStatus在AVAILABLE/PREPARING/CHARGING時每 2 秒輪詢(connectorHooks.ts)。 -
chargePointHooks.useGetById為了取ocppConnectionStatus,離線時每 10 秒輪詢,並用prevOnlineRef在恢復連線時補一次 refetch。 - start/stop 後
setTimeout(3000)硬等 3 秒才 refetch。
同一份 Connector 狀態,LIFF(#1399)已改為 WebSocket 推送,後台仍是輪詢,時序無法對齊。
本案不是要後台照抄 LIFF 的顯示或操作方式。 driverState/actions[] 是車主語意,授權綁住戶關聯與 transaction owner,管理員不適用。後台維持自己的維運語意(status + ocppConnectionStatus 雙軸判斷,沿用 #1382 的區分原則),只讓資料取得管道與來源一致。
實作範圍¶
- 改吃 BE 提供的 WebSocket snapshot(endpoint 與 payload 見對應 BE ticket):
- 移除
useConnectorStatus的 2 秒refetchInterval。 - 移除
chargePointHooks的 10 秒refetchInterval與prevOnlineRef補償邏輯。 - 移除 start/stop 後的
setTimeout(3000),改由推送更新畫面。
- 移除
- WebSocket 斷線時明確標示「即時狀態已中斷」並停用充電操作按鈕,不得讓管理員對著過期畫面下指令。重連策略比照 #1399(短暫重連,失敗即明確告知),不做無限重試。
- 顯示層維持後台維運語意,不引入
driverState/actions[]。BE 新欄位到位後補上:-
FAULTED顯示實際errorCode/vendorErrorCode/vendorId,取代目前只有的「故障/請聯繫技術支援」。 - 目前交易(
currentTransactionId)與使用者(currentIdTag)。 - 離線時長(
lastHeartbeatAt)與狀態停滯時間(statusChangedAt)。 -
activeOperation未終結時停用充電按鈕,避免管理員重複送指令。
-
- 操作面沿用
POST /api/tx/charge/start/stop(ss3a 管理員身分),依 BE 提供的穩定errorCode分流錯誤訊息,不再比對message字串;保存回傳的operationId並寫入前端操作紀錄。
可先行處理(不依賴 BE)¶
修正 mutations.retry: 1。 react/branch/provider/providers.client.tsx:20 的 QueryClient 全域設定,讓 useStartCharge/useStopCharge 在失敗或逾時後自動重送一次充電指令。
這與 #1399 在 LIFF 修掉的是同一個 bug(LIFF 已改為全域 mutations.retry: 0)。充電指令非 idempotent,最危險的是逾時情境:第一次請求可能已送達 Branch、只是回應遺失,自動重試就會送出第二筆真實的充電指令,並在 audit log 產生重複紀錄。
需盤點後台其餘 mutation 是否有適合自動重試者,再決定全域關閉或逐一覆寫。
不在本次範圍¶
- Connector 列表頁的狀態顯示(仍走既有查詢;本案先處理單一 Connector 詳細狀態)。
- Engineer tools(#1380)既有的 OCPP/Modbus 指令面板。
- LIFF 端行為(#1397/#1398/#1399)。
驗收重點¶
- 後台狀態分頁不再有任何定時輪詢;狀態變化由推送即時反映。
- 充電樁離線/恢復時,
status與ocppConnectionStatus來自同一份 payload,不再出現兩者短暫矛盾。 -
FAULTED時畫面可看到實際故障代碼,而非只有「故障」。 - 送出 start/stop 後不靠固定等待;同一時間不可送出第二筆指令。
- 網路中斷時操作按鈕立即停用,恢復後需收到新 snapshot 才恢復可操作。
- 充電指令在失敗或逾時後不會被自動重送(可先行驗證)。
對應 BE ticket¶
BE 契約見 #1400。本 ticket 第 1~4 項需等 BE 完成並提供 endpoint/payload 後才能實作;「可先行處理」段落不受此限。
備註記錄(1 則)¶
#1 - 鍾正剛(2026-07-31T16:02:59Z)¶
2026-08-01 已完成「可先行處理」項目:mutation 全域關閉自動重試¶
依 Ryan 指示先處理不依賴 BE(#1400)的 retry 修正。
- Branch:
feature/redmine-1401-branch-admin-connector-ws-status(自main1f4e841開出) - Commit:
f6dd8d0 - 尚未建立 MR。
問題¶
react/branch/provider/providers.client.tsx 的 QueryClient 全域預設為 mutations: { retry: 1 }。React Query 只在 mutationFn reject 時重試,因此充電指令一旦被拒絕或逾時,就會自動再送一次。與 #1399 在 LIFF 修掉的是同一個成因。
盤點 branch admin 上全部繼承此設定的 mutation,沒有任何一支是 idempotent:
| 位置 | 操作 | 自動重試的後果 |
|---|---|---|
connector/hooks/connectorHooks.ts:112 |
/api/tx/charge/start |
重複 RemoteStart 與稽核紀錄 |
connector/hooks/connectorHooks.ts:143 |
/api/tx/charge/stop |
重複 RemoteStop |
lib/query-api/hooks/crudHooksFactory.ts:150/194/238 |
resident/connector/cp/building 的 create/update/delete | 重複建檔、重複刪除 |
bill/hooks/billHooks.ts:157 |
/api/bills/export |
重複產出帳單檔 |
bill/hooks/billHooks.ts:177 |
/api/bills/status/update |
重複狀態異動 |
invoice/hooks/invoiceHooks.ts:69 |
/api/invoice/status/update |
重複發票狀態異動 |
settings/hooks/settingsHooks.ts:43 |
/api/settings/basic-charge |
重複寫入基本費設定 |
最危險的是逾時情境:第一次請求可能已送達後端、只是回應遺失,自動重試就會送出第二筆真實的指令。
修正¶
- 全域
mutations.retry由1改為0,重試一律由使用者明確再操作一次。 -
useStartCharge/useStopCharge另外明確宣告retry: 0並加註不可移除的理由,確保全域設定日後被改動時,這兩支安全關鍵的 API 仍不會被重送。
變更檔案僅兩支,共 +23/-2。
驗證狀態¶
-
npx tsc --noEmit:通過 -
yarn build:通過 -
yarn lint:無法執行。main上 branch admin 尚未有 ESLint 設定檔,next lint會進入互動式初始化提示。此為既有狀態,與本次改動無關,本次未修改任何 lint/build 設定。 - 實機驗證:尚未執行。建議驗證方式為在充電樁離線狀態下按「開始充電」,確認 Network 只出現一次
/api/tx/charge/start。
E2E Catalog Impact¶
應為 Update(後台充電操作的回歸案例需涵蓋「指令失敗或逾時後不得自動重送」)。
但本 branch 自 main 開出,main 上尚無 documents/EMS_E2E_TEST_CASE_CATALOG.md/documents/E2E_TEST_CASE_INVENTORY.md,也沒有 .claude/skills/maintain-ems-e2e-catalog/(皆仍在未合併的 feature branch 上),因此本次無檔案可維護。待 catalog 併入 main 後補上此案例。
後續¶
#1401 其餘項目(改吃 WebSocket、移除兩條輪詢與 3 秒硬等、新增維運欄位顯示)需等 #1400 提供 endpoint 與 payload 後才能實作。