159 Commits
Author SHA1 Message Date
saphidandClaude Opus 5.5 9a5d697579 Merge main (0.4.8: Android game backups) into the four-tab structure; Home's shortcuts focus on the first tap
Main's Import Android game panel joins Quest and Meta under Apps > Browse,
and its Import button (androidBackupOpen, which lived in the old Android
store header and would have thrown at startup here) is now an Add / Install
choice. docs/android-backups.md points to the new places.

Home's shortcuts (Type on the Frame and the rest) now show their tool before
focusing it, so typing works on the first tap and phones open the keyboard
(Sol review P3). A tool panel already in its workspace isn't moved again, which
also dropped focus on the follow-up hashchange.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:12:49 +11:00
saphidandClaude Opus 5.5 be9e7b5cda Merge feat/android-store-redesign (with main) into the Apps / Games / Tools / Settings structure
Keeps the four primary destinations and ports main's newer work into them:
the Home "Right now" panel (volume, cast, quick actions) replaces the old
device block, whose readings now live in the header's headset popover; the
install queue sits above the Apps views; Import from my Quest and My Meta
library sit with Browse; the pad, panel switcher, Mac view and media panels
take main's markup. Scrolling follows main's #scroll container. Settings
stays a tab, so main's header gear is not added. The old .home-status
toggle is gone (main's startup test caught it touching a missing node).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:49:13 +11:00
saphidandClaude Opus 5.5 fed2249776 Apps / Games / Tools / Settings, one Add / Install flow, and the whole-app review fixes
Primary tabs become Apps (Installed / Browse / Linux apps), Games (Installed /
Owned / Store), Tools and Settings, with shortcuts 1-4 and one Add / Install
chooser for catalogs, Steam, Linux apps, files and install links. Installed
rows show identity, one state and Open/Stop; the rest moves to app details.

Review fixes (#106): input, touch, approvals, artwork repair, keyboard helper
delivery and catalogue jobs stay bound to the headset they were meant for;
report preview follows the latest consent; failed Store reads are not an empty
library; keyboard access to device details and the Store sheet; the iOS
bundle ships the artwork renderer and OpenXR resources; typed addresses and
custom SSH ports survive setup; WebKit crashes recover; native preferences
survive relaunch; APK update rollback and repair stay retryable.

Refs #101, #104, #106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:08:09 +11:00
SAPHID b2485500e1 Import Android APK and OBB backups together 2026-10-20 03:54:17 +00:00
SAPHID c5521a917f fix(ui): reconcile PR 97 with current main 2026-10-18 00:37:46 +00:00
Alex Southwell 16b37bc667 Merge pull request #50 from saphid/flat2vr-mods
VR mods: UEVR install, play in VR and remove from Frame Control
2026-10-18 11:04:17 +11:00
SAPHID d546568a11 fix(store): preserve resolver cleanup and test lookup budgets deterministically 2026-10-17 23:58:05 +00:00
Alex Southwell 5b531c84fb Merge pull request #90 from saphid/feat/repo-auth
Store: repositories behind a login, and a GitHub token in source settings
2026-10-18 10:57:12 +11:00
SAPHID 57a9d8c717 chore: resolve conflicts with main 2026-10-17 22:10:19 +00:00
SAPHID 63017e1969 chore: resolve conflicts with main 2026-10-17 22:10:04 +00:00
saphid 5b35e9cc5d Merge current main and prepare Frame Control 0.4.7 2026-10-15 13:09:59 +11:00
saphidandClaude Opus 5.5 443813a088 Install queue: Android store, Quest and Meta installs wait their turn
Installs from the Android store, Import from my Quest and My Meta library
now go into one server-side queue and run one after another, instead of
being refused while another runs. The page shows the queue with Remove
for installs that haven't started; each queued install runs only on the
headset it was queued for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:38:53 +11:00
saphidandClaude Opus 5.5 e885e9adba APK Store in the headset on the new store; images load first time; phone layout
- ui/store.html, scripts/store-on-frame.sh and the /store route (built in
  another thread and left uncommitted in the main checkout) join this branch,
  rebuilt on /api/search: the same sources, categories, phone-only rule and
  artwork as the Store, as a 1280x800 laser panel. A category rail with
  Installed, eight art cards a page, a featured VR app on Home, a detail
  sheet with screenshots, install progress and game data, the thumbstick
  turning pages and the on-screen keyboard.
- Artwork: the proxy allowed four name lookups at once and failed the rest
  immediately, so a page of cards lost most of its images until the retry.
  Public answers are now kept for five minutes and a lookup waits for a slot
  within its own budget.
- Phones: one-line shelf headings, a shorter store header, wider shelf
  cards, and a two-column library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:31:34 +11:00
saphidandClaude Opus 5.5 7f46cd54a4 Android tab: a Store with Frame categories and a card Library
The store browsed F-Droid alphabetically, so it opened on proxies, port
checkers and root tools. It now groups apps by what a headset is good for
(VR, Games, Emulators, Watch & listen, Read & learn, Create, Work, Web &
chat, Tools) from F-Droid's own categories, hides phone-only apps unless
asked, and browses each source's best-illustrated, newest apps first.

- Store: a category sidebar (chips on narrow windows), one shelf per
  category on the home page with no repeats, a featured VR app, and cards
  that show a banner, phone screenshots side by side, or the icon over its
  own blur instead of a letter, plus whether it fits the Frame.
- Library: its own view with art cards, Play, and a menu for Test, Report
  and Remove; filters for VR, flat and running apps; display settings and
  reports below. Artwork comes from the saved source indexes.
- Images: listings register three screenshots per app instead of twelve,
  handles are indexed by URL, and the bound is 16,384, so a large browse no
  longer forgets its own images before the page loads them.
- F-Droid cache version 3 keeps each app's categories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:37:27 +11:00
saphid 8b9bd2119c Add searchable Steam Frame Control Center with live metrics 2026-10-14 18:40:59 +11:00
saphidandClaude Opus 5.5 f5bed08931 My Meta library: Sign in with Meta drops abandoned attempts, holds frames to the list
Review fixes for the sign-in window:
- Each sign-in has a random attempt id. Closing, timing out or signing out
  stops cookie intake first, then tells the server (new cancel-signin action)
  not to save a connect from that attempt, even one still waiting on Meta
  (frame_meta.connect/cancel_signin; 409 from the server). Disconnect signs
  out of the window before deleting the token.
- will-frame-navigate holds every frame to the allow-list; a frame going
  elsewhere is stopped, never opened outside.
- The window is destroyed, not closed, and will-prevent-unload is overridden,
  so beforeunload can't keep it alive; sign-out clears the session only after
  the window is gone.
- Permission checks refused as well as requests; no developer tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:39:25 +11:00
saphid e144325375 Merge branch 'feat/quest-import' into feat/meta-library 2026-10-14 09:26:42 +11:00
saphidandClaude Opus 5.5 9697812984 Merge origin/main into feat/quest-import
One conflict, in ui/frame_android.py where main added COPY_MIN_RATE (size-scaled
copy timeouts) beside the install lock this branch made reentrant: both kept,
the lock stays an RLock. ui/index.html and ui/server.py merged on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:22:56 +11:00
saphidandClaude Opus 5.5 7fe7e504da My Meta library: never surface a failed log read; linear-time log and token scans
- A Frame error during a test (an ssh failure with empty stderr raises its
  stdout, which can be the game's log) becomes the fixed "couldn't read the
  game's log from the Frame" message, with no original text and no chained
  cause. run_test wraps every step; the /api/meta test job also maps any
  other exception to a fixed text, and the CLI and Install and test go
  through run_test. Close failures no longer echo their error text.
- Log scans are linear: signatures look at the first 4 KB of each line and
  every repetition followed by more required text is bounded; excerpts
  split the redacted line once and fullmatch only words of 64 characters or
  less; the generic OC/FRL token pattern matches each run once (no
  lookahead) and keeps it unless it has a digit. Timing tests: 100 KB
  adversarial lines through classify, redact and every excerpt each finish
  well under 200 ms (the old library and token patterns took 19 s and 9 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:38:46 +11:00
saphidandClaude Opus 5.5 621330d397 My Meta library (Experimental): install and test Quest games you own
Opt-in, off until you connect: paste your own Meta access token, which is
checked with a read-only "who am I" query and kept owner-only in
data_dir/meta-account.json. Lists the Quest apps the account owns, downloads
an owned app's APK and OBB from Meta's download server into the install
cache (size limits, declared-size and optional SHA-256 checks, cleaned up
after every install), and installs it through Import from my Quest's checks,
install and two-step data copy. Install and test runs the game, classifies
the outcome from its log (runs, runs with issues, quits, crashes, hangs) with
a plain-English cause, and records it through the existing compatibility
report path with source meta-store and the version code, shared only per the
Privacy settings.

The token goes only to graph.oculus.com and securecdn.oculus.com over
HTTPS, is dropped on redirects elsewhere, and is redacted (any case,
percent-encoded, before truncation) from every message. A disconnect wins
over a connect in flight and stops downloads. No licence-check bypass.
Protocol facts come from the public QAVS, OculusGraphQLApiLib, DownQuest and
BSManager sources (docs/meta-library.md); the code is our own.

UI: Android -> My Meta library. CLI: ui/frame_meta.py. API: /api/meta.
Tested offline with a fake Meta and fake Frame (tests/test_meta.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:32:28 +11:00
saphidandClaude Opus 5.5 baec984449 Quest import: fix round-2 review findings (versions, locking, strict split reads)
- An older Quest copy no longer replaces a newer install on the Frame: installs
  now record versionCode, and a downgrade is refused unless allowed
  (--allow-downgrade, "allowDowngrade", or the panel's checkbox).
- The game-data copy only targets this package's own install record and
  instance at the Quest's exact version code, and checks Android's own
  dumpsys in that instance before copying.
- After the (possibly long) pulls the target is checked again, and the final
  checks, the transfer and closing the game hold the shared install lock,
  now reentrant so install_obb and install can be called under it.
- Closing the game is confirmed; a failed or incomplete close is reported
  instead of claiming the game was closed.
- Split members are read from their raw bytes: stored members must be exactly
  their declared size, deflated streams must end exactly there, then the
  checksum; forged sizes with prefix checksums are refused.
- A split's resource table counts as empty only if no type chunk defines any
  entry (bags included) and every chunk is understood.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:24:30 +11:00
saphidandClaude Opus 5.5 7faf80fc2b Import from my Quest: copy games you own off a USB-connected Quest
New ui/frame_quest.py (CLI: devices | list | import), GET/POST /api/quest and
an Import from my Quest panel on the Android page. It finds a Quest on this
computer's adb (never Frame Control's own tunnelled serials), lists third-party
apps with split, OBB and Android/data details, pulls the APKs into a per-user
cache and checks them before copying anything else: Meta's Platform SDK
licence check (libovrplatformloader.so) and 32-bit-only builds are refused,
VrApi-only builds flagged. It then installs through the normal APK path, adds
OBBs with the existing helper and, if asked, Android/data files, and always
removes the cache. No licence-check bypass is bundled, called or linked.

Tests use a fake adb (tests/fakeadb.py). Docs: docs/quest-import.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:17:11 +11:00
saphidandClaude Opus 5.5 84912ea4e1 Store: repositories behind a login, and a GitHub token in source settings
F-Droid repositories can now be added with HTTP basic auth, either as
https://user:pass@host/fdroid/repo (as F-Droid's client accepts) or with
separate username/password fields (store form, server API, and the CLI's
--username/--password-stdin). The login is stripped from the saved URL and
kept in a new shared module, apk_sources/_credentials.py, which writes
source-credentials.json in the data folder with mode 0600.

A credential is only ever added per request to its exact origin (scheme,
host, port): redirects elsewhere get nothing and have Authorization
removed. HTTPS only. 401 (or 403 with a login) on add gives a "check the
username and password" error and saves nothing; signature, pin, TOFU,
hash and rollback checks are unchanged. Removing a repository deletes its
login.

GitHub gets a user-settable token (FRAME_GITHUB_TOKEN still wins), sent as
Bearer only to api.github.com. If GitHub answers 401, the request is
retried once without it and the rejection is reported in source settings.
The source list shows only whether a token is set, never its value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:46:35 +11:00
saphidandClaude Opus 5.5 b64082c974 Comfort: run the helper cached on the headset; send the sources only when missing
Every call (one per 5 s poll) sent ~35 KB of frame_comfort.py, frame_status.py
and frame_steam.py over ssh, though they are already cached on the headset in a
content-addressed folder. Now the first ssh only checks that folder and runs
the cached copy; if any file is missing it answers {"comfort": "missing"} and
a second call writes the sources as before (umask 077, temp file then rename,
same version hash).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:29:32 +11:00
saphidandClaude Opus 5.5 7c1d698400 Comfort: don't report the poll meeting an unreachable Frame, or poll while it is
The page polled POST /api/comfort status every 5 s whatever the connection,
and every POST Failure >= 500 went to diagnostics. ssh already marks link
failures, but those are still sent once per session per category, so each
session with a sleeping, away or re-keyed headset sent up to three reports
from this poll (most of 0.4.2's error reports). GET /api/status never reports
Failures at all.

- server: a polled POST read (POLLS: comfort status) that couldn't reach the
  headset isn't a diagnostic, as with GET /api/status. Real comfort errors,
  and link failures from starting or cancelling a session, still are.
- page: the comfort poll starts after the first status check and skips its
  ssh while the Frame is offline (online === false); status checks bring
  it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:29:32 +11:00
saphidandClaude Opus 5.5 9b26c907ce Telemetry: count headsets, not just installs
frame_connected now carries headset_id, a one-way hash of the headset's
/etc/machine-id worked out on the headset (frame_status.py), and
headsets_saved, how many headsets this copy of the app has set up. It is
sent once per build for each headset. The hash goes to usage statistics
only: the status endpoint and MCP clients don't get it. docs/privacy.md
says what is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:42:03 +11:00
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 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 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
Alex Southwell 4f99cd75d0 Merge pull request #80 from saphid/fix/server-sigterm-segfault
Server: no more occasional segfault on SIGTERM (exit without interpreter teardown)
2026-10-08 20:17:41 +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 db9bee2903 Server: clear in-flight staging folders on the way out, and a dead server's at start
Leaving through os._exit skips the weakref finalizers that clean up a
TemporaryDirectory still in use by a background thread, so quitting during
a patched VR APK transfer left the APK behind (and likewise a file staged
for the assistant, or a half-downloaded app index in the cache folder).

Those folders now carry the server's PID (frame-vr-, frame-agent-, and
.download- in the apk-sources cache folder), like the web-install and title
staging folders already did. sweep_tmp covers all five; it still runs at
start for dead servers' folders, and with own=True at the end of the
shutdown cleanup for this server's. The exit comment now says which exit
work is skipped and why that is safe.

Tests: the sweep test covers every prefix and own=True; a new server test
stops a server with in-flight folders and checks they are gone, and that a
dead server's are removed at the next start.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:58:44 +11:00
saphidandClaude Opus 5.5 0a9bb2e0c3 Telemetry: keep the link-failure mark through re-raised errors
Review round 4 of #78: /api/android and /api/titles re-raise frame_android's
FrameError as Failure(str(e)), which dropped frame_link_failed, so their
connection failures were still reported after the status poll's first one.
Both now raise ... from e, and diagnostic() follows __cause__
(frame_telemetry.link_failed). server.ssh() judges a link failure on ssh's
stderr only, never the command's stdout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:46:54 +11:00
saphidandClaude Opus 5.5 b25855d13a Server: leave without interpreter teardown after a clean stop (SIGTERM segfault)
On Linux with Python 3.13 (GitHub's ubuntu-latest runner, CPython 3.13.16),
about 1 stop in 100 crashed the server with SIGSEGV. A native backtrace
taken at the crash shows background threads inside OpenSSL
(X509_STORE_set_default_paths_ex, called from _ssl while the app-index
download threads build their TLS context) while the main thread was already
in exit(), where libcrypto's own atexit cleanup frees the state those
threads are using.

After the shutdown cleanup has run, the server now flushes stdout/stderr and
calls os._exit(0). Daemon threads (index downloads, stdin watcher,
telemetry, contact, headset link, request handlers) cannot be stopped
promptly, and nothing in the server registers atexit work; the OS frees the
one-server lock with the process. The stop test now runs its scenario five
times with faulthandler on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:41:38 +11:00
saphidandClaude Opus 5.5 9e70cdb48a Telemetry: decide "couldn't reach the Frame" where ssh runs, not from text
Review round 3 of #78: matching ssh's words in the message could still be
spoofed by a download's file name ("Connection closed by frame port 22.zip"
in a checksum failure lost its $exception after a poll timeout). Now
server.ssh and frame_android.ssh mark the exception (frame_host.link_failure)
when ssh itself failed to reach the headset: a time out, or exit 255 with a
line ssh starts (frame_host.ssh_link_failed). diagnostic() holds back only
marked errors in frame_unreachable / frame_not_set_up, once per session;
everything else keeps the 10-minute window. Web-link download and checksum
failures are classified download_failed before any other category.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:34:25 +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 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
saphidandClaude Opus 5.5 f9a87e76e8 Web-link APK installs report an unexpected install failure once
Review pr75-review-r2: _webinstall_run catches non-FrameError APK install
failures without a diagnostic, and apk_installed now leaves those to the
caller, so they went unreported. Report them there; test the full worker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:03:54 +11:00
saphidandClaude Opus 5.5 95ccae5494 Show the missing-layer warning on every install path; report an unexpected install failure once
Review findings on PR #75 (pr75-review-r1):
- Catalogue, alternate-version and web-link installs now append the
  "installed without the OpenXR layer" warning to their completion message
  (frame_android.layer_note); dropped files already showed vr_issues.
- A non-FrameError that install() re-raises is reported by whoever catches
  it (job or request handler, with its traceback), so apk_installed sends
  install_finished without its own diagnostic for it (install_finished
  diagnose=False). Still exactly one install_finished.
- Tests run the real job path for both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:58:01 +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
Alex Southwell c3e651cd25 Merge pull request #67 from saphid/fix/contact-email-review-followups
Contact email: withdrawal covers reports, strict consent, review follow-ups to #59
2026-10-05 23:06:11 +11:00
saphid 99fc15bd79 Merge remote-tracking branch 'origin/main' into tmp/contact67 2026-10-05 22:55:13 +11:00
saphid e63dc43c2f Merge remote-tracking branch 'origin/main' into fix/windows-ssh-config-acl 2026-10-05 22:52:52 +11:00
saphidandClaude Opus 5.5 07f44f9082 Windows: fix ssh config ACL, link-local IPv6, and setup under python -I
- ~/.ssh/config writes swapped in a temp file that inherited the .ssh folder's
  ACL; Windows' OpenSSH refuses one granting another account (even a deleted
  one) more than read: "Bad owner or permissions". Writes now give the file an
  owner-only ACL (frame_host.make_private), and the server repairs a refused
  config once per run and retries.
- frame_link.probe named a link-local IPv6 zone with if_indextoname, which on
  Windows is "ethernet_32769"; Windows' ssh can't resolve that, so a headset
  found at fe80:: showed as "can't find the Frame". Use the zone number there.
- frame_connect.py imports frame_host (since #60), but the app runs it with
  python -I, which leaves its folder off sys.path: Set Up Connection exited
  with ModuleNotFoundError. Add the folder, as server.py does.

Verified on a Windows 11 VM against OpenSSH_for_Windows 9.5p2: the old write
reproduces the reported error with an orphan SID's Modify ACE; the new write,
repair and server retry all leave a config ssh accepts; ssh to %ethernet_32769
fails to resolve while %5 connects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:43:51 +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
Alex Southwell a4031db052 Merge pull request #60 from saphid/fix/windows-test-suite
Windows: stop ssh/scp/ssh-keygen hanging when stderr is captured
2026-10-05 22:39:14 +11:00
saphidandClaude Opus 5.5 049d50f43a Merge main into fix/contact-email-review-followups
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 21:38:54 +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