Skip to content

feat: Suppot live control#184

Open
hhvrc wants to merge 6 commits intodevelopfrom
feature/live-control
Open

feat: Suppot live control#184
hhvrc wants to merge 6 commits intodevelopfrom
feature/live-control

Conversation

@hhvrc
Copy link
Copy Markdown
Contributor

@hhvrc hhvrc commented Apr 1, 2026

No description provided.

@hhvrc hhvrc self-assigned this Apr 1, 2026
Copilot AI review requested due to automatic review settings April 1, 2026 09:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts to manage Live Control WebSocket connections, per-shocker live state, and a periodic “frame” send loop.
  • Update shockers/own page UI to group shockers per hub, show online/live connection indicators, and render LiveControlModule when live-connected.
  • Introduce LiveControlModule + LiveSlider UI 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.

Comment on lines +35 to +47
// 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);
}
}
}
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$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.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +70
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();
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +122
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;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +31
liveState.isDragging = false;
y = 1;
liveState.intensity = 0;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
liveState.isDragging = false;
y = 1;
liveState.intensity = 0;
y = 1;
liveState.intensity = 0;
liveState.isDragging = false;

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +56
<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"
>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 1, 2026

Deploying openshockapp with  Cloudflare Pages  Cloudflare Pages

Latest commit: 68191ec
Status: ✅  Deploy successful!
Preview URL: https://db487fa3.openshockapp.pages.dev
Branch Preview URL: https://feature-live-control.openshockapp.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants