Astra review: a switch landing between the check and the assignment could still
install the old headset's tunnel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration review findings: the scripts they run ssh'd to whatever 'frame' means
in ~/.ssh/config. They now take FRAME_ALIAS and FRAME_SSH_OPTS from the server's route.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Mac view's tunnel is its own ssh, so it now takes the headset's route (and its
pinned identity, which also checks the USB-C address), and closes when the app
switches headset. The MCP adapter's private server (FRAME_PRIVATE_SSH=1) skips the
one-server lock and can't add, remove or switch headsets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stop always calls systemctl and treats exit 5 (unit already collected)
as done, so there is no is-active/stop race. Cleanup never masks the
copy error, the play ssh timeout covers the remote worst case, and
tests cover stop exit codes and systemd-run stderr reporting.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Stop is a no-op when the collected player unit is already gone
(raw systemctl stop exits 5 on the Frame; verified 2026-09-29).
- Surface systemd-run stderr when the player can't start.
- Keep the copy error if the cleanup ssh also fails; reject upload
names that the play path can never accept.
- Allow 60 s for play (ffprobe 30 s + systemd-run 15 s remote).
- Docs: four-hour cap is unconditional; no delete action yet; fix a
garbled timing sentence.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HTTPServer.server_bind calls socket.getfqdn, which stalled past the MCP
backend's 10-second startup window on GitHub's macOS runners.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two servers each connected, reconnected and edited the headsets on their own,
and several review findings were ways one could move the other's install to a
different headset. A lock file in the data folder now refuses a second server
with a plain message; FRAME_CONTROL_DATA_DIR still gives a separate one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ssh's %C hashes only address, user and port, so two headsets reached at one
address shared a ControlMaster and one's commands could run on the other: the
ControlPath now names the headset. Another Frame Control server choosing a
different headset no longer moves this one's commands mid-install.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every change now takes a lock file shared across processes and starts from
what's on disk; reads pick up a newer file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- While the connector is taking a queued reconnect off its list, the old
connection no longer counts as live, so no install starts on it.
- Importing a block without a Port takes the port ssh would really use
(ssh -F <config> -G), e.g. one a later Host * sets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- An upload's answer arriving after a switch opens nothing; the APK
alternatives dialog installs on the headset the APK was checked for.
- A handshake that goes silent (e.g. a jump host's forward hanging) moves on
to the next address.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Behind a jump host, a forward it couldn't open moves on to the next address;
only a refused key stops (judged by ssh's words, not the step).
- Add a headset suggests an alias no Host in ~/.ssh/config already uses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A jump host's own "Authenticated to" line no longer counts as the headset's,
and a master that logs in but doesn't start lets the next address be tried.
- Devices tab changes refresh through the ordered list load, so a late answer
can't undo a newer selection.
- A probe's time out starts after the name lookup: macOS can take 5 s to look
up a .local name (found on the real Frame once its USB link went away).
- An attempt's ending is published from a method, not a return in finally.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A switch the server refuses no longer drops answers the page is waiting for
(an install's job id): only an actual change of headset does.
- Test now goes through a jump host when the alias uses one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>