Commit Graph
38 Commits
Author SHA1 Message Date
saphidandClaude Opus 5.5 8315c7c3aa Merge vr-library (Steam library art, Play/Stop, refresh-art) into the store
Resolve CLI usage and POST table conflicts. Store installs now hand the
source's own image URLs (icon, banner, screenshots) to frame_android.install
as Steam artwork; before, they passed UI proxy paths (or nothing), so every
store install fell back to generated art.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:45:31 +10:00
saphidandGPT-6 Astra f695f398de Render complete Steam artwork for every sideload and fix VR shortcut identity
Add optional SteamGridDB settings, source-first fallbacks rendered with Steam canvas, backfill commands and shared APK/native library details. Verify live artwork and Open Saber Steam Play/Stop; document SuperTux's clipboard crash.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 22:20:27 +10:00
saphidandClaude Opus 5.5 9b0fedf602 Store: OBB game data becomes a follow-up 'Add game data' step
install_obb needs the app's running instance, which doesn't exist straight after
install, so the store no longer calls it there. The install result says the app
needs its game data; after opening the app once, 'Add game data' copies the
downloaded OBB files (a background job).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:13:42 +10:00
saphidandClaude Opus 5.5 a9d78679ec Store: add repositories in a background job and show the TOFU fingerprint
Adding a repository downloads and verifies its whole index, so it now runs as a
job (runJob in the UI) and reports 'Trusted on first use: <fingerprint>' when
no pin was given. fdroidrepos:// links pass the server check, as documented.
Jobs report SourceError messages without a 'SourceError:' prefix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:13:04 +10:00
saphidandClaude Opus 5.5 029c92bccb F-Droid: serve an expired index as stale while refreshing in the background
Searches no longer wait for (or fail on) a refresh of an expired index; the
store notes which sources show saved listings. A failed refresh keeps the old
index and is retried after 10 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:11:04 +10:00
saphidandClaude Opus 5.5 7c439fdf28 Store: per-host backoff after 403/429 honouring Retry-After
A host that answers 403/429 is left alone until its Retry-After (or GitHub's
rate-limit reset; default 10 minutes). Meanwhile cached data is served, or the
source reports 'limited' with its own name, e.g. 'GitHub is limiting requests;
try again in 10 minutes'. Covers _web reads/downloads and F-Droid fetches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:09:55 +10:00
saphidandClaude Opus 5.5 69f790b83a Store: real-source fixes found by running search against live repos
- F-Droid: percent-encode repo file names (a '#' in one screenshot name broke
  the whole main repo); a bad image name drops that image, not the app.
- Search: sources still fetching report 'loading' (UI says so and refreshes
  quietly); indexes warm up at server start; page-only SideQuest is not
  searched and appears as a 'Browse SideQuest' link instead of an error.
- Browse (empty query) ranks VR, artwork and recent updates first; the F-Droid
  archive is off by default (old versions only).
- Throttled sources fall back to their last cached copy; per-host message.
- Curated GitHub list gains Open Saber Plus (MIT) with icon and screenshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:02:38 +10:00
saphidandGPT-6 Astra 7844577be8 Redesign APK discovery as an artwork-led app store
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:47:38 +10:00
saphidandGPT-6 Astra f1b12a6eb6 Add unified APK source search and source management
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-09-28 21:04:48 +10:00
saphid 35ef3af54b Merge remote-tracking branch 'origin/main' into vr-apks
# Conflicts:
#	ui/frame_android.py
#	ui/index.html
2026-09-28 17:49:47 +10:00
saphidandClaude Opus 5.5 224340edc9 APK alternatives: refresh the catalogue after an install job; pin the test's premise
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:29:29 +10:00
saphidandClaude Opus 5.5 c814cb95d0 Merge main into apk-alternatives: install other versions as background jobs
Main now runs Android installs as background jobs and maps SSH failures to
one offline message. Alternative-version installs go through the same job,
the alternatives dialog waits on it with runJob, and send_error_json keeps
the apk blocker that opens the dialog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:21:55 +10:00
saphid a1bc6522c3 Detect VR APKs and repair launchers with pure Python v2 signing 2026-09-28 16:16:56 +10:00
saphidandClaude Opus 5.5 a1fa4ce140 Fix the cross-provider review's findings on APK alternatives
One malformed or unreachable repo no longer hides the others; skip bad
index entries; a refreshed raw index outdates its reduced copy; style the
dialog like the others; validate package ids with PKG_RE.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:15:11 +10:00
saphid 32196b4260 Reduce F-Droid indexes and fetch APK alternatives asynchronously 2026-09-28 14:26:40 +10:00
saphid fbe7ba9575 Find installable APK alternatives in F-Droid main and archive 2026-09-28 14:16:43 +10:00
saphid 0016d9200c Merge remote-tracking branch 'origin/iphone-app' into iphone-app 2026-09-27 21:27:10 +10:00
saphidandClaude Opus 5.5 fe87a9d826 Keep the page clear of iOS safe areas everywhere; research typing, pointing and mirroring into the Frame
- Header, content, tab bar, status strip, drawer and toasts add the notch,
  Dynamic Island, rounded-corner and home-indicator insets on every side
  (zero on desktops). Phones on their side use the bottom tab bar layout.
- The phone tab bar hides while a text field has focus, instead of riding
  on the keyboard.
- DEBUG hook FRAME_TEST_LANDSCAPE for checking this in the Simulator.
- docs/streaming.md: iPhone mirroring (UxPlay, broadcast extension) and
  keyboard/mouse input (uinput needs no sudo on the Frame, verified).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 21:26:32 +10:00
saphidandClaude Opus 5.5 ca2a991f03 Sideloaded titles: use ids Steam's create-shortcut accepts
On the Frame, Steam refused to register titles whose id had a hyphen
(fc-smoke-exe) with "missing/invalid arguments", and registered the same
program as FCSmokeProbe (headset smoke test, 2026-09-27, BUILD_ID
20260922.6101926). Valve's client only allows ^[A-Za-z_][A-Za-z0-9_.]+$.

title_id now makes ids of letters, digits and _, not starting with a
digit, 2 to 64 long; new installs are checked against that, while titles
already on the Frame are still listed, launched and removed. Steam's error
text is trimmed before it's quoted, and the "install it again with Steam
running" hint only follows a Steam-not-running error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 17:40:09 +10:00
saphidandClaude Opus 5.5 a910f83ac1 Fix the iPhone review's findings
- Cancelling (Change headset, Forget) invalidates the attempt in flight; a
  connection only becomes the app's once every step finished for it.
- A server that stops while the tunnel opens is noticed (exit callbacks are
  synchronised and replayed), and the app isn't left showing a dead page.
- The port is read only once the digits are complete, and range-checked.
- Other versions in ~/.cache/frame-control stay unless untouched for 14 days,
  so a second phone or iPad isn't cut off.
- The key is appended on its own line even if authorized_keys lacks a final
  newline.
- The app checks every 20 s, and on returning to the foreground, that the SSH
  session still answers, and reconnects if not.
- The ssh stand-in runs the command as its own process group and passes a
  TERM on to all of it, so a live-video ffmpeg stops with its stream.
- Touch screens show the library's Play buttons (the rule now follows the
  base one); the failure screen only says it's retrying when it is; the page
  says the app reconnects rather than naming a desktop menu.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:30:40 +10:00
saphidandClaude Opus 5.5 13187e8f6a iPhone and iPad app: the same features, served from the Frame
An iPhone can't run Python or ssh, but the Frame can. The app (ios/, SwiftUI)
connects with its own SSH key (Citadel), copies the server and helpers to
~/.cache/frame-control/<version> on the Frame once per version, starts
ui/server.py there with FRAME_LOCAL=1 on the Frame's 127.0.0.1, and shows the
page through an SSH tunnel. The server exits when the phone disconnects.

Server: FRAME_LOCAL=1 puts ui/local-bin on PATH, whose ssh stand-in runs each
`ssh frame COMMAND` locally (and serves as rsync's transport), so desktop and
phone share one code path. Android display goes through podman exec there, as
the Frame has no adb. FRAME_UI_KEY replaces the fixed X-Frame-UI value with a
per-session key. Power actions take the Developer Mode password via sudo -S.
--port 0 now prints the port it took.

Page: a bottom tab bar and safe areas on phones, Play buttons visible on touch
screens, saving through the share sheet, SSH/SFTP/Steam Link/remote desktop
opening in their iOS apps, and a password dialog for power.

App: pairing with the Developer Mode password once (never stored) or with a
key the user adds; host key pinned on first use; plain-language connection
errors with quiet retries; frame-control://install links; alerts and confirms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:18:15 +10:00
saphidandClaude Opus 5.5 0f770dc88a Tabs, one offline banner, background installs, drop anywhere
The page was one 6,800px scroll with nine nav links (hidden below 1150px).
It is now four tabs, Home, Games, Android and Tools, switched with 1-4; old
section links still land on the right tab.

When the Frame can't be reached, the server turns ssh's connection errors into
one plain message (503, offline: true), the page shows a single banner with
Retry and Set Up Connection, retries every 8 s, and reloads every panel when
the Frame answers. Panels say "Waiting for the Frame" instead of raw ssh text.

Flatpak and Android catalogue installs run as background jobs the page polls,
so a slow install no longer holds a request for up to 15 minutes or reports a
false failure; the bottom bar counts running installs.

Files can be dropped anywhere in the window, as the README already said.
Recent reports show the newest five, with Show all. Android display explains
an empty or failed read. A topped-up headset on a charger reads as not
charging rather than "still draining, using 0.0 W".

Fixes a race where the catalogue and reports loads wrote the compat-db
mirror's .tmp file at once and one failed with a 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 10:23:24 +10:00
saphidandClaude Opus 5.5 eb78eba0dc Give the install dialog its own snapshot of installed titles
A Refresh in flight could leave the shared list stale when the dialog
opened. The drop now fetches the list itself and hands it to the dialog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 22:55:42 +10:00
saphidandClaude Opus 5.5 fd0a284942 Refuse APK entries Android can't read; ignore stale title lists
- Only stored and deflated entries are read: Python 3.9's bzip2 and lzma
  readers inflate without bound before trimming.
- loadTitles drops a response that a newer request has overtaken, so the
  install dialog's replace warning uses the fresh list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 22:49:18 +10:00
saphidandClaude Opus 5.5 dfacf43e55 Fix the follow-up review's findings
- APK reader: read members with a bounded read, since ZipFile.read inflates
  a member fully before trimming it to a forged declared size; the reference
  walk counts every entry it examines, dead ends and cycles included.
- Titles: a path that exists under the root wins over stripping the archive
  prefix; the prefix is taken before a linked folder is staged elsewhere
  (another drive on Windows); the install dialog loads a fresh title list
  first and says so if it couldn't check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 22:44:07 +10:00
saphidandClaude Opus 5.5 39d28790a1 Fix the cross-provider review's findings
- APK reader: cap AndroidManifest.xml, resources.arsc and icon sizes before
  inflating them (APKs can come from install links), and follow resource
  references without cycles and with a result budget.
- Sideloading: reserve devkit-steam (SteamOS's sideloaded-client trampoline);
  the install dialog warns when a name replaces an installed title; a
  manifest's exe may name the program as it is in the archive, above the
  folder the installer steps into.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 22:35:16 +10:00
saphidandClaude Opus 5.5 2ab2ffa65a Fix the final review's findings on the combined features
- Links to localhost need FRAME_CONTROL_LOCAL_LINKS=1: otherwise any website's
  link could make the app fetch from services on this computer.
- Title staging folders (unzipped titles) carry the server's PID and are swept
  on the next start like download folders, so quitting mid-install doesn't leave
  gigabytes behind.
- An install from a link refreshes Sideloaded titles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:49:21 +10:00
saphidandClaude Opus 5.5 636a4a47b7 Merge sideloading, install links and devkit pairing
Combines the three feature branches on bundle-deps. Conflicts in server.py,
index.html, preload.js, README and test_server.py keep both sides. The
web-install downloader now uses urllib's default HTTPS context, so the
bundled CA list from 770f26c applies to it on Windows too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:39:52 +10:00
saphidandClaude Opus 5.5 6c4d387ef3 Sideload Linux and Windows builds as Steam Devkit Games
Dropping a game's .zip, folder or .exe on Send to Frame now adds it to the
headset's Steam library through Valve's SteamOS Devkit title path, with the
runtime picked from the program's header: Windows PE -> Proton Experimental
(steam_play=1), aarch64 ELF -> SteamLinuxRuntime_4-arm64, x86-64 ELF ->
SteamLinuxRuntime_4 (through FEX). Other architectures are refused.

- frame/devkit-utils: Valve's devkit-utils vendored unmodified (MIT,
  steamos-devkit v0.20260925.1), synced to ~/devkit-utils by stamp, bundled in
  the app and compiled in CI.
- ui/frame_titles.py: inspect (safe unzip, ELF/PE classification, launch
  target ranking), install(path, name=None, exe=None, runtime=None,
  progress=None), list, launch, remove, plus a CLI.
- ui/server.py: /api/titles (inspect/install/discard/launch/remove),
  /api/titles/job progress, and an upload mode 'title'.
- ui/index.html: confirm dialog (name, launch target, runtime), install
  progress, and a Sideloaded titles list with Launch and Remove. The app's
  preload passes a dropped folder's path.
- tests and docs/sideloading.md. Device-side behaviour is inferred from
  Valve's source; the headset was offline, so none of it has been checked on
  a Frame yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:38:14 +10:00
saphidandClaude Opus 5.5 dd9c009206 Install links for websites: frame-control://install
A site can link to frame-control://install?manifest=URL (or ?url=URL) to
install a title with Frame Control. Manifests use FrameDrop's format, so
framedrop.install/v1 is accepted as well as frame-control.install/v1.

- app/install-link.js parses links; main.js registers the scheme (plus
  electron-builder protocols for Info.plist and the .desktop file), takes
  links from open-url, second-instance argv and the first argv, and holds
  them until the page asks for them through preload's onInstallLink.
- ui/frame_webinstall.py checks the URLs (HTTPS only; localhost over http
  only when the link itself is local; no userinfo; every address public,
  rechecked on redirects and pinned for the connection), reads the
  manifest, downloads with a size cap and sha256 check, and dispatch()
  sends .apk to frame_android and .zip/.exe to frame_titles when present.
- server.py adds /api/webinstall/check, start, job and cancel behind the
  existing Host and X-Frame-UI guards; a start needs a one-time id from
  check. Downloads stop on cancel and on shutdown, and leftovers from a
  killed server are swept by PID.
- index.html asks before anything downloads (name, source host, file,
  type, size, whether a sha256 was given) and shows progress.
- docs/web-install.md, docs/install.html (landing page, unpublished).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:18:39 +10:00
saphidandClaude Opus 5.5 1a0e54d8bd Bundle Python and adb so the app needs nothing installed
- app/build/fetch-deps.js downloads a standalone Python 3.12
  (python-build-standalone) for every build and adb from Google's
  platform-tools, pinned by SHA-256, and prunes what the server never
  uses. It replaces the Windows-only embeddable Python.
- The app runs the bundled Python with -I -u -B -X utf8, so a PYTHONHOME
  or PYTHONPATH meant for another Python can't break it and nothing is
  written inside the signed macOS bundle.
- An adb you already have still goes first, so two adb versions don't
  keep restarting each other's server. arm64 Linux has no official
  platform-tools and keeps using the system adb.
- ui/frame_apk.py reads APK badging (package, label, version, min SDK,
  ABIs, icon) from the binary manifest and resources.arsc, replacing
  aapt2. It matches aapt2 on nine F-Droid APKs and finds launcher icons
  the old path missed with adaptive icons.
- The app reads the computer's clipboard through Electron, so Linux no
  longer needs wl-clipboard or xclip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 16:42:49 +10:00
saphidandClaude Opus 5.5 e210407f31 Fix review findings and Windows setup issues found in testing
- Run the server with -X utf8: the bundled Windows Python ignores PYTHON* variables.
- Quote every argument in Windows terminal commands, so cmd metacharacters are literal.
- frame_connect: accept HOST:PORT, validate input, retry the config swap while
  Windows' ssh.exe holds ~/.ssh/config locked, and don't apply 0o700 on Windows.
- Never use rsync on Windows; unbounded stream queue; validate FRAME_ALIAS;
  more Linux terminals; bundle the window icon; docs and wording fixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:41:04 +10:00
saphidandClaude Opus 5.5 f6e77cd98c WIP: run Frame Control on Linux and Windows
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 08:47:00 +10:00
saphidandClaude Opus 5.5 60571dbfac Prepare Frame Control 0.2.0 for public testing
- Without the maintainer's key, Android compatibility reports stay on the
  Mac and the UI says so; the shared database is never contacted.
- Remove personal infrastructure details from scripts and docs: the Drive
  folder and gog wrapper now come from the environment, and the Chromium
  build host is required instead of defaulted.
- Add an MIT license, tester instructions in the README, and bump to 0.2.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:37:37 +10:00
saphidandClaude Opus 5.5 f324aac690 Live H.264 video of the headset view in Frame Control
Live in the headset view now streams video instead of polling stereo
screenshots (~2 fps). SteamVR's steamvr-v4l2cam.service mirrors the headset
view into /dev/video99; ffmpeg on the Frame encodes it with x264 (720p30 by
default, AUD + repeated SPS/PPS), /api/stream relays the raw H.264 over SSH,
and the page splits it on access unit delimiters and decodes it with
WebCodecs into the existing viewer. Capture still takes a stereo still; the
desktop panel keeps capture polling, and the page falls back to it if the
video can't start.

The remote ffmpeg runs under a shell that kills it when the SSH channel
closes, stderr goes to a temp file, and a 10 s stall ends the stream. One
stream at a time; a new one supersedes the last.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:11:33 +10:00
saphidandClaude Opus 5.5 eabf4cd1f9 Add Steam screenshots, Tailscale remote access, VR-video fixes
- Screenshots: list the Frame's Steam screenshots, open them in the
  viewer, and save new ones to ~/Pictures/SteamFrame. Ids are validated
  before any shell, and copies land atomically.
- Tailscale: scripts/tailscale-on-frame.sh installs a userspace tailscaled
  as a lingering systemd --user service with no sudo, SHA-256 checked, safe
  to re-run, with --uninstall. docs/tailscale.md covers setup and warns that
  in userspace mode every Frame port, including loopback-only DevTools and
  ADB, is reachable from the tailnet.
- push-vr-video.sh: filenames starting with "-" are safe, symlinks are
  followed, and a real Videos\VR directory triggers a warning.
- Tests cover the screenshot routes (19 total).

Docs keep placeholder addresses for the headset and tailnet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 23:11:14 +10:00
saphidandClaude Opus 5.5 99653151c4 Address review findings in the app, server and tests
- app: startup shell, python and ssh probes run asynchronously so a slow
  shell profile can't freeze the window; PATH comes from the user's real
  login shell and a failed lookup isn't cached; a server that never
  answers is killed; the setup offer runs once per launch, only after the
  UI loads, and decides from HostName alone; connect.sh is started through
  `env ... zsh` so it works whatever the login shell is.
- server: volume validates the level before muting or changing anything.
- Steam: null-safe install-manager fields, http.client errors caught in
  store ratings, price fallback when a sale has no final price.
- tests: server output kept for diagnosis, any startup error retried, and
  captures asserted non-cacheable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 22:44:54 +10:00
saphidandClaude Opus 5.5 d4486a7681 Frame Control: Mac app, web UI, Android and Steam tooling
Package the Frame Control web UI as an installable Electron Mac app and
bring in the tooling built alongside it.

- app/: Electron wrapper that starts ui/server.py on a free loopback port,
  hardened window (sandbox, no navigation, runAsNode fuse off), login-shell
  PATH so Homebrew tools work from Finder, first-run offer to run
  connect.sh, ad-hoc signed DMG/zip via electron-builder.
- ui/: headset view (OpenVR screenshots), device status, library, Steam
  "Get games" (owned games, install, store search), Android apps as
  persistent Lepton instances with a rated F-Droid catalogue and a private
  compatibility database, Android display controls over ADB, file and
  clipboard transfer, Flatpaks, remote and power actions.
- apk-catalog/, compat-db/, frame/: catalogue build pipeline, Lakebed
  capsule for compatibility reports, Frame-side launchers.
- tests/ and CI: server guard and validation tests plus Steam helper tests,
  run on Python 3.9 with script and app syntax checks.
- Docs: README leads with the Mac app; new Android, panels, Steam games and
  field-notes docs; security notes on LAN-exposed ADB ports.

Screenshot values for the headset's IP and Wi-Fi name are placeholders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 22:21:20 +10:00