Conversation
There was a problem hiding this comment.
Pull request overview
Adds “Live Control” support to the shockers “own” page by introducing a per-hub live connection state (WebSocket to a live control gateway) and rendering a dedicated live control UI when connected.
Changes:
- Add
live-control-state.svelte.tsto manage Live Control WebSocket connections, per-shocker live state, and a periodic “frame” send loop. - Update
shockers/ownpage UI to group shockers per hub, show online/live connection indicators, and renderLiveControlModulewhen live-connected. - Introduce
LiveControlModule+LiveSliderUI for live intensity/type selection.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/(app)/shockers/own/+page.svelte | Groups shockers by hub and adds LIVE connect toggle + live rendering path. |
| src/lib/state/live-control-state.svelte.ts | New state module handling live gateway discovery, WebSocket lifecycle, and frame ticking. |
| src/lib/components/ControlModules/LiveControlModule.svelte | New live control card UI (type selector + slider). |
| src/lib/components/ControlModules/impl/LiveSlider.svelte | New pointer-driven vertical intensity slider for live control. |
| package.json | Bumps pnpm packageManager version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Eagerly create LiveDeviceConnection and LiveShockerState entries | ||
| // so template reads never mutate state (Svelte 5 forbids mutation in $derived/templates). | ||
| $effect(() => { | ||
| for (const [hubId, hub] of ownHubs) { | ||
| ensureLiveConnection(hubId); | ||
| const conn = getLiveConnection(hubId); | ||
| if (conn) { | ||
| for (const shocker of hub.shockers) { | ||
| conn.ensureShockerState(shocker.id); | ||
| } | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
$effect eagerly creates LiveDeviceConnection entries for every hub but never cleans them up when a hub disappears from ownHubs (e.g., after refresh/unpair). This can leave stale connections/state in memory; consider removing liveConnections entries for hubIds no longer present (and disconnecting them) when ownHubs changes.
| async connect() { | ||
| this.disconnect(); | ||
| this.state = LiveConnectionState.Connecting; | ||
|
|
||
| try { | ||
| const res = await hubManagementV1Api.devicesGetLiveControlGatewayInfo(this.deviceId); | ||
| if (!res.data) { | ||
| throw new Error('No LCG data returned'); | ||
| } | ||
|
|
||
| this.gateway = res.data.gateway; | ||
| this.country = res.data.country; | ||
|
|
||
| const ws = new WebSocket(`wss://${this.gateway}/1/ws/live/${this.deviceId}`); | ||
| this.ws = ws; | ||
|
|
||
| ws.onopen = () => { | ||
| this.state = LiveConnectionState.Connected; | ||
| this.startTickLoop(); |
There was a problem hiding this comment.
connect() has an async race: if the user toggles Live Control off while devicesGetLiveControlGatewayInfo() is still awaiting, the rest of connect() will still run and may open a WebSocket / set state back to Connected after a disconnect. Add a connection-attempt token (or similar guard) checked after each await and before mutating state / assigning this.ws.
| if (this.ws) { | ||
| this.ws.onclose = null; | ||
| this.ws.onerror = null; | ||
| this.ws.close(); | ||
| this.ws = null; | ||
| } | ||
| this.state = LiveConnectionState.Disconnected; | ||
| this.latency = 0; | ||
| } | ||
|
|
||
| private handleDisconnect() { | ||
| this.stopTickLoop(); | ||
| this.ws = null; |
There was a problem hiding this comment.
disconnect() only nulls onclose/onerror, leaving onopen/onmessage attached. In combination with quick connect/disconnect, a late onopen/onmessage from an old socket can still mutate state. Clear all WebSocket handlers (onopen/onmessage/onclose/onerror) when disconnecting/aborting a connection attempt.
| if (this.ws) { | |
| this.ws.onclose = null; | |
| this.ws.onerror = null; | |
| this.ws.close(); | |
| this.ws = null; | |
| } | |
| this.state = LiveConnectionState.Disconnected; | |
| this.latency = 0; | |
| } | |
| private handleDisconnect() { | |
| this.stopTickLoop(); | |
| this.ws = null; | |
| this.cleanupWebSocket(); | |
| this.state = LiveConnectionState.Disconnected; | |
| this.latency = 0; | |
| } | |
| private cleanupWebSocket() { | |
| if (this.ws) { | |
| this.ws.onopen = null; | |
| this.ws.onmessage = null; | |
| this.ws.onclose = null; | |
| this.ws.onerror = null; | |
| this.ws.close(); | |
| this.ws = null; | |
| } | |
| } | |
| private handleDisconnect() { | |
| this.stopTickLoop(); | |
| this.cleanupWebSocket(); |
| liveState.isDragging = false; | ||
| y = 1; | ||
| liveState.intensity = 0; |
There was a problem hiding this comment.
When the user releases the slider, stopDrag() sets liveState.intensity = 0, but the gateway send loop only transmits frames while liveState.isDragging is true. This means no message is sent at release time, so the last non-zero intensity frame may remain effective until the server times out. Consider sending an explicit final frame with intensity 0 (and/or ControlType.Stop) on pointerup/cancel.
| liveState.isDragging = false; | |
| y = 1; | |
| liveState.intensity = 0; | |
| y = 1; | |
| liveState.intensity = 0; | |
| liveState.isDragging = false; |
| <div class="relative h-full w-full select-none p-4"> | ||
| <div | ||
| bind:this={container} | ||
| class="relative h-full w-full cursor-pointer overflow-hidden rounded-md border border-border" | ||
| onpointerdown={startDrag} | ||
| onpointermove={onPointerMove} | ||
| onpointerup={stopDrag} | ||
| onpointercancel={stopDrag} | ||
| role="slider" | ||
| aria-valuenow={intensity} | ||
| aria-valuemin={0} | ||
| aria-valuemax={maxIntensity} | ||
| aria-label="Live intensity" | ||
| tabindex="0" | ||
| > |
There was a problem hiding this comment.
The element is given role="slider" and tabindex="0", but there is no keyboard interaction (arrow keys/home/end) or focus-visible styling. For ARIA slider compliance and accessibility, add onkeydown handling (or provide an associated <input type="range">) so keyboard users can adjust intensity.
Deploying openshockapp with
|
| Latest commit: |
68191ec
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://db487fa3.openshockapp.pages.dev |
| Branch Preview URL: | https://feature-live-control.openshockapp.pages.dev |
No description provided.