討論 #1424
是由 陳國瑋 於 28 天 前更新
## 背景
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;WAITING duration 僅顯示 |
| 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 分鐘 |
| Live Load(#1462 LOAD-DEC-001/004) LOAD-DEC-001) | currentPowerKw、LIVE/TODAY/SEVEN_DAYS open buckets、跨界 finalized bucket、latestMeterAt、freshness/source currentPowerKw、latestMeterAt、freshness/source status;不含推算的案場契約容量、利用率或剩餘容量 | WebSocket + REST bootstrap | MeterValue 事件聚合,最多每 5 秒推一次;不重送完整 series 秒推一次 |
| Live Load 歷史 series(#1462 LOAD-DEC-003/004) LOAD-DEC-003) | LIVE:最近 60 分鐘/1 分鐘 bucket;TODAY:案場當日 00:00 至現在/15 分鐘 bucket;SEVEN_DAYS:含今天 7 個案場日曆日/1 小時 bucket;Y 值為 time-weighted averagePowerKw | REST + WebSocket bucket replacement | REST 提供完整 snapshot;WS 只更新 open/finalized buckets,不重送整段 首載與 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(#1461 USG-DEC-001~009) | 最近 30 個案場日曆日(含今天)的有效 kWh、valid Sessions、平均每次充電量;固定 30 daily buckets;成功無使用為 0、未知為 null;全段零為 COMPLETE + EMPTY;initial/refresh error、lastSuccessfulAt、STALE;依 building timezone 的交易開始日歸屬;異常 Completed 排除筆數與 PARTIAL status;不含時間使用率 | REST | 首載、5 分鐘 refresh;有 cache 時保留最後成功 snapshot;平均值由 Backend 計算;Sessions 僅 COMPLETED + positive energy;ACTIVE 不估算;跨日不拆分 |
| 結算流程(#1460 BIL-DEC-001/002/003) | periodYear/periodMonth/timezone、Connector 結算、門牌結算、Billing status、Invoice status | REST | 首載、15 分鐘 refresh、手動刷新;唯讀;導覽 route 由 FE 固定 |
## 不適合透過 WebSocket 的資料
- 歷史趨勢整段 series。
- 結算流程沿用 attachment #1110 四步驟面板;透過 REST 提供正式資料與 Backend 決定的最近結算期間,詳見 #1460 BIL-DEC-001/002。
- 完整 Connector inventory、搜尋結果與分頁。
- 案場名稱、時區、Charge Group 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 分鐘 |
| 結算流程 | 帳務異動/月結後由下一次 REST refresh 反映 | REST 每 15 分鐘或手動刷新;不新增 WS |
實際秒數需以 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。
- kWh/Sessions 切換使用同一個固定 30 日 REST range,不重建不相關的 live socket;V1 不送出 7/90 日或自訂 range 查詢。
- Usage daily buckets 必須直接使用 Backend 回傳的完整 30 日序列;Frontend 不自行補缺日為 0。明確的 0 顯示零值,null 顯示資料缺口並標示 PARTIAL。
- 時間使用率已依 #1461 USG-DEC-005 移除;第三個摘要依 USG-DEC-006 改為 Backend REST 回傳的 averageEnergyPerSessionKWh,不新增 WebSocket event。
- Connector detail drawer 開啟後才沿用既有單 Connector socket。
- 結算面板的 /bill/list 與 /invoice/list 導覽由 FE 使用既有 route;Backend REST/WS contract 不回傳 URL。
- Usage initial error 不得顯示 0;refresh error 時可保留同 building 最後成功 snapshot,但必須顯示 lastSuccessfulAt/refresh error,並依 Backend freshness deadline 轉為 STALE。
- 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 上限。
- [ ] REST Live Load series 明確回傳 range、building timezone、bucket 起訖、averagePowerKw、partialBucket 與 bucketStatus;0 與 null 不得混用。
- [ ] LIVE/TODAY/SEVEN_DAYS 分別符合 60 分鐘/1 分鐘、當日/15 分鐘、7 個案場日曆日/1 小時定義。
- [ ] LIVE_LOAD_UPDATED 不重送完整 series;一般 event 更新三種 open buckets,跨界 event 正確提供 finalized/new open bucket。
- [ ] FE 只以 buildingId + rangeCode + bucketStartAt replacement 合併;gap/重連後以 REST snapshot 恢復。
- [ ] Usage trend、四步驟結算流程、完整 Connector list 不走 WS;結算流程依 #1460 使用 REST。
- [ ] 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。
- [x] WebSocket 中斷期間啟用每 60 秒完整 Overview REST polling fallback。
- [x] Live Load 採 Backend server-side 5 秒 coalescing,最多每 5 秒推送一次完整負載摘要。
- [x] V1 Live Load 不以 Charge Group contract capacity 加總推算案場契約容量、利用率或剩餘容量(DEC-012/#1462)。
- [x] Live Load 三種 X 軸與 bucket、Y 軸 time-weighted averagePowerKw 依 DEC-013/#1462 LOAD-DEC-003。
- [x] REST 提供完整 series,WebSocket 只更新 currentPowerKw、三種 open buckets 與跨界 finalized bucket(DEC-014/#1462 LOAD-DEC-004)。
- [x] Operational Event propagation SLO:正常運作條件下,從 Backend DB commit 成功至 FE 收到並套用更新的 P95 ≤ 2 秒。
- [x] REST overview 保留完整 live data,作為 bootstrap、手動刷新與 WebSocket fallback。
- [x] V1 不支援同一 Branch deployment scope 多個 `ems_branch` replicas;Dashboard WebSocket fan-out 以單一 active instance 為部署前提。
- [x] V1 延用現有 raw WebSocket,以版本化 envelope 定義 contract,不引入 STOMP/message broker sub-protocol。
- [x] 所有案場的 source stale cutoff 由 Backend 依資料來源集中定義與判定,並把 freshness/updatedAt/cutoff metadata 回傳給 FE;不得硬編碼 A17 或由 FE 自行設定。
- [x] 允許受控的 per-building stale cutoff override;生效順序為 building override > 全系統各資料來源預設值,並由 Backend 管理權限與稽核。
## 建議預設決策
- V1 延續現有 raw WebSocket,不為單一 Dashboard 引入 STOMP。
- REST overview 保留所有區塊,作為 bootstrap 與降級來源。
- 一條 Dashboard socket 推送 bounded section replacement。
- Live Load 5 秒 coalesce;其他 operational event propagation P95 ≤ 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 不做無限重試,應停止並顯示可診斷錯誤。
### DEC-004:Live Load 採 5 秒 Server-side Coalescing
**狀態:已確認(2026-09-02,Ken)**
- MeterValue 更新由 Backend 在 server side 合併;Dashboard browser 不接收每筆 raw MeterValue。
- 同一 building 的 Live Load 最多每 5 秒推送一次,5 秒窗內的多筆讀值合併為一份重新計算後的完整 `LIVE_LOAD_UPDATED` section replacement。
- Payload 至少帶 `buildingId`、`occurredAt`、`sequence`、`currentPowerKw`、`latestMeterAt` 與 freshness/source status。依 DEC-012/#1462,不回傳由 Charge Group contract capacity 加總推算的案場 `contractCapacityKw`、`utilizationPercent` 或 `remainingCapacityKw`。
- 聚合只納入 Backend 判定為未 stale 的 enabled Connector 最新有效 Power value;未知值保留 `null`,不得以 0 代替。
- 沒有新 MeterValue 或沒有實際 Live Load/freshness 變化時,不需固定每 5 秒推送。
- Connector FAULTED、Charge Point connection transition、Transaction start/stop、Queue/off-peak/rotation 等 operational event 使用獨立更新路徑,不得為等待 Live Load 5 秒窗口而延遲。
- Coalescing key 必須包含 `buildingId`,不可把不同案場的讀值合併。
- V1 預設窗口為 5 秒,實作時由 Backend configuration 管理;若 A17 壓測證明需調整,必須保留同樣的事件語意並更新需求決策。
- 驗收需量測 A17 的 MeterValue 頻率、單一窗口合併數、WebSocket payload size、server buffer 與 browser CPU/memory。
### DEC-005:Operational Event 更新延遲 SLO 採 P95 ≤ 2 秒
**狀態:已確認(2026-09-02,Ken)**
- 正式名稱使用 **Operational Event propagation SLO**,不使用帶有對外合約/賠償語意的 SLA。
- 正常運作條件下,Operational Event 更新延遲目標為 **P95 ≤ 2 秒**。
- 計時起點:產生 Dashboard 狀態變化的 Backend database transaction 成功 commit。
- 計時終點:Frontend 收到、通過 `schemaVersion`/`buildingId`/`sequence` 驗證,並將 replacement event 套用至 Dashboard state。
- 適用事件:
- Connector runtime status transition。
- Charge Point OCPP connection transition。
- Transaction start/stop lifecycle。
- Queue/off-peak/rotation lifecycle 與 blocked transition。
- 由上述事件衍生的 operational alerts、KPI、Group/Connector overview 更新。
- 不包含設備實際狀態發生至 Branch 收到 OCPP/Modbus 訊息前的延遲;該區段屬設備與通訊層。
- 不適用於 Live Load;Live Load 依 DEC-004 採 5 秒 server-side coalescing。
- 不適用於 Usage Trend、四步驟結算流程與完整搜尋;這些區塊依 REST refresh 規則。
- WebSocket 中斷時不套用 2 秒 SLO,改依 DEC-003 的 60 秒 REST polling fallback,並明示非即時模式。
- 驗收需記錄至少 event type、buildingId、commit timestamp、event occurredAt、client received/applied timestamp、latency 與成功/失敗結果。
- 報告至少提供 sample size、P50、P95、P99、max 與未達標比例;P95 達標不代表可忽略遺失、亂序或無法解析的事件。
- 測試時的 client/server clock 必須同步;若 client timestamp 不可靠,需以可重現的 server-side correlation 或測試 harness 量測。
### DEC-006:Stale Cutoff 由 Backend 集中管理,適用所有案場
**狀態:已確認(2026-09-02,Ken)**
- Dashboard 是所有案場共用功能;A17 只是第一個驗證/導入案場,不是功能範圍、資料模型或 freshness 規則的唯一依據。
- Backend 依資料來源分別定義 stale cutoff,例如 OCPP Heartbeat、Connector Status、MeterValue 與其他 operational source;不同來源不得共用一個沒有語意的固定秒數。
- REST overview 與 WebSocket event 必須使用同一個 Backend freshness policy,回傳 `updatedAt`、`freshness` 與可解釋的 cutoff metadata。
- FE 只依 Backend 回傳的 stable code 呈現 FRESH/STALE/ERROR/NOT_CONFIGURED 等狀態,不自行硬編碼秒數或以 browser clock 另建一套規則。
- Cutoff 的預設值應由協定行為、服務排程、裝置正常回報週期及營運容忍度共同決定,不能從單一 A17 當下樣本直接推廣到所有案場。
- A17 Phase 0 的用途是驗證通用預設值、查詢效能與 UI 行為,若結果不合理應修正通用 policy 或提出有治理的 override 機制,不得在程式碼加入 `if buildingId == A17` 類型特例。
- 任何案場切換都必須套用該 building 經 Backend 解析後的 freshness policy,並與 DEC-001 的 `buildingId` scope 一致。
- 是否允許 per-building override、由哪個設定來源管理及 fallback precedence,另列待確認事項,不在本決策中預先假定。
### DEC-007:允許受控的 Per-building Stale Cutoff Override
**狀態:已確認(2026-09-02,Ken)**
- 每一種資料來源都有全系統預設 stale cutoff;沒有 building override 時必須使用該 source default。
- 允許個別 building 因設備正常回報週期、網路特性或營運需求設定 override。
- 生效 precedence 固定為:`building + source override` > `global source default`。
- Override 必須以 source 為粒度,例如 Heartbeat 與 MeterValue 分開設定;不得用單一 building 秒數覆蓋所有資料來源。
- 設定、驗證與 effective policy 解析均由 Backend 負責;FE 不保存、不推導也不硬編碼案場特例。
- REST overview 與 WebSocket event 必須回傳相同的 effective cutoff/freshness 結果,並可識別目前使用 global default 或 building override。
- 只有具對應管理權限的角色可以修改 override;Dashboard 的一般唯讀權限不可修改設定。
- 每次新增、修改或移除 override 都必須保存 buildingId、source、before/after、operator、changedAt 與變更理由,以供稽核。
- Backend 必須拒絕 null 語意不明、負數、零或超出合理上下限的設定;移除 override 應使用明確操作並恢復 global default。
- 不允許以程式碼條件、環境變數命名或 FE 判斷建立 A17/特定案場的隱性例外。
- Global default、允許範圍、儲存位置及管理 API/UI 的實作細節,在進入開發前由 Backend plan 明確列出並經確認。
### DEC-008:V1 延用 Raw WebSocket,不引入 STOMP
**狀態:已確認(2026-09-02,Ken)**
- V1 延續現有 Spring `WebSocketHandler`/JSON message 模式,不啟用 STOMP sub-protocol、topic subscription 或 STOMP broker relay。
- Dashboard browser 對一個 `buildingId` 建立一條 socket;授權仍使用短效、一次性、admin identity + building-bound ticket。
- 所有訊息使用統一、版本化 envelope,至少包含 `schemaVersion`、`type`、`buildingId`、`sequence`、`occurredAt` 與 `data`。
- Event type 採明確 allowlist;FE 對未知 type 應安全忽略並記錄可診斷資訊,不可把未知 payload 套入既有區塊。
- FE 不支援 `schemaVersion` 時應停止套用資料、顯示版本不相容並重新 bootstrap/要求更新,不得陷入無限重連。
- 每次新 socket session 先送 `DASHBOARD_LIVE_SNAPSHOT`,再送後續 replacement events;sequence 的作用域與重置規則必須在 contract tests 固定。
- Server 端沿用 synchronized/decorated session send、send time limit 與 buffer size limit,並對慢速 client 執行可觀測的關閉與恢復。
- Raw WebSocket contract 雖不由 OpenAPI 自動產生,Backend DTO、FE TypeScript type、fixture、parser 與 contract test 必須同步維護。
- 若未來出現多種動態 topic、client-side subscribe/unsubscribe、跨服務 broker routing 或大規模 fan-out,再另立決策評估 STOMP;不得在 V1 預先增加未使用的協定層。
### DEC-009:V1 限定單一 Ems Branch Instance
**狀態:已確認(2026-09-03,Ken)**
- Dashboard 功能仍適用所有案場;本決策只限制同一個 Branch deployment scope 的執行拓撲,不代表只支援單一案場。
- V1 假設同一 deployment scope 只有一個 active `ems_branch` application instance 負責 REST、Dashboard WebSocket session 與 event fan-out。
- V1 可沿用 in-memory ticket store、session registry、coalescing timer 與 per-session sequence;不實作跨 instance session discovery 或 distributed sequence。
- 部署文件與 release checklist 必須明確標示 `ems_branch replicas = 1`;在未完成 multi-instance 設計前,不得把同一 scope 水平擴充至兩個以上 replicas。
- Sticky session 不能單獨解決問題:業務事件可能在沒有該 browser session 的另一 instance commit;因此不得把 load-balancer affinity 宣稱為完整跨 instance fan-out。
- 若誤以多 instance 部署,REST overview 仍可能可用,但 WebSocket 可能漏推、重複或亂序;系統不得將這種拓撲標示為受支援。
- 未來若需要 HA/水平擴充,必須另案設計 shared event fan-out、跨 instance authorization/ticket、session routing、sequence/deduplication、coalescing ownership 與 failure recovery;可評估既有 RabbitMQ,但不能直接假設現有 Connector exchange 已滿足 Dashboard contract。
- Multi-instance 支援完成前,部署變更的驗收需確認實際 replica count,並測試 restart 後依 DEC-003 重新 bootstrap/恢復 socket。
### DEC-010:Dashboard Queue 資料通道維持唯讀
**狀態:已確認(2026-09-03,Ken)**
- REST/WebSocket 可傳送既有 queue source、status、priority、statusReason、waitingSince/waiting duration,供 Dashboard 顯示。
- `BLOCKED` 沿用既有 Backend 狀態進入 Needs Attention;WAITING/ELIGIBLE 不新增 timeout 或 severity 判定。
- 本 Dashboard 不新增 Queue SLA、threshold/override、Admin 設定、Rotation scheduler-health 或對應 persistence。
- Dashboard publisher/aggregation 不得寫回 queue、改變 priority/slot、觸發 rotation dispatch 或送出 OCPP command。
- 此決策只收斂 Dashboard V1 scope,不影響既有 queue/off-peak/rotation lifecycle event 繼續觸發畫面更新。
### DEC-011:結算流程期間由 Backend 回傳
**狀態:已確認(2026-09-03,Ken)**
- 四步驟結算流程維持 REST-only;WebSocket 不推送月份或帳務狀態。
- Overview response 依 buildingId 回傳最近結算資料期間的 periodYear、periodMonth 與 timezone。
- Frontend 只格式化 Backend 回傳的期間,不用 browser 日曆或「目前月份減一」自行推算。
- 最新期間即使為 pending/partial/error,仍回傳並照實呈現,不得自動退回前一期掩蓋異常。
- 新一期資料尚未建立時顯示資料庫中最近存在的期間;完全沒有資料時回明確 Empty 語意與 null 期間。
- REST initial load、15 分鐘 refresh 與手動 refresh 使用同一套 period selection 規則。
- 此決策只定義 Dashboard read model,不更動既有結算排程、重算或 Billing/Invoice lifecycle。
- 詳細 UI 與驗收例子見 #1460 BIL-DEC-002。
### DEC-012:Live Load 不推算案場層級契約容量
**狀態:已確認(2026-09-03,Ken)**
- 本決策修正 DEC-004 原 payload 對案場 contractCapacityKw/utilizationPercent 的假設;DEC-004 的 5 秒 server-side coalescing、currentPowerKw 與 freshness 規則維持不變。
- Live Load 的 REST/WebSocket read model 只需回傳案場 currentPowerKw、latestMeterAt 與 freshness/source status,不回傳由 SUM(group.contractCapacity) 產生的案場 contractCapacityKw、utilizationPercent 或 remainingCapacityKw。
- 原因是現有 contractCapacity 屬 Charge Group 排程容量;系統沒有 authoritative building-level 契約容量或足以判斷群組容量是否可加總的上游電力拓撲。
- Group Capacity 仍透過 REST 回傳各群組自己的 contract capacity/allocated power,並透過 Hybrid channel 更新 occupied/available/waiting/blocked 等既有 runtime 資訊。
- FE 不得從群組陣列自行相加或計算案場利用率/餘裕;未知 currentPowerKw 仍依 null/freshness 規則呈現。
- V1 不新增 building capacity 設定、拓撲、告警門檻或管理 UI,也不修改 SlotCalculator、queue、rotation、OCPP 或其他作業邏輯。
- 詳細 UI、Backend 與驗收規則見 #1462 LOAD-DEC-001。
### DEC-013:Live Load Series 軸線與 Bucket Contract
**狀態:已確認(2026-09-03,Ken)**
- 保留 LIVE、TODAY、SEVEN_DAYS 三種 read-only 檢視。
- LIVE:rangeStartAt = generatedAt 往前 60 分鐘,bucketDurationSeconds = 60,最多 60 點。
- TODAY:依 building timezone 從案場當日 00:00 至 generatedAt,bucketDurationSeconds = 900,最多 96 點。
- SEVEN_DAYS:依 building timezone 從今天前第 6 個日曆日 00:00 至 generatedAt,bucketDurationSeconds = 3600,最多 168 點;語意是含今天的 7 個日曆日,不是 rolling 168 hours。
- Y 軸固定為「充電功率(kW)」;每個 point 為該 bucket 的 time-weighted averagePowerKw,不是 kWh、契約容量、容量利用率或 bucket peak。
- Backend 必須先依 Connector 最新有效 Power.Active.Import 重建 site power timeline,再做 time-weighted average;不可直接對不同 Connector、不同回報頻率的 raw MeterValue rows 加總或算術平均。
- REST series 至少回傳 rangeCode、rangeStartAt、rangeEndAt、timezone、bucketDurationSeconds 與 points;每個 point 至少包含 bucketStartAt、bucketEndAt、averagePowerKw、partialBucket、bucketStatus。
- 最後一個尚未結束的 bucket 標示 partialBucket = true;成功確認為零時 averagePowerKw = 0,資料不可確認時為 null 並搭配 PARTIAL/ERROR,不得補零。
- Y 軸從 0 起算,上限依有效 series 自動取整;不使用群組容量合計作為固定上限或容量線。
- FE 可依 viewport 減少可見 tick label 避免重疊,但不得刪除/合併 Backend points 或改變 range;tooltip 顯示完整 bucket 起訖與區間平均功率。
- currentPowerKw 大數字是 generatedAt 的目前功率,與最後一個進行中 bucket 的區間平均值可以不同,UI 必須分別標示。
- 歷史 series 由 REST 提供,WebSocket 依 DEC-004 提供 currentPowerKw;open/finalized currentPowerKw;進行中 bucket 更新通道已由 DEC-014 確認。 的後續更新通道另行確認,不在本決策中假定。
- 本決策只增加唯讀查詢、DTO 與圖表呈現,不改變 MeterValue 採集間隔、設備控制或充電作業。
### DEC-014:REST Series Snapshot + WebSocket Bucket Replacement
**狀態:已確認(2026-09-03,Ken)**
- REST 是完整 Live Load series 的 authoritative snapshot;用於 bootstrap、range 切換、手動 refresh、WebSocket reconnect 與 sequence gap recovery。
- LIVE_LOAD_UPDATED 一般 event 只包含 currentPowerKw 與 LIVE/TODAY/SEVEN_DAYS 三個 open bucket replacements,不包含完整歷史陣列。
- 三種 open buckets 一併推送,server 不追蹤 client 目前選取的 range,也不新增 range subscription protocol。
- 每個 bucket 以 rangeCode + bucketStartAt 識別,並帶 bucketEndAt、averagePowerKw、partialBucket、bucketStatus 與 freshness metadata。
- 跨越 1 分鐘/15 分鐘/1 小時邊界時,event 必須傳送剛完成的 finalized bucket;同一 event 可同時傳送下一個 open bucket。
- FE 只做 replacement:相同 key 取代,新的 key 加入並依 DEC-013 裁切視窗;不得用 currentPowerKw 或 event arrival time 自行積分/重算平均。
- currentPowerKw 是目前 snapshot;open bucket averagePowerKw 是從 bucketStartAt 至 event occurredAt 的區間平均,兩者可以不同。
- sequence gap、重連、schemaVersion 不支援或 buildingId 不符時,FE 不補猜 bucket,重新取得 REST snapshot。
- WebSocket 斷線期間沿用 DEC-003 的 60 秒 REST polling fallback;恢復後先取得 authoritative snapshot 再繼續 replacements。
- 沿用 DEC-004 的 5 秒 server-side coalescing,沒有資料/freshness 變化時不固定空推送。
- 本決策不改變 MeterValue 採集、charging、queue、rotation 或 OCPP 作業。
## 需求管理狀態
- Parent:#1422
- Related:#1423(Connector 數量彈性顯示規則)、#1462(案場即時負載與群組容量顯示規則)
- Requirement status:**本票架構決策已確認,待其他 Dashboard 需求完成後整併**
- Final specification:全部決策完成後整併至 #1422 的 v1.0 最終需求文件
- Implementation tickets:需求鎖定後才建立 FE/BE/QA child issues
返回