- Ticking follow-up questions on a report makes that address the contact
email (follow-up ticked, update choice unchanged), so Settings shows it
and Remove my email withdraws it like any other.
- Reports carry contact_rev; the inbox takes a report's follow-up
permission back when a later change from that copy (higher rev) no
longer agrees, whatever the clocks say.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A report with follow-up ticked carries this copy's contact id, and the
inbox marks its permission withdrawn when a later choice from that copy
no longer agrees to follow-up questions at that address.
- The one-time prompt never appears in a visit that showed the privacy
notice, even if the Frame connects just after it's dismissed.
- Saving contact details isn't headset work: it can't hold up switching
headsets or be refused after a switch.
- Consent flags must be JSON true/false; "false" is no longer consent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Third review follow-ups:
- A sender that found nothing waiting checks again after letting go of the
send lock, so a change saved in that moment is sent, not left for a retrier.
- A report is compared with a removal using its full-precision start time, so
a report sent after the address was removed is logged as sent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Second review follow-ups:
- Saving returns once the choice is stored; a send already under way picks up
the newest change, or the background retry is woken.
- A problem report still being sent when its address is removed is logged as
<removed>, checked under the same lock the removal holds.
- The prompt re-checks the privacy notice after fetching its state.
- docs/privacy.md: offline contact changes are sent later by themselves; the
prompt never follows straight after the privacy notice.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-ups:
- Each contact_consent event carries a rev that goes up with every change,
sends are serialized, and `contacts` picks every field from the highest
rev per copy, so a withdrawal can't lose to an earlier event sent in the
same second or with a skewed clock.
- Removing the address also replaces it with <removed> in the local
sent log (earlier contact events and problem reports).
- The prompt is rechecked when the Frame connects, not only at page load.
- No thanks hides the bar only once the dismissal is saved.
- docs/privacy.md: say that the analytics switches don't block a report or
contact change the person sends deliberately.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem reports arrive with no way to reply. People can now leave an email
address with two separate opt-ins: occasional update notices, and follow-up
questions from the maintainer.
- ui/frame_contact.py keeps the address and choices locally and sends each
change privately to PostHog as a contact_consent event under its own random
contact id; removing the address sends a withdrawal without it. Changes made
offline wait and are retried.
- A one-time, dismissible prompt appears after the Frame first connects; No
thanks and showing it once are both remembered.
- Privacy & updates gains a Contact email section to add, change or remove it.
- The report form's contact field now goes with a report only when "may
contact me with follow-up questions" is ticked (contact_followup).
- frame_report.py contacts [updates|followup] lists who agreed to what,
using the newest event per copy.
- docs/privacy.md says what is collected, why, where and how to remove it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review (GPT-6 Astra, P2): the still-image loop stopped calling show() once
the screen took its first frame, so a surround refused during standby was
never retried and stayed missing for PNG and splat playback until restart.
hold() now keeps draining pending uploads after the screen is shown, until
both are up.
Also: stills and the surround wait out standby without counting as dropped
video frames or tripping the five-minute limit (video only); teardown
errors no longer overwrite a finished status; Stop is ignored once the
outcome is decided.
Tests: PNG and splat where the screen is accepted before the surround
recovers (fail on the old loop); fake-clock coverage of the five-minute
limit and its reset; status keeps filename/metadata layout sources and
explicit layouts stay explicit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Preserve the complete store, artwork, telemetry, input, media and agent route table alongside the newly landed VR utilities and performance HUD.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Keep the union of server routes, desktop resources and responsive controls. Preserve OpenXR install defaults and telemetry hooks alongside library artwork. Adapt the resource test to single-file entries and avoid a completed-refresh race in the F-Droid test.
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Also from review: a SteamVR build without the timing exports can't break status
(AttributeError), and the device test class runs when the file is run directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Real-Frame testing (2026-09-29) found an unworn headset enters standby
within seconds; SetOverlayRaw then returns RequestFailed (23) and the
movie died. The player now drops frames during standby, keeps audio
and pacing, re-sends stills and the theatre surround after waking, and
only errors after five minutes without an accepted frame.
A Stop arriving while the player is already shutting down is ignored,
so a finished video stays 'ended' instead of 'error: Stopped'. The
status now reports the layout's real source (filename/metadata).
Docs record the end-to-end device matrix (API, web UI, CLI).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Live view: a Desktop view that stays still, and Control to tap on the Frame
The live view gets a second source and a way to use the Frame from it:
- Desktop: the app panel in use in the headset, streamed from its own window
(x11grab of gamescope's redirected window), so it doesn't move as the
wearer looks around. A picker shows any other panel, view only.
- Control: on the Desktop view a tap or click lands exactly where you put it;
drag is a mouse drag, press and hold right-clicks, two fingers scroll, and
on a computer the mouse, wheel and keyboard work directly. On the headset
view the view is a trackpad. A text field and key row type from a phone.
Input goes through gamescope's own EIS socket (the way Steam feeds Remote
Play input) with the libei already on the image: ui/frame_touch.py, over
the same long-lived ssh machinery as the keyboard agent, nothing to
install. It reaches the panel that has focus on either X display, which
the KDE Connect route can't. Verified on the Frame and from the iPhone app
in the Simulator.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: fixes from review
- Keys held on the Frame are released with buttons when Control stops or
the view loses focus; keys for the Frame no longer trigger Frame Control's
own shortcuts.
- Taps only act when the picture on screen is the panel in use; positions,
presses, keys, text and scrolls name their panel (display and window: ids
repeat across :0 and :1, told apart by pid), and the Frame drops them if
focus has moved on. Releases always go.
- While connecting, a tap keeps its position; on an error only releases wait
and retries back off; trimming a long queue never drops a release.
- Lifting one of two scrolling fingers ends the scroll; a cancelled touch
isn't a tap; clicks and holds on the bars around the picture do nothing.
- A capture loop from before a Live restart can't stop the new video.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: close the targeting gaps from the second review
- The focused panel's display comes from GAMESCOPE_FOCUS_DISPLAY (gamescope
packs ":1" into the first value), so a window id repeated across :0 and :1
can't be mistaken; the pid is only the fallback.
- Presses, keys, text and scrolls read focus afresh on the Frame; only moves
use a reading up to a second old.
- A gesture remembers the panel it started on and does nothing more if that
stops being the one in use; a press with no panel to aim at isn't sent.
- Opening a screenshot clears the panel Control would act on; switching to
another app releases held keys and buttons.
- Trimming keeps a click with its position; the error backoff holds for new
input too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: fixes from the SWE-2 Max review
- The Frame side tracks keys as well as buttons and lets go of both when the
session ends.
- A stale tap tells the page, which re-reads the panels at once.
- A paused input device waits instead of ending the session; only a
disconnect does. An OS error on one event skips it.
- Presses check focus with two property reads and do the full lookup only
when it changed.
- Writes to an agent's stdin are serialized, so two devices sending at once
can't tear a line (the keyboard agent too).
- Connecting gives up with a message after 15 s instead of hanging on
"Connecting…"; text goes in 100-character pieces so releases don't wait
behind a long paste; a cancelled mouse gesture releases what's held.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Control: stale clears, pauses reconverge, pastes split per request
- The Frame says it has caught up as soon as an aimed event lands after a
stale one, so the page stops re-reading the panels.
- After a device pause it lets go of everything it holds (releases that
arrived while paused were dropped), and waits for the device once per
batch, not once per event.
- The quick focus check no longer freshens the panel geometry's age.
- Each request carries at most about 100 characters of text.
- Turning Control off while it connects doesn't report an error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* frame_touch: build the socket path on the Frame, so Windows can import it for tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* frame_touch: any event that goes through clears the stale flag
A trackpad move names no panel, so waiting for an aimed event could leave
the page re-reading panels for the rest of the session; a release still
aimed at the old panel doesn't count.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
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>
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>