專案

一般

配置概況

討論 #1424

是由 陳國瑋 於 30 天 前更新

## 背景 

 A17 Dashboard 主需求為 #1422。 

 本票用於定義 Dashboard 各區塊的資料更新通道:哪些資料使用 REST API、哪些適合透過 WebSocket 即時推送,以及兩者如何在 initial load、重連、stale、partial data 與權限控制下協作。 

 **目前是 Proposed Design/需求待確認,不代表已進入開發。** 

 ## 現有程式碼觀察 

 ### 已有能力 

 - `react/branch` 已有 `useAdminConnectorSocket()`。 
 - `java/ems_branch` 已有 `/ws/admin/connectors/{connectorId}`。 
 - 既有流程先以 SS3A REST API 取得 30 秒、一次性、connector-bound ticket,再建立 WebSocket。 
 - 新連線會收到 `CONNECTOR_SNAPSHOT`,後續可收到 `OPERATION_RESULT`。 
 - `ConnectorEventPublisher` 已涵蓋 Connector status、MeterValue、Transaction、queue、off-peak、offline 等多條異動路徑,並在 commit 後重查 authoritative snapshot。 
 - Server 已使用 `ConcurrentWebSocketSessionDecorator` 保護並行推送。 

 ### 現有限制 

 - 現有 Admin WebSocket 是「一個 Connector 一條連線」,不適合 Dashboard 同時顯示 20/30/100 個 Connector。 
 - 若 Dashboard 為每個 Connector 建立 socket,會造成 N 張 ticket、N 條連線、N 組重連與 browser/server 資源放大。 
 - 現有 WebSocket contract 是 FE 手寫型別,不在 OpenAPI 內;Dashboard event contract 必須另外版本化與測試。 
 - Charge Point ONLINE/heartbeat、billing、usage trend 並非全部都有現成 Dashboard event publisher。 
 - WebSocket 只是一種 transport,不可取代資料庫 authoritative state,也不可讓斷線後的舊資料看起來仍是即時資料。 

 ## Proposed Architecture 

 採用: 

 > 一次 REST bootstrap + 一條案場層級 Dashboard WebSocket + REST drill-down 

 ### Initial load 

 1. `GET /api/dashboard/overview` 載入完整初始畫面、歷史資料與非即時資料。 
 2. 以具 Dashboard function 的 REST endpoint 取得一次性、短效、scope-bound ticket。 
 3. 建立一條 `/ws/admin/dashboard/{scopeId}?ticket=...`。 
 4. WebSocket 先回 `DASHBOARD_LIVE_SNAPSHOT`,作為即時區塊的 authoritative initial state。 
 5. 後續只推送有變化的「區塊 replacement event」,不傳 raw database rows,也不要求 FE 自行重算全站統計。 

 `scopeId` 最終是 buildingId、siteId,或由 Branch/JWT 隱含決定,需配合 #1422 的單站/多站決策;ticket 必須綁定使用者與已授權 scope,不信任 client 任意指定的 scope。 

 ## 區塊與資料通道矩陣 

 | Dashboard 區塊 | 資訊 | Primary channel | 更新策略 | 
 |---|---|---|---| 
 | Header | 案場名稱、timezone、選擇的 range | REST | 首次載入/切換範圍 | 
 | Header | REST `generatedAt`、WS connection state、最後 event 時間、各 source freshness | Hybrid | REST 提供 source snapshot;FE 顯示 socket 狀態;WS 推 freshness transition | 
 | Needs Attention | Connector FAULTED、CP OFFLINE、stale、queue BLOCKED/超時、容量告警 | WebSocket + REST bootstrap | post-commit/stale transition 後重算;severity-first replacement | 
 | Needs Attention | billing overdue、invoice/settlement 異常、未配置 tariff/group | REST | 5–15 分鐘 refetch 或手動刷新;V1 不新增 billing WS | 
 | KPI Row | CP Online/Offline、Connector status、active sessions、waiting/blocked、目前總功率 | WebSocket + REST bootstrap | 即時區塊 replacement | 
 | KPI Row | CP/Connector 配置總數、今日 kWh、本月帳務 | REST | 資產異動時手動/60 秒 refresh;usage 5 分鐘;billing 15 分鐘 | 
 | Live Load | currentPowerKw、utilization、latestMeterAt | WebSocket + REST bootstrap | MeterValue 事件聚合,建議最多每 5 秒推一次 | 
 | Live Load | TODAY/7 DAYS 歷史 series | REST | 首載與 range 切換;不透過 WS 重送整段 series | 
 | Group Capacity | contract capacity、allocated power、reserved slots 等設定 | REST | 首載/手動刷新/低頻刷新 | 
 | Group Capacity | occupied、available、waiting、blocked、longest wait | WebSocket + REST bootstrap | Transaction/queue/status commit 後更新 | 
 | Connector Grid | summary、bounded preview、remaining abnormal count | WebSocket + REST bootstrap | Server 統一排序並推送整個 bounded preview replacement | 
 | Connector Full List | 搜尋、filter、pagination/virtualization | REST | 使用/擴充既有 search API | 
 | Connector Detail Drawer | 單支 Connector 完整狀態 | 既有單 Connector WebSocket | Drawer 開啟時才建立;關閉即釋放 | 
 | Usage Trend | 7/30/90 日 kWh、sessions | REST | 首載、range 切換、5 分鐘 refresh | 
 | Billing Pipeline | billing/settlement/invoice counts 與金額 | REST | 首載、15 分鐘 refresh、手動刷新 | 

 ## 不適合透過 WebSocket 的資料 

 - 歷史趨勢整段 series。 
 - Billing/Settlement/Invoice pipeline。 
 - 完整 Connector inventory、搜尋結果與分頁。 
 - 案場名稱、時區、contract capacity 等低頻設定。 
 - Export/報表。 
 - 任何可由 REST 查詢且容許 1–15 分鐘延遲的資料。 

 理由:這些資料更新頻率低、payload 可能較大,或需要 query parameter/pagination;WebSocket 不會因此提供足夠效益,反而增加事件契約與一致性成本。 

 ## Dashboard WebSocket Event Contract 

 建議 envelope: 

 ```json 
 { 
   "schemaVersion": "1", 
   "type": "CONNECTOR_OVERVIEW_UPDATED", 
   "scopeId": "A17", 
   "sequence": 42, 
   "occurredAt": "2026-08-10T16:00:00+08:00", 
   "data": {} 
 } 
 ``` 

 V1 event types: 

 - `DASHBOARD_LIVE_SNAPSHOT` 
 - `NETWORK_HEALTH_UPDATED` 
 - `ALERTS_UPDATED` 
 - `LIVE_LOAD_UPDATED` 
 - `CHARGE_GROUP_OVERVIEW_UPDATED` 
 - `CONNECTOR_OVERVIEW_UPDATED` 
 - `SOURCE_FRESHNESS_UPDATED` 

 規則: 

 - Event 以「區塊 replacement」為主,不讓 FE 由細碎 delta 自行推導 summary/排序。 
 - 每條連線的 `sequence` 單調遞增;initial live snapshot 必須先於後續 update。 
 - FE 忽略較舊 sequence;發現 gap、重連或 schemaVersion 不支援時,重新取得 snapshot。 
 - 所有 event 帶 server `occurredAt`;各資料區塊另帶 source `updatedAt`/freshness。 
 - Dashboard preview 不回 currentIdTag 等不必要的敏感欄位;詳細資料留在既有權限控制的 detail flow。 
 - Unknown/null 不可轉成 Available/0。 

 ## Event Trigger 與節流 

 | Source | Trigger | 建議推送 | 
 |---|---|---| 
 | Connector StatusNotification | status commit 後 | 2 秒內更新 Connector/KPI/Alerts | 
 | Transaction start/stop | transaction commit 後 | 2 秒內更新 Connector/KPI/Group | 
 | Queue/off-peak/rotation | priority lifecycle commit 後 | 2 秒內更新 Group/Connector/Alerts | 
 | CP offline watchdog/connection transition | ONLINE/OFFLINE/UNKNOWN transition | 2 秒內更新 Network/Connector/Alerts | 
 | MeterValue | 有新 power value | coalesce;最多每 5 秒更新 Live Load/受影響 preview | 
 | Heartbeat | 每次 heartbeat | 不逐次推送;只在 connection/freshness class 改變時推 | 
 | Usage aggregate | 時間累積 | REST 每 5 分鐘 | 
 | Billing pipeline | 帳務異動/月結 | REST 每 15 分鐘或手動刷新 | 

 實際秒數需以 A17 MeterValue 頻率、同時在線 Connector 數與 browser 消費能力驗證後鎖定。Browser WebSocket 沒有 backpressure,因此不得把每筆 raw MeterValue 原樣 fan-out。 

 ## 斷線、Stale 與降級 

 - Dashboard 是唯讀頁,可採 capped exponential reconnect;不能只顯示一次錯誤後永久停止更新。 
 - WS 斷線時保留最後資料,但立即顯示「即時連線中斷」;不可清成 0。 
 - 超過各 source cutoff 後,該資料標示 STALE,且 stale power 不納入 current total。 
 - WebSocket 無法恢復時,允許以 REST 30/60 秒 polling 降級,但 UI 必須明示「定時更新模式」,不可冒充即時。 
 - 手動 Refresh 同時重新取得 REST overview 與新的 WebSocket ticket。 
 - Browser 回到 visible state 時檢查 connection 與 freshness;必要時重新 bootstrap。 
 - Server 重啟、sequence gap 或 event parse failure 均以重新 snapshot 恢復,不嘗試從 client cache 猜測遺失事件。 

 ## Backend 開發影響 

 - 新增 Dashboard-level ticket、handler、session registry、broadcaster、live snapshot service 與 typed WS DTO。 
 - 沿用現有一次性 ticket、post-commit authoritative re-query 與 concurrent send 保護模式。 
 - 新增 CP lifecycle/freshness transition 的 Dashboard event hook;不可假設 Connector publisher 能涵蓋所有 CP online/heartbeat 異動。 
 - Connector/queue/transaction/MeterValue event 需做 section invalidation 與 debounce/coalesce。 
 - Billing/usage 不新增 WebSocket publisher。 
 - 對 Dashboard WS 與 ticket 加入 SS3A Dashboard function 授權。 
 - 若未來採多 instance,in-memory session registry 與 sequence 需另行評估跨 instance fan-out;V1 不應默認單 instance 永遠成立。 

 ## Frontend 開發影響 

 - 新增一個 Dashboard socket hook,不可對 preview 每張卡呼叫 `useAdminConnectorSocket()`。 
 - 將 REST overview 與 WS live section 分層保存;以 server event replacement 更新對應 section。 
 - Header 明確區分:REST 最後成功更新、WS 連線狀態、source freshness。 
 - range/trendDays 只重新查 REST 歷史資料,不重建不相關的 live socket。 
 - Connector detail drawer 開啟後才沿用既有單 Connector socket。 
 - Production API/WS error 不得 fallback 到 fixture。 
 - WebSocket contract 為手寫型別時,需有 fixture/parser/unknown event/schema version tests。 

 ## Acceptance Criteria 

 - [ ] Dashboard 不論有 1、2、30 或 100 個 Connector,本體只建立 1 條案場級 WebSocket。 
 - [ ] 只有開啟 Connector detail drawer 時,才可額外建立 1 條既有 connector-bound WebSocket。 
 - [ ] 初始 REST 與 initial WS snapshot 不會發生舊事件覆蓋新資料。 
 - [ ] Connector status/transaction/queue/CP connection transition 在 commit 後於目標延遲內反映。 
 - [ ] MeterValue 不逐筆無限制推送;Live Load 有明確 coalesce 上限。 
 - [ ] Usage trend、Billing pipeline、完整 Connector list 不走 WS。 
 - [ ] WS 斷線時立即顯示非即時狀態,資料不清 0;超過 cutoff 正確標示 STALE。 
 - [ ] WS 無法恢復時可進入明示的 REST polling fallback。 
 - [ ] sequence gap/server restart/schemaVersion 不支援會重新 snapshot。 
 - [ ] 未具 Dashboard function 無法取得 ticket;ticket 一次性、短效且綁定使用者與 scope。 
 - [ ] Dashboard event 不洩漏 currentIdTag、JWT、ticket 或不必要個資。 
 - [ ] 390/768/1024/1440 px 的 loading、connected、reconnecting、fallback、stale state 均可辨識。 
 - [ ] A17 壓測驗證同時在線管理者、MeterValue 頻率、payload size、server buffer 與 browser CPU/memory。 

 ## 待確認事項 

 - [x] V1 的 REST、ticket、WebSocket URL 與 event envelope 明確帶 `buildingId`。 
 - [x] [ ] Dashboard WebSocket 採 capped exponential backoff:1、2、4、8、16、30 秒,30 秒封頂並加入 ±20% jitter。 read-only socket 的 reconnect 次數/backoff 上限? 
 - [x] WebSocket 中斷期間啟用每 60 秒完整 Overview [ ] WS 失敗後是否允許 REST polling fallback。 fallback;30 秒或 60 秒? 
 - [ ] Live Load coalesce 是否採 5 秒? 
 - [ ] Status/queue/transaction update 的 SLA 是否採 2 秒? 
 - [x] REST overview 保留完整 live data,作為 bootstrap、手動刷新與 WebSocket fallback。 
 - [ ] 多 instance 部署是否在 V1 scope;若是,跨 instance event fan-out 必須一起設計。 
 - [ ] Dashboard WS contract 採現有 raw WebSocket envelope,或引入 STOMP? 
 - [ ] source stale cutoff 是否集中在 backend config 回傳給 FE? 

 ## 建議預設決策 

 - V1 延續現有 raw WebSocket,不為單一 Dashboard 引入 STOMP。 
 - REST overview 保留所有區塊,作為 bootstrap 與降級來源。 
 - 一條 Dashboard socket 推送 bounded section replacement。 
 - Live Load 5 秒 coalesce;其他 operational event 2 秒內。 
 - read-only Dashboard 可持續 capped exponential reconnect,並在失敗時切 60 秒 REST polling。 
 - stale cutoff 由 Backend 統一定義並在 response/event 回傳,不由 FE 各自硬編碼。 

 ## 已確認決策 

 ### DEC-001:V1 明確使用 buildingId 作為 Dashboard scope 

 **狀態:已確認(2026-08-10,Ken)** 

 - `GET /api/dashboard/overview` 明確接受/解析 `buildingId`。 
 - Dashboard WebSocket ticket request 必須指定 `buildingId`,ticket 同時綁定 admin identity 與 `buildingId`。 
 - WebSocket URL 採 `/ws/admin/dashboard/{buildingId}?ticket=...`。 
 - 所有 Dashboard event envelope 都必須帶相同 `buildingId` scope;原提案的泛稱 `scopeId` 在 V1 contract 具體化為 `buildingId`。 
 - Backend 必須由 JWT/SS3A 權限驗證使用者可存取該 building;不得只因 client 傳入 buildingId 就允許訂閱。 
 - FE 切換 building 時,必須關閉舊 socket、清除舊 building 的 live state、重新取得 overview 與新 ticket,避免跨案場資料混合。 
 - 即使 A17 目前只有單一 building,V1 contract 仍保留明確 scope,避免未來多案場時重做 API、ticket 與事件契約。 

 ### DEC-002:Overview API 保留完整即時摘要 

 **狀態:已確認(2026-09-01,Ken)** 

 - `GET /api/dashboard/overview?buildingId={buildingId}` 必須回傳 Dashboard 所需的完整初始資料,包含即時摘要與非即時區塊。 
 - 即時摘要至少包含 Network Health、operational alerts、Connector summary/bounded preview、active sessions、queue/group runtime 與 current live load。 
 - WebSocket 的用途是接續 REST snapshot 提供後續增量更新,不是 Dashboard 唯一資料來源。 
 - 首次畫面可先以 REST 完成顯示;取得 WebSocket initial snapshot 後,再依 server sequence/occurredAt 套用較新的資料,避免舊資料覆蓋新資料。 
 - 手動 Refresh 必須重新取得完整 overview,並重新建立對應 building 的 WebSocket session。 
 - WebSocket 無法建立或持續中斷時,Overview API 可作為 polling fallback;UI 必須明示目前為定時更新模式。 
 - REST 與 WebSocket 必須共用相同的 Dashboard aggregation/presentation service 或明確共用規則,避免同一 KPI 在兩種通道出現不同公式。 
 - Partial/stale/null 語意在 REST 與 WebSocket 必須一致;未知值不得轉為 0 或 AVAILABLE。 

 ### DEC-003:WebSocket 指數退避重連與 60 秒 REST 降級 

 **狀態:已確認(2026-09-01,Ken)** 

 - WebSocket 非預期中斷後採 capped exponential backoff,重連等待序列為 1、2、4、8、16、30 秒,後續每次最多等待 30 秒。 
 - 每次等待加入 ±20% jitter,避免多個 Dashboard client 在服務恢復時同時重連。 
 - WebSocket 一中斷,FE 必須立即顯示「即時連線中斷/定時更新模式」,不可繼續把最後 snapshot 標示為即時。 
 - 中斷期間保留最後成功資料,不清為 0;各資料仍依 Backend 提供的 cutoff 判斷 freshness/stale。 
 - WebSocket 中斷期間每 60 秒重新呼叫完整 `GET /api/dashboard/overview?buildingId={buildingId}`。 
 - REST polling 與 WebSocket 背景重連可以同時進行,但同一 building 同時間只允許一個 reconnect loop 與一個 polling timer。 
 - Browser network 由 offline 轉 online,或頁面由 hidden 回到 visible 時,可立即嘗試一次重連,不必等待目前 backoff timer。 
 - WebSocket 恢復後,必須先收到/取得新的 `DASHBOARD_LIVE_SNAPSHOT`,確認 buildingId 與版本正確,再停止 REST polling;僅 socket open 不視為資料已恢復即時。 
 - 使用者切換 building 或離開 Dashboard 時,必須取消舊 building 的 socket、backoff timer 與 polling timer。 
 - 預期的授權錯誤(401/403)、不支援的 schemaVersion 或無效 building scope 不做無限重試,應停止並顯示可診斷錯誤。 

 ## 需求管理狀態 

 - Parent:#1422 
 - Related:#1423(Connector 數量彈性顯示規則) 
 - Requirement status:**待確認** 
 - Final specification:全部決策完成後整併至 #1422 的 v1.0 最終需求文件 
 - Implementation tickets:需求鎖定後才建立 FE/BE/QA child issues

返回