- 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>
- Removing every headset leaves none in use (commands fail at once) instead of
falling back to the `frame` alias.
- ssh goes to the IP that answered for IPv6 too, with a link-local address's
interface (verified: frame.local over fe80::…%en9 on the real Frame).
- Find results only show in the panel of the headset they were for.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A switch publishes the new headset at once, so the page clears the old one's
panels and the lists behind them (games, store, Android apps, screenshots).
- The page names the headset its changes are for (X-Frame-Device); the server
refuses one meant for a headset it has switched away from (409).
- A rejected address edit changes nothing.
- Test now goes to the IPv4 address that answered, like the connection.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A switch clears every headset-specific list and its buttons at once.
- SSH goes to the IPv4 address that answered the probe, not the name again.
- A rejected headset edit changes nothing.
- The SteamOS/Lepton builds recorded in reports are read again per headset.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Attempts carry a generation: one overtaken by a switch, a removal or a login
change routes nothing back to the old headset and can't report connected.
- Removing the active headset or changing its user/port reroutes at once,
before anything that can fail.
- One known_hosts file per headset (~/.ssh/frame-control-hosts/<id>):
forgetting one headset's key can't drop another's, whoever else writes.
- learn() checks, under the config lock, that the block is still what the
attempt started from before writing to it.
- A switch stops live video and drops captures from the previous headset.
- A probe shares its time between the addresses a name resolves to.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Switching headsets is serialized with the start of any install; background
work counts as running from before its thread starts. use() reroutes every
command at once and makes ensure() wait for the new headset.
- A new user or port reroutes commands even if the attempt then fails.
- ~/.ssh/config edits take a lock file shared with Set Up Connection
(frame_connect.py and connect.sh, which now also writes atomically).
- A finished attempt no longer writes its older settings over a change Set
Up Connection made meanwhile.
- Pin edits are locked and swapped atomically.
- A bare alias behind ProxyJump/ProxyCommand is left to ssh to reach.
- The page drops answers about the previous headset after a switch; the
header switcher takes clicks in the macOS title bar.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- No switching headsets (or changing the active one's user/port, or removing
it) while installs run: they read the ssh settings step by step.
- Switching reroutes every command to the new headset at once, even if it
never answers.
- ~/.ssh/config edits are serialized, use unique temp files, and back off if
another program wrote the file meanwhile.
- stop() ends a handshake in progress and joins the connector.
- Pinned keys are written unhashed (HashKnownHosts=no); hashed ones are still
found and forgotten via ssh-keygen.
- Set Up Connection changing a headset's user or port updates the registry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Frame Control can now manage more than one Steam Frame, and reach each at any
of several addresses (LAN IPs per network, its .local name, Tailscale). A
connector in the server tries them all at once, picks the best one that
answers, follows ssh -v through each stage (network, finding, SSH, identity,
login) and streams that to the page. The header shows it live; a new Devices
tab (key 5) manages headsets, addresses and network names.
- ui/frame_devices.py: registry in devices.json, imported from the managed
~/.ssh/config blocks; per-headset host key pinning; config block updates.
- ui/frame_network.py: gateway IP+MAC fingerprint, Wi-Fi name, Tailscale.
- ui/frame_link.py: the connector, Test now, Tailscale/mDNS discovery, API.
- server.py: ensure_master delegates to the connector; /api/connection,
/api/connection/events (SSE), /api/devices.
- Electron: headset switcher and Devices item in the Frame menu.
- frame_connect.py --alias; FRAME_CONTROL_DATA_DIR / FRAME_CONTROL_SSH_DIR
keep tests off real data.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>