Feature #1399
進行中Feature #1397: [功能開發] LIFF DriverState、離峰充電與 Remote Operation 狀態契約重構
[Frontend] LIFF 串接 DriverState actions 與 WebSocket 即時狀態
100%
概述
目標¶
依父項 #1397 及其英文/繁體中文規格附件,完成 LIFF 對新版 DriverState、semantic actions、長期離峰充電與 WebSocket 即時狀態契約的串接。
附件內容為本子任務的需求與驗收基準。
實作範圍¶
- BE OpenAPI contract 完成後重新產生 types。畫面依據
driverState.title、description、actions與 semantic action code 呈現,不自行由 raw connector status 重建業務狀態。 -
PREPARING顯示「準備中」,並停用重複開始操作。 -
FINISHING顯示「停止中」,並停用重複停止操作。 -
OFF_PEAK_WAITING與QUEUED都提供「取消排隊」主要操作,但必須透過 action code 呼叫正確 command,不得以按鈕文字判斷操作。 -
PUBLICConnector 不顯示離峰 switch;此項顯示差異保留在 FE。 -
PRIVATEConnector 的離峰 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,並處理以下結果:- 結構化
403/409 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 才能恢復操作
- 立即停用舊 snapshot 的
- 補齊必要的 loading、pending、confirmation、error modal 與重連狀態,避免重複點擊與狀態倒退。
驗收重點¶
-
PUBLIC/PRIVATE、MANUAL/OFF_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 與
PUBLICslot 設定問題,分別由 #1395、#1396 處理。
備註記錄(8 則)¶
#1 - 陳國瑋(2026-07-31T03:09:43Z)¶
2026-07-31 BE/FE WebSocket contract 補充(Ken 已確認)¶
- 新版 Backend 會直接移除舊的
driverState.start/driverState.stop;FE 必須只使用driverState.actions[]與 semantic action code,不得保留對舊欄位的依賴。 -
driverState、driverState.reason與driverState.actions[]是 Connector 共用狀態,會推送給所有正在觀看該 Connector 的合法 sessions。 - Transient
operationResult是使用者定向通知,不得當成 Connector 共用 modal 廣播:- 手動 Start/Stop/Cancel 的非同步結果,以實際
requestedByUserId為 target。 - Scheduler 發起的 OFF_PEAK RemoteStart 結果,以長期離峰設定的
enabledByUserId為 target。
- 手動 Start/Stop/Cancel 的非同步結果,以實際
- 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 OK與202 Acceptedresponse body:ConnectorCommandResponse -
403 Forbidden與409 Conflictresponse 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_CHARGINGSTOP_CHARGINGCANCEL_STARTCANCEL_MANUAL_STARTENABLE_OFF_PEAKDISABLE_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 的
driverState與enableOffPeak校正畫面。 - 只有 202 且有
operationId時,才等待 WebSocketoperationResult顯示一次性 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-docs 的 POST /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/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_PEAKaction 決定,FE 不再維護 business matrix;停止離峰充電顯示規格 §11.5 三項說明的確認 modal。
與 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與 Branche_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=90d(env.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 --noEmit 與 yarn 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 --noEmit 與 yarn build 皆通過。
是由 陳國瑋 於 3 天 前更新
h3. 2026-07-31 BE/FE WebSocket contract 補充(Ken 已確認)
- 新版 Backend 會直接移除舊的
driverState.start/driverState.stop;FE 必須只使用driverState.actions[]與 semantic action code,不得保留對舊欄位的依賴。 -
driverState、driverState.reason與driverState.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 OK與202 Acceptedresponse body:ConnectorCommandResponse -
403 Forbidden與409 Conflictresponse 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_CHARGINGSTOP_CHARGINGCANCEL_STARTCANCEL_MANUAL_STARTENABLE_OFF_PEAKDISABLE_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 的
driverState與enableOffPeak校正畫面。 - 只有 202 且有
operationId時,才等待 WebSocketoperationResult顯示一次性 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-docs 的 POST /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@ 皆通過。