Skip to content

Commit 52fcfd8

Browse files
committed
update
1 parent b2eca32 commit 52fcfd8

21 files changed

Lines changed: 1840 additions & 394 deletions

File tree

ai/spec-sse-transports-draft.md

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-04-18

openspec/changes/archive/2026-04-19-use-sse-transports-from-backend/design.md

Lines changed: 192 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
## Why
2+
3+
The backend has committed to a transport-based SSE subscription model that replaces the current appliance-level subscription: instead of shipping the full `ApplianceJson` on every state change, the server delivers only the specific scalar paths each view asks for, at a per-subscription interval, with optional server-side `sum` / `avg` aggregation. Under the current `sse-client` capability, the Kiosk lights-sum card receives ~50 full appliance payloads every 3 seconds just to compute one aggregate wattage — this is the root cause of event pileup on tablets. The backend's contract (`../../JAVA/java-overmind-server/ai/draft-sse-transports-for-frontend.md`) is ready; this change migrates every GUI use of SSE to that model in a single build.
4+
5+
## What Changes
6+
7+
- **BREAKING** — Replace the entire appliance-level subscription API in `SseClient`: `subscribe(applianceIds, callback, minInterval)` / `unsubscribe(handle)` becomes `registerTransport(spec, callback)` / `unregisterTransport(handle)`.
8+
- **BREAKING** — Replace `update` event dispatch (full `ApplianceJson` per entry) with `transport-update` event dispatch (either a `values` list of `{applianceId, path, value}` triples, or an `aggregate` payload `{op, value, sampleCount, totalCount}`).
9+
- **BREAKING** — Replace `POST /sse/appliances/register` and `POST /sse/appliances/deregister` with `POST /sse/transports/register` and `POST /sse/transports/deregister`. Old endpoints are removed from `store/rest.ts`.
10+
- **BREAKING** — The internal `Map<number, Appliance>` cache keyed by appliance id is replaced by a cache keyed by `(applianceId, path)`. `getLatest(applianceId)` is removed; components hold path-scoped state from callback deliveries instead.
11+
- **Migrate `KioskPowerPanel.vue`** — front of card becomes a server-side `sum` aggregate transport over `relays[0].power` (one number per tick instead of 50 full payloads). Flipping the card registers a second non-aggregate transport for the per-appliance breakdown; flip-back deregisters it.
12+
- **Migrate `Floorplan.vue` compact view** — single `perAppliance` transport at mount, with type-scoped path lists (lights/plugs/dimmers → `relays[*].power`; HT → `temperatures[0].temperature`+`humidities[0].humidity`; contact → `closures[0].open`; motion → `motions[0].motion`; battery-driven → `batteries[0].batteryLevel`). Deregistered on unmount.
13+
- **Migrate Floorplan detail dialogs** — each dialog registers a per-dialog transport for its single appliance on `mounted` with a broad type-appropriate path list at `minInterval: 300–500ms` for live sliders; deregistered on `beforeDestroy`.
14+
- **Migrate `Appliances.vue`** — list view uses a `perAppliance` transport scoped to what the list row renders (power / on-off / battery / `lastTimeOnline`), replacing the current `subscribe(ids, ..., 3000)` call.
15+
- **Replace client-side aggregations with server-side aggregates** where the reduction is pure (`sum` or `avg`). The Kiosk lights-sum card is the driving case; any other card doing the same pattern (summing/averaging a scalar across many appliances in the compact view) gets the same treatment.
16+
- **Do not double-bootstrap from `GET /setup/appliances`** — the `registerTransport` call resolves only after the synchronous initial snapshot arrives, so fetching the same values via REST first races the snapshot. `/setup/appliances` remains the appliance-list bootstrap (names, types, configs), nothing more.
17+
- **Deregister on in-app lifecycle events only** — server cleans up transports when the SSE connection drops, so deregister on dialog close / card flip / route change, not on disconnect.
18+
- **Reconnect re-registration** — on a new `connectionId`, re-register every active transport with the new id (existing reconnect shape in `sseClient.ts`, different body).
19+
20+
## Capabilities
21+
22+
### New Capabilities
23+
24+
- `sse-transport-client`: The singleton SSE transport client service — one `EventSource`, lazy connection, `connectionId` tracking, `registerTransport` / `unregisterTransport`, `transport-update` dispatch demultiplexed by `transportId` supporting both value-list and aggregate payload shapes, path-keyed internal cache, automatic reconnect with re-registration of every active transport.
25+
26+
### Modified Capabilities
27+
28+
- `sse-client`: All requirements **removed**. The appliance-level subscription model (subscribe/unsubscribe, `update` event routing by appliance id, `Map<number, Appliance>` cache, `getLatest(applianceId)`, `POST /sse/appliances/register`+`deregister` endpoints) is replaced by `sse-transport-client`. The capability ceases to exist once this change is archived.
29+
- `sse-connection-indicator`: Unchanged at the requirement level — still reads a `connected` boolean from the SSE client. Implementation will read from the new `sse-transport-client` singleton.
30+
31+
## Impact
32+
33+
- **`src/utils/sseClient.ts`** — substantive rewrite. Public API changes; internal cache is re-keyed; `update` event handler replaced by `transport-update` dispatcher; reconnect flow rebuilds transports instead of subscriptions.
34+
- **`src/store/rest.ts`** — add `sseTransportsRegister` (`/sse/transports/register`) and `sseTransportsDeregister` (`/sse/transports/deregister`); remove `sseAppliancesRegister` and `sseAppliancesDeregister`.
35+
- **`src/views/Appliances.vue`** — migrate `subscribe` call site.
36+
- **`src/components/floorplan/Floorplan.vue`** — migrate compact-view `subscribe` call site to a `perAppliance` transport with type-scoped paths.
37+
- **`src/components/floorplan/dialogs/FloorplanDialogFactory.vue`** plus individual dialogs (`FloorplanPlugDialog`, `FloorplanBulbDialog`, `FloorplanHTDialog`, etc.) — add per-dialog transport registration on `mounted`, deregistration on `beforeDestroy`.
38+
- **`src/components/KioskPowerPanel.vue`** — front-of-card aggregate transport; flip-card detail transport with add/remove on flip.
39+
- **`src/App.vue`** — no behavior change (reads `connected` boolean only).
40+
- **Helpers** — a small `pathsForApplianceType(type)` lookup somewhere in `utils/` for Floorplan / Appliances compact-view path selection.
41+
- **Backend dependency**`POST /sse/transports/register`, `POST /sse/transports/deregister`, `transport-update` SSE event. Contract in `../../JAVA/java-overmind-server/ai/draft-sse-transports-for-frontend.md` (authoritative) and backed by the backend's openspec change `sse-transports` in `java-overmind-server`. GUI build must land only after the backend's dual-stack deployment is live.
42+
- **No changes** to `POST /execute` or any other REST endpoint. `GET /setup/appliances` is unchanged and remains the appliance-list bootstrap.
43+
- **Outstanding pushback to relay to backend before they implement** — drop `pingable` from the top-level listener slots (see `project_sse_transports` memory); it's used once in `AppliancePanel.vue:194` and derivable from `lastTimeOnline` age. Keep `lastTimeOnline``overmindUtils.ts:168` derives `onOffState='error'` from its staleness.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## REMOVED Requirements
2+
3+
### Requirement: Singleton SSE client service
4+
**Reason**: Capability replaced by `sse-transport-client`. The singleton class and `getInstance()` pattern are retained in the new capability.
5+
**Migration**: See `sse-transport-client` → Requirement: Singleton transport client service.
6+
7+
### Requirement: Lazy SSE connection
8+
**Reason**: Capability replaced by `sse-transport-client`. Lazy-connection behavior is retained; the trigger is now the first `registerTransport()` call instead of `subscribe()`.
9+
**Migration**: See `sse-transport-client` → Requirement: Lazy SSE connection.
10+
11+
### Requirement: SSE endpoint URL from store config
12+
**Reason**: Capability replaced by `sse-transport-client`. URL construction is unchanged.
13+
**Migration**: See `sse-transport-client` → Requirement: SSE endpoint URL from store config.
14+
15+
### Requirement: Subscribe to appliance updates
16+
**Reason**: The appliance-level subscription model (subscribe by applianceId list, receive full `ApplianceJson` on every change) is replaced by per-value transports. The backend has removed `POST /sse/appliances/register` in favor of `POST /sse/transports/register`.
17+
**Migration**: See `sse-transport-client` → Requirement: Register a transport. Call sites using `subscribe(ids, cb, minInterval)` migrate to `await registerTransport({ minInterval, selection: { applianceIds: ids, paths: pathsForApplianceType(type, 'compact') } }, cb)`.
18+
19+
### Requirement: Unsubscribe from appliance updates
20+
**Reason**: Paired with the removed `subscribe` method.
21+
**Migration**: See `sse-transport-client` → Requirement: Unregister a transport. `unsubscribe(handle)` becomes `unregisterTransport(handle)`.
22+
23+
### Requirement: Event routing by appliance ID
24+
**Reason**: The `update` event is gone. Routing now happens by `transportId`, not by appliance id, because multiple transports may observe the same appliance at different cadences.
25+
**Migration**: See `sse-transport-client` → Requirement: Transport-update event dispatch.
26+
27+
### Requirement: Parse state and config before routing
28+
**Reason**: The `update` event shipped full `ApplianceJson` entries with `state` and `config` as JSON-encoded strings requiring parsing. `transport-update` payloads carry already-typed scalar values; no string parsing is needed at this layer.
29+
**Migration**: None. Callbacks now receive `{ values: [...] }` or `{ aggregate: {...} }` directly — no appliance object, no stringified JSON.
30+
31+
### Requirement: Internal appliance cache
32+
**Reason**: The `Map<number, Appliance>` cache keyed by appliance id cannot be rebuilt from transport payloads without re-synthesising partial appliance objects, reintroducing the fan-out this change removes.
33+
**Migration**: See `sse-transport-client` → Requirement: Path-keyed internal cache. `getLatest(applianceId)` is replaced by `getLatestPath(applianceId, path)`. The sole current caller (`KioskPowerPanel.vue:330-335`, used to seed initial values) is replaced by the synchronous initial snapshot delivered by `registerTransport`.
34+
35+
### Requirement: Automatic reconnection with re-registration
36+
**Reason**: Reconnection is retained; what it re-registers changes from appliance-level subscriptions to transports.
37+
**Migration**: See `sse-transport-client` → Requirement: Automatic reconnection with transport re-registration.
38+
39+
### Requirement: Connected state property
40+
**Reason**: Retained in the new capability unchanged.
41+
**Migration**: See `sse-transport-client` → Requirement: Connected state property. `sse-connection-indicator` reads the same boolean from the same singleton; no change needed there.
42+
43+
### Requirement: Ignore updates for removed subscriptions
44+
**Reason**: Equivalent race-condition guard is retained in the new capability, keyed by `transportId` instead of appliance id.
45+
**Migration**: See `sse-transport-client` → Requirement: Ignore transport-update events for unknown handles.
46+
47+
### Requirement: REST endpoint configuration
48+
**Reason**: Endpoint paths change: `sseAppliancesRegister` / `sseAppliancesDeregister` are replaced by `sseTransportsRegister` / `sseTransportsDeregister`. `sseAppliances` (the SSE stream endpoint) is unchanged.
49+
**Migration**: See `sse-transport-client` → Requirement: Transport REST endpoint configuration. Update `store/rest.ts` accordingly.

0 commit comments

Comments
 (0)