Commit Graph
194 Commits
Author SHA1 Message Date
Alex Southwell 311fb5f6c7 Merge pull request #85 from saphid/fix/host-key-recovery
Recover from changed, missing and refused SSH keys inside the app
2026-10-12 15:24:25 +11:00
saphidandClaude Opus 5.5 93ae2b1bd1 Host key recovery: quote the jump hop's ProxyCommand; IPv6 jumps (review pr85-review-r3)
- Every word of the hardened ProxyCommand is quoted for the shell ssh runs
  it with (shlex.quote; list2cmdline on Windows) and its % doubled for
  ssh's token expansion. The ssh path is always the absolute one, spaces
  and all; an ssh at a path with $(), backticks, quotes or ; no longer runs
  shell code.
- The forward is -W [%h]:%p (an IPv6 headset), and an IPv6 jump host is
  passed bare with -p.
- The stand-in ssh now runs ProxyCommand as OpenSSH does (tokens, then
  /bin/sh -c exec), with regressions for hostile paths and IPv6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:15:44 +11:00
saphidandClaude Opus 5.5 12bd10ee42 Host key recovery: harden jump hops, keep identity history (review pr85-review-r2)
- Key check through a ProxyJump host: build the hop as our own ProxyCommand
  (no agent/X11/port forwarding, no LocalCommand or RemoteCommand, no tty,
  no further proxy), with -F none for the device; ProxyJump's own ssh would
  inherit ForwardAgent and LocalCommand from ~/.ssh/config. Refuse
  ProxyCommand and multi-hop routes, which can't be hardened.
- Identity history is judged on the current registry (named_route used a
  frozen copy and fell back to accept-new).
- A key ssh saves on first contact is recorded, made private and the route
  tightened as soon as it's saved, even if the login is then refused, and
  for keys commands save (the loop settles them every tick).
- identity_established is a device-level flag that never clears (migrated
  from last_ok), so editing or removing the address that worked can't make
  a headset new again.
- The handshake waits for its reader's end marker before taking ssh's exit
  as the end, so a late "Permission denied" isn't lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:56:02 +11:00
saphidandClaude Opus 5.5 a8d5c4b44d Panels: one deadline for the whole run; read gamescope's properties once
Round-2 review of #83: the display search ran before the usual 5 s property
and geometry reads, and its 30 s memory never helped because the server
starts a fresh `python3 - panels` per request. With X tools answering in
4.6 s the run passed the server's 20 s.

- One xprop call reads all of gamescope's root properties; :0 is read first
  and, when it has them, used with no other probe. Other displays are only
  asked (2 s each) if :0 has none.
- focus() and focusable() reuse that reading instead of re-reading.
- A `panels` run has one deadline (12 s); every xprop/xwininfo is cut to
  what's left, and past it the run returns what it found with a "why"
  saying the headset was too slow.
- The display memory is documented as being for the long-lived input helper
  only.
- Tests: `frame_touch.py panels` run as a fresh process with fake xprop and
  xwininfo on PATH (lists panels; 4.6 s tools return in about 12 s, where
  the previous head ran past 60 s), plus unit tests for the single read,
  probing, the deadline and partial results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:36:53 +11:00
saphidandClaude Opus 5.5 8aeeaef666 Host key recovery: fail closed on a lost pin, read keys only (review pr85-review-r1)
- Key check: ssh writes the key during the key exchange and is stopped at once;
  no authentication, agent, forwarding, commands (-N), terminal or local
  command, all forced ahead of ~/.ssh/config, and -F none unless a jump host
  needs the config. A device accepting "none" gets nothing.
- accept-new only for a headset that never had an identity: the saved key's
  fingerprint is recorded in devices.json, and a recorded identity, an
  existing pin file or a connection that ever worked makes a deleted, empty,
  unreadable or unparseable pin fail closed ("missing or can't be read").
- forget-identity is refused (it let the next contact save any key unseen).
- A key read before the headset's port, user or that address changed can't be
  trusted.
- Without a ControlMaster (Windows) the handshake's stderr goes to a file read
  as it grows, not a pipe.
- Pin files and the check's throwaway known_hosts get make_private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:32:08 +11:00
saphidandClaude Opus 5.5 5fb02770db Recover from changed, missing and refused SSH keys inside the app
0.4.2 sent $exception reports from the status poll (POST /api/comfort status,
frame_auth) on Linux and Windows for three states the person could not fix from
the app:

- A changed host key (REMOTE HOST IDENTIFICATION HAS CHANGED): the advice was
  "Run Set Up Connection again", which checks ~/.ssh/known_hosts and never
  replaces the headset's pinned key; Forget identity trusted whatever answered
  next without showing it.
- No pinned key ("No ED25519 host key is known for frame-control-<id> and you
  have requested strict checking"): every command said
  StrictHostKeyChecking=yes, so any command that ran before (or without) the
  connector's first handshake saving the key failed.
- Permission denied (publickey,password): not something a retry fixes.

Commands now use accept-new until a key is saved and yes after (accept-new
never replaces a saved key). A changed key fails at the identity stage with its
fingerprint; the banner and Devices tab offer "Check the new key", which reads
the key with a throwaway known_hosts file and no login, shows it beside the
saved one, and pins exactly that key only when the person confirms. Refused
keys point to Set Up Connection. ssh's own host key and login refusals are
marked as link failures and frame_auth joins the expected categories, so each
is reported at most once a session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:10:50 +11:00
saphidandClaude Opus 5.5 fcf2d12411 Desktop view: say why no panel was found; desktop-closed clipboard is a 409
Issue #73 (SteamOS 0.4.5): Capture on the Desktop view failed with "no app
panel is open in the headset" while the Plasma desktop was plainly open.
frame_touch.py's panels() came back empty and the page only knew to say
"none open".

- frame_touch.py: read gamescope's properties from :0 first, probing the
  other displays if :0 has none; list displays from /proc/net/unix's
  abstract X sockets as well as /tmp/.X11-unix; when nothing is found,
  return a "why" (X tools missing, listed windows that aren't X11 windows,
  desktop running but not listed, or nothing open).
- index.html: the Desktop view's Capture and Control show that reason, or
  the listing error, instead of a bare "no app panel".
- server.py: /api/clipboard answers 409 when the headset's desktop isn't
  open (plasmashell not running), so it no longer reaches telemetry as an
  $exception; other paste failures stay 502.

Fixes #73

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:59:30 +11:00
saphid f221a5f588 Merge remote-tracking branch 'origin/main' into feat/report-connection-diagnostics 2026-10-08 20:00:12 +11:00
saphidandClaude Opus 5.5 b1f33c804c Connection log: whole known names before ssh's operands; spare only real tokens
Round-4 review (pr79-review-r4), opt-in log only:
- The headset's own names are replaced whole (longest first, any case) before
  ssh's hostname/host/to operands, so "talking to Jane Doe's work headset"
  can't be cut to "<host> Doe's work headset".
- Only the scrubbers' own tokens (<host>, <user>, <ip>, ...) are exempt from
  name replacement, so a display name like "<Jane Doe>" goes too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 20:00:10 +11:00
saphidandClaude Opus 5.5 7871aa1ca0 Connection log: redact ssh's host operands first, and the headset's alias and name
Round-3 review (pr79-review-r3), opt-in log only:
- Each attempt's redaction set now includes the headset's ssh alias and
  display name (any case), and "has no addresses" no longer names it.
- ssh's hostname/host/to operands are redacted before the known names, known
  names never replace inside an existing <placeholder>, and names that are
  ssh's own words (host, hostname, to, port) aren't treated as names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:49:57 +11:00
saphid 9da1dea974 Merge remote-tracking branch 'origin/main' into feat/report-connection-diagnostics 2026-10-08 19:32:34 +11:00
saphidandClaude Opus 5.5 241e35d3b6 Reports: no custom alias, banner-only ssh version, whole user@host fields
Round-2 review (pr79-review-r2):
- The summary names the alias only as default ("frame") or custom, and the
  ~/.ssh/config line says "for the active alias" without naming it.
- ssh -V is parsed only as a banner at the start (OpenSSH_N.N[pN], optional
  LibreSSL/OpenSSL N.N.N) and rebuilt from fixed names and numbers; anything
  else is "unknown".
- scrub: ssh's whole user@host field (spaces, DOMAIN\ prefix, full domain
  names, "user@host's password:") goes before the email rule; a home folder's
  whole name runs to the next separator (apostrophes too), prefixes matched
  whatever their case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:32:34 +11:00
saphidandClaude Opus 5.5 70309d3cec Reports: connection summary in fixed words only; scrub log lines at capture
Independent review (pr79-review-r1) found leaks in the first version: a
failure's raw text was hidden with the current headset's hosts (so switching
headsets let the old one's name through), unknown hosts and other cases
survived, Windows user names with spaces partly survived, the PATH scan ran on
every preview, and the log throttle only caught consecutive repeats.

- The always-on summary has no free text: the current error and last failure
  are a stage plus the telemetry error category (decided when the failure
  happens) and how long ago. ssh -V is reduced to its version words.
- The opt-in server log line is scrubbed when written, with that attempt's
  hosts (case-insensitive), any name ssh gives after hostname/host/to, and
  then the shared scrubber.
- frame_telemetry.scrub: a Windows home folder's whole name (spaces too) and
  the whole user part of ssh's user@host (spaces, DOMAIN\user) become <user>.
- The ssh discovery runs once in the background from server start; a report
  waits at most 0.3 s for it.
- The log throttle keeps per-failure timestamps and counts, bounded to 32.
- Regression tests for each case; privacy.md says exactly what's sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:21:14 +11:00
saphid 76878403c3 Merge origin/main into fix/telemetry-connection-noise
# Conflicts:
#	tests/test_telemetry.py
2026-10-08 19:21:09 +11:00
saphid 6f5aa33197 Merge remote-tracking branch 'origin/main' into feat/report-connection-diagnostics 2026-10-08 19:13:53 +11:00
saphidandClaude Opus 5.5 e47eefec7c Telemetry: hold back only ssh's own link failures for the session
Review of #78: the once-per-session hold applied to anything classified
frame_unreachable, so after one status-poll timeout a web-link download whose
connection reset ("download failed: ... Connection reset by peer") lost its
$exception. Now the hold needs ssh's own wording (connect to host, could not
resolve, timed out talking to, banner exchange, mux/client_loop, connection
closed/reset by HOST port N); everything else keeps the 10-minute window.
Web-link download failures are download_failed. A bare "ssh exited 255" is
no longer frame_unreachable: the command on the Frame may have exited 255.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:13:05 +11:00
saphid d92a9fc7ae Merge origin/main into fix/telemetry-connection-noise 2026-10-08 19:09:12 +11:00
saphid 875e92ea7b Merge remote-tracking branch 'origin/main' into fix/apk-install-instant-failures 2026-10-08 19:03:54 +11:00
saphidandClaude Opus 5.5 f5f3a1f9ca Releases: exact draft match and a fixed release page (review fixes)
- publish-release.sh: the fallback takes only an untagged-... draft titled
  exactly "Frame Control X.Y.Z" (optionally ": subtitle"); a pre-release's
  draft ("9.8.7-rc.1") or one bound to another tag is refused.
- updater.js: ignore the page in update.json/the API entirely and always
  link to releases/tag/v<version> built from the validated version, so a
  path like tag/..\..\other/repo can't reach shell.openExternal.
- Tests for both, including backslash and %2e%2e traversal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:50:00 +11:00
saphidandClaude Opus 5.5 a41f635a7c Releases: publish drafts through REST and link update.json to the tag page
publish-release.sh took update.json's "page" from the draft's url, which is
releases/tag/untagged-... and dies on publishing; 0.4.0's manifest shipped
that dead link. It also looked the draft up with `gh release view <tag>`,
which reports "release not found" while a draft's tag_name still reads
untagged-... (and uses rate-limited GraphQL).

- publish-release.sh: REST only. Checks the tag exists, finds the release by
  tag_name or else the one untagged draft titled "Frame Control X.Y.Z",
  keeps the 8-installer digest check, replaces update.json via the uploads
  API, then PATCHes tag_name/draft/prerelease/make_latest. Adds --dry-run.
- updater.js: trust only this repo's releases/tag/<tag> pages (never
  untagged-...); otherwise link to releases/tag/v<version>.
- release.yml: one draft job before the matrix (no racing creates), with
  --verify-tag and tag_name set explicitly.
- Tests: tests/test_publish_release.py with a stand-in gh (tests/fakegh/gh),
  and an updater test for the page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:41:37 +11:00
saphidandClaude Opus 5.5 67bb71a708 Reports: always include a scrubbed connection summary; log connector failures
Three Windows reports said "ssh frame works in the terminal, but the app can't
find the headset", and their diagnostics held only versions: activity and the
server log are opt-in, and the connector never logged its failures anyway.

- frame_report.diagnostics always adds a connection summary (under "Include
  diagnostics"): connector phase, failed stage, attempt and reason; headset
  count and active alias; the current error and last failure plus ssh's last
  line, with the headset's addresses/hosts and user@ hidden and then the usual
  scrub; each address tried as its kind only (.local, ipv4, ipv6 link-local,
  tailscale, hostname, alias, and what a name resolved to) with its probe
  state; gateway/Tailscale; the ssh the app runs by kind (never its path),
  `ssh -V`, other ssh kinds on PATH; and whether ~/.ssh/config has the managed
  block and a hand-written Host for the alias.
- frame_link prints each failed attempt (stage, message, ssh's last line,
  address kinds) to stderr, scrubbed, so it lands in server.log; a failure that
  repeats every retry is written at most every 5 minutes.
- docs/privacy.md and the report dialog say exactly what the summary contains.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:39:32 +11:00
saphidandClaude Opus 5.5 0b9a165d83 APK installs: install VR apps without a missing OpenXR layer, fail packaging without it, categorize failures
- frame_android: a missing, unreadable or empty OpenXR compat layer no longer
  aborts a VR install; it installs without the layer and says so (vr_issues),
  unless the layer was asked for explicitly. Patch failures are named, and any
  exception (not only FrameError) reaches the install hooks/telemetry.
- app/build/check-resources.js (electron-builder afterPack): the build fails if
  the layer's arm64-v8a library or other required resources are missing.
- frame_telemetry: new fixed error categories layer_missing, apk_repack_failed,
  tool_missing; ok=false without an error reports "other".
- server: install_finished carries xr_layer_missing when a VR APK installed
  without the layer. docs/privacy.md lists every category and the new field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:38:15 +11:00
saphidandClaude Opus 5.5 ba9706b69e Telemetry: stop flooding error tracking with "Frame unreachable"
The status poll (POST /api/comfort status) meets an asleep or absent headset
every few seconds, and each ssh failure became a $exception: ~760 in two weeks
from 0.4.0, nearly all connection timeouts. Errors whose category only means
the Frame couldn't be reached (frame_unreachable, frame_not_set_up) are now
sent at most once per category per session; other errors keep the 10-minute
per-message window.

The classifier now also files "Host is down", Windows' "Unknown error" and
"banner exchange: Connection to UNKNOWN port -1", "ssh exited 255", and
dropped shared connections (mux_client_, client_loop) as frame_unreachable
instead of "other".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:32:03 +11:00
saphid 99fc15bd79 Merge remote-tracking branch 'origin/main' into tmp/contact67 2026-10-05 22:55:13 +11:00
saphid c305daae15 Merge remote-tracking branch 'origin/main' into tmp/rdp61
# Conflicts:
#	ui/frame_host.py
#	ui/server.py
2026-10-05 22:41:12 +11:00
saphidandClaude Opus 5.5 3a95d3b638 privacy.md: a report saves the address first; sending it may wait
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 21:36:46 +10:00
saphidandClaude Opus 5.5 989962aecc Contact email: a report's rev is its own change; another address starts fresh
- from_report applies its change and reads the id and rev together, so a
  removal made while that change is sending is newer than the report; the
  report's redaction window now starts before the address is saved.
- A report with a different address replaces the saved one with follow-up
  questions only: update notices aren't carried over to an address nobody
  agreed them for, and the form says so before sending.
- Settings refreshes after every report send, whatever the box shows by then.
- privacy.md: a report with follow-up ticked also saves and sends the address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 21:25:22 +10:00
saphidandClaude Opus 5.5 08d75e3ffb Contact email: follow-up given with a report is kept and removable; match by rev
- 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>
2026-10-01 21:08:16 +10:00
saphidandClaude Opus 5.5 01d5c612c0 Contact email: withdrawal covers earlier reports; consent is a real true
- 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>
2026-10-01 20:59:23 +10:00
saphidandClaude Opus 5.5 63c1a9ff55 docs: xrdp sign-in from Windows verified on a real Frame
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 20:25:49 +10:00
saphidandClaude Opus 5.5 72ffec45c2 Remote desktop: mention the Frame's certificate warning
Seen on Windows 11 against a real Frame: mstsc warns about xrdp's own
certificate before xrdp's login box appears.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:53:01 +10:00
saphidandClaude Opus 5.5 3f0f09b138 Contact email: don't block Save on a slow send; redact reports still in flight
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>
2026-09-30 10:29:17 +10:00
saphidandClaude Opus 5.5 b8ed53f2ff Contact email: newest choice wins by rev, removal wipes the local log
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>
2026-09-30 10:22:45 +10:00
saphidandClaude Opus 5.5 19a0d0af18 Ask for an optional contact email, with separate update and follow-up consent
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>
2026-09-30 10:15:53 +10:00
saphidandClaude Opus 5.5 7308ac09b1 Remote desktop from Windows: sign in as steamos, and say when xrdp isn't there
A Windows user reported "RDP not working". Frame Control ran `mstsc /v:HOST`,
which offers the Windows account; the Frame's xrdp (TLS, no NLA) only accepts
steamos with the Developer Mode password. The app also said "Opened Remote
Desktop" without checking that anything answered on port 3389.

- open_rdp checks port 3389 first and explains how to turn xrdp on
- On Windows, launch mstsc with a .rdp file naming user steamos (CRLF)
- Every platform's message says to sign in as steamos with the Developer Mode password
- The server no longer logs a page closing mid-reply (WinError 10053 on
  Windows) as a 500 with an error diagnostic

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:11:57 +10:00
Alex Southwell fa6d4fd81b Merge pull request #47 from saphid/linux-vr-streaming
docs: repeat Frame streaming client feasibility under test lock
2026-09-29 12:54:50 +10:00
saphidandClaude Opus 5.5 02b9413f78 Merge main into devices: several headsets alongside the app store, comfort, panels and media
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:44:30 +10:00
Alex Southwell 1a08258e3d Merge pull request #49 from saphid/theatre-media-device-fixes
Media player: keep playing through headset standby (real-Frame test fixes)
2026-09-29 12:40:35 +10:00
Alex Southwell a84d6b912b Merge pull request #44 from saphid/apk-store-fixes
Android game store: every source in one search, and proper Steam library entries
2026-09-29 12:39:16 +10:00
Alex Southwell 31de17aab8 Merge pull request #48 from saphid/frame-mcp-verified
Record real-Frame MCP end-to-end results
2026-09-29 12:27:26 +10:00
saphidandGPT-6 Astra 5aea46afd0 Merge latest origin/main VR utilities into apk-store-fixes
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>
2026-09-29 12:09:04 +10:00
saphidandGPT-6 Astra 8fca0fe0a2 Merge origin/main into apk-store-fixes, preserving store and headset features
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>
2026-09-29 12:07:09 +10:00
saphidandClaude Opus 5.5 3f280ba59a Merge main into vr-utilities: the performance HUD alongside comfort, keyboard, panels and media
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>
2026-09-29 11:59:15 +10:00
saphidandClaude Opus 5.5 094c12c6d8 Keep media playing through headset standby
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>
2026-09-29 11:54:05 +10:00
saphidandClaude Opus 5.5 3ef7312654 Merge main into panel-workspaces (testing notes)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:50:17 +10:00
Alex Southwell 70e7f7f5e5 Merge pull request #39 from saphid/family-comfort
Family and comfort: safe timers, casting, alerts and breaks
2026-09-29 11:38:47 +10:00
saphidandClaude Opus 5.5 2bd86207c7 Merge main into panel-workspaces: the panel switcher alongside media, analytics and the Mac view
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:37:18 +10:00
saphid a38d0cda6e Document shared Frame test procedure and blocked recheck 2026-09-29 11:16:29 +10:00
saphidandClaude Opus 5.5 c46bf68dd0 Record real-Frame MCP end-to-end results, including approved mutations
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:15:07 +10:00
Alex SouthwellandClaude Opus 5.5 e702adbd77 Live view: a Desktop view that stays still, and Control to tap on the Frame (#46)
* 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>
2026-09-29 11:13:21 +10:00