mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-04 22:00:10 +02:00
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>
This commit is contained in:
1 parent
f324aac690
commit
60571dbfac
16 files changed
+192
-57
No files matched your search
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: steam-frame
|
||||
description: Operate the user's Valve Steam Frame headset from the Mac through the ~/projects/steam-frame helpers and field notes. Use for Steam Frame SSH, screen streaming, clipboard, file push, APK or Flatpak installs, launching apps on the headset, arranging floating windows or panels in VR space, or debugging SteamOS/gamescope/SteamVR on the Frame.
|
||||
description: Operate the user's Valve Steam Frame headset from the Mac through this repo's helpers and field notes. Use for Steam Frame SSH, screen streaming, clipboard, file push, APK or Flatpak installs, launching apps on the headset, arranging floating windows or panels in VR space, or debugging SteamOS/gamescope/SteamVR on the Frame.
|
||||
---
|
||||
|
||||
# Steam Frame
|
||||
|
||||
The repo is `~/projects/steam-frame`. SSH works through the `frame` alias
|
||||
SSH works through the `frame` alias
|
||||
(user `steamos`). The headset has to be awake for anything that touches its
|
||||
desktop or panels.
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 saphid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,8 +1,13 @@
|
||||
# Steam Frame ↔ Mac
|
||||
|
||||
This repo holds notes and Mac-side helpers for controlling a Valve Steam Frame
|
||||
(standalone VR headset: SteamOS 3, Arch-based, arm64, Snapdragon 8 Gen 3) from
|
||||
this Mac, with as little typing on the headset's virtual keyboard as possible.
|
||||
**Frame Control** is a Mac app for managing a Valve Steam Frame (standalone VR
|
||||
headset: SteamOS 3, Arch-based, arm64, Snapdragon 8 Gen 3) over SSH: live
|
||||
headset view, battery and status, your Steam library, Android (Lepton) apps,
|
||||
file and clipboard transfer. This repo also holds the scripts behind it and
|
||||
field notes on how the Frame's software works, all aimed at as little typing
|
||||
on the headset's virtual keyboard as possible.
|
||||
|
||||
It's an unofficial hobby project, not affiliated with Valve.
|
||||
|
||||
Status: written 2026-09-25 and checked against a real Frame the same day
|
||||
(SteamOS 0.3.0, variant `vr`, build 20260922). The **Frame Control** Mac app
|
||||
@@ -10,8 +15,41 @@ and most scripts are **verified** on the device. The scripts table below marks
|
||||
each one, and [docs/open-questions.md](docs/open-questions.md#verified-on-device-2026-09-25)
|
||||
lists what's still unchecked.
|
||||
|
||||
**Quick start:** set up SSH once (next section), then install
|
||||
[Frame Control](#frame-control-mac-app) from the DMG.
|
||||
## Trying it out
|
||||
|
||||
You need:
|
||||
|
||||
- A Steam Frame with **Developer Mode** on (next section; it's a toggle).
|
||||
- A Mac with Apple Silicon (M1 or later). Tested on macOS 26. There's no Intel
|
||||
build.
|
||||
- `python3` on the Mac (`xcode-select --install` provides it).
|
||||
- Optional: `adb` for Android apps (`brew install android-platform-tools`).
|
||||
|
||||
Steps:
|
||||
|
||||
1. Download the DMG from the
|
||||
[latest release](https://github.com/saphid/steam-frame/releases/latest),
|
||||
open it and drag **Frame Control** to Applications.
|
||||
2. The app isn't notarized (no paid Apple developer account), so macOS will
|
||||
say it's damaged or can't be checked. Clear the download quarantine once:
|
||||
```sh
|
||||
xattr -dr com.apple.quarantine "/Applications/Frame Control.app"
|
||||
```
|
||||
3. Open it. With no `frame` SSH alias yet, it offers to run the connection
|
||||
setup in Terminal. That asks for the Developer Mode password once, then
|
||||
uses a key from then on.
|
||||
|
||||
**Feedback:** please open a
|
||||
[GitHub issue](https://github.com/saphid/steam-frame/issues) with what you
|
||||
tried, your SteamOS build (Steam Settings → System) and the server log
|
||||
(**Frame → Show Server Log**, at `~/Library/Logs/Frame Control/server.log`).
|
||||
Features are marked **verified** or not below; the unverified ones are the
|
||||
most useful to hear about.
|
||||
|
||||
**What it changes on your Frame:** only what you click. Installs go to your
|
||||
user account (`--user` Flatpaks, Lepton instances, Steam downloads), and
|
||||
nothing needs `sudo` except the power buttons. On the Mac it adds a `Host
|
||||
frame` entry to `~/.ssh/config` and a key at `~/.ssh/id_ed25519_frame`.
|
||||
|
||||
## Minimum typing on the headset
|
||||
|
||||
@@ -33,8 +71,10 @@ On the Frame:
|
||||
|
||||
On the Mac:
|
||||
|
||||
To use the scripts from a checkout instead of the app:
|
||||
|
||||
```sh
|
||||
cd ~/projects/steam-frame
|
||||
git clone https://github.com/saphid/steam-frame.git && cd steam-frame
|
||||
./scripts/connect.sh # or: ./scripts/connect.sh 192.168.1.50
|
||||
ssh frame # passwordless from now on
|
||||
```
|
||||
@@ -127,9 +167,10 @@ the same UI in a browser without packaging:
|
||||
- **Android apps**: search about 4,500 F-Droid apps rated for the Frame, install
|
||||
one with a click as its own Lepton instance (it keeps its data and shows in the
|
||||
Steam library), then launch, stop, test or remove it. **Report an APK** records whether any APK
|
||||
worked (F-Droid or not: pick a file, type a package, or use an installed app). The
|
||||
ratings go into our private compatibility database (a Lakebed capsule only the
|
||||
app can use, backed up daily to Google Drive; see `compat-db/README.md`)
|
||||
worked (F-Droid or not: pick a file, type a package, or use an installed app). Your
|
||||
reports are saved on your Mac and change the verdicts you see. They aren't
|
||||
uploaded anywhere: the shared database is maintainer-only for now (see
|
||||
`compat-db/README.md`)
|
||||
- **Android display**: pick a running Lepton instance (by the app in it) and set
|
||||
its resolution (Native 1920×1080, or Sharp 2560×1440 with density scaled to
|
||||
match), UI scale (Smaller / Default / Larger, or an exact dpi) and text size
|
||||
@@ -177,9 +218,7 @@ Finder. Each time it starts while there's no `frame` SSH alias, the app offers
|
||||
to run `connect.sh` in Terminal. **Frame → Set Up Connection…** does the same
|
||||
at any time. The Frame menu also shows the server log at
|
||||
`~/Library/Logs/Frame Control/server.log`. Installing APKs needs `adb`
|
||||
(`brew install android-platform-tools`). The Android ratings database needs
|
||||
its key in the Keychain (see `compat-db/README.md`); without it, the app uses
|
||||
its offline copy.
|
||||
(`brew install android-platform-tools`). The F-Droid ratings are bundled with the app.
|
||||
|
||||
The build is ad-hoc signed and not notarized. A copy you build yourself opens
|
||||
normally. A copy downloaded from GitHub Releases is quarantined; clear it with
|
||||
@@ -202,7 +241,7 @@ showed live status and the library.
|
||||
| `scripts/run-on-frame.sh` | Mac → Frame | Start an app on the headset desktop, e.g. `mac-screen` opens Remmina straight into the Mac (**verified**) |
|
||||
| `scripts/frame-ui.sh` | Mac | Start the Frame Control web UI (`ui/server.py`) and open it (**verified**) |
|
||||
| `scripts/apk-catalog.sh` | Mac | Refresh the rated F-Droid catalogue that Frame Control's Android section shows (**verified**) |
|
||||
| `scripts/compat-db-backup.sh` | Mac | Back up the compatibility database locally and to Google Drive (daily LaunchAgent) (**verified**) |
|
||||
| `scripts/compat-db-backup.sh` | Mac | Maintainer-only: back up the shared compatibility database locally and to Google Drive (**verified**) |
|
||||
| `scripts/push-vr-video.sh` | Mac → Frame | Upload VR180/360 videos to `~/Videos/VR`, linked into DeoVR's Proton prefix; `--launch` starts DeoVR (**verified**: upload and link; in-headset playback of local files not yet checked). See [docs/vr-video.md](docs/vr-video.md) |
|
||||
| `scripts/push.sh` | Mac → Frame | `rsync` files to `~/Downloads` (or a given path) on the Frame (**verified**) |
|
||||
| `scripts/serve-bootstrap.sh` | Mac | Fallback: serve `bootstrap-on-frame.sh` with your public key embedded |
|
||||
@@ -238,3 +277,8 @@ may find (Xcode Command Line Tools), plus syntax checks for every script and the
|
||||
Electron main process (`.github/workflows/checks.yml`). Anything that touches the
|
||||
headset is verified by hand against a real Frame, and the docs label it
|
||||
**verified** or **inferred**.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE). Steam, Steam Frame and SteamVR are trademarks of Valve
|
||||
Corporation. This project isn't affiliated with or endorsed by Valve.
|
||||
@@ -27,8 +27,8 @@ rules and the evidence behind them are in [docs/apks.md](../docs/apks.md).
|
||||
|
||||
## Compatibility reports
|
||||
|
||||
Reports live in Frame Control's private database, a Lakebed capsule at
|
||||
`https://frame-compat.lakebed.app` that only the app can read or write (see
|
||||
Reports are saved on your Mac. The maintainer's copy of Frame Control also
|
||||
syncs them to a private Lakebed database (see
|
||||
[compat-db/README.md](../compat-db/README.md), including backups). **Test**
|
||||
records whether an app stays up in its own instance (`result`); **Report**
|
||||
(on any installed app, catalogue card, or **+ Report an APK** for anything else, e.g. an
|
||||
|
||||
Generated
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.1.1",
|
||||
"license": "UNLICENSED",
|
||||
"version": "0.2.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
"electron-builder": "^26.15.3"
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Mac app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
"license": "UNLICENSED",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "env -u ELECTRON_RUN_AS_NODE electron .",
|
||||
"icon": "env -u ELECTRON_RUN_AS_NODE electron build/make-icon.js",
|
||||
|
||||
+7
-7
@@ -1,11 +1,12 @@
|
||||
# compat-db: Frame Control's compatibility database
|
||||
|
||||
A private [Lakebed](https://docs.lakebed.dev/) capsule holding compatibility
|
||||
reports for Android apps on the Steam Frame. Only Frame Control can read or
|
||||
write it.
|
||||
reports for Android apps on the Steam Frame. For now only the maintainer's
|
||||
copy of Frame Control has the key to read or write it. Everyone else's reports
|
||||
stay on their own Mac (see `shared()` in `ui/frame_compat_db.py`).
|
||||
|
||||
- Live: `https://frame-compat.lakebed.app` (deploy `dep_dDmcsosVSiFirpW6`,
|
||||
owned by `saphid`, doesn't expire). The browser page only says it's private.
|
||||
claimed, so it doesn't expire). The browser page only says it's private.
|
||||
- Access: `GET /v1/reports?since=<createdAt>` and `POST /v1/reports` with
|
||||
`{"reports": [...]}`. Both need the `x-frame-control-key` header. There are
|
||||
no Lakebed queries or mutations, so nothing else can reach the rows.
|
||||
@@ -22,10 +23,9 @@ write it.
|
||||
`scripts/compat-db-backup.sh` exports every report through the app key and
|
||||
keeps dated copies in
|
||||
`~/Library/Application Support/Frame Control/compat-db/backups` (newest 60).
|
||||
When the data has changed, it also uploads them to Google Drive
|
||||
(**the backup folder**, folder
|
||||
`<drive-folder-id>`) with `gog`. The LaunchAgent
|
||||
`frame-compat-backup` runs it daily at 03:40; the log is
|
||||
When the data has changed, it also uploads them with `gog` to the Google
|
||||
Drive folder named by `DRIVE_FOLDER_ID` (set it in the LaunchAgent's
|
||||
`EnvironmentVariables`). A LaunchAgent runs it daily at 03:40 and logs to
|
||||
`~/Library/Logs/frame-compat-backup.log`. If an export has fewer reports than
|
||||
the last good backup (`backups/.last-good`), it's kept as `refused-*.json`,
|
||||
nothing is uploaded, and every later run refuses too until you rerun with
|
||||
|
||||
+7
-8
@@ -135,11 +135,11 @@ collects community reports for Steam games only and has no public API, and
|
||||
Valve's "Great on Frame" badges and each Steam app's `recommended_runtime`
|
||||
(for example `lepton-stable`) also cover Steam games only
|
||||
([VR.org](https://vr.org/articles/steam-frame-lepton-android-runtime-52-of-130-certified-2026)).
|
||||
So we keep our own, in a private Lakebed database
|
||||
(`https://frame-compat.lakebed.app`) that only Frame Control can read or write.
|
||||
So Frame Control keeps its own. Your reports are saved on your Mac; the
|
||||
maintainer's copy also syncs them to a private Lakebed database.
|
||||
**Test** records whether the app stays up in its own instance, and **Report**
|
||||
(for any APK, F-Droid or not) records whether it worked, how it was run, where it came from, and notes, each with the SteamOS and Lepton build ids.
|
||||
A daily job backs it up locally and to Google Drive. See
|
||||
See
|
||||
[compat-db/README.md](../compat-db/README.md) and
|
||||
[apk-catalog/README.md](../apk-catalog/README.md).
|
||||
|
||||
@@ -188,13 +188,12 @@ But pasta runs with `--map-gw`, so the **gateway address inside Lepton
|
||||
|
||||
T3 Code v2 on the Mac listens only on `127.0.0.1:3873`. To reach it:
|
||||
|
||||
1. The LaunchAgent `~/Library/LaunchAgents/frame-t3-tunnel.plist`
|
||||
keeps `ssh -N -R 127.0.0.1:3873:127.0.0.1:3873 frame` running. launchd
|
||||
restarts it if it drops. Log: `~/Library/Logs/frame-t3-tunnel.log`.
|
||||
1. Keep `ssh -N -R 127.0.0.1:3873:127.0.0.1:3873 frame` running on the Mac,
|
||||
for example from a LaunchAgent with `KeepAlive`, so launchd restarts it if
|
||||
it drops.
|
||||
2. In the app on the Frame, the environment host is `192.168.1.1:3873`.
|
||||
|
||||
The app on the Frame was built from the v2 nightly source (fork commit
|
||||
`d0c468e3`) with `expo prebuild` and `gradlew assembleRelease
|
||||
The app on the Frame was built from the T3 Code v2 nightly source with `expo prebuild` and `gradlew assembleRelease
|
||||
-PreactNativeArchitectures=arm64-v8a`, using Homebrew `openjdk@17` and the
|
||||
`android-commandlinetools` SDK. It's signed with the debug key.
|
||||
|
||||
|
||||
@@ -51,14 +51,14 @@ installs the sysroot, builds `chrome` with `symbol_level=0` and proprietary
|
||||
codecs, and packs `chromium-xr-arm64.tar.xz`. Progress is logged to
|
||||
`~/chromium-xr/stage`. The build aborts if `/` drops below 12 GB free.
|
||||
|
||||
First run: buildhost (12 cores, 31 GB RAM), started 2026-09-25.
|
||||
First run: a 12-core, 31 GB x64 Linux box, started 2026-09-25.
|
||||
|
||||
## Running it on the Frame
|
||||
|
||||
[`scripts/chromium-xr.sh`](../scripts/chromium-xr.sh):
|
||||
|
||||
```sh
|
||||
scripts/chromium-xr.sh install # scp from buildhost, unpack to ~/chromium-xr
|
||||
BUILD_HOST=my-linux-box scripts/chromium-xr.sh install # your build host; scp, unpack to ~/chromium-xr
|
||||
scripts/chromium-xr.sh launch [URL] # headset desktop, --enable-features=OpenXR
|
||||
scripts/chromium-xr.sh check # prints isSessionSupported('immersive-vr')
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Linux-side (x64 host): cross-compile arm64 Chromium with the Linux OpenXR CLs
|
||||
# (8441736 + 8132979, bug 506004811) so WebXR immersive-vr works on the Frame.
|
||||
# Needs ~90 GB free, no sudo. Takes hours; run it detached, e.g. on buildhost:
|
||||
# Needs ~90 GB free, no sudo. Takes hours; run it detached on the build host:
|
||||
# scp scripts/build-chromium-xr.sh buildhost:chromium-xr/build.sh
|
||||
# ssh buildhost 'cd ~/chromium-xr && tmux new -d -s chromium-xr "./build.sh > build.log 2>&1"'
|
||||
# Progress: ~/chromium-xr/stage. Output: ~/chromium-xr/chromium-xr-arm64.tar.xz,
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
# Flatpak, so SteamVR's sockets and the XR sandbox work unmodified).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/chromium-xr.sh install [TARBALL] # default: fetch from $BUILD_HOST
|
||||
# scripts/chromium-xr.sh install [TARBALL] # default: scp from $BUILD_HOST
|
||||
# scripts/chromium-xr.sh launch [URL] # opens in the headset desktop
|
||||
# scripts/chromium-xr.sh check # isSessionSupported via DevTools
|
||||
set -euo pipefail
|
||||
|
||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||
BUILD_HOST=${BUILD_HOST:-buildhost}
|
||||
BUILD_HOST=${BUILD_HOST:-}
|
||||
BUILD_TARBALL=${BUILD_TARBALL:-chromium-xr/chromium-xr-arm64.tar.xz}
|
||||
DEVTOOLS_PORT=${DEVTOOLS_PORT:-9223}
|
||||
here=${0:A:h}
|
||||
@@ -23,6 +23,7 @@ case "${1:-}" in
|
||||
install)
|
||||
tarball=${2:-}
|
||||
if [[ -z "$tarball" ]]; then
|
||||
[[ -n "$BUILD_HOST" ]] || { print -u2 "Pass a tarball, or set BUILD_HOST to the build machine"; exit 2; }
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
tarball=$tmp/chromium-xr-arm64.tar.xz
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
#
|
||||
# Exports every report through the app's own key (ui/frame_compat_db.py), keeps
|
||||
# dated copies in ~/Library/Application Support/Frame Control/compat-db/backups (newest
|
||||
# 60), and uploads to Google Drive (the backup folder) when
|
||||
# the data changed since the last upload. Run daily by the LaunchAgent
|
||||
# frame-compat-backup (see docs/apks.md).
|
||||
# 60), and uploads to the Google Drive folder DRIVE_FOLDER_ID when the data
|
||||
# changed since the last upload. Maintainer-only: it needs the database key.
|
||||
# Run it daily from a LaunchAgent (see compat-db/README.md).
|
||||
#
|
||||
# Usage: scripts/compat-db-backup.sh [--no-upload] [--force-upload] [--accept-shrink]
|
||||
# Env: DRIVE_FOLDER_ID, GOG_WRAPPER
|
||||
@@ -14,8 +14,8 @@ set -euo pipefail
|
||||
|
||||
ROOT="${0:A:h}/.."
|
||||
DEST="$HOME/Library/Application Support/Frame Control/compat-db/backups"
|
||||
DRIVE_FOLDER_ID=${DRIVE_FOLDER_ID:-<drive-folder-id>}
|
||||
GOG_WRAPPER=${GOG_WRAPPER:-$HOME/bin/gog-with-keyring.sh}
|
||||
DRIVE_FOLDER_ID=${DRIVE_FOLDER_ID:-}
|
||||
GOG_WRAPPER=${GOG_WRAPPER:-$(command -v gog || true)}
|
||||
upload=1 force=0 accept_shrink=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
@@ -60,9 +60,10 @@ if (( upload )); then
|
||||
print "==> Unchanged since the last Drive upload; skipped"
|
||||
exit 0
|
||||
fi
|
||||
[[ -x "$GOG_WRAPPER" ]] || { print -u2 "gog wrapper not found at $GOG_WRAPPER"; exit 1; }
|
||||
[[ -n "$DRIVE_FOLDER_ID" ]] || { print -u2 "Set DRIVE_FOLDER_ID, or pass --no-upload"; exit 1; }
|
||||
[[ -n "$GOG_WRAPPER" && -x "$GOG_WRAPPER" ]] || { print -u2 "gog not found; install it or set GOG_WRAPPER"; exit 1; }
|
||||
"$GOG_WRAPPER" drive upload "$out" --parent "$DRIVE_FOLDER_ID" --json --no-input >/dev/null
|
||||
"$GOG_WRAPPER" drive upload "$out.sha256" --parent "$DRIVE_FOLDER_ID" --json --no-input >/dev/null
|
||||
print -r -- "$digest" > "$last"
|
||||
print "==> Uploaded to Google Drive (the backup folder)"
|
||||
print "==> Uploaded to Google Drive"
|
||||
fi
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Compatibility reports without the maintainer's key: saved locally, never sent.
|
||||
|
||||
Run: python3 -m unittest discover -s tests
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "ui"))
|
||||
|
||||
import frame_compat_db as db # noqa: E402
|
||||
|
||||
|
||||
class NoKey(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
state = tmp.name
|
||||
for name, value in (("STATE", state), ("OUTBOX", os.path.join(state, "outbox.jsonl")),
|
||||
("MIRROR", os.path.join(state, "mirror.json"))):
|
||||
p = mock.patch.object(db, name, value)
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
db._mem.update(at=0, reports=None, source=None)
|
||||
env = mock.patch.dict(os.environ, {}, clear=False)
|
||||
env.start()
|
||||
self.addCleanup(env.stop)
|
||||
os.environ.pop("FRAME_CONTROL_KEY", None)
|
||||
# No Keychain entry, and any network use fails the test.
|
||||
no_key = mock.patch.object(db.subprocess, "run",
|
||||
return_value=mock.Mock(returncode=44, stdout=""))
|
||||
no_key.start()
|
||||
self.addCleanup(no_key.stop)
|
||||
net = mock.patch.object(db._opener, "open", side_effect=AssertionError("network used"))
|
||||
net.start()
|
||||
self.addCleanup(net.stop)
|
||||
|
||||
def test_report_is_kept_locally(self):
|
||||
self.assertFalse(db.shared())
|
||||
r = db.add({"package": "org.example.app", "date": "2026-09-26T10:00:00", "rating": "works"})
|
||||
reports = db.load()
|
||||
self.assertEqual([x["id"] for x in reports], [r["id"]])
|
||||
self.assertEqual(db._mem["source"], "mirror")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+15
-2
@@ -40,6 +40,16 @@ def key():
|
||||
return p.stdout.strip()
|
||||
|
||||
|
||||
def shared():
|
||||
"""Whether reports reach the shared database. Without the key (anyone but the
|
||||
maintainer), reports stay in this Mac's outbox and ratings come from the catalogue."""
|
||||
try:
|
||||
key()
|
||||
return True
|
||||
except DBError:
|
||||
return False
|
||||
|
||||
|
||||
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
"""Never follow redirects: urllib would copy the key header to the new host."""
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
@@ -169,6 +179,8 @@ def load():
|
||||
now = time.time()
|
||||
if _mem['reports'] is None or now - _mem['at'] > TTL:
|
||||
try:
|
||||
if not shared():
|
||||
raise DBError('no key')
|
||||
try:
|
||||
flush()
|
||||
except Exception:
|
||||
@@ -196,8 +208,9 @@ def add(report):
|
||||
with _lock, open(OUTBOX, 'a') as f:
|
||||
f.write(json.dumps(r, ensure_ascii=False) + '\n')
|
||||
try:
|
||||
flush()
|
||||
_mem['at'] = 0 # refetch on next load
|
||||
if shared():
|
||||
flush()
|
||||
_mem['at'] = 0 # refetch on next load
|
||||
except Exception:
|
||||
pass # stays queued; load() shows it and a later call sends it
|
||||
return r
|
||||
|
||||
+7
-4
@@ -434,8 +434,8 @@
|
||||
<div class="panel">
|
||||
<div class="shelf-head"><h2>Recent reports</h2><span class="count" id="repCount"></span></div>
|
||||
<div class="list" id="repList"><div class="sub">Loading…</div></div>
|
||||
<div class="hint">Reports go to Frame Control's private compatibility database and change the
|
||||
verdicts in the catalogue. Any APK can be reported, including ones not on F-Droid.</div>
|
||||
<div class="hint" id="repHint">Reports change the verdicts in the catalogue. Any APK can be
|
||||
reported, including ones not on F-Droid.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
@@ -1456,9 +1456,12 @@ const RLABEL = { works: "Works", issues: "Problems", broken: "Doesn't work", run
|
||||
crashes: "Crashed (test)", install_failed: "Won't install", instance_failed: "Didn't start (test)" };
|
||||
const RCLASS = { works: "works", runs: "works", issues: "maybe" };
|
||||
async function loadReports() {
|
||||
let reps;
|
||||
try { reps = (await api("/api/android/reports")).reports; }
|
||||
let reps, shared;
|
||||
try { ({ reports: reps, shared } = await api("/api/android/reports")); }
|
||||
catch (e) { $("repList").innerHTML = `<div class="sub">${esc(e.message)}</div>`; return; }
|
||||
$("repHint").textContent = shared
|
||||
? "Reports go to Frame Control's shared compatibility database and change the verdicts in the catalogue. Any APK can be reported, including ones not on F-Droid."
|
||||
: "Reports are saved on this Mac and change the verdicts you see. They aren't uploaded: the shared database is maintainer-only for now. Any APK can be reported, including ones not on F-Droid.";
|
||||
$("repCount").textContent = reps.length ? `${reps.length} newest` : "";
|
||||
$("repList").innerHTML = reps.length ? reps.slice(0, 40).map(r => {
|
||||
const k = r.rating || r.result;
|
||||
|
||||
+4
-2
@@ -438,7 +438,8 @@ def android(body):
|
||||
runtime=body.get("runtime") or "instance",
|
||||
label=body.get("label"), source=body.get("source"))
|
||||
name = r.get("label") or pkg
|
||||
return {"message": f"Saved your report for {name}", "report": r}
|
||||
where = "" if frame_catalog.compat_db.shared() else " on this Mac"
|
||||
return {"message": f"Saved your report for {name}{where}", "report": r}
|
||||
except frame_android.FrameError as e:
|
||||
raise Failure(str(e))
|
||||
raise Failure("unknown action", 400)
|
||||
@@ -793,7 +794,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif path == "/api/android/displays":
|
||||
self.send_json(android_displays())
|
||||
elif path == "/api/android/reports":
|
||||
self.send_json({"reports": frame_catalog.recent_reports()})
|
||||
self.send_json({"reports": frame_catalog.recent_reports(),
|
||||
"shared": frame_catalog.compat_db.shared()})
|
||||
elif path == "/api/android/catalog":
|
||||
self.send_json({"apps": frame_catalog.catalog()})
|
||||
elif path == "/api/status":
|
||||
|
||||
Reference in new issue
Block a user