專案

一般

配置概況

動作

Feature #1399

進行中

Feature #1397: [功能開發] LIFF DriverState、離峰充電與 Remote Operation 狀態契約重構

[Frontend] LIFF 串接 DriverState actions 與 WebSocket 即時狀態

是由 陳國瑋5 天 前加入. 於 約 14 小時 前更新.

狀態:
Resolved
優先權:
Normal
被分派者:
開始日期:
2026-08-02
完成日期:
完成百分比:

100%

預估工時:
(總計: 0.00 小時)

概述

目標

依父項 #1397 及其英文/繁體中文規格附件,完成 LIFF 對新版 DriverState、semantic actions、長期離峰充電與 WebSocket 即時狀態契約的串接。

附件內容為本子任務的需求與驗收基準。

實作範圍

  • BE OpenAPI contract 完成後重新產生 types。畫面依據 driverState.titledescriptionactions 與 semantic action code 呈現,不自行由 raw connector status 重建業務狀態。
  • PREPARING 顯示「準備中」,並停用重複開始操作。
  • FINISHING 顯示「停止中」,並停用重複停止操作。
  • OFF_PEAK_WAITINGQUEUED 都提供「取消排隊」主要操作,但必須透過 action code 呼叫正確 command,不得以按鈕文字判斷操作。
  • PUBLIC Connector 不顯示離峰 switch;此項顯示差異保留在 FE。
  • PRIVATE Connector 的離峰 switch 保持可點擊。送出後以 API authoritative response 判斷成功,或顯示不允許原因,不在 FE 複製完整 business matrix。
  • 在離峰 CHARGING 時,不允許直接關閉 off-peak switch;停止本輪充電時需顯示確認 modal。
  • 確認停止後,本次 current off-peak window 不再自動充電;若使用者反悔,需先切換為 OFF,再切換為 ON 重新排程。
  • Start/Stop/Off-Peak command 使用 operationId/idempotency contract,並處理以下結果:
    • 結構化 403409
    • Rejected
    • timeout
    • 暫態失敗訊息
  • Connector 詳細頁只使用 WebSocket initial snapshot 取得 connector info:
    • 不再額外呼叫 detail REST API
    • 不進行輪詢
    • 不使用 REST 作為斷線 fallback
  • 初次 WebSocket 連線失敗時,只進行一次短暫重連;若仍失敗,則退回列表並顯示 modal。
  • 已連線後發生斷線時:
    • 立即停用舊 snapshot 的 driverState.actions
    • 不得使用 stale actions 執行 Start、Stop 或 Off-Peak
    • 只進行一次短暫重連
    • 重連失敗時,退回列表並顯示 modal
    • 重連成功後,必須收到 fresh snapshot 才能恢復操作
  • 補齊必要的 loading、pending、confirmation、error modal 與重連狀態,避免重複點擊與狀態倒退。

驗收重點

  • PUBLICPRIVATEMANUALOFF_PEAK 的 switch、主要按鈕與 modal 行為符合 #1397 附件 matrix。
  • CloudLink/Fortune 在「先插槍、尚未按開始」時,仍顯示正確的可開始操作;只有真正送出且尚未終結的 RemoteStart,才顯示 PREPARING/「準備中」。
  • 離峰開啟成功後,OFF_PEAK_WAITING 立即顯示「取消排隊」;建立 Priority 後轉為 QUEUED,操作仍為取消排隊。
  • RemoteStart/RemoteStop 失敗時,會向車主顯示本次操作未成功,不會無提示地回到可重複按開始/停止的畫面。
  • WebSocket 尚未連上、已斷線或 snapshot 已過期時,使用者不能沿用舊 actions 執行操作。
  • 測試範圍涵蓋:
    • 公樁/私樁
    • CloudLink/Fortune
    • 手動/離峰
    • 成功/拒絕/逾時
    • 初次連線失敗
    • 既有連線中斷

邊界

  • API 授權、command 合法性與 operation lifecycle 由 BE authoritative 判斷;FE 僅依 contract 呈現並送出語意化 action。
  • Charge Group 與 PUBLIC slot 設定問題,分別由 #1395、#1396 處理。

備註記錄(8 則)

#1 - 陳國瑋(2026-07-31T03:09:43Z)

2026-07-31 BE/FE WebSocket contract 補充(Ken 已確認)

  • 新版 Backend 會直接移除舊的 driverState.startdriverState.stop;FE 必須只使用 driverState.actions[] 與 semantic action code,不得保留對舊欄位的依賴。
  • driverStatedriverState.reasondriverState.actions[] 是 Connector 共用狀態,會推送給所有正在觀看該 Connector 的合法 sessions。
  • Transient operationResult 是使用者定向通知,不得當成 Connector 共用 modal 廣播:
    • 手動 Start/Stop/Cancel 的非同步結果,以實際 requestedByUserId 為 target。
    • Scheduler 發起的 OFF_PEAK RemoteStart 結果,以長期離峰設定的 enabledByUserId 為 target。
  • Branch MQ payload 會提供 operationResult.targetUserId;HQ WebSocket 依已驗證 session 對應的 Branch user identity 篩選,只將 operation result 傳給 target user。這只是通知路由,不在 HQ 重做 command authorization。
  • FE 收到 operationResult 時,依 operationId 去重後顯示一次結果 modal;不得因後續共用 driverState 更新而重複顯示同一結果。
  • WebSocket initial snapshot 不重播已終結的 transient operation result;重新開頁或 reconnect 不應顯示過期失敗 modal。仍持續生效的問題由目前 driverState.reason/description 顯示。
  • 非 target 的其他關聯住戶仍會收到共用 driverState 更新,但不會收到該次 operation modal。

此契約由 Backend #1398 實作,FE #1399 依最終 OpenAPI/WebSocket DTO 串接;兩張 ticket 必須協調部署。

#2 - 陳國瑋(2026-07-31T03:59:32Z)

#1397 需求附件已於 2026-07-31 修訂並重新上傳:

  • 英文附件 #1102
  • 繁中附件 #1103

FE 請依新版契約調整:移除舊 driverState.start/stop 與既有 LIFF start/stop/dequeue/offpeak-switch 呼叫,統一改呼叫 POST /api/connector/commands,action 使用 START_CHARGING、STOP_CHARGING、CANCEL_START、CANCEL_MANUAL_START、ENABLE_OFF_PEAK、DISABLE_OFF_PEAK。開始/停止成功 dispatch 為 202 並取得 operationId;用 operationId 去重 targeted WebSocket operationResult 並只顯示一次 modal。WebSocket initial snapshot 不會重播 terminal result,只可能帶未完成的 activeOperation。公樁仍由 FE 隱藏離峰 switch。

#3 - 陳國瑋(2026-07-31T10:11:35Z)

2026-07-31 BE/FE 人工 E2E 交接

#1398 Backend 已部署到 Ken Branch 與 docker1 HQ staging,安全 API/WebSocket contract 驗證已通過;完整 E2E 將等待本 #1399 完成並部署後,由 LIFF 起點人工執行。

父項 #1397 已新增附件 #1106:
redmine-1397-liff-manual-e2e-test-plan-zh-TW.md

FE 完成後的共同驗收重點包括:

  • 詳細頁只用 WebSocket initial snapshot,不另 call detail REST。
  • public 不顯示離峰 switch;private 顯示 switch。
  • READY/OFF_PEAK_WAITING/QUEUED/PREPARING/CHARGING/FINISHING 與 semantic actions。
  • operationId、一次性 targeted operationResult、initial snapshot 不重播 terminal result。
  • WebSocket 斷線時舊 actions 立即不可操作,一次重連失敗後退列表並顯示 modal。
  • 離峰 charging 停止確認、current-window suppression,以及 OFF→ON 才重新排程。
  • PUBLIC 非 transaction owner 停止被拒絕;PRIVATE 任一目前關聯住戶可停止。

附件另含 Ken 環境 CP001-001/002 的公私樁切換、資料準備、16 個案例、證據與還原步驟。此 note 只做驗收交接,不變更 #1399 目前狀態。

#4 - 陳國瑋(2026-07-31T10:33:36Z)

2026-07-31 POST /api/connector/commands OpenAPI/FE 使用補充

FE 回報 POST /api/connector/commands 的 200/202/403/409 response body 在 OpenAPI 只顯示通用 object,導致 yarn openapi:refresh 無法產生可用型別。這不是 FE 使用方式問題,而是 #1398 Backend 原本 endpoint 使用 ResponseEntity<?>@ApiResponse 未明確指定 schema。

#1398 已補 OpenAPI schema annotation:

  • 200 OK202 Accepted response body:ConnectorCommandResponse
  • 403 Forbidden409 Conflict response body:ConnectorCommandErrorResponse

請 FE 在新版 OpenAPI 產出後依下列契約串接。

Request

POST /api/connector/commands

Header:

  • X-Line-Id: 由 LIFF session 帶入,FE 不傳 userId。

Body:

{
  "buildingId": "B001",
  "connectorId": "CP001-001",
  "action": "START_CHARGING"
}

action 必須直接使用目前 snapshot 的 driverState.actions[].code,可用值:

  • START_CHARGING
  • STOP_CHARGING
  • CANCEL_START
  • CANCEL_MANUAL_START
  • ENABLE_OFF_PEAK
  • DISABLE_OFF_PEAK

FE 不要用按鈕文字反推 action,也不要自行組 OCPP command。

Success response:200/202 = ConnectorCommandResponse

欄位:

  • action: 本次執行的 semantic action。
  • operationId: 只有實際 dispatch RemoteStart/RemoteStop 時會有值;同步 command 可能為 null。
  • operationStatus: 只有非同步 operation 會有值,通常為 SENT
  • enableOffPeak: Branch 實際離峰偏好;FE switch 應以此值校正。
  • driverState: command 後最新狀態;FE 應立即用它更新畫面。
  • activeOperation: 尚未終結的 RemoteStart/RemoteStop operation;沒有則為 null。

200 OK:同步完成,例如建立/取消 queue、開關離峰偏好、取消尚未 dispatch 的 start 等。通常不代表有 operationId

202 Accepted:RemoteStart/RemoteStop 已送出,但尚未等到 OCPP 最終結果。FE 進入 pending UI,保存 operationId,等待 WebSocket targeted operationResult,並用 operationId 去重 modal。

範例:

{
  "action": "START_CHARGING",
  "operationId": "18456",
  "operationStatus": "SENT",
  "enableOffPeak": false,
  "driverState": {
    "code": "PREPARING",
    "title": "準備中",
    "description": "正在要求充電樁開始充電",
    "reason": null,
    "actions": []
  },
  "activeOperation": {
    "operationId": "18456",
    "type": "START",
    "source": "MANUAL",
    "status": "SENT",
    "requestedAt": "2026-07-31T18:20:00",
    "deadlineAt": "2026-07-31T18:20:30"
  }
}
Error response:403/409 = ConnectorCommandErrorResponse

欄位:

  • errorCode: 穩定 business error code,供 FE 做必要分流。
  • message: 可顯示給車主的錯誤訊息。
  • enableOffPeak: Branch 實際離峰偏好;尤其 off-peak switch 被拒絕時,FE 必須用它回復 switch 狀態。
  • driverState: 拒絕當下最新狀態;FE 應用它更新主要按鈕與狀態文字。

403 Forbidden:使用者或 connector 存取權限不允許,例如 PUBLIC 非 transaction owner 停止、PRIVATE 未關聯住戶操作等。

409 Conflict:使用者有權看該 connector,但目前狀態不允許該 action,例如 charging 中重複 start、pending operation 中重複操作、離峰 current-window suppression 等。

範例:

{
  "errorCode": "COMMAND_NOT_ALLOWED",
  "message": "目前狀態不允許執行此操作",
  "enableOffPeak": true,
  "driverState": {
    "code": "CHARGING",
    "title": "充電中",
    "description": "車輛正在充電",
    "reason": null,
    "actions": []
  }
}
FE AI 實作重點
  • 呼叫前:只允許使用 fresh WebSocket snapshot 裡 enabled 的 driverState.actions[]。WebSocket 斷線期間不得沿用舊 actions。
  • 呼叫後:無論 200/202/403/409,都要以 response body 的 driverStateenableOffPeak 校正畫面。
  • 只有 202 且有 operationId 時,才等待 WebSocket operationResult 顯示一次性 modal。
  • 403/409 不會再透過 operationResult 補一次;FE 應直接顯示 response body 的 message
  • PUBLIC connector 不顯示 off-peak switch;PRIVATE connector 可送出 switch command,由 API 回應決定成功或拒絕。

#5 - 陳國瑋(2026-07-31T13:25:31Z)

2026-07-31 HQ staging OpenAPI 已更新,可重新產生 FE types

#1398 Backend 已 push 並重新部署 HQ staging。

  • Branch:feature/redmine-1398-driver-state-offpeak-backend
  • Latest commit:a21baa3
  • HQ staging container:ems-hq-staging-api 已重建並為 Up

已驗證 staging HQ /api-docsPOST /api/connector/commands response schema:

  • 200/202:ConnectorCommandResponse
  • 403/409:ConnectorCommandErrorResponse

Ryan/FE AI 現在可以重新執行 yarn openapi:refresh。若產生的 client 對非 2xx response 不直接回傳 typed body,FE 仍需在 HTTP error handler 讀取 response body,body schema 依本 ticket 前一則 contract note 的 ConnectorCommandErrorResponse 處理。

#6 - 鍾正剛(2026-07-31T14:36:18Z)

2026-07-31 FE 實作完成(待部署後人工 E2E)

已完成 #1399 前端實作,branch: feature/redmine-1399-liff-driver-state-actions(commit 2e4386b),依 #1397 附件 #1102/#1103 與 note #4940 的 command API 契約串接。尚未建立 MR。

主要變更
  • 所有車主操作統一改呼叫 POST /api/connector/commandsaction 直接使用 driverState.actions[].code,不以按鈕文字反推指令;移除舊的 start/stop/dequeue/offpeak-switch 與 driverState.startstop
  • 詳細頁改為 WebSocket-only:移除 detail REST 呼叫、輪詢與斷線 fallback。
  • 新增 activeOperationoperationResult 支援;202 進入 pending,以 operationId 去重、結果 modal 只顯示一次,initial snapshot 不重播。
  • WebSocket 斷線立即停用舊 actions;一次短暫重連仍失敗即退回列表並顯示 modal。
  • 200/202/403/409 一律以回應的 driverStateenableOffPeak 校正畫面。
  • 離峰 switch 改由 ENABLE_OFF_PEAKDISABLE_OFF_PEAK action 決定,FE 不再維護 business matrix;停止離峰充電顯示規格 §11.5 三項說明的確認 modal。
與 Ken 確認後的調整
  • 公私樁合併為單一路由 /charger/[id],刪除 /charger/[id]/private 與兩份重複 dashboard,差異改由 WebSocket snapshot 的 areaTypeactions[] 導出。順帶修正既有問題:rate-info.tsx 的「返回設備」原本一律連到公樁畫面,私樁使用者會看不到離峰 switch。
  • 移除全部 FE 狀態轉換與死碼getChargerStatusChargerStatus 及七個未使用型別、useConnector(未使用且有 10 秒輪詢)、logUserAction
  • 操作行為紀錄:原 logUserAction 呼叫的 /api/logs/user-actions 會被 rewrite 轉到 HQ,而 HQ 沒有任何 log endpoint,實際上每次開始/停止都在等一個必定 404 的請求且結果被吞掉,等於沒有留下紀錄。已改走 logUI(→ /api/logs → winston)並統一 metadata:actionphaseresulthttpStatusoperationIderrorCodedriverState 前後值/被阻擋原因。operationId 與 Branch e_remote_transaction_log.id 相同,可依附件 #1106 §11.2 串起 LIFF、HQ、Branch、OCPP 與 transaction。另修正 WS log 原本寫入完整 payload 的問題,改為只記摘要。log 保存期由 14d 調整為 90d
部署注意(重要)

依規格不提供舊契約相容層,因此本 branch 必須與 #1398 協調部署。2026-07-31 查核:staging hq-api.sylksoft.com 已是新契約;正式站 hq-api.cloudlinkems.com 仍為舊版且 DriverConnectorPresentationDto 尚未存在。若在正式站 HQ 升級前部署本前端,充電功能會全面失效。

另需同步更新各環境的 LOG_MAX_FILES=90denv.example 已更新,實際部署環境變數需一併調整)。

驗證狀態
  • npx tsc --noEmit:通過
  • yarn build:通過
  • yarn lint無法執行,既有問題(eslint-config-next 以 CommonJS 匯出,next lint 讀取 rules 具名匯出失敗),發生在載入 ESLint 設定階段,與本次改動無關,本次未修改任何 lint/build 設定。
  • 實機 E2E:尚未執行,依附件 #1106 於部署後由 LIFF 起點人工執行,未預先標記任何案例為 PASS。
E2E Catalog Impact

No catalog change。本次 FE 行為已完整涵蓋於 #1398 宣告的 UI-002、UI-010、CHG-033~035、ROT-021~024;為避免與 #1398 branch 對 documents/E2E_TEST_CASE_INVENTORY.md 的大幅修改衝突,本 ticket 不重複編輯該檔。

完整交付說明見 branch 內 documents/redmine-1399-liff-driver-state-fe-notes.md

#7 - 鍾正剛(2026-07-31T15:07:09Z)

2026-07-31 修正:command 被送出兩次

回報按下停止充電時 POST /api/connector/commands 會送兩次。已定位並修正,commit dc82cd6

原因

src/components/query-provider.tsx 的 QueryClient 全域預設為 mutations: { retry: 1 }。React Query 只在 mutationFn reject 時重試,因此只要 command 被拒絕(403/409)或逾時,就會自動再送一次,總共兩次。

也就是說:看到兩次請求,代表第一次其實已經失敗,第二次是自動重送。

風險

connector command 不是 idempotent —— Branch 每次成功 dispatch 都會持久化一筆 remote operation。自動重試違反規格:

  • §9:operation 未完成期間不得盲目重送 RemoteStart。
  • §10:不自動重送 RemoteStop。
  • §15.2:同一 Connector 不得存在重疊的未完成 operation。

最危險的是逾時情境:第一次請求可能已送達 Branch、只是回應遺失,自動重試就會送出第二筆真實的充電指令,並在 audit log 產生重複的 operationId 紀錄。

修正

useConnectorCommand() 明確覆寫 retry: 0,重試一律改由車主明確再按一次。已加註解說明不可移除的理由。

驗證:npx tsc --noEmityarn build 皆通過。

另外提醒(不在本次修改範圍)

全域 mutations.retry: 1 仍套用在其他 mutation 上(例如綁定流程)。這些同樣不是 idempotent,建議評估是否將全域預設改為 retry: 0、再由個別需要重試的查詢自行開啟。若要調整,因影響範圍跨功能,建議另開 ticket 處理。

#8 - 鍾正剛(2026-07-31T15:11:57Z)

2026-07-31 補充:mutation 全域關閉自動重試

依 Ryan 指示,將 QueryClient 的全域 mutations.retry 由 1 改為 0,commit 524aaaf

盤點結果:專案內共 5 支 mutation,先前全部繼承 retry: 1,且全部都是有副作用、非 idempotent 的操作:

位置 操作 自動重試的後果
app/charger/hooks/use-connector-commands.ts connector command 重複的 RemoteStart/RemoteStop operation 與 audit 紀錄
src/hooks/use-binding.ts:206 sendVerificationCode 重複發送 OTP 簡訊,使用者收到兩組驗證碼
src/hooks/use-binding.ts:219 verifyCode 多消耗一次驗證次數,可能觸發鎖定
src/hooks/use-binding.ts:191 executeBinding 重複執行綁定
app/setting/hooks/use-unbind.ts:28 unbindUser 重複解除綁定

沒有任何一支適合自動重試,因此改為全域 retry: 0,重試一律由使用者明確再操作一次。useConnectorCommand 仍保留自身的 retry: 0 宣告,確保全域設定日後被改動時,這支安全關鍵的 API 仍不會被重送。

驗證:npx tsc --noEmityarn build 皆通過。


子任務 1 (0 進行中1 已結束)

Bug #1405: [Frontend Bug] LIFF connector 狀態 badge 顯示 driverState.code 而非中文文案Rejected鍾正剛2026-08-02動作

是由 陳國瑋3 天 前更新

h3. 2026-07-31 BE/FE WebSocket contract 補充(Ken 已確認)

  • 新版 Backend 會直接移除舊的 driverState.startdriverState.stop;FE 必須只使用 driverState.actions[] 與 semantic action code,不得保留對舊欄位的依賴。
  • driverStatedriverState.reasondriverState.actions[] 是 Connector 共用狀態,會推送給所有正在觀看該 Connector 的合法 sessions。
  • Transient operationResult 是使用者定向通知,不得當成 Connector 共用 modal 廣播:
    ** 手動 Start/Stop/Cancel 的非同步結果,以實際 requestedByUserId 為 target。
    ** Scheduler 發起的 OFF_PEAK RemoteStart 結果,以長期離峰設定的 enabledByUserId 為 target。
  • Branch MQ payload 會提供 operationResult.targetUserId;HQ WebSocket 依已驗證 session 對應的 Branch user identity 篩選,只將 operation result 傳給 target user。這只是通知路由,不在 HQ 重做 command authorization。
  • FE 收到 operationResult 時,依 operationId 去重後顯示一次結果 modal;不得因後續共用 driverState 更新而重複顯示同一結果。
  • WebSocket initial snapshot 不重播已終結的 transient operation result;重新開頁或 reconnect 不應顯示過期失敗 modal。仍持續生效的問題由目前 driverState.reason/description 顯示。
  • 非 target 的其他關聯住戶仍會收到共用 driverState 更新,但不會收到該次 operation modal。

此契約由 Backend #1398 實作,FE #1399 依最終 OpenAPI/WebSocket DTO 串接;兩張 ticket 必須協調部署。

是由 陳國瑋3 天 前更新

#1397 需求附件已於 2026-07-31 修訂並重新上傳:

  • 英文附件 #1102
  • 繁中附件 #1103

FE 請依新版契約調整:移除舊 driverState.start/stop 與既有 LIFF start/stop/dequeue/offpeak-switch 呼叫,統一改呼叫 POST /api/connector/commands,action 使用 START_CHARGING、STOP_CHARGING、CANCEL_START、CANCEL_MANUAL_START、ENABLE_OFF_PEAK、DISABLE_OFF_PEAK。開始/停止成功 dispatch 為 202 並取得 operationId;用 operationId 去重 targeted WebSocket operationResult 並只顯示一次 modal。WebSocket initial snapshot 不會重播 terminal result,只可能帶未完成的 activeOperation。公樁仍由 FE 隱藏離峰 switch。

是由 陳國瑋3 天 前更新

2026-07-31 BE/FE 人工 E2E 交接

#1398 Backend 已部署到 Ken Branch 與 docker1 HQ staging,安全 API/WebSocket contract 驗證已通過;完整 E2E 將等待本 #1399 完成並部署後,由 LIFF 起點人工執行。

父項 #1397 已新增附件 #1106:
redmine-1397-liff-manual-e2e-test-plan-zh-TW.md

FE 完成後的共同驗收重點包括:

  • 詳細頁只用 WebSocket initial snapshot,不另 call detail REST。
  • public 不顯示離峰 switch;private 顯示 switch。
  • READY/OFF_PEAK_WAITING/QUEUED/PREPARING/CHARGING/FINISHING 與 semantic actions。
  • operationId、一次性 targeted operationResult、initial snapshot 不重播 terminal result。
  • WebSocket 斷線時舊 actions 立即不可操作,一次重連失敗後退列表並顯示 modal。
  • 離峰 charging 停止確認、current-window suppression,以及 OFF→ON 才重新排程。
  • PUBLIC 非 transaction owner 停止被拒絕;PRIVATE 任一目前關聯住戶可停止。

附件另含 Ken 環境 CP001-001/002 的公私樁切換、資料準備、16 個案例、證據與還原步驟。此 note 只做驗收交接,不變更 #1399 目前狀態。

是由 陳國瑋3 天 前更新

h3. 2026-07-31 POST /api/connector/commands OpenAPI/FE 使用補充

FE 回報 POST /api/connector/commands 的 200/202/403/409 response body 在 OpenAPI 只顯示通用 object,導致 yarn openapi:refresh 無法產生可用型別。這不是 FE 使用方式問題,而是 #1398 Backend 原本 endpoint 使用 ResponseEntity<?>@ApiResponse 未明確指定 schema。

#1398 已補 OpenAPI schema annotation:

  • 200 OK202 Accepted response body:ConnectorCommandResponse
  • 403 Forbidden409 Conflict response body:ConnectorCommandErrorResponse

請 FE 在新版 OpenAPI 產出後依下列契約串接。

h4. Request

POST /api/connector/commands

Header:

  • X-Line-Id: 由 LIFF session 帶入,FE 不傳 userId。

Body:

{
  "buildingId": "B001",
  "connectorId": "CP001-001",
  "action": "START_CHARGING"
}

action 必須直接使用目前 snapshot 的 driverState.actions[].code,可用值:

  • START_CHARGING
  • STOP_CHARGING
  • CANCEL_START
  • CANCEL_MANUAL_START
  • ENABLE_OFF_PEAK
  • DISABLE_OFF_PEAK

FE 不要用按鈕文字反推 action,也不要自行組 OCPP command。

h4. Success response:200/202 = ConnectorCommandResponse

欄位:

  • action: 本次執行的 semantic action。
  • operationId: 只有實際 dispatch RemoteStart/RemoteStop 時會有值;同步 command 可能為 null。
  • operationStatus: 只有非同步 operation 會有值,通常為 SENT
  • enableOffPeak: Branch 實際離峰偏好;FE switch 應以此值校正。
  • driverState: command 後最新狀態;FE 應立即用它更新畫面。
  • activeOperation: 尚未終結的 RemoteStart/RemoteStop operation;沒有則為 null。

200 OK:同步完成,例如建立/取消 queue、開關離峰偏好、取消尚未 dispatch 的 start 等。通常不代表有 operationId

202 Accepted:RemoteStart/RemoteStop 已送出,但尚未等到 OCPP 最終結果。FE 進入 pending UI,保存 operationId,等待 WebSocket targeted operationResult,並用 operationId 去重 modal。

範例:

{
  "action": "START_CHARGING",
  "operationId": "18456",
  "operationStatus": "SENT",
  "enableOffPeak": false,
  "driverState": {
    "code": "PREPARING",
    "title": "準備中",
    "description": "正在要求充電樁開始充電",
    "reason": null,
    "actions": []
  },
  "activeOperation": {
    "operationId": "18456",
    "type": "START",
    "source": "MANUAL",
    "status": "SENT",
    "requestedAt": "2026-07-31T18:20:00",
    "deadlineAt": "2026-07-31T18:20:30"
  }
}

h4. Error response:403/409 = ConnectorCommandErrorResponse

欄位:

  • errorCode: 穩定 business error code,供 FE 做必要分流。
  • message: 可顯示給車主的錯誤訊息。
  • enableOffPeak: Branch 實際離峰偏好;尤其 off-peak switch 被拒絕時,FE 必須用它回復 switch 狀態。
  • driverState: 拒絕當下最新狀態;FE 應用它更新主要按鈕與狀態文字。

403 Forbidden:使用者或 connector 存取權限不允許,例如 PUBLIC 非 transaction owner 停止、PRIVATE 未關聯住戶操作等。

409 Conflict:使用者有權看該 connector,但目前狀態不允許該 action,例如 charging 中重複 start、pending operation 中重複操作、離峰 current-window suppression 等。

範例:

{
  "errorCode": "COMMAND_NOT_ALLOWED",
  "message": "目前狀態不允許執行此操作",
  "enableOffPeak": true,
  "driverState": {
    "code": "CHARGING",
    "title": "充電中",
    "description": "車輛正在充電",
    "reason": null,
    "actions": []
  }
}

h4. FE AI 實作重點

  • 呼叫前:只允許使用 fresh WebSocket snapshot 裡 enabled 的 driverState.actions[]。WebSocket 斷線期間不得沿用舊 actions。
  • 呼叫後:無論 200/202/403/409,都要以 response body 的 driverStateenableOffPeak 校正畫面。
  • 只有 202 且有 operationId 時,才等待 WebSocket operationResult 顯示一次性 modal。
  • 403/409 不會再透過 operationResult 補一次;FE 應直接顯示 response body 的 message
  • PUBLIC connector 不顯示 off-peak switch;PRIVATE connector 可送出 switch command,由 API 回應決定成功或拒絕。

是由 陳國瑋3 天 前更新

h3. 2026-07-31 HQ staging OpenAPI 已更新,可重新產生 FE types

#1398 Backend 已 push 並重新部署 HQ staging。

  • Branch:feature/redmine-1398-driver-state-offpeak-backend
  • Latest commit:a21baa3
  • HQ staging container:ems-hq-staging-api 已重建並為 Up

已驗證 staging HQ /api-docsPOST /api/connector/commands response schema:

  • 200/202:ConnectorCommandResponse
  • 403/409:ConnectorCommandErrorResponse

Ryan/FE AI 現在可以重新執行 yarn openapi:refresh。若產生的 client 對非 2xx response 不直接回傳 typed body,FE 仍需在 HTTP error handler 讀取 response body,body schema 依本 ticket 前一則 contract note 的 ConnectorCommandErrorResponse 處理。

是由 鍾正剛3 天 前更新

h3. 2026-07-31 FE 實作完成(待部署後人工 E2E)

已完成 #1399 前端實作,branch: @feature/redmine-1399-liff-driver-state-actions@(commit 2e4386b),依 #1397 附件 #1102/#1103 與 note #4940 的 command API 契約串接。尚未建立 MR。

h4. 主要變更

  • 所有車主操作統一改呼叫 @POST /api/connector/commands@,@action@ 直接使用 @driverState.actions[].code@,不以按鈕文字反推指令;移除舊的 start/stop/dequeue/offpeak-switch 與 @driverState.start@/@stop@。
  • 詳細頁改為 WebSocket-only:移除 detail REST 呼叫、輪詢與斷線 fallback。
  • 新增 @activeOperation@/@operationResult@ 支援;202 進入 pending,以 @operationId@ 去重、結果 modal 只顯示一次,initial snapshot 不重播。
  • WebSocket 斷線立即停用舊 actions;一次短暫重連仍失敗即退回列表並顯示 modal。
  • 200/202/403/409 一律以回應的 @driverState@ 與 @enableOffPeak@ 校正畫面。
  • 離峰 switch 改由 @ENABLE_OFF_PEAK@/@DISABLE_OFF_PEAK@ action 決定,FE 不再維護 business matrix;停止離峰充電顯示規格 §11.5 三項說明的確認 modal。

h4. 與 Ken 確認後的調整

  • 公私樁合併為單一路由 @/charger/[id]@,刪除 @/charger/[id]/private@ 與兩份重複 dashboard,差異改由 WebSocket snapshot 的 @areaType@ 與 @actions[]@ 導出。順帶修正既有問題:@rate-info.tsx@ 的「返回設備」原本一律連到公樁畫面,私樁使用者會看不到離峰 switch。
  • 移除全部 FE 狀態轉換與死碼:@getChargerStatus@/@ChargerStatus@ 及七個未使用型別、@useConnector@(未使用且有 10 秒輪詢)、@logUserAction@。
  • 操作行為紀錄:原 @logUserAction@ 呼叫的 @/api/logs/user-actions@ 會被 rewrite 轉到 HQ,而 HQ 沒有任何 log endpoint,實際上每次開始/停止都在等一個必定 404 的請求且結果被吞掉,等於沒有留下紀錄。已改走 @logUI@(→ @/api/logs@ → winston)並統一 metadata:@action@/@phase@/@result@/@httpStatus@/@operationId@/@errorCode@/@driverState@ 前後值/被阻擋原因。@operationId@ 與 Branch @e_remote_transaction_log.id@ 相同,可依附件 #1106 §11.2 串起 LIFF、HQ、Branch、OCPP 與 transaction。另修正 WS log 原本寫入完整 payload 的問題,改為只記摘要。log 保存期由 14d 調整為 90d

h4. 部署注意(重要)

依規格不提供舊契約相容層,因此本 branch 必須與 #1398 協調部署。2026-07-31 查核:staging @hq-api.sylksoft.com@ 已是新契約;正式站 @hq-api.cloudlinkems.com@ 仍為舊版且 @DriverConnectorPresentationDto@ 尚未存在。若在正式站 HQ 升級前部署本前端,充電功能會全面失效。

另需同步更新各環境的 @LOG_MAX_FILES=90d@(@env.example@ 已更新,實際部署環境變數需一併調整)。

h4. 驗證狀態

  • @npx tsc --noEmit@:通過
  • @yarn build@:通過
  • @yarn lint@:無法執行,既有問題(@eslint-config-next@ 以 CommonJS 匯出,@next lint@ 讀取 @rules@ 具名匯出失敗),發生在載入 ESLint 設定階段,與本次改動無關,本次未修改任何 lint/build 設定。
  • 實機 E2E:尚未執行,依附件 #1106 於部署後由 LIFF 起點人工執行,未預先標記任何案例為 PASS。

h4. E2E Catalog Impact

No catalog change。本次 FE 行為已完整涵蓋於 #1398 宣告的 UI-002、UI-010、CHG-033~035、ROT-021~024;為避免與 #1398 branch 對 @documents/E2E_TEST_CASE_INVENTORY.md@ 的大幅修改衝突,本 ticket 不重複編輯該檔。

完整交付說明見 branch 內 @documents/redmine-1399-liff-driver-state-fe-notes.md@。

是由 鍾正剛3 天 前更新 · 已被編輯

h3. 2026-07-31 修正:command 被送出兩次

回報按下停止充電時 @POST /api/connector/commands@ 會送兩次。已定位並修正,commit @dc82cd6@。

h4. 原因

@src/components/query-provider.tsx@ 的 QueryClient 全域預設為 @mutations: { retry: 1 }@。React Query 只在 @mutationFn@ reject 時重試,因此只要 command 被拒絕(403/409)或逾時,就會自動再送一次,總共兩次。

也就是說:看到兩次請求,代表第一次其實已經失敗,第二次是自動重送。

h4. 風險

connector command 不是 idempotent —— Branch 每次成功 dispatch 都會持久化一筆 remote operation。自動重試違反規格:

  • §9:operation 未完成期間不得盲目重送 RemoteStart。
  • §10:不自動重送 RemoteStop。
  • §15.2:同一 Connector 不得存在重疊的未完成 operation。

最危險的是逾時情境:第一次請求可能已送達 Branch、只是回應遺失,自動重試就會送出第二筆真實的充電指令,並在 audit log 產生重複的 operationId 紀錄。

h4. 修正

在 @useConnectorCommand()@ 明確覆寫 @retry: 0@,重試一律改由車主明確再按一次。已加註解說明不可移除的理由。

驗證:@npx tsc --noEmit@ 與 @yarn build@ 皆通過。

h4. 另外提醒(不在本次修改範圍)

全域 @mutations.retry: 1@ 仍套用在其他 mutation 上(例如綁定流程)。這些同樣不是 idempotent,建議評估是否將全域預設改為 @retry: 0@、再由個別需要重試的查詢自行開啟。若要調整,因影響範圍跨功能,建議另開 ticket 處理。

是由 鍾正剛3 天 前更新 · 已被編輯

h3. 2026-07-31 補充:mutation 全域關閉自動重試

依 Ryan 指示,將 QueryClient 的全域 @mutations.retry@ 由 1 改為 0,commit @524aaaf@。

盤點結果:專案內共 5 支 mutation,先前全部繼承 @retry: 1@,且全部都是有副作用、非 idempotent 的操作:

|. 位置 |. 操作 |_. 自動重試的後果 |
| @app/charger/hooks/use-connector-commands.ts@ | connector command | 重複的 RemoteStart/RemoteStop operation 與 audit 紀錄 |
| @src/hooks/use-binding.ts:206@ | sendVerificationCode | 重複發送 OTP 簡訊,使用者收到兩組驗證碼 |
| @src/hooks/use-binding.ts:219@ | verifyCode | 多消耗一次驗證次數,可能觸發鎖定 |
| @src/hooks/use-binding.ts:191@ | executeBinding | 重複執行綁定 |
| @app/setting/hooks/use-unbind.ts:28@ | unbindUser | 重複解除綁定 |

沒有任何一支適合自動重試,因此改為全域 @retry: 0@,重試一律由使用者明確再操作一次。@useConnectorCommand@ 仍保留自身的 @retry: 0@ 宣告,確保全域設定日後被改動時,這支安全關鍵的 API 仍不會被重送。

驗證:@npx tsc --noEmit@ 與 @yarn build@ 皆通過。

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

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

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

  • 子任務 #1403 已新增

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

  • 子任務 已刪除 (#1403)

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

  • 子任務 #1405 已新增

是由 鍾正剛約 14 小時 前更新

  • 狀態New 變更為 Resolved
動作

匯出至 Atom PDF