Skip to content

feat: share a panel to LAN devices — 📡 LAN, persistent room key, /ws/sync relay (#21)#22

Merged
topkoa merged 5 commits into
mainfrom
feat/lan-follower
Jul 22, 2026
Merged

feat: share a panel to LAN devices — 📡 LAN, persistent room key, /ws/sync relay (#21)#22
topkoa merged 5 commits into
mainfrom
feat/lan-follower

Conversation

@topkoa

@topkoa topkoa commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Implements #21 — "pop out to LAN": share a panel to browsers on other devices, live-synced to this machine's playback. Consumes the core /ws/sync/{session_id} relay (got-feedBack/feedBack#1030, merged in feedback#1032). Closes #21.

What you get

  • A 📡 LAN button on every panel (top-right, next to ⇱ Pop). It starts sharing that panel's config and opens a dialog showing the six-char room key big, the join URL(s), Copy, and Stop sharing.
  • Any browser on the network opens http://<host>:<port>/?ss=K7TR4M — nothing else in the URL — and gets a full-window synced follower view (which can re-split itself 1/2/2/4, exactly like a local popup).
  • The room key is persistent (generated once, shown in Settings → Split Screen with a Regenerate button), so viewers bookmark the URL once and reuse it every session.
  • Crash recovery: an active share re-arms itself on the next load (splitscreenLanShareActive), viewers reconnect with backoff and re-hello on every open — kill the host mid-song, relaunch, and every viewer resumes with zero interaction. main-closed puts remote viewers in a recoverable "waiting" overlay; only an explicit Stop sharing (share-ended) is terminal.

Design (matches the issue spec)

  • Config over the relay, not the URL. Viewers send {type:'hello', popupId}; the host answers each with {type:'config', filename, cfg, t, playing} (only once a song is loaded — viewers hello-poll every 3 s until then). Re-answering every hello is what makes late join, refresh, and reconnect all work with a stateless relay.
  • One shared dispatch (_followerBusHandler) for both transports — the existing BroadcastChannel handler body, extracted. Popups behave exactly as before; remote viewers feed relay messages into the same function. FOLLOWER became let so the remote boot can assign it (with remote: true) before running the unchanged bootFollowerMode().
  • Mirrors, not moves: sharing leaves the panel in the main layout. Remote viewers hide the Dock button, never post docked/closed, and have the note-detect fields stripped both in the shared cfg and again in makeRemoteFollowerCfg (viewers must never inherit mic/device bindings).
  • Chart data doesn't ride the relay — each viewer panel opens its own /ws/highway stream against the server, as followers always have. Only clock/session messages do, with time throttled to ~20 Hz on the network leg (_lanSend); the local BroadcastChannel stays ≤60 Hz. The follower interp makes 20 Hz visually indistinguishable.
  • Room key: 6 chars from ABCDEFGHJKMNPQRSTVWXYZ23456789 (no 0/O/1/I/L/U — read-aloud-safe), case-insensitive entry, crypto.getRandomValues. Regenerating rotates the key and stops any live share (its viewers would otherwise wait forever on an abandoned room).
  • Join URLs: built from the desktop preload's network.getLanAccess().urls when present (the host renderer itself runs on 127.0.0.1); falls back to location.origin (Docker/standalone, where the user is already on the LAN address). If desktop LAN access is off, the dialog says so instead of handing out a dead URL.

Compatibility

  • Old server (no /ws/sync): the share socket just never connects (retry loop is silent); local pop-out and everything else unchanged.
  • No behavior change for local popups: same messages, same ch.onmessage wiring, same dock semantics.
  • New localStorage keys: splitscreenRoomKey, splitscreenLanShareActive, splitscreenLanShareCfg (documented in CLAUDE.md).

Testing

  • Unit (node tests/screen.test.js, 15 → 25): room-key alphabet excludes every lookalike glyph; generate/normalize/ensure round-trips (case-insensitivity, trim, junk rejection, invalid-stored-key replacement); buildShareUrl/getSyncUrl shapes; makeRemoteFollowerCfg (detect stripped, remote flag, sane defaults); ?ss= load in a WebSocket-less env neither boots nor throws. Note: modern node ships a global WebSocket, so the end-of-IIFE boot is additionally gated out of the test env — a capability check alone would open real sockets under CI.
  • Live protocol smoke against a real feedback server (current main, with the merged relay): host + viewer + late joiner over ws://…/ws/sync/K7TR4M exercising the exact frames screen.js sends — hello→config handshake, no self-echo, 20 Hz time frames, song-changed, playstate, late-join config, share-ended fan-out. 8/8 pass.
  • Manual test plan (needs two machines / a phone):
    1. Desktop: enable LAN access, play a song, split, click 📡 LAN on a panel → dialog shows key + http://<lan-ip>:<port>/?ss=<key>.
    2. Second device: open the URL → "Connecting to host…" → panel appears, scrolls in sync with room audio; play/pause/seek/song-change follow.
    3. Kill the host app mid-song → viewer shows "Connection lost — reconnecting…"; relaunch host, play → viewer resumes untouched.
    4. Stop sharing → viewer shows the terminal "Sharing ended" overlay.
    5. Regenerate key in settings → old bookmark waits forever (empty room); new URL works.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added LAN sharing for split-screen panels across devices on the same network.
    • Hosts can share a six-character room key and join URL, copy access details, stop sharing, or regenerate keys.
    • Remote viewers stay synchronized with playback and can reconnect after temporary interruptions.
    • Added LAN sharing controls and room-key management to the settings interface.
  • Documentation

    • Added setup instructions, lifecycle details, requirements, and troubleshooting guidance for LAN sharing.
  • Bug Fixes

    • Improved handling of remote viewer states, reconnections, and ended sharing sessions.

Where ⇱ Pop opens a follower window on this machine, 📡 LAN lets a
browser on another machine watch a panel live-synced to this machine's
playback (splitscreen#21), consuming the core session-sync relay
(feedBack#1030/#1032).

The follower architecture already crossed machines everywhere except the
sync channel: followers boot from a config, chart data streams per-panel
over /ws/highway, and the follower clock interpolates between time
messages. This adds the missing transport plus the join flow:

- Persistent 6-char room key (unambiguous alphabet, case-insensitive),
  generated once, shown in settings with Regenerate. Viewers join at
  /?ss=<key> — nothing else in the URL, so it's typeable and
  bookmarkable across sessions.
- hello/config handshake over the relay: the host answers every hello
  with the shared panel cfg + filename + playhead, which gives late
  join, refresh, and reconnect for free on a stateless relay.
- _followerBusHandler extracted as the shared dispatch for both
  transports; FOLLOWER became `let` so the remote boot can assign it
  (remote: true) and run the unchanged bootFollowerMode().
- Mirrors, not moves: the shared panel stays in the main layout; remote
  viewers hide Dock, never post docked/closed, and note-detect is
  stripped at both capture and boot.
- Crash recovery: shares re-arm on load, viewers reconnect with backoff
  and re-hello; main-closed is a recoverable waiting state for remote
  viewers — only an explicit share-ended (Stop sharing) is terminal.
- time frames throttled to ~20 Hz on the network leg; the local
  BroadcastChannel leg is unchanged (≤60 Hz).

Unit tests 15 → 25; live protocol smoke (host + viewer + late joiner
against a real server's relay) passes 8/8. Plugin 1.11.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@topkoa, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d23d8a64-c9d2-421e-9447-ee30c1101530

📥 Commits

Reviewing files that changed from the base of the PR and between d592d53 and e4da6d1.

📒 Files selected for processing (2)
  • screen.js
  • tests/screen.test.js
📝 Walkthrough

Walkthrough

Split-screen adds LAN panel sharing through persistent six-character room keys and a WebSocket relay. Hosts relay configuration and playback events, while remote viewers bootstrap as passive followers with reconnect handling, lifecycle overlays, and dedicated sharing controls.

Changes

LAN sharing

Layer / File(s) Summary
Sharing contracts and controls
screen.js, settings.html
Room-key generation, normalization, persistence, share URLs, remote follower configuration, and LAN controls are added.
Host relay lifecycle
screen.js
Hosts capture panel configuration, relay playback and song events, reconnect automatically, persist active shares, and support stopping or re-targeting shares.
Remote viewer lifecycle
screen.js
Remote viewers join through ?ss=, bootstrap from relay configuration, receive follower messages, reconnect, and handle recoverable or terminal host states.
Validation and release support
tests/screen.test.js, README.md, CLAUDE.md, plugin.json
LAN behavior is documented, the manifest version is updated, and helper, URL, configuration, persistence, and boot-gating tests are added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant Relay
  participant Viewer
  Host->>Relay: Open room and send config
  Viewer->>Relay: Send hello
  Relay->>Viewer: Return config
  Host->>Relay: Send time, playstate, and song changes
  Relay->>Viewer: Forward synchronization messages
  Host->>Relay: Send share-ended or main-closed
  Relay->>Viewer: Forward lifecycle state
Loading

Possibly related issues

  • got-feedBack/feedBack/1030 — Its objective covers the /ws/sync/{session_id} relay consumed by this LAN sharing implementation.

Possibly related PRs

Suggested reviewers: byrongamatos

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the LAN sharing feature, persistent room key, and sync relay introduced by this PR.
Linked Issues check ✅ Passed The changes cover the LAN share flow, room-key persistence, viewer boot/handshake, sync relays, recovery states, and docs required by #21.
Out of Scope Changes check ✅ Passed The PR stays focused on LAN sharing and its documentation, with no obvious unrelated code changes beyond the version bump.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lan-follower

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds LAN-based “remote follower” support to the Split Screen plugin by mirroring the existing popup sync protocol over the core /ws/sync/{session_id} relay, backed by a persistent 6-character room key and a new 📡 LAN UI flow.

Changes:

  • Implement LAN share host + remote viewer join path in screen.js (room key generation/persistence, relay connection, hello/config handshake, share modal, crash-resume).
  • Add settings UI for displaying/regenerating the persistent room key (settings.html) and document the feature (README.md, CLAUDE.md).
  • Expand node unit tests for the new pure helpers and bump plugin version (tests/screen.test.js, plugin.json).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
screen.js Implements LAN share + remote viewer boot over /ws/sync, adds persistent room key helpers, and unifies follower message handling.
settings.html Adds a “LAN room key” display and Regenerate button in Split Screen settings.
tests/screen.test.js Adds unit coverage for room key helpers, sync URL building, and remote follower config shaping.
README.md Documents how to use 📡 LAN sharing and key caveats/requirements.
plugin.json Version bump and description update to include LAN sharing.
CLAUDE.md Documents new localStorage keys and LAN share architecture details.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread screen.js Outdated
Comment thread screen.js Outdated
Comment thread settings.html Outdated
Comment thread tests/screen.test.js
…misc

- Gate both `config` consumers (pre-boot + recovery) on our own popupId:
  the relay room broadcasts every viewer's config reply to everyone.
- The main-closed waiting state now re-hellos (shared hello-poll helper,
  self-stopping) and treats any live host traffic (time / playstate /
  song-changed) as proof the host is back — previously a relaunched host
  could leave the overlay stuck over already-synced panels if no fresh
  hello ever fetched a config.
- settings.html: Regenerate button gets an explicit type="button".
- tests: remote-config fixture uses the real _captureMode() encoding
  (`viz:<pluginId>`), not the saved-prefs `__viz__:` sentinel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread screen.js
_onFollowerOrphaned now stops the hello-poll and closes the relay
WebSocket. Orphaned is terminal, but previously the socket stayed open
in the room (inert thanks to the _followerOrphaned guards, yet a live
network connection) and the poll could fire one more tick. _remoteWs is
nulled before close so its onclose is a no-op — no reconnect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread screen.js
Comment thread screen.js
- _showLanShareModal: invocation nonce so a second open during the
  getLanAccess() await can't stack a second (orphaned) overlay — only
  the latest invocation renders.
- Copy URL: navigator.clipboard doesn't exist in non-secure contexts,
  which is exactly where LAN hosts live (http://192.168.x.x Docker
  sessions). Fall back to hidden-textarea + execCommand('copy'), and
  flash a "Copy failed — select it above" notice instead of silently
  doing nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread screen.js Outdated
Comment thread tests/screen.test.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/screen.test.js (1)

194-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover an existing valid persisted key.

This only verifies a key generated during the test. Seed splitscreenRoomKey with a valid lowercase/trimmed value and assert ensureRoomKey() returns its normalized form, so persisted-key normalization is covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/screen.test.js` around lines 194 - 200, Update the ensureRoomKey test
to seed localStorage with a valid persisted splitscreenRoomKey before calling
mod.ensureRoomKey(). Assert that the method returns the key’s normalized
lowercase/trimmed form and preserves that normalized value in storage, while
retaining the existing persistence and repeat-call assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 84: Update the README compatibility requirement near the /ws/sync relay
description to state an evaluable requirement: specify the feedBack release that
includes the `#1030` relay, or explicitly require a feedBack version containing
`#1030`. Preserve the existing behavior note for older servers.

---

Nitpick comments:
In `@tests/screen.test.js`:
- Around line 194-200: Update the ensureRoomKey test to seed localStorage with a
valid persisted splitscreenRoomKey before calling mod.ensureRoomKey(). Assert
that the method returns the key’s normalized lowercase/trimmed form and
preserves that normalized value in storage, while retaining the existing
persistence and repeat-call assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: be41b754-faea-4947-9e7d-b0188348d526

📥 Commits

Reviewing files that changed from the base of the PR and between b85406e and d592d53.

📒 Files selected for processing (6)
  • CLAUDE.md
  • README.md
  • plugin.json
  • screen.js
  • settings.html
  • tests/screen.test.js

Comment thread README.md
_ensureMainBroadcasterAndListener guarded BOTH its halves with one flag,
so a crash-recovery _maybeResumeLanShare() running before #audio exists
would mark the whole function done and the play/pause listeners never
attached — silencing every `playstate` message, which followers rely on
exactly when `time` ticks stop (paused). Split the guards: the channel
handler stays once-ever; the audio listeners re-attempt per call, keyed
to the element they're bound to (also covers a replaced <audio>).

Also refresh a stale test comment: modern node ships a global WebSocket;
the ?ss= load test pins the _nodeTestEnv gate, not a missing API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@topkoa
topkoa merged commit cdd30d8 into main Jul 22, 2026
4 checks passed
@topkoa
topkoa deleted the feat/lan-follower branch July 22, 2026 21:37
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.

"Pop out to LAN": share a panel as a URL so browsers on the network can follow live

2 participants