All skills
hyperb1iss avatar

/leptos-ui-development

@05acaca

This skill should be used when working on the Hypercolor web UI in crates/hypercolor-ui/. Triggers on "UI component", "Leptos signal", "WASM build", "Trunk build", "WebSocket frame", "canvas preview", "effect card", "control panel", "device page", "SilkCircuit token", "theme switching", "sidebar", "layout builder", "Leptos context", "web-sys binding", "UI state", "optimistic update", "WebGL texture", "toast notification", "command palette", "color wheel", "device pairing", "leptoaster", or any work in crates/hypercolor-ui/.

  • 3 files
  • 40.2 KB
  • Updated last month
  • GitHub

Use this Skill: https://skilld.dev/gh/hyperb1iss/hypercolor/leptos-ui-development

This session only. Nothing lands on disk.

referenceswebsocket-protocol.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

WebSocket Protocol Reference

The daemon WebSocket endpoint at /api/v1/ws streams real-time data to the UI.

Connection Lifecycle

  1. Connect to ws://127.0.0.1:9420/api/v1/ws with the subprotocol hypercolor-v1 (HYPERCOLOR_WS_PROTOCOL in hypercolor-leptos-ext::ws). The daemon offers it back through ws.protocols([HYPERCOLOR_WS_PROTOCOL]) and advertises it in the manifest, but it does not gate the upgrade: the only rejection in ws_handler is the Origin check, and a connect that names no subprotocol still upgrades. Send it anyway, because it is the versioning handle. When the client holds a verified session token it rides as a ?token= query parameter, re-read on every connect so a rotated token takes effect.

  2. Set binary type: ws.set_binary_type(BinaryType::Arraybuffer) — required for canvas frames

  3. Subscribe on open, three topics, one carrying a config:

    {
      "type": "subscribe",
      "topics": [
        { "topic": "events" },
        { "topic": "metrics", "config": { "fps": 2.0 } },
        { "topic": "sensors" }
      ]
    }

    Preview topics are never in the initial subscribe; they are added on demand as consumers register. A timeout guards the first subscribed acknowledgment: if it does not arrive, the client closes and reconnects rather than sitting on a half-open socket.

  4. Receive: Binary messages (canvas/frame data) and JSON messages (events/metrics/audio)

Binary Frame Format

Preview transport v2 can carry canvas data in the legacy 0x03 envelope, the wide 0x0b envelope with u32 dimensions, or the chunk 0x0f envelope. Cancellations use 0x10. Never inspect these layouts by hand in UI code. Decode them through PreviewBinaryDecoder, which uses the canonical hypercolor-leptos-ext::ws codecs and negotiates the daemon's advertised transport capability.

The generated byte-level contract lives in docs/content/api/websocket-binary-frames.md.

JSON Message Types

// Event (note: subtype key is "event", NOT "event_type")
{ "type": "event", "event": "effect_started", "data": {...} }

// Audio arrives as an event subtype, not a separate message type
{ "type": "event", "event": "audio_level_update", "data": { "level": 0.45, "bass": 0.3, "mid": 0.5, "treble": 0.2, "beat": true } }

// Device events also arrive as event subtypes
{ "type": "event", "event": "device_connected", "data": { "device_id": "..." } }

// Metrics (top-level type, structured data payload)
{ "type": "metrics", "data": { "fps": {...}, "frame_time": {...}, "stages": {...}, ... } }

// Backpressure warning for a drop-with-notice topic
{ "type": "backpressure", "dropped_frames": 12, "topic": "frames", "recommendation": "reduce_fps", "suggested_fps": 15 }

// Hello (sent on connect, runtime state only; content comes from GET /scene)
// fps is { target, capacity, delivered }; there is no "actual" key.
// capacity is paced_fps(avg_frame_time, target), clamped to target, so it
// never exceeds target no matter how much headroom the frame loop has.
// brightness is a percent (0-100). layout is always null today.
{
  "type": "hello",
  "state": {
    "running": true,
    "paused": false,
    "brightness": 80,
    "fps": { "target": 30, "capacity": 30.0, "delivered": 29.8 },
    "scene": { "id": "...", "name": "Desk", "snapshot_locked": false },
    "layout": null,
    "device_count": 3,
    "total_leds": 214
  }
}

// Subscribed (confirmation after subscribe request)
{ "type": "subscribed", "topics": [{ "topic": "canvas", "config": { "fps": 30 } }] }

Reconnection State Machine

Connected → (socket close/error) → Disconnected
Disconnected → (wait backoff) → Connecting
Connecting → (success) → Connected
Connecting → (failure) → Disconnected (increment attempt, increase backoff)

Backoff: 500ms initial, doubles each attempt, caps at 15s. Resets on successful connection.

On reconnect: re-subscribe to topics, wait for the first subscribed acknowledgment, then refetch every REST-backed mirror. Reset FPS smoothing and clear stale frame data before accepting new preview publications. Events are not replayed across the socket gap.

Backpressure Handling

Each topic declares its backpressure behavior. Preview topics use latest-value delivery. Drop-with-notice topics report their dropped publication count.

The UI receives BackpressureNotice messages for frames, spectrum, metrics, and device_metrics. Canvas preview does not emit notices.

Event Types That Trigger UI Updates

Event Type UI Reaction
effect_started Refetch the active scene and output state
effect_stopped Refetch the active scene and output state
device_connected Refetch device list (if device not already known)
device_disconnected Refetch device list (if device was known)
device_discovered Refetch device list
config_changed Reload config resources
device_state_changed Refetch device list (if device was known)
device_discovery_completed Refetch when the list is empty and the scan found devices
active_scene_changed Refetch the active scene and output state
scene_library_changed Refetch the saved scene library
scene_settings_changed Refetch the active scene
zone_changed Refetch the active scene, unless the change kind is controls_patched
effect_control_changed Refresh live control values
layer_health_changed Update WsContext::layer_health; no refetch
control_surface_changed Refetch the control-surface list
extension_state_changed Extension-owned; filter on source/kind and refetch that extension's REST state

SCENE_EVENTS, DEVICE_LIFECYCLE_EVENTS, LAYER_HEALTH_EVENTS, and CONTROL_SURFACE_EVENTS in src/ws/messages.rs:54-69 are the authoritative lists; the hint signals on WsContext are populated from them.

Closure Lifetime Management

WebSocket callbacks must stay alive for exactly the socket lifetime. Retain the callback handles next to the socket and drop both together before reconnecting:

let handlers =
    WebSocketEventHandlers::attach(&ws, on_open, on_close, on_error, on_message);
socket_callbacks.set_value(Some(handlers));

The constructor is attach, and the argument order is open, close, error, message. The closures are all FnMut with distinct event types (Event, CloseEvent, Event, MessageEvent), so swapping close and message is a type error rather than a silent miswire, but swapping open and error is not.

Do not call .forget(). Reconnect loops would leak every previous callback. dispose_existing_socket clears the browser callbacks and drops the retained handler bundle before installing the next socket.

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 05acaca. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated last month
version
1.0.0

README badge

README badge for hyperb1iss/hypercolor/leptos-ui-development