Tools → Backup & recovery copies game saves (Proton prefixes' AppData,
Documents and Saved Games, Unity and Godot saves), Steam controller layouts,
desktop settings and Android app data (the existing backup-data helper) to
~/Documents/Frame Control Backups/<time>/ with a manifest of every file's size
and SHA-256 and the headset's BUILD_ID. Backing up only reads the headset;
sign-in files, keys and keyrings are refused by name wherever they are.
Restore checks each part against the headset first and skips, rather than
fails, games not installed yet, prefixes Proton hasn't made, a missing Steam
account and closed or missing Android apps. It asks before replacing anything,
writes only inside each part's folders and keeps every replaced file under
~/.local/share/frame-control/restore-previous/.
Return to Game Mode reads the SDDM session config and runs the switch Valve's
devkit tools use (steamos-session-select / holo-session-select gamescope),
only when the headset isn't already in Game Mode and only after confirmation.
Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record on Capture & Live runs ffmpeg on the Frame (x264 from /dev/video99, the
default output's monitor through PipeWire's PulseAudio server), stops it when the
SSH channel closes, remuxes to .mp4 and copies it to ~/Videos/SteamFrame. If the
audio can't be read, it records video only and says so. 20-minute limit, free
space checked on both ends.
Steam clips lists Steam's game recordings (DASH sessions under
userdata/<account>/gamerecordings) and Export joins each stream's init segment
and chunks, then has the Frame's ffmpeg copy them into one .mp4, so clips longer
than Steam's own 60 s export come out whole.
The logic is in ui/frame_clips.py for reuse. Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home, Tools and the Control Center now call the keyboard "Type on the
headset", and T opens it from anywhere. The clipboard moves from Send to
Frame to sit beside the keyboard, with a new Copy the headset's clipboard
button: the server reads Klipper's getClipboardContents on the headset
desktop's bus (base64 back, 1 MB limit, empty and image clipboards say so),
and the desktop app, the iPhone app or frame_host.set_clipboard_text puts
it on the clipboard here. Clipboard text is never logged.
Not tested on a headset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Steam's charge limit goes through SteamOS Manager's BatteryChargeLimit1,
and upstream steam-frame.toml configures no backend for it. The Frame's
battery is a max1720x fuel gauge with no charge_control_end_threshold.
Conclusion: don't build it yet; read-only headset checks are listed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
- 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>
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>
A live sign-in with a real Meta account goes auth.oculus.com -> auth.meta.com
-> www.facebook.com -> www.instagram.com -> auth.meta.com. The window only
allowed oculus.com, meta.com and facebook.com, so it stopped the Instagram
step and sat blank. instagram.com is now allowed (same HTTPS-only checks;
the cookie is still read only from oculus.com). A page change the window
stops is remembered by host only, so closing the stuck window says why; it
is forgotten once the window loads a page again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review: the time zone setting could hold any text (a TZ like
home/alex/private passed the name check), so the app now looks the zone up
in a table made from the tz database (scripts/timezones.py) and sends only
its two-letter country; anything not in the table sends nothing.
Turning a setting on no longer counts as having seen the new notice, and
the country is taken out at send time until it has been, so it can't
leave early whatever order things happen in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opens, installs and updates carry the computer's time zone name (from
Electron, which knows it on every OS), so we can see roughly which
countries Frame Control is used in without ever seeing an address.
The first-run notice now has a version. Copies that saw the older notice
see it again, and keep sending their usual statistics, but leave the time
zone out until they've seen the new one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- cancel_signin marks an in-flight attempt and, independently, deletes the
account if that attempt saved it, so a cancel landing after the save but
before the connect's cleanup still removes the token.
- Sign-out runs one at a time (a second call joins it) and refuses new
sign-ins while it runs; it goes round until no window, connect or cancel is
left. The page keeps Sign in with Meta off from pressing Disconnect until
the server has deleted the token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the sign-in window is closed, times out or is signed out of while a
connect is under way, the page is answered at once, but cancel-signin is
posted only after that connect settles (saved, refused or failed), so the
server always sees the cancel after the connect and deletes the token if
that connect saved it. Nothing sent means no cancel. Sign-out, Disconnect's
first step, waits for any such cancel (also from a window closed earlier)
before the page posts its disconnect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- frame_meta keeps only in-flight sign-in attempts (removed when their connect
finishes) instead of a bounded list of cancelled ids, so other cancels can't
push one out, and records which attempt saved the account. Cancelling an
attempt being checked stops its save; cancelling the attempt that just saved
deletes that token; a later sign-in's or a pasted token's account is never
touched; unknown attempts change nothing.
- will-redirect applies the per-frame rule: frames may be redirected to any
https: page, the window only to Meta's sites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Meta's login can embed captcha and CDN frames from other sites (Arkose Labs,
hCaptcha, reCAPTCHA, fbcdn), so frames inside the sign-in window may now go to
any https: address (or about:blank/about:srcdoc); other schemes (http:, data:,
blob:, javascript:, file:...) are still stopped. The window itself stays on
the Meta/Oculus/Facebook allow-list. Frames can't reach the cookie read or IPC.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The desktop app can now open Meta's own login page (https://secure.oculus.com,
as BSManager's "Log in with Meta") in a window with its own session
(persist:meta-signin: sandboxed, context isolation, no preload or Node,
permissions refused). Navigation stays on https oculus.com, meta.com and
facebook.com; other links open in the system browser and other redirects are
stopped. The oc_ac_at cookie is read only from that session and only for
oculus.com, sent once per value through the server's existing connect path
(check_token, who am I, 0600 file), and the window closes. The page only gets
the server's answer, never the token. Disconnect also signs out of Meta in
Frame Control by clearing that session. Pasting a token stays as the fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
- check_token refuses values with no lower-case letter (real tokens are mixed
case), so the kept upper-case-constant shape can never hold an accepted
token, even with a _SUFFIX appended.
- The token-shape pattern has no word boundary: a token is found after any
character (_OC..., /OC..., libOC...), and its run takes every following
token character, so an accepted token is always inside one match. Still
one run per match, linear (timing tests unchanged).
- Whole-token redaction runs before access_token= and Authorization
redaction, so OCaccess_token=123456 goes whole, prefix included.
The property test now also surrounds each accepted random token with random
printable-ASCII neighbours (and _FLAG, access_token=) and checks the output is
a start of the left side, one <redacted>, and an end of the right side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_token accepted any printable character, more than the generic redaction
looked for, so an accepted value like OCAbcdef.GhijkLmnopQrstUvwx wasn't
redacted and could show as a library name. TOKEN_CHARS (letters, digits and
the base64 / URL-safe base64 characters + / = - _) and TOKEN_MIN (20) now
define both: check_token refuses anything else ("That doesn't look like a
Meta token"), including upper-case underscore constants the redaction keeps,
and the generic pattern is built from the same constants. The public tools
only describe tokens of letters and digits; widen TOKEN_CHARS in one place
if a real token needs more. Property test: 3000 random OC/FRL values over the
allowed set; every one check_token accepts is redacted in full in five
contexts and never shown as a library name. Dotted regression too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
- _clean matches each token character literally, percent-encoded (%XX) or
percent-encoded twice (%25XX), either letter case, hex in any case, still
with 8+ character starts. This restores the double-encoding the port from
feat/itch-purchases dropped; regression cases for double-encoded, recased
and prefix forms.
- A shown log excerpt (library, extension, exception or signal name) is
taken from the line after the whole line is redacted, never cut from
something longer, and dropped unless it looks like a real identifier
(64 characters at most, no 32+ alphanumeric run, no 16+ run mixing upper,
lower and digits). Tested with the token, and the tail of a longer one,
as a library name in a dlopen failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Redirects off Meta's hosts are refused, before any lookup or connection,
when the address carries the token in any form anywhere (host, path, query,
fragment; literal, recased, percent-encoded, 8+ character starts) or an
access_token parameter, instead of stripping only access_token.
- Connections resolve once, require every address to be public (an
allow-list that refuses ff02::, fec0::, NAT64, 6to4, Teredo and the IPv4
special blocks), connect to exactly a checked address with hostname-
verified TLS within one shared deadline, and use no environment proxies.
- Redaction uses the itch branch's _clean (each character literal or
percent-encoded in either case, 8+ character starts).
These three mirror _web.public_address / resolve_public /
_public_connection and _itch_account._clean / _carries_key on
feat/itch-purchases, so the two can be unified when both merge.
- One byte budget per install (16 GB) counts every byte as it arrives,
including files of unknown size; overflow stops and removes the file.
- Test causes are fixed texts; reports never carry log text. A short
excerpt (extension, library, exception or signal name) is shown only,
redacted before it is shortened.
- A running game must be confirmed closed before a test; a failed close
after a hang is reported ("the game may still be running").
- A licence failure needs explicit failure evidence; a game open without a
VR-session line is "inconclusive" and not reported to the compat DB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>