專案

一般

配置概況

Feature #1401

是由 陳國瑋約 24 小時 前更新

## h2. 背景 

 Branch Admin 「連接器 → 編輯 → 充電樁狀態」分頁(`react/branch/app/(dashboard)/connector/components/connector-status.tsx`)目前全靠 充電樁狀態」分頁(@react/branch/app/(dashboard)/connector/components/connector-status.tsx@)目前全靠 REST 輪詢: 

 * `useConnectorStatus` @useConnectorStatus@`AVAILABLE`/`PREPARING`/`CHARGING` @AVAILABLE@/@PREPARING@/@CHARGING@ 時每 **2 秒**輪詢(`connectorHooks.ts`)。 *2 秒*輪詢(@connectorHooks.ts@)。 
 * `chargePointHooks.useGetById` @chargePointHooks.useGetById@ 為了取 `ocppConnectionStatus`,離線時每 **10 秒**輪詢,並用 `prevOnlineRef` @ocppConnectionStatus@,離線時每 *10 秒*輪詢,並用 @prevOnlineRef@ 在恢復連線時補一次 refetch。 
 * start/stop 後 `setTimeout(3000)` @setTimeout(3000)@ 硬等 3 秒才 refetch。 

 同一份 Connector 狀態,LIFF(#1399)已改為 WebSocket 推送,後台仍是輪詢,時序無法對齊。 

 **本案不是要後台照抄 *本案不是要後台照抄 LIFF 的顯示或操作方式。** `driverState`/`actions[]` 的顯示或操作方式。* @driverState@/@actions[]@ 是車主語意,授權綁住戶關聯與 transaction owner,管理員不適用。後台維持自己的維運語意(`status` owner,管理員不適用。後台維持自己的維運語意(@status@ + `ocppConnectionStatus` @ocppConnectionStatus@ 雙軸判斷,沿用 #1382 的區分原則),只讓**資料取得管道與來源一致**。 的區分原則),只讓*資料取得管道與來源一致*。 

 ## h2. 實作範圍 

 1. # 改吃 BE 提供的 WebSocket snapshot(endpoint 與 payload 見對應 BE ticket): 
    * 
 ** 移除 `useConnectorStatus` @useConnectorStatus@ 的 2 秒 `refetchInterval`。 
    * @refetchInterval@。 
 ** 移除 `chargePointHooks` @chargePointHooks@ 的 10 秒 `refetchInterval` @refetchInterval@`prevOnlineRef` @prevOnlineRef@ 補償邏輯。 
    * 
 ** 移除 start/stop 後的 `setTimeout(3000)`,改由推送更新畫面。 @setTimeout(3000)@,改由推送更新畫面。 
 2. # WebSocket 斷線時明確標示「即時狀態已中斷」並停用充電操作按鈕,不得讓管理員對著過期畫面下指令。重連策略比照 #1399(短暫重連,失敗即明確告知),不做無限重試。 
 3. 顯示層維持後台維運語意,**不引入** `driverState`/`actions[]`。BE # 顯示層維持後台維運語意,*不引入* @driverState@/@actions[]@。BE 新欄位到位後補上: 
    * `FAULTED` 
 ** @FAULTED@ 顯示實際 `errorCode`/`vendorErrorCode`/`vendorId`,取代目前只有的「故障/請聯繫技術支援」。 
    * 目前交易(`currentTransactionId`)與使用者(`currentIdTag`)。 
    * 離線時長(`lastHeartbeatAt`)與狀態停滯時間(`statusChangedAt`)。 
    * `activeOperation` @errorCode@/@vendorErrorCode@/@vendorId@,取代目前只有的「故障/請聯繫技術支援」。 
 ** 目前交易(@currentTransactionId@)與使用者(@currentIdTag@)。 
 ** 離線時長(@lastHeartbeatAt@)與狀態停滯時間(@statusChangedAt@)。 
 ** @activeOperation@ 未終結時停用充電按鈕,避免管理員重複送指令。 
 4. # 操作面沿用 `POST /api/tx/charge/start`/`stop`(ss3a @POST /api/tx/charge/start@/@stop@(ss3a 管理員身分),依 BE 提供的穩定 `errorCode` @errorCode@ 分流錯誤訊息,不再比對 `message` @message@ 字串;保存回傳的 `operationId` @operationId@ 並寫入前端操作紀錄。 

 ## h2. 可先行處理(不依賴 BE) 

 **修正 `mutations.retry: 1`。** `react/branch/provider/providers.client.tsx:20` *修正 @mutations.retry: 1@。* @react/branch/provider/providers.client.tsx:20@ 的 QueryClient 全域設定,讓 `useStartCharge`/`useStopCharge` 在失敗或逾時後**自動重送一次充電指令**。 @useStartCharge@/@useStopCharge@ 在失敗或逾時後*自動重送一次充電指令*。 

 這與 #1399 在 LIFF 修掉的是同一個 bug(LIFF 已改為全域 `mutations.retry: 0`)。充電指令非 @mutations.retry: 0@)。充電指令非 idempotent,最危險的是逾時情境:第一次請求可能已送達 Branch、只是回應遺失,自動重試就會送出第二筆真實的充電指令,並在 audit log 產生重複紀錄。 

 需盤點後台其餘 mutation 是否有適合自動重試者,再決定全域關閉或逐一覆寫。 

 ## h2. 不在本次範圍 

 * Connector 列表頁的狀態顯示(仍走既有查詢;本案先處理單一 Connector 詳細狀態)。 
 * Engineer tools(#1380)既有的 OCPP/Modbus 指令面板。 
 * LIFF 端行為(#1397/#1398/#1399)。 

 ## h2. 驗收重點 

 * 後台狀態分頁不再有任何定時輪詢;狀態變化由推送即時反映。 
 * 充電樁離線/恢復時,`status` 充電樁離線/恢復時,@status@`ocppConnectionStatus` @ocppConnectionStatus@ 來自同一份 payload,不再出現兩者短暫矛盾。 
 * `FAULTED` @FAULTED@ 時畫面可看到實際故障代碼,而非只有「故障」。 
 * 送出 start/stop 後不靠固定等待;同一時間不可送出第二筆指令。 
 * 網路中斷時操作按鈕立即停用,恢復後需收到新 snapshot 才恢復可操作。 
 * 充電指令在失敗或逾時後不會被自動重送(可先行驗證)。 

 ## h2. 對應 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`(自 `main` `1f4e841` 開出) 
 * 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` | 重複寫入基本費設定 | 

 最危險的是逾時情境:第一次請求可能已送達後端、只是回應遺失,自動重試就會送出第二筆真實的指令。 

 ##### 修正 

 1. 全域 `mutations.retry` 由 `1` 改為 `0`,重試一律由使用者明確再操作一次。 
 2. `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 後才能實作。 

返回