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>
The live API rejects mimes=image/jpeg on /logos and /icons, so every logo and
icon lookup fell back to generated art. Found with a real key: SuperTux now
gets grid, wide, hero and logo; Beat Saber all five.
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>
- A set-up headset whose alias goes through a jump host (ProxyJump or
ProxyCommand in ~/.ssh/config) is reached through it, address by address,
still pinned per headset.
- A refused switch puts the header's switcher back on the headset in use.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A command that fails after a switch doesn't make the connector drop the new
headset's connection.
- On first import, the app keeps using the `frame` headset even when Set Up
Connection put another block above it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Terminals, power and a reconnect's probes use the route commands have now
(a pinned bare-alias destination, a login change still deferred).
- Saving port 22 keeps an explicit Port line where there was one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Title removal ran the Steam shortcut tidy-up before steamos-delete, which
finds the Proton prefix through that shortcut, so compatdata was left behind
(e2e caught it). steamos-delete runs first again; art/collection tidy-up after.
- Test fixtures are byte-exact: never convert line endings (a text-looking
fixture APK got CRLF on Windows and failed its SHA-256).
- Read index.html/artwork-settings.js as UTF-8 in tests; app-data backup and
OBB shell tests run only on POSIX (they exercise the Frame-side scripts).
- e2e expects the icon under artwork/ now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A bare alias's route is pinned to where ~/.ssh/config sent it when it was
routed (HostName, Port, User), so editing that file can't move an install.
- Renaming during an install is allowed: only a real user or port change waits.
- The Devices tab follows a network change even while the headset is offline.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Final review follow-up. A failed Thread.start() leaked a resolver slot (four
failures disabled artwork lookups). GIF graphic-control blocks must have the
fixed 4-byte payload (otherwise dropped) and an image with no pixel data is
rejected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Retry now (while connected) and Forget identity wait for running installs.
- Terminal windows get the headset's address by name, so a link-local IPv6
zone never has to pass through Windows' console.
- Renaming the headset in use shows at once in the header.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>