mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 08:01:53 +01:00
Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68c5584a9 | ||
|
|
b5d50a8d89 | ||
|
|
0c9bed1254 | ||
|
|
741735aa98 | ||
|
|
4133ea1c7d | ||
|
|
3aa510a0e0 | ||
|
|
637813024f | ||
|
|
c87e5c6e8a | ||
|
|
c9777851c9 | ||
|
|
af03081f27 | ||
|
|
ef99d4953b | ||
|
|
17a7b0b80e | ||
|
|
c38e7e8da4 | ||
|
|
fc00878a23 | ||
|
|
657b4449f7 | ||
|
|
30a65facde | ||
|
|
9135f355f4 | ||
|
|
784118b71e | ||
|
|
e0115f356d | ||
|
|
56bde24be1 | ||
|
|
59c270a7ad | ||
|
|
a4d8073e31 | ||
|
|
c0275b483a | ||
|
|
ff49b44350 | ||
|
|
7e6fb7e342 | ||
|
|
57c1c4564b | ||
|
|
9aa26a8322 | ||
|
|
38c388f3f3 | ||
|
|
27e3c42ed0 | ||
|
|
c2be12fa75 | ||
|
|
c164606fe3 | ||
|
|
8d9d4807c3 | ||
|
|
7f8a743cb5 | ||
|
|
40df801cdb | ||
|
|
35d53e6010 | ||
|
|
1b02d7f558 | ||
|
|
edfa48d1f5 | ||
|
|
c89ddffcbd | ||
|
|
5183f30068 | ||
|
|
37e2faacc7 | ||
|
|
b0c4647b85 | ||
|
|
ff3b0dd6f9 | ||
|
|
5c9ca110ed | ||
|
|
e3540cab5e | ||
|
|
0eaa9f2088 | ||
|
|
59f7348aa5 | ||
|
|
67a95d4f4c |
No files matched your search
@@ -128,6 +128,7 @@ Choose the operating system and processor of the computer you will use.
|
||||
| | Download | Needs |
|
||||
|---|---|---|
|
||||
| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-mac-arm64.dmg) | Nothing extra |
|
||||
| **macOS** (Intel, v0.4.39+) | [Frame-Control-mac-x64.dmg](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-mac-x64.dmg) | Nothing extra |
|
||||
| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-win-x64.zip) | OpenSSH Client (normally installed) |
|
||||
| **Linux** (x64) | [AppImage](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-amd64.deb) | `ssh` (most desktops have it) |
|
||||
| **Linux** (arm64) | [AppImage](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/frame-control/releases/latest/download/Frame-Control-linux-arm64.deb) | `ssh`, and `adb` for Android apps (`sudo apt install adb`) |
|
||||
@@ -150,8 +151,8 @@ The [source](https://github.com/saphid/frame-control) and [release build workflo
|
||||
|
||||
日本語: [5 ステップの接続ガイド](docs/connection-faq.md#日本語-5-ステップで接続)を参照してください。v0.4.26 以降は公式リリースの SHA256SUMS とファイルの SHA-256 値を照合してください。Windows は未署名、macOS は開発者 ID による署名・公証がありません。
|
||||
|
||||
**Which build?** On a Mac, check Apple menu → About This Mac: the download is
|
||||
for Apple Silicon (M-series); no Intel Mac build is published. On Windows,
|
||||
**Which build?** On a Mac, check Apple menu → About This Mac: use `mac-arm64`
|
||||
for Apple Silicon (M-series), or `mac-x64` for Intel (available from v0.4.39). On Windows,
|
||||
check Settings → System → About → System type: the release is x64, not a native
|
||||
Windows ARM build. On Linux, `uname -m` reports `x86_64` (x64) or `aarch64`
|
||||
(arm64). Use `.deb` on Debian/Ubuntu, or AppImage on other distributions.
|
||||
@@ -178,20 +179,25 @@ notarization. The DMG is unsigned. First verify the download against the
|
||||
`SHA256SUMS` from the same release, then open the DMG and drag **Frame Control**
|
||||
to **Applications**.
|
||||
|
||||
Check your processor before opening: v0.4.36 and earlier publish only
|
||||
Check your processor before opening: v0.4.38 and earlier publish only
|
||||
`mac-arm64` for Apple Silicon. On an Intel Mac, this build cannot run;
|
||||
removing quarantine or installing Rosetta does not make an ARM app run on Intel.
|
||||
On a real Intel MacBook Air with macOS 15.7.7, opening v0.4.35 and v0.4.36
|
||||
reported: **“The application cannot be opened because it has an incorrect
|
||||
executable format.”** This is an architecture failure, not an observed
|
||||
Gatekeeper warning. See the [real-Mac test record](docs/evidence/mac-desktop-2026-10-26.md).
|
||||
Gatekeeper warning. The official **v0.4.39 Intel DMG** was then verified,
|
||||
installed and launched on that Air. Both CLI and T3 browser downloads lacked
|
||||
quarantine metadata; no Gatekeeper dialog appeared, while `spctl` rejected the
|
||||
ad hoc signed app. This does not verify Safari/Chrome warnings or Open Anyway.
|
||||
See the [released Intel test](docs/evidence/mac-v0439-2026-10-26.md) and
|
||||
[earlier architecture failure](docs/evidence/mac-desktop-2026-10-26.md).
|
||||
|
||||
On a compatible Mac, if macOS blocks an unverified developer, try opening the
|
||||
app once, then go to **System Settings → Privacy & Security → Open Anyway**
|
||||
and confirm **Open**. Only do this for a download you verified and trust.
|
||||
[Apple's opening instructions](https://support.apple.com/102445) describe the
|
||||
current security controls. The exact warning and this override flow have not
|
||||
yet been verified here on Apple Silicon.
|
||||
yet been verified here on either Mac architecture.
|
||||
|
||||
If a trusted, verified copy still reports “damaged” and offers no Open Anyway
|
||||
option, the Terminal fallback removes only this app's download quarantine
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.39",
|
||||
"version": "0.4.41",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.39",
|
||||
"version": "0.4.41",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.39",
|
||||
"version": "0.4.41",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# What we have tested
|
||||
|
||||
Current for Frame Control **v0.4.39**, 26 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
Release baseline: Frame Control **v0.4.41**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
|
||||
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|
||||
|---|---|---|---|---|
|
||||
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): v0.4.35/v0.4.36 DMGs verified and installed; both fail to launch because they are ARM64-only. Separate Intel candidate launched: UI, native preference read/write (restored), bundled Python logs, latest-version check and offline OBS Start/Stop checked; released UI browsed 116 Decky store entries | No | Candidate results are not released-build acceptance. Native Open at Login state, Gatekeeper override, real OBS/Frame stream, native settings across restart, headset transfer and wearer interaction remain unverified | 26 Oct, v0.4.35/v0.4.36 and Intel candidate `6ea9eb2`: [real-Mac evidence](evidence/mac-desktop-2026-10-26.md) |
|
||||
| Core title smoke test | Linux lxso2 installed and removed reserved native/Proton titles on the real Frame; all 13 steps passed. ARM64 and Windows titles launched | No | The x86_64 title could not launch because Steam Linux Runtime 4.0 was not installed. The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, v0.4.38 exact tag `d87b863`, Frame OS 0.5.6 build `20261022.6153909`: [latest release smoke](evidence/v0438-release-smoke-2026-10-26.md); [earlier detailed results](evidence/round2-smoke-2026-10-25.json) |
|
||||
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): official v0.4.39 x64 DMG verified, installed and launched. Native UI, preference persistence across restart (original file restored), bundled Python logs, update check, offline OBS Start/Stop and 116-entry Decky store checked. A 40-byte relay upload/readback/delete passed under coordinator/headset locks | No | No quarantined Safari/Chrome download or Gatekeeper override test; native Open at Login/menu automation unavailable. Direct frame.local had no route and the app refused a stale SSH identity. Relay transfer is not native Mac transfer acceptance. Real OBS/Framey VR unverified. Quit exposed a server cleanup race; fix #284 shipped in v0.4.40 with physical-Mac source regression coverage, but the tagged Intel installer was not launched on physical Intel hardware | 26 Oct, official v0.4.39 tag `fd0b0a9`: [Intel release evidence](evidence/mac-v0439-2026-10-26.md); [earlier ARM64 failure](evidence/mac-desktop-2026-10-26.md) |
|
||||
| Core title smoke test | Linux lxso2 ran the exact v0.4.41 tagged source against the real Frame; 13/13 reserved title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Optional Steam Linux Runtime 4 was absent, so the x86_64 test executable did not actually launch. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.41 tag `b5d50a8`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0441-release-2026-10-26.md); [earlier candidate smoke](evidence/v0441-candidate-frame-2026-10-26.md) |
|
||||
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
|
||||
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
|
||||
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
|
||||
| OBS headset source on Linux | Real Frame to OBS 30.0.2 on a Linux host: 187.367 s, 720p30 H.264/AAC stereo, 5,621 frames, no encoder skips; room passthrough appeared in the recording | No | Synthetic tone was measured, not real game audio or audible listening. Default audio led by about 0.43 s in this setup; a measured +480 ms OBS offset removed residual skew at test resolution. Source-image latency was 1.431 s in one trial; other hosts and long sessions are unverified | 25 Oct, v0.4.30 package plus PR #219 runtime: [validation](evidence/obs-validation-2026-10-25.md) |
|
||||
| OBS headset source on Windows | No physical Windows PC test | Windows 11 VM helper fixture passed, but a native bridge to the real Frame sent no packets | In-app OBS controls remain hidden; native Windows OBS and firewall path are unverified | 25 Oct, PR #219: [validation](evidence/obs-validation-2026-10-25.md), [OBS guide](obs-headset-source.md) |
|
||||
| OBS headset source on macOS | No | No | Helper and OBS Media Source not tested on a Mac with a Frame | 25 Oct, v0.4.22: [OBS PR #174](https://github.com/saphid/frame-control/pull/174), [OBS guide](obs-headset-source.md) |
|
||||
| OBS headset source on macOS | Physical Intel Air: official v0.4.39 native Start/Stop returned to idle offline | No | Real headset stream, OBS receiver and audio/video remain unverified; the app rejected its stale SSH identity | 26 Oct, v0.4.39: [Mac evidence](evidence/mac-v0439-2026-10-26.md), [OBS guide](obs-headset-source.md) |
|
||||
| OBS headset source on a SteamOS / Steam Machine PC | No SteamOS PC or Flatpak OBS runtime test | No SteamOS host VM test | v0.4.27 AppImage launched on Ubuntu x86_64 under Xvfb; the helper sent H.264/AAC from a real Frame to a local UDP receiver. SteamOS read-only root, desktop UI and Flatpak OBS remain unverified | 25 Oct, v0.4.27: [host test](evidence/obs-steamos-host-2026-10-25.md), [OBS guide](obs-headset-source.md#steamos--steam-machine-pc-host) |
|
||||
| Wired connection doctor | No physical PC/Frame cable test of the doctor | No | Synthetic route/SSH tests; a separate Mac USB-C address observation does not test the doctor or game stream | 25 Oct, v0.4.23: [PR #178](https://github.com/saphid/frame-control/pull/178), [wired guide](wired-connection.md) |
|
||||
| Windows Wireless Adapter doctor | No physical Steam Wireless Adapter/dropout test | Windows 11 VM ran read-only checks without the adapter or WLAN | Adapter identification, dropouts and recovery on hardware | 24–25 Oct, v0.4.20–0.4.25: [VM log](evidence/windows-wireless-2026-10-24.md), [doctor guide](windows-wireless-doctor.md), [comparison PR #184](https://github.com/saphid/frame-control/pull/184) |
|
||||
@@ -23,8 +23,8 @@ Current for Frame Control **v0.4.39**, 26 October 2026. “Real Frame + physical
|
||||
| Passthrough in recordings | Real room pixels in OBS and a production MP4 after safe SteamVR wake | No | **Yes** for the checked 0.5.6 mirror. Raw cameras are not opened. Other games/modes and Steam clips may differ | 25 Oct: [validation](evidence/obs-validation-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
|
||||
| Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
|
||||
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.39: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [new standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. The exact-version official catalog covers 116 plugins: 1 works (Notebook default flow only), 45 partial, 59 broken and 11 not applicable. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Three official audio plugins had local real-backend controls and independent virtual-audio readbacks, but their ARM64 Frame audio sessions and physical output remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.39: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; 98 local Chromium initializers were ready and 18 failed. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.39: [latest unworn sample evidence](evidence/framey-vr-standby-gate-2026-10-26.md), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.40: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [post-#292 unworn sample exercise](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.41 classifies the 116 exact-version plugins as 3 works, 48 partial, 54 broken and 11 not applicable. Notebook works for its default flow; Volume Mixer and Mixer work for tested per-app silent-stream volume control only. TabMaster is partial. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Volume Mixer and Mixer controlled a named silent stream through real ARM64 Decky with independent Pulse readbacks; Volume Boost default-sink reads matched, but real writes were denied. Audible output, Bluetooth playback, internal-node controls and worn VR remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.41: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [ARM64 audio evidence](evidence/framey-audio-arm64-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#294 unworn sample and scroll evidence](evidence/framey-scroll-probe-2026-10-26.md#post-294-exact-main-sample-check), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
@@ -0,0 +1,384 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"timeUtc": "2026-10-26T04:05:15.145624+00:00",
|
||||
"baseCommit": "56bde24",
|
||||
"frameOS": "0.5.6",
|
||||
"decky": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"scope": "Local Chromium opaque Framey panels, production SSH/CDP bridge and actual unmodified official backends in ARM64 Decky; real user audio session, silent test stream only; no worn VR",
|
||||
"plugins": [
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Volume Mixer",
|
||||
"version": "1.2.1-1",
|
||||
"sha256": "11b259ad234d0e97cfef4e6a2af3f1edfb0908d5482765af56b86b0a4601c5f3"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "Volume Boost",
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc"
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"name": "Mixer",
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106"
|
||||
}
|
||||
],
|
||||
"readOnlySession": {
|
||||
"timeUtc": "2026-10-26T04:03:54.509107+00:00",
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"id": 38,
|
||||
"method": "get_sink_inputs",
|
||||
"args": [
|
||||
{}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "1.2.1-1",
|
||||
"sha256": "11b259ad234d0e97cfef4e6a2af3f1edfb0908d5482765af56b86b0a4601c5f3",
|
||||
"result": {
|
||||
"success": true,
|
||||
"streamCount": 1,
|
||||
"volumes": [
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc",
|
||||
"result": {
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "right"
|
||||
}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc",
|
||||
"result": {
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "get_settings",
|
||||
"args": [],
|
||||
"legacy": false,
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106",
|
||||
"result": {
|
||||
"result": {
|
||||
"filters": [],
|
||||
"boostLimit": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "get_playbacks",
|
||||
"args": [],
|
||||
"legacy": false,
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106",
|
||||
"result": {
|
||||
"count": 6,
|
||||
"internalAudioNodeCount": 5,
|
||||
"steamStreamCount": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"fixedLogAbsent": true
|
||||
},
|
||||
"lockReleased": true
|
||||
},
|
||||
"liveSilentStream": {
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"source": "paplay --raw --format=s16le --rate=48000 --channels=2 --client-name=FrameySilentARM64 --stream-name=\"Framey ARM64 silent verification\" /dev/zero",
|
||||
"writes": [
|
||||
{
|
||||
"id": 38,
|
||||
"method": "set_input_volume",
|
||||
"args": [
|
||||
{
|
||||
"index": 31400,
|
||||
"volume": 0.5
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 32768,
|
||||
"value_percent": "50%",
|
||||
"db": "-18.06 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 32768,
|
||||
"value_percent": "50%",
|
||||
"db": "-18.06 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"method": "set_input_volume",
|
||||
"args": [
|
||||
{
|
||||
"index": 31400,
|
||||
"volume": 0.45
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 29491,
|
||||
"value_percent": "45%",
|
||||
"db": "-20.81 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 29491,
|
||||
"value_percent": "45%",
|
||||
"db": "-20.81 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "set_volume",
|
||||
"args": [
|
||||
199,
|
||||
37
|
||||
],
|
||||
"result": true,
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 24248,
|
||||
"value_percent": "37%",
|
||||
"db": "-25.91 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 24248,
|
||||
"value_percent": "37%",
|
||||
"db": "-25.91 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "set_volume",
|
||||
"args": [
|
||||
199,
|
||||
38
|
||||
],
|
||||
"result": true,
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 24904,
|
||||
"value_percent": "38%",
|
||||
"db": "-25.21 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 24904,
|
||||
"value_percent": "38%",
|
||||
"db": "-25.21 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumeMixerReopen": {
|
||||
"id": 38,
|
||||
"method": "get_sink_inputs",
|
||||
"args": [
|
||||
{}
|
||||
],
|
||||
"result": {
|
||||
"result": [
|
||||
{
|
||||
"name": "FrameySilentARM64",
|
||||
"index": 31400,
|
||||
"volume": 0.3800048828125
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"mixerReadAfterChange": {
|
||||
"id": 144,
|
||||
"method": "get_playbacks",
|
||||
"args": [],
|
||||
"result": [
|
||||
{
|
||||
"id": 199,
|
||||
"name": "FrameySilentARM64",
|
||||
"mediaName": "Framey ARM64 silent verification",
|
||||
"volume": 38
|
||||
}
|
||||
]
|
||||
},
|
||||
"volumeBoostReads": [
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"fixedLogAbsent": true
|
||||
},
|
||||
"lockReleased": true
|
||||
},
|
||||
"independentCleanupAudit": {
|
||||
"testPathsAbsent": true,
|
||||
"units": {
|
||||
"framey-audio-kp0z857q58-32363fd7": "MainPID=0\nLoadState=not-found\nActiveState=inactive",
|
||||
"framey-audio-kp0z857q58-c08f3f5d": "MainPID=0\nLoadState=not-found\nActiveState=inactive",
|
||||
"framey-audio-kp0z857q58-c08f3f5d-silence": "MainPID=0\nLoadState=not-found\nActiveState=inactive"
|
||||
},
|
||||
"silentStreamsAbsent": true
|
||||
},
|
||||
"limitations": [
|
||||
"Physical audible output, default-sink writes, boost above 100%, Bluetooth playback controls and worn VR unverified.",
|
||||
"Mixer also exposes internal Frame PipeWire nodes and Reset; neither was changed or invoked.",
|
||||
"Private Decky/installed-plugin outer status fixture; real RPC used production framey_panel.call_method over SSH and live Steam CEF, not a deployed headset HTTP endpoint test.",
|
||||
"Mixer filter save/restart was verified on amd64 in the earlier evidence, not repeated on ARM64."
|
||||
],
|
||||
"catalogRefresh": {
|
||||
"timeUtc": "2026-10-26T04:11:59.449535+00:00",
|
||||
"visibleCount": 116,
|
||||
"idsVersionsDigestsUnchanged": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
# Official audio plugins on the ARM64 Frame
|
||||
|
||||
Volume Mixer 1.2.1-1 (38) and Mixer 0.1.0 (144) now qualify as **works for
|
||||
per-app volume control**. Their exact official, unmodified Python backends ran
|
||||
in published Decky 3.3.0 ARM64 on Frame OS 0.5.6. Local Chromium rendered the
|
||||
actual Framey panels and changed only a deliberately created silent playback
|
||||
stream in the Frame's real user audio session. Independent Pulse readbacks
|
||||
confirmed the changes. This is useful backend behavior, not initializer-only
|
||||
or fake-backend evidence.
|
||||
|
||||
Volume Boost 0.0.7 (58) remains **partial**. Its real ARM64 default-sink reads
|
||||
matched independent `pactl` reads, and its controls rendered. Default-sink writes
|
||||
were denied throughout the real-Frame run. Its earlier slider/numeric write
|
||||
checks used an isolated amd64 virtual sink.
|
||||
|
||||
[Digest-pinned observations and cleanup](framey-audio-arm64-2026-10-26.json)
|
||||
record both runs. No physical audible output or worn-VR result is claimed.
|
||||
|
||||
| Plugin | Real Frame result | Remaining limits |
|
||||
| --- | --- | --- |
|
||||
| Volume Mixer | `pulsectl`/`ravel` import; real Pulse session discovery; silent-stream slider click 100 → 50%, ArrowDown → 45%; reopen reads back the 38% subsequently set by Mixer | Bluetooth discovery/playback commands, audible output and worn-VR input unverified. The range input rounds the reopened 0.38000488 backend value to its configured step; no extra write occurred. |
|
||||
| Volume Boost | `pactl` default sink and stereo channel parsing; both backend reads return 0 and agree with independent 0% / 0% sink reads; local panel shows 0 | No real default-sink writes or boosts; no audible output or worn-VR result. Zero alone is not proof: upstream returns 0 when its command fails. |
|
||||
| Mixer | `pw-dump`/`wpctl` available; real playbacks/settings reads; exact silent-stream numeric input 37%, ArrowUp → 38%, independently read back | Exposes internal Frame PipeWire nodes as well as apps. Internal-node controls and Reset were not exercised. Filter persistence passed on amd64 earlier, not ARM64. Audible output and worn-VR input unverified. |
|
||||
|
||||
## Runtime and transport
|
||||
|
||||
The normal atomic `/tmp/frame-test.lock` was acquired separately for each run.
|
||||
An existing owner would abort testing; the lock was never stolen. The headset
|
||||
was not woken, and no power setting was changed.
|
||||
|
||||
The actual Frame UID is 1000. The legacy plugins' hard-coded
|
||||
`XDG_RUNTIME_DIR=/run/user/1000` therefore reaches this Frame's existing audio
|
||||
session. Volume Mixer's bundled pure Python wrappers successfully loaded the
|
||||
system ARM64 Pulse and D-Bus libraries. No native rebuild was needed. It also
|
||||
sets `HOME=/home/deck` inside its own plugin process; this test does not establish
|
||||
that arbitrary Deck paths work on the Frame.
|
||||
|
||||
The upstream `PluginLoader-arm64` release was verified against GitHub's
|
||||
published SHA-256 and ELF machine 183 before transfer. Two private, bounded,
|
||||
removable user-service installs used their own Decky plugins/settings bases.
|
||||
Neither service was enabled. The normal managed Decky service was already
|
||||
failed, and the normal headset-panel service inactive; both states remained
|
||||
unchanged. A pre-existing `/tmp/VolumeMixer-decky.log` would have aborted the
|
||||
run because the upstream plugin truncates that fixed path during import.
|
||||
|
||||
The local relay served actual `ui/headset.html`, vendored React, `framey-host.js`
|
||||
and digest-checked official frontend bytes in an opaque `allow-scripts` iframe.
|
||||
Only outer status/install responses were fixtures. Plugin RPC used production
|
||||
`framey_panel.call_method`, SSH through lxso2, `frame_steam.Page`, the live Steam
|
||||
CEF `DeckyBackend.call`, and actual Decky plugin subprocesses. Private install
|
||||
paths were redirected to the temporary base. No second Decky websocket client
|
||||
was attached. This does not verify the deployed headset HTTP endpoint or a VR
|
||||
compositor interaction.
|
||||
|
||||
## Safe control check
|
||||
|
||||
The first run was read-only. Independent `pactl info`, default-sink volume and
|
||||
`pw-dump` commands exited successfully, so empty/zero fallback values were not
|
||||
accepted on their own. Volume Mixer saw a live Steam stream; Mixer also saw
|
||||
Frame spatialization, echo-cancellation and ALSA internal nodes.
|
||||
|
||||
For the second run, a separate task-owned transient user service ran:
|
||||
|
||||
```sh
|
||||
paplay --raw --format=s16le --rate=48000 --channels=2 \
|
||||
--client-name=FrameySilentARM64 \
|
||||
--stream-name='Framey ARM64 silent verification' /dev/zero
|
||||
```
|
||||
|
||||
This sends zero-valued samples to the existing user server. It is an explicit
|
||||
silent client, not the earlier private null-server fixture. No server, device,
|
||||
default-route or physical-sink configuration was changed. The relay resolved
|
||||
its live stream IDs before every write and allowed only this exact application
|
||||
name, bounded to 80% volume. The spatialization node with a similar prefix was
|
||||
excluded. All Volume Boost writes, Mixer Reset/settings writes and Bluetooth
|
||||
commands were denied. Existing streams and internal nodes were never write
|
||||
targets. This broader real-session check supersedes the earlier suggested
|
||||
null-server-only ARM64 follow-up; that older fixture could not prove access to
|
||||
the Frame's real session.
|
||||
|
||||
Every browser-triggered volume RPC was followed by independent
|
||||
`pactl --format=json list sink-inputs` channel readback. Only the named silent
|
||||
stream's rows are published. Background streams can appear/disappear naturally;
|
||||
no whole-session stability claim is made.
|
||||
|
||||
## Cleanup and catalog interpretation
|
||||
|
||||
Both runs removed their private Decky units/bases and the test-created fixed
|
||||
plugin log. The live run also stopped its transient silent client. Decky's main
|
||||
process exited during shutdown but left children waiting; those remaining
|
||||
processes were killed only within the test service's systemd cgroup before
|
||||
cleanup completed. Both locks were released after cleanup. An independent
|
||||
fresh-lock audit confirmed all three test units unloaded/inactive with PID 0,
|
||||
private paths absent, and no named silent stream. Battery was 100 → 100 and
|
||||
independent default-sink reads stayed 0% / 0% in both runs.
|
||||
|
||||
The canonical store data and generated compatibility table now report
|
||||
**3 works / 48 partial / 54 broken / 11 N/A**. “Works” describes the tested
|
||||
per-app control behavior; read each row's remaining limits. Historical
|
||||
[fake-Frame archive checks](framey-module-lookup-2026-10-26.md) cover transport
|
||||
with plugin execution stubbed, and the
|
||||
[earlier local audio checks](framey-audio-backends-2026-10-26.md) cover isolated
|
||||
amd64 controls/filter persistence. Neither is relabelled as a real ARM64 test.
|
||||
|
||||
## Repository checks
|
||||
|
||||
The full Python suite passed 1,621 tests (16 skips), and the focused Framey
|
||||
suite passed all 43 tests. `scripts/framey-compatibility-doc.py --check` and
|
||||
`git diff --check` passed. A fresh official-store fetch at 04:11:59 UTC found
|
||||
all 116 IDs, versions and digests unchanged. This PR changes catalog/evidence
|
||||
and generated documentation; it adds no backend shim or plugin patch.
|
||||
@@ -1,5 +1,9 @@
|
||||
# Official audio plugins: local real-backend checks
|
||||
|
||||
Historical amd64 run. The [later ARM64 Frame check](framey-audio-arm64-2026-10-26.md)
|
||||
supersedes the ARM64 gaps and follow-up plan below; it promotes the two per-app
|
||||
mixers to works for their verified scope. Volume Boost remains partial.
|
||||
|
||||
Volume Mixer 1.2.1-1 (38), Volume Boost 0.0.7 (58) and Mixer 0.1.0 (144)
|
||||
passed useful audio controls in local Chromium with their **unmodified official
|
||||
Python backends running in actual Decky 3.3.0**. All three remain **partial**:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"dateUtc": "2026-10-26",
|
||||
"plugin": {
|
||||
"storeId": 135,
|
||||
"name": "Deck Shelves",
|
||||
"version": "2.0.0",
|
||||
"sha256": "b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91"
|
||||
},
|
||||
"mode": "opaque allow-scripts iframe in local Chromium, exact archived frontend, backend and network denied",
|
||||
"baselineError": "relative JavaScript chunk imports cannot resolve from a blob URL",
|
||||
"result": {
|
||||
"initializerReady": true,
|
||||
"error": null,
|
||||
"moduleResponses200": 9,
|
||||
"settingsVisible": [
|
||||
"Enable shelves on Home",
|
||||
"Create default shelves",
|
||||
"Behavior",
|
||||
"Shelves"
|
||||
],
|
||||
"attemptedBackendMethods": [
|
||||
"get_settings"
|
||||
],
|
||||
"backendMethodsExecuted": [],
|
||||
"steamLibraryRead": "denied by audit",
|
||||
"pluginNetwork": "denied by audit"
|
||||
},
|
||||
"limits": [
|
||||
"No Steam Home shelf injection or settings write was verified",
|
||||
"No official backend executed",
|
||||
"No official Deck Shelves interaction occurred on the real Frame or in worn VR",
|
||||
"Settings visibility alone does not prove the plugin works"
|
||||
],
|
||||
"realFrameGate": {
|
||||
"sourceCommit": "b0c4647b856035c970ae27ea4e2ddbf6e51f6963",
|
||||
"headsetOsBuild": "20261022.6153909",
|
||||
"preflightActivity": 3,
|
||||
"preflightBatteryPercent": 100,
|
||||
"postBatteryPercent": 99,
|
||||
"sampleExercisePassed": true,
|
||||
"sampleSettingPersistedAfterFreshIframe": true,
|
||||
"syntheticOpenVrPointerTrusted": true,
|
||||
"stereoPixelsChanged": true,
|
||||
"mirrorCapturePresent": true,
|
||||
"nativeScrollMoved": false,
|
||||
"powerValuesRestored": true,
|
||||
"recoveryTimerRemoved": true,
|
||||
"lockReleased": true,
|
||||
"cleanupErrors": [],
|
||||
"genericFrameSmokePassed": 13,
|
||||
"genericFrameSmokeFailed": 0,
|
||||
"officialDeckShelvesBackendOrVrTested": false,
|
||||
"physicalControllerVerified": false,
|
||||
"wornVisualComfortVerified": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Deck Shelves chunked frontend, 26 October 2026
|
||||
|
||||
The exact official Deck Shelves 2.0.0 archive (store ID 135, SHA-256 `b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`) contains `dist/index.js` and eight JavaScript chunks. Its API v1 `connect(1, "Deck Shelves")` bridge sits in `chunks/decky-api-WgbNuFYS.js`, not in the entry file; the original static scanner therefore reported its frontend API as unknown. Framey's prior blob import failed before initialization because a blob URL has no directory in which to resolve `./chunks/...`. The [numeric observation](framey-deck-shelves-browser-2026-10-26.json) and [local Chromium capture](framey-deck-shelves-browser-2026-10-26.png) record a targeted follow-up using the unchanged official frontend.
|
||||
|
||||
Framey now serves JavaScript files from the managed plugin's `dist` directory using hierarchical module URLs. Its opaque `allow-scripts` iframe still has no Frame UI key. Local Chromium received HTTP 200 for the entry and eight chunks, reported the initializer ready, and rendered the settings panel with “Enable shelves on Home,” “Create default shelves,” “Behavior,” and “Shelves.” The browser audit denied the attempted `get_settings` backend call, Steam library read and plugin network access. No backend method executed.
|
||||
|
||||
The catalog moves this exact version from broken to **partial** for visible local settings only. The plugin's main feature injects shelves into Steam Home; Framey has no verified Steam Home patch target. Settings writes, official backend behavior, real Frame module serving, wearer-visible VR, and physical controller input remain unverified. The [compatibility table](../framey-compatibility.md) and store views retain these limits.
|
||||
|
||||
## Exact source real Frame gates
|
||||
|
||||
The exact source commit `b0c4647b856035c970ae27ea4e2ddbf6e51f6963` ran the unattended `scripts/framey-vr-check.py --exercise` on SteamOS build `20261022.6153909` under its unique shared-lock owner. Preflight found SteamVR activity 3 (standby), battery 100%, and proximity 6.25. The repository sample panel appeared, stereo compositor pixels changed, and `/dev/video99` supplied a mirror frame. Synthetic OpenVR pointer input changed the sample setting through actual Decky; the value survived a fresh iframe reopen. Native scroll did not move. The runner removed its temporary services and recovery timer, read back the original `false`/`5` power values, found battery 99%, reported no cleanup errors, and released its lock.
|
||||
|
||||
The same source then passed `scripts/frame-smoke.sh` **13/13** on the real Frame, starting from standby at battery 99%. Its final cleanup step passed. The [sanitized result fields](framey-deck-shelves-browser-2026-10-26.json) omit room pixels, raw overlays, identifiers, and settings contents; private captures were not committed. These gates exercise the Framey repository sample and generic install/launch/remove paths. They do **not** exercise Deck Shelves' official ARM64 backend or prove its settings or shelves in VR. No person wore the headset and no physical controller was checked.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"timeUtc": "2026-10-26T03:29:05.328492+00:00",
|
||||
"baseCommit": "c89ddff",
|
||||
"scope": "Finite Framey-local DFL component/CSS registry and explicit unsupported optional UI-audio export",
|
||||
"upstreamReferences": [
|
||||
"https://github.com/SteamDeckHomebrew/decky-frontend-lib/blob/main/src/webpack.ts",
|
||||
"https://github.com/SteamDeckHomebrew/decky-frontend-lib/blob/main/src/class-mapper.ts"
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Audio Loader",
|
||||
"version": "1.6.1",
|
||||
"sha256": "fd5a090c2e2cd4da6723d7bd08937ab31b9f8d4663510431713da531343e303f",
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel.",
|
||||
"browser": {
|
||||
"id": 16,
|
||||
"name": "Audio Loader",
|
||||
"ready": false,
|
||||
"error": "Cannot read properties of undefined (reading 'm_AudioPlaybackManager')",
|
||||
"logs": [
|
||||
{
|
||||
"kind": "warn",
|
||||
"message": "Resolve failed: [object Object] promise [object Promise]"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
"store_read"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "SteamGridDB",
|
||||
"version": "1.8.1",
|
||||
"sha256": "e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0",
|
||||
"status": "broken",
|
||||
"reason": "Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel.",
|
||||
"browser": {
|
||||
"id": 36,
|
||||
"name": "SteamGridDB",
|
||||
"ready": false,
|
||||
"error": "Cannot convert undefined or null to object",
|
||||
"logs": [],
|
||||
"methods": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "TabMaster",
|
||||
"version": "2.15.1",
|
||||
"sha256": "e08333859a32b0c177b8e9a7959c5eeeafe38f946578f7f1d161c721f0e05da5",
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified.",
|
||||
"browser": {
|
||||
"id": 61,
|
||||
"name": "TabMaster",
|
||||
"ready": true,
|
||||
"error": null,
|
||||
"logs": [
|
||||
{
|
||||
"kind": "error",
|
||||
"message": "%c TabMaster %c ERROR %c background: #ff6d05; color: black; background: #c70808; color: black; background: transparent; Could not find mobx.reaction"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
"log_message"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 84,
|
||||
"name": "Junk-Store",
|
||||
"version": "1.1.13",
|
||||
"sha256": "f7ad2a0103c85bf308929b2e96ec9aeeb0d76bcc6ae97eb1e4b5f31ffba0cbb1",
|
||||
"status": "broken",
|
||||
"reason": "Local CSS lookup fixes the loading view crash. Actual Decky 3.3.0 amd64 startup/static InitActions return a script execution/JSON error; the published scripts are not executable after extraction. Store install/login/game launch and ARM64 remain unverified.",
|
||||
"browser": {
|
||||
"id": 84,
|
||||
"name": "Junk-Store",
|
||||
"ready": true,
|
||||
"error": null,
|
||||
"logs": [],
|
||||
"methods": [
|
||||
"fetch_rss_feed",
|
||||
"execute_action"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "DeckSP",
|
||||
"version": "1.1.3",
|
||||
"sha256": "d6cbedcd64430e1f94a2b12caf936ba270f1ceafe77c6252808ff39c88640b40",
|
||||
"status": "broken",
|
||||
"reason": "Optional UI-audio lookup succeeds, but the published frontend raises \"Cannot access PluginManager before initialization\" when Steam MobX reaction lookup is unavailable. JamesDSP installation/audio routing and ARM64 remain unverified.",
|
||||
"browser": {
|
||||
"id": 102,
|
||||
"name": "DeckSP",
|
||||
"ready": false,
|
||||
"error": "Cannot access 'PluginManager' before initialization",
|
||||
"logs": [],
|
||||
"methods": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"fullCatalogFrontend": {
|
||||
"attempted": 116,
|
||||
"ready": 102,
|
||||
"failed": 14,
|
||||
"previousReady": 101,
|
||||
"newlyReady": [
|
||||
61
|
||||
],
|
||||
"regressions": [],
|
||||
"backendAndNetworkDenied": true,
|
||||
"library": "Private detached real Frame snapshot replay, not live Steam or backend verification"
|
||||
},
|
||||
"focusedBrowser": {
|
||||
"host": "Actual ui/headset.html, React 18.3.1, opaque allow-scripts iframe, exact official bundles",
|
||||
"installedPluginAndStatusResponses": "Fixture-shaped; no headset HTTP endpoint exercised",
|
||||
"backend": "Actual official Decky 3.3.0 amd64 in unprivileged network-none container; no host audio/device/data mounts",
|
||||
"deckySha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
|
||||
"allowedMethods": {
|
||||
"TabMaster": [
|
||||
"log_message",
|
||||
"get_setting",
|
||||
"get_users_dict",
|
||||
"get_tags"
|
||||
],
|
||||
"Junk-Store": [
|
||||
"execute_action only init/InitActions"
|
||||
]
|
||||
},
|
||||
"checks": [
|
||||
"TabMaster QAM renders; library editing controls remain disabled",
|
||||
"TabMaster Open Docs -> Overview renders",
|
||||
"TabMaster Filters tab renders",
|
||||
"Back to plugin returns to QAM",
|
||||
"Junk-Store static InitActions via real backend returns Type Error; not promoted"
|
||||
],
|
||||
"denied": [
|
||||
"External RSS/network",
|
||||
"Other execute_action arguments",
|
||||
"Login, installation, game launch, library patching, audio playback"
|
||||
],
|
||||
"directBackendReads": {
|
||||
"TabMaster.get_setting": "default string returned",
|
||||
"TabMaster.get_users_dict": {},
|
||||
"TabMaster.get_tags": [],
|
||||
"Junk-Store.scriptMode": "0644, Python shebang present"
|
||||
}
|
||||
},
|
||||
"headset": {
|
||||
"newPluginDeviceExecution": false,
|
||||
"lockProbe": "Normal atomic lock acquired/released for read-only dependency availability probe; no service or power changes",
|
||||
"ARM64OfficialBackend": "Not exercised in this theme",
|
||||
"wornVR": false
|
||||
},
|
||||
"verification": {
|
||||
"frameyTests": 42,
|
||||
"fullServerTests": {
|
||||
"count": 1620,
|
||||
"skipped": 16,
|
||||
"result": "OK"
|
||||
},
|
||||
"siteTestFilesPassed": 2,
|
||||
"fakeFrame": {
|
||||
"attempted": 116,
|
||||
"installedRemoved": 60,
|
||||
"expectedBlockers": 56,
|
||||
"backendExecuted": false,
|
||||
"privateProject": "framey-catalog-6456d3d939",
|
||||
"projectRemoved": true,
|
||||
"baseCommit": "c89ddff"
|
||||
}
|
||||
},
|
||||
"storeRefresh": {
|
||||
"timeUtc": "2026-10-26T03:29:46.606326+00:00",
|
||||
"currentCount": 116,
|
||||
"pinnedCount": 116,
|
||||
"unchanged": true,
|
||||
"changedIds": []
|
||||
},
|
||||
"postMainMerge": {
|
||||
"mainCommit": "38c388f",
|
||||
"mergedPRs": [
|
||||
286,
|
||||
287,
|
||||
288
|
||||
],
|
||||
"frameyTests": 43,
|
||||
"fullCatalogFrontend": {
|
||||
"attempted": 116,
|
||||
"ready": 103,
|
||||
"failed": 13,
|
||||
"additionalReadyFromUpstream": [
|
||||
135
|
||||
],
|
||||
"regressionsComparedWithPreMergeAudit": []
|
||||
},
|
||||
"catalog": {
|
||||
"works": 1,
|
||||
"partial": 50,
|
||||
"broken": 54,
|
||||
"na": 11
|
||||
},
|
||||
"fullServerTests": {
|
||||
"count": 1621,
|
||||
"skipped": 16,
|
||||
"result": "OK"
|
||||
}
|
||||
},
|
||||
"cleanup": {
|
||||
"focusedBackendContainerAbsent": true,
|
||||
"loopback18770Closed": true,
|
||||
"loopback18771Closed": true,
|
||||
"fakeProjectAndVolumesRemoved": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Framey-local module lookup: official plugin checks
|
||||
|
||||
TabMaster 2.15.1 now renders its QAM and bundled documentation in local Chromium.
|
||||
Opening its docs, selecting Overview and Filters, and returning to the panel
|
||||
passed. Its status moves from **broken to partial**: library editing controls
|
||||
remain disabled because Steam login, live MobX reactions and library-page
|
||||
patches are unavailable. This is not working library-tab customization.
|
||||
|
||||
The [observations](framey-module-lookup-2026-10-26.json) pin each tested official
|
||||
archive and the actual Decky 3.3.0 amd64 binary. The complete current official
|
||||
store was refreshed: all 116 IDs, versions and digests match the catalog.
|
||||
|
||||
| Plugin | Result | Remaining limitation |
|
||||
| --- | --- | --- |
|
||||
| TabMaster 61 | Partial: QAM and documentation navigation render | Steam library editing unavailable; ARM64 and worn VR unverified |
|
||||
| Junk-Store 84 | Loading and backend-error views render without the CSS lookup crash | Actual startup/static InitActions return an execution/JSON error; extracted scripts lack executable permission; remains broken |
|
||||
| SteamGridDB 36 | Module mapping is callable | Still needs Steam's LibraryContextMenu implementation; remains broken |
|
||||
| DeckSP 102 | Optional UI-audio lookup succeeds | Published bundle accesses PluginManager before initialization when MobX reaction is absent; remains broken |
|
||||
| Audio Loader 16 | Optional UI-audio lookup succeeds | Still requires Steam's mutable audio playback store; remains broken |
|
||||
|
||||
## What ran
|
||||
|
||||
The host exposes a finite registry of its own components and styled CSS maps,
|
||||
with DFL module/export/class lookup and mapping semantics. Unknown matches return
|
||||
undefined or an empty mapping. An optional audio export explicitly reports
|
||||
unavailable; invoking playback throws. No Steam webpack cache, function-source
|
||||
signature, audio store or library patch was manufactured.
|
||||
|
||||
The full exact-bundle browser pass ran with backend and network calls denied.
|
||||
It completed 102 initializers and failed 14, versus 101/15 before this theme.
|
||||
Initializer readiness alone does not establish rendered or useful behavior.
|
||||
After integrating main’s #286/#287/#288, all 116 were retried: 103 initializers
|
||||
completed and 13 failed. The additional completion is Deck Shelves from #286;
|
||||
no previously completed initializer regressed.
|
||||
|
||||
Focused checks used actual `ui/headset.html`, React 18.3.1 and opaque
|
||||
`allow-scripts` iframes. Status/installed-plugin responses were fixture-shaped,
|
||||
and the private detached library snapshot from the earlier Frame SSH check was
|
||||
replayed. The relay allowed only TabMaster log/settings reads and Junk-Store's
|
||||
static `init/InitActions` action. TabMaster's official backend returned the
|
||||
default setting, empty users dictionary and empty tags. Junk-Store returned a
|
||||
structured Type Error, which its panel displayed. RSS/network, login, installs,
|
||||
launch actions and other backend methods were denied.
|
||||
|
||||
The unmodified Python backends ran in actual Decky 3.3.0 inside a disposable
|
||||
unprivileged amd64 container with no network, host audio, devices or user-data
|
||||
mounts. This bypasses Steam CEF and is not a headset HTTP/RPC check. The normal
|
||||
headset lock was briefly acquired and released for read-only dependency/service
|
||||
availability; no official plugin ran there in this theme. No ARM64 or worn-VR
|
||||
result is claimed.
|
||||
|
||||
## Regression checks
|
||||
|
||||
- 1,621 server tests passed, 16 skipped; all 43 focused Framey tests passed
|
||||
after integrating the latest main.
|
||||
- Both site test files passed.
|
||||
- All 116 exact archives passed the fake-Frame audit: 60 install/remove flows
|
||||
and 56 expected blockers. Official backend execution remained stubbed.
|
||||
- The unique fake-Frame project and its volumes were removed. The focused
|
||||
backend container and both loopback helpers were removed after the checks.
|
||||
|
||||
The catalog and generated compatibility table use the same status/reasons as
|
||||
both store views. The result is **1 works / 50 partial / 54 broken / 11 N/A** (including main’s independent Deck Shelves improvement);
|
||||
the majority goal remains open.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Unworn Framey scroll transport probe, 26 October 2026
|
||||
|
||||
The previous `scripts/framey-vr-check.py --exercise` runs reported `scrollY=0` before and after one injected smooth-scroll event. That was inconclusive because the repository sample may fit inside the iframe. This probe changed only the **test script**: it created a temporary 160-pixel-high scroll region with 716 pixels of content, moved the native OpenVR pointer over it, and measured its own `scrollTop` and captured wheel events. No production Framey panel code changed.
|
||||
|
||||
Two local diagnostic versions ran on the real Frame from base main `784118b` after PR #292. The final version in this PR tried OpenVR `VREvent_ScrollSmooth` (309) and `VREvent_ScrollDiscrete` (305), using Valve's documented `VREvent_Scroll_t` ABI. The overlay listing showed the temporary Frame Control dashboard visible with `+smoothscroll`; a trusted native pointer move reached the iframe. Neither scroll event reached Chromium's capture listener and the region stayed at `scrollTop=0`. This establishes only that `IVROverlayView_003.PostOverlayEvent` did not deliver these synthetic scroll events in this run. A physical controller may follow a different path.
|
||||
|
||||
| Check | First probe | Final probe |
|
||||
| --- | --- | --- |
|
||||
| Unique shared lock and standby gate | Acquired; activity 3, battery 99% | Acquired; activity 3, battery 100% |
|
||||
| Scroll region | 160 px viewport, 716 px content | 160 px viewport, 716 px content |
|
||||
| Native pointer | Moved over region | Trusted pointer move reached iframe |
|
||||
| Injected scroll | Smooth 309 | Smooth 309, then discrete 305 |
|
||||
| Wheel events / offset | 0 / 0 → 0 | 0 / 0 → 0 |
|
||||
| Other sample checks | Passed | Passed |
|
||||
| Cleanup | Power restored to `false/5`, recovery timer removed, owned lock released; battery 99% | Same cleanup; battery 99% |
|
||||
|
||||
Both runs captured changed stereo compositor frames and a `/dev/video99` mirror frame, saved only in private folders `/tmp/framey-native-scroll-probe/` and `/tmp/framey-native-scroll-events/`. No room pixels are committed. The final run's `result.json` reported `passed=true`, `cleanupErrors=[]`, `powerRecoveryRemoved=true` and `lockReleased=true`. Here `passed` covers the sample setting, quick action, slider, text and cleanup checks; **native scroll did not pass**. The final immediate SteamVR activity was 0 after the temporary wake; original power settings were restored and read back. No person wore the headset, and physical controller scrolling, rays, haptics, keyboard, optical readability and comfort remain unverified. The hourly worn-check schedule remains enabled.
|
||||
|
||||
## Post-#294 exact-main sample check
|
||||
|
||||
After PR #294 merged, the **unmodified** `scripts/framey-vr-check.py --exercise` from exact main `c38e7e8da459e44a9a717d15e6a07ab8a8e8ef8d` exited 0 on the real Frame. Locked preflight found activity 3 standby, proximity 6.3125 and battery 99%. The sample saved `{"enabled": true}` through trusted synthetic OpenVR input and the live Decky backend, read the file independently, then reopened a fresh iframe with the value retained. Quick action, slider value 50 and text entry completed; compositor frames changed and the mirror capture returned a frame. The old scroll diagnostic still reported no offset movement.
|
||||
|
||||
The run removed its temporary services and recovery timer, restored/read back power `false/5`, reported no cleanup errors, and released only its owned lock. Its immediate activity was 0 after the temporary wake. Private captures/result are under `/tmp/framey-post294/` outside Git. This was one setting activation, so it does not prove the new rapid-double-input guard on physical controllers. No person wore the headset.
|
||||
@@ -5,6 +5,10 @@ Host: **MacBook Air (MacBookAir9,1), Intel Core i3 1.1 GHz, macOS 15.7.7
|
||||
server with display/automation restrictions. No Apple Silicon runtime result
|
||||
is inferred from this test.
|
||||
|
||||
**Follow-up:** the official v0.4.39 Intel installer later launched on this Air.
|
||||
See the [released-build test](mac-v0439-2026-10-26.md); the results below remain
|
||||
the earlier ARM64 failure and separate candidate test.
|
||||
|
||||
## Released installers: verified, installed, cannot run on Intel
|
||||
|
||||
GitHub's latest release was v0.4.35 at the start; v0.4.36 became latest during
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Official Intel Mac release test, 26 October 2026
|
||||
|
||||
Physical **MacBookAir9,1, Intel Core i3 1.1 GHz, macOS 15.7.7 (24G720)**.
|
||||
The earlier ARM64 failure led to [#262](https://github.com/saphid/frame-control/pull/262),
|
||||
now merged. This test uses the published **v0.4.39**, tag `fd0b0a9`, rather
|
||||
than the locally built candidate. [Earlier results](mac-desktop-2026-10-26.md)
|
||||
remain a separate historical record.
|
||||
|
||||
## Download, installation and Gatekeeper observations
|
||||
|
||||
Downloaded `Frame-Control-mac-x64.dmg` and its same-release `SHA256SUMS`
|
||||
with `gh release download`. `shasum -a 256 -c SHA256SUMS --ignore-missing`
|
||||
passed for the downloaded asset:
|
||||
|
||||
```text
|
||||
f47b5a188b5090f97a72467394d4fa65ccc73fe3169a6276e2a32cab5c8019d8 Frame-Control-mac-x64.dmg
|
||||
```
|
||||
|
||||
Also clicked that asset in T3's Chromium browser. Its 169,230,398-byte download
|
||||
had the same hash. **Neither download had `com.apple.quarantine`.** T3's
|
||||
browser is not evidence of Safari/Chrome download metadata.
|
||||
|
||||
Mounted the verified CLI DMG read-only, preserved the task's earlier ARM64
|
||||
installation, and copied `Frame Control.app` into `/Applications` with `ditto`
|
||||
(the command-line equivalent of dragging it there). The executable is x86_64;
|
||||
`codesign -dv` reports ad hoc signing. `open "/Applications/Frame Control.app"`
|
||||
returned success and the app started its bundled Python server.
|
||||
|
||||
**No Gatekeeper warning dialog was observed.** `spctl --assess --type execute
|
||||
-vv "/Applications/Frame Control.app"` returned exit 3 and exactly:
|
||||
|
||||
```text
|
||||
/Applications/Frame Control.app: rejected
|
||||
```
|
||||
|
||||
Opening this unquarantined copy needed no override. This does not establish
|
||||
the warning or opening steps for a quarantined browser download. The README
|
||||
keeps Apple's Open Anyway guidance, explicitly unverified here. No quarantine
|
||||
attribute was removed and no global security setting was changed. System
|
||||
Events inspection timed out; an AppleEvent quit request returned “User
|
||||
cancelled” (-128). Native console/menu automation remains unavailable.
|
||||
|
||||
## Official native app results
|
||||
|
||||
After the normal launch, a second run disabled telemetry, used a task-local
|
||||
server data directory and enabled a temporary loopback renderer debug port.
|
||||
The app bundle was not patched. The following checks used its real renderer
|
||||
and `window.frameApp` IPC bridge; they are not physical mouse/menu tests.
|
||||
|
||||
| Check | Result and limit |
|
||||
|---|---|
|
||||
| UI | Apps, Games, Tools, Settings, Privacy & updates, Capture and Decky views rendered with the expected headings. The activity log showed the stale SSH identity warning. [Native Apps screenshot](mac-v0439-native-2026-10-26.png) |
|
||||
| Settings | Initial native preferences were `{}`. `mvSeparate="0"` saved and read back, including after a real app restart. The original preferences file was restored byte-for-byte in `finally` (or removed if originally absent) |
|
||||
| Open at Login | Still opt-in in source; no login item was enabled. Actual menu checkbox and OS login behavior remain unverified because native automation was unavailable |
|
||||
| Logs | `~/Library/Logs/Frame Control/server.log` recorded startup with the installed app's `Contents/Resources/python/bin/python3` and `ui/server.py`. Show Server Log menu interaction remains unverified |
|
||||
| Update check | Native check returned current/latest `0.4.39`, status `none`, error `null`. No update installed |
|
||||
| OBS | Native Start on port 15123 returned `starting`; Stop returned `stopping`, then `idle`. This was offline: no OBS receiver or real headset audio/video |
|
||||
| Decky store | Native Load store returned 116 entries; 40 initial rows rendered. Search found ProtonDB Badges 1.3.4 and its compatibility text. Install remained disabled; no plugin/loader installed |
|
||||
| Framey preview | No standalone local Framey preview control found. No VR runtime result claimed |
|
||||
|
||||
Closing the app through `window.close()` stopped its processes and closed
|
||||
the debug port. **The final shutdown log exposed a bug:** stdin EOF and the
|
||||
app's SIGTERM can overlap, raising `KeyboardInterrupt` while cleanup is
|
||||
installing its ignore handler. Cleanup can then stop before releasing staging
|
||||
or SSH resources. [#284](https://github.com/saphid/frame-control/pull/284)
|
||||
changes SIGTERM to request HTTP shutdown without raising into cleanup.
|
||||
|
||||
The regression test injects SIGTERM at that exact cleanup boundary. On this
|
||||
physical Mac with the official app's bundled Python 3.12, it failed before
|
||||
the fix with exit -2 and the observed traceback. After the fix, all **45
|
||||
server integration tests** passed, including owned staging-file cleanup.
|
||||
This validates source from the fix PR, not a fixed published app.
|
||||
|
||||
## Headset relay and small transfer
|
||||
|
||||
`frame.local` resolved but TCP 22 returned **No route to host**. The native
|
||||
app refused a stale SSH identity; no pin was replaced. The trusted lxso2
|
||||
coordinator relay could reach the Frame. Both shared locks were absent.
|
||||
|
||||
The relay test atomically created `/tmp/frame-test.lock` on the coordinator
|
||||
and the headset, each with this job's owner marker. It then:
|
||||
|
||||
- Read battery **99%, Not charging** before and after; storage total
|
||||
**996,984,774,656 bytes**, free **769,077,035,008 bytes**.
|
||||
- Listed **7** Steam screenshot JPEGs matching
|
||||
`userdata/*/760/remote/*/screenshots/*.jpg`; no image was copied. This narrower
|
||||
layout-specific count differs from the earlier broad JPEG search.
|
||||
- Sent a **40-byte** test payload from this Mac through lxso2, created one
|
||||
reserved file in the Frame user's home using exclusive creation, and read
|
||||
it back. SHA-256 matched
|
||||
`457485db13f80d819d5301d337502c3df050c6b3eb6202be7aac85d2af8e0b92`.
|
||||
- Deleted that exact test file and released only the matching owned locks.
|
||||
A separate SSH read verified the file and both locks were absent.
|
||||
|
||||
This proves a bounded relay transfer, not native Mac app file transfer or
|
||||
direct `frame.local` connectivity. No existing headset file, app, service,
|
||||
power setting or SteamVR state was changed. The headset was not wiped or
|
||||
rebooted. Real OBS and wearer/controller interaction remain unverified.
|
||||
|
||||
The DMG was ejected; the native app, debug port and task browser tab were
|
||||
closed; the desk hold was released. The official app remains installed for
|
||||
reproduction. No social post was made; PR #113 was left alone.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 529 KiB |
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"tag": "v0.4.39",
|
||||
"tagCommit": "fd0b0a946f11d92fd1cc480cd9d1a83be795bf71",
|
||||
"releaseUrl": "https://github.com/saphid/frame-control/releases/tag/v0.4.39",
|
||||
"releaseId": 423799494,
|
||||
"releaseWorkflow": {
|
||||
"runId": 40258561639,
|
||||
"conclusion": "success",
|
||||
"jobsPassed": 6
|
||||
},
|
||||
"publishedLatest": true,
|
||||
"assets": [
|
||||
{
|
||||
"name": "Frame-Control-linux-amd64.deb",
|
||||
"size": 131444552,
|
||||
"digest": "sha256:9ca74551fe47ea244f500e1b4119c02307d185d1acea6287a02a5f6c055c674e"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.AppImage",
|
||||
"size": 156170292,
|
||||
"digest": "sha256:66bd8f644f6a0b06f1e46ff635c5b50e53d3f2a9c6736f12b737ee0ad1667335"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.deb",
|
||||
"size": 118529048,
|
||||
"digest": "sha256:f6b476a2f3163028d0862a13d0c8890760bdc8fe52c1006f106d9e540de32a6a"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-x86_64.AppImage",
|
||||
"size": 164587947,
|
||||
"digest": "sha256:240023c89bea049f0864da75f83ec235c138375c4fd1c4612953a4c5863610ca"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.dmg",
|
||||
"size": 162169626,
|
||||
"digest": "sha256:c0243317555975b1a5588640ec6fe71db5014788858ff00dd45914b77c1fe99a"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.zip",
|
||||
"size": 162054585,
|
||||
"digest": "sha256:08191f80d6c78ac8576e2e2fe7937f5c1689ebed780528e63b6fb41b3c90d823"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.dmg",
|
||||
"size": 169230398,
|
||||
"digest": "sha256:f47b5a188b5090f97a72467394d4fa65ccc73fe3169a6276e2a32cab5c8019d8"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.zip",
|
||||
"size": 169141014,
|
||||
"digest": "sha256:bafc1aec0c11c1fae097dbe1e4b585ebc13a0f8eaf1b249717c14d78811e08d7"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-Setup-x64.exe",
|
||||
"size": 136500288,
|
||||
"digest": "sha256:44a9f4c072b8c10ec81b060484764af10607763c69d55ecdad6bd0b103b7207e"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-win-x64.zip",
|
||||
"size": 183446427,
|
||||
"digest": "sha256:14f3dae327f054431ecb7ed204144dc529e04a03b35b646925bd9812dc54c504"
|
||||
}
|
||||
],
|
||||
"sha256sumsDigest": "sha256:59cff0ab59e2a681656c0b15b3445ca0baa0546d03196f8c4ae58eae8785db25",
|
||||
"updateJsonDigest": "sha256:afef87fa3869be3acf06dfaa4831a97b29ce9c5d275940a2202bc958c62f4d67",
|
||||
"publisherDryRun": {
|
||||
"passed": true,
|
||||
"installerDigests": 10,
|
||||
"digestAndChecksumChecks": 11,
|
||||
"completeNotesCharacters": 3837
|
||||
},
|
||||
"downloadedAppImage": {
|
||||
"name": "Frame-Control-linux-x86_64.AppImage",
|
||||
"sha256": "240023c89bea049f0864da75f83ec235c138375c4fd1c4612953a4c5863610ca",
|
||||
"isolatedXvfbHttpStatus": 200,
|
||||
"serverStopped": true,
|
||||
"serverHeaderPrefix": "FrameControl/1"
|
||||
},
|
||||
"downloadedIntelMacZip": {
|
||||
"name": "Frame-Control-mac-x64.zip",
|
||||
"sha256": "bafc1aec0c11c1fae097dbe1e4b585ebc13a0f8eaf1b249717c14d78811e08d7",
|
||||
"machOCpu": "x86_64",
|
||||
"inspected": [
|
||||
"app executable",
|
||||
"bundled Python 3.12",
|
||||
"Electron helper"
|
||||
],
|
||||
"physicalRuntime": "unverified"
|
||||
},
|
||||
"realFrameSmoke": {
|
||||
"sourceTag": "v0.4.39",
|
||||
"started": "2026-10-26T02:20:45Z",
|
||||
"buildId": "20261022.6153909",
|
||||
"preflight": {
|
||||
"activity": 3,
|
||||
"proximity": [
|
||||
6.3125
|
||||
],
|
||||
"battery": [
|
||||
100
|
||||
]
|
||||
},
|
||||
"passed": 13,
|
||||
"failed": 0,
|
||||
"steps": [
|
||||
{
|
||||
"name": "cleanup: leftovers from earlier runs",
|
||||
"passed": true,
|
||||
"seconds": 8.2
|
||||
},
|
||||
{
|
||||
"name": "devkit service: properties.json",
|
||||
"passed": true,
|
||||
"seconds": 0.0
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"passed": true,
|
||||
"seconds": 0.6
|
||||
},
|
||||
{
|
||||
"name": "arm64: install",
|
||||
"passed": true,
|
||||
"seconds": 8.0
|
||||
},
|
||||
{
|
||||
"name": "arm64: launch",
|
||||
"passed": true,
|
||||
"seconds": 3.5
|
||||
},
|
||||
{
|
||||
"name": "arm64: remove",
|
||||
"passed": true,
|
||||
"seconds": 6.8
|
||||
},
|
||||
{
|
||||
"name": "x86_64: install",
|
||||
"passed": true,
|
||||
"seconds": 7.9
|
||||
},
|
||||
{
|
||||
"name": "x86_64: launch",
|
||||
"passed": true,
|
||||
"seconds": 3.5
|
||||
},
|
||||
{
|
||||
"name": "x86_64: remove",
|
||||
"passed": true,
|
||||
"seconds": 6.6
|
||||
},
|
||||
{
|
||||
"name": "exe: install",
|
||||
"passed": true,
|
||||
"seconds": 8.0
|
||||
},
|
||||
{
|
||||
"name": "exe: launch",
|
||||
"passed": true,
|
||||
"seconds": 3.4
|
||||
},
|
||||
{
|
||||
"name": "exe: remove",
|
||||
"passed": true,
|
||||
"seconds": 6.5
|
||||
},
|
||||
{
|
||||
"name": "cleanup: everything this run installed",
|
||||
"passed": true,
|
||||
"seconds": 8.1
|
||||
}
|
||||
],
|
||||
"ownedLockReleased": true
|
||||
},
|
||||
"postReleasePr282": {
|
||||
"headCommit": "ce6e8f807f44b614e5bbd5c5a5a6a2093236d917",
|
||||
"mergedCommit": "67b9151513bc9fb550f5dfb2d2eec925df390ab3",
|
||||
"exactHeadCiPassed": 11,
|
||||
"unwornVrExercise": {
|
||||
"passed": true,
|
||||
"beforeActivity": 3,
|
||||
"beforeBattery": [
|
||||
99
|
||||
],
|
||||
"afterBattery": [
|
||||
99
|
||||
],
|
||||
"overlayVisible": true,
|
||||
"compositorChanged": true,
|
||||
"stereoCaptureSha256": "c7edc11877aca35dd3aebe480feee93253d04825fe9bd75d02136bb7e3510b97",
|
||||
"mirrorCaptureSha256": "8e5932fb0046b218a1781a6b06cfaa6e3c13dc7874799e287066ed6e43640cd4",
|
||||
"syntheticPointerInputPassed": true,
|
||||
"liveSettingReopened": true,
|
||||
"nativeScrollMoved": false,
|
||||
"powerRecoveryRemoved": true,
|
||||
"ownedLockReleased": true,
|
||||
"cleanupErrors": [],
|
||||
"physicalControllerVerified": false,
|
||||
"wornVisualVerified": false,
|
||||
"officialPluginsInVrVerified": false
|
||||
},
|
||||
"realFrameSmoke": {
|
||||
"passed": 13,
|
||||
"failed": 0,
|
||||
"beforeActivity": 3,
|
||||
"battery": [
|
||||
99
|
||||
],
|
||||
"buildId": "20261022.6153909",
|
||||
"ownedLockReleased": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# v0.4.39 release gate, 26 October 2026
|
||||
|
||||
The [published release](https://github.com/saphid/frame-control/releases/tag/v0.4.39) is GitHub's latest release. Its tag resolves to main commit `fd0b0a946f11d92fd1cc480cd9d1a83be795bf71`, after PRs #280 and #281 merged. The [tagged workflow](https://github.com/saphid/frame-control/actions/runs/40258561639) passed all six jobs: draft, Apple Silicon Mac, Intel Mac, Windows, Linux and checksums. The release has ten installers, `SHA256SUMS` and `update.json`. [Sanitized numeric results](v0439-release-2026-10-26.json) contain the exact asset sizes and GitHub digests.
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `scripts/publish-release.sh --dry-run v0.4.39` from the exact tag | Passed: all ten installer digests, regenerated `SHA256SUMS` digest, complete update notes and ten-asset `update.json` |
|
||||
| Downloaded Linux x86_64 AppImage | SHA-256 matched `240023c89bea049f0864da75f83ec235c138375c4fd1c4612953a4c5863610ca`; the tagged isolated Xvfb check received HTTP 200 with a `FrameControl/1` server header prefix and confirmed the server stopped |
|
||||
| Downloaded Intel Mac ZIP | SHA-256 matched `bafc1aec0c11c1fae097dbe1e4b585ebc13a0f8eaf1b249717c14d78811e08d7`; ZIP inspection found x86_64 Mach-O app executable, bundled Python 3.12 and Electron helper |
|
||||
| Exact tagged `scripts/frame-smoke.sh` on the real Frame | **13/13** steps passed on SteamOS build `20261022.6153909`; locked preflight found SteamVR activity 3, proximity raw 6.3125 and battery 100%. Reserved test titles were removed and the script released its owner-token lock. |
|
||||
| Published release readback | GitHub reports `draft=false`, `prerelease=false`, latest tag `v0.4.39`; downloaded `update.json` lists ten unique assets, the stable release URL and complete safety notes |
|
||||
|
||||
The first local AppImage launch attempt stopped before app startup because the execution sandbox denied Electron's sandbox host (`Operation not permitted`). Repeating the same tagged check with host permissions passed. The bundled release CI also ran its own AppImage launch check.
|
||||
|
||||
The unworn VR runner's candidate evidence is [documented separately](framey-vr-standby-gate-2026-10-26.md). Its successful stereo and `/dev/video99` panel captures and synthetic OpenVR input are not a worn or physical-controller test. Two other attempts found an intermittently hidden overlay and failed safely. No person wore the headset for this release check. Physical controller input, optical comfort, VR keyboard, native scroll and official-plugin wearer-visible behavior remain unverified.
|
||||
|
||||
The physical Intel MacBook Air ran an earlier candidate, but was unreachable for this exact tagged installer. The architecture and hash checks above do not prove physical Mac runtime. Physical Windows PC and macOS OBS/headset capture also remain unverified. No private room pixels or raw Steam log lines are included here.
|
||||
|
||||
## Post-release Framey library change (#282)
|
||||
|
||||
After v0.4.39 publication, [PR #282](https://github.com/saphid/frame-control/pull/282) merged at `67b9151513bc9fb550f5dfb2d2eec925df390ab3`. All eleven exact-head checks passed on `ce6e8f807f44b614e5bbd5c5a5a6a2093236d917`. This commit is **not** in v0.4.39. The [library-context evidence](framey-library-context-2026-10-26.md) covers its three newly partial official plugins and separate backend limits.
|
||||
|
||||
The exact #282 source ran `scripts/framey-vr-check.py --exercise` under the shared lock from standby (activity 3, battery 99%). The sample panel became visible; stereo compositor pixels changed and `/dev/video99` mirror bytes were captured. Trusted synthetic OpenVR pointer input reached the repository sample's live Decky setting and the saved value survived reopening. Native scroll did not move. The runner removed its recovery timer, restored original power values, reported no cleanup errors and released its owner-token lock. Private images and room pixels remain outside this repository. The source came from an exact commit archive, so the runner's `sourceCommit` field was empty; the archive object ID was verified before extraction.
|
||||
|
||||
Once the Frame returned to standby, the exact #282 source passed `scripts/frame-smoke.sh` **13/13** on the real SteamOS build `20261022.6153909`, with battery 99% at preflight. Test titles were removed and the lock was released. These checks cover the Framey sample and generic Frame smoke, not Steamback, PlayTime or Deck Settings in VR. No wearer or physical controller was involved; those limits remain open.
|
||||
@@ -0,0 +1,9 @@
|
||||
# v0.4.40 candidate: locked real Frame gates, 26 October 2026
|
||||
|
||||
Source commit `9aa26a8322f74144c13528b6fb41edc451ec7eea` contains merged PRs #286–#288 and the candidate 0.4.40 version bump. The Frame ran SteamOS 0.5.6 build `20261022.6153909`. These are source checks before tagged installers exist.
|
||||
|
||||
The unworn `scripts/framey-vr-check.py --exercise` acquired its random-owner `/tmp/frame-test.lock` from lxso2. Its locked read-only preflight found SteamVR activity 3 (standby), proximity 6.25, battery 100%, and original power values `false` and `5`. The repository sample panel became visible, the stereo compositor image changed, and `/dev/video99` supplied a mirror frame. Synthetic OpenVR pointer input produced a trusted click, changed the sample's live Decky setting, and the saved value survived a fresh iframe reopen. The quick action and native slider/text control paths completed; native scrolling did not move. The run exited 0, removed its temporary Decky/panel services and recovery timer, restored and read back the original power values, reported no cleanup errors, found battery 99%, and released its own lock. Private room and UI captures remain outside the repository.
|
||||
|
||||
After the Frame returned to standby, `scripts/frame-smoke.sh` acquired a separate random-owner lock. Its preflight found activity 3, proximity 6.25, and battery 100%. All **13/13** steps passed, including ARM64, x86-64 and Windows-executable install/launch/remove and final cleanup. The command exited 0, which includes successful release of its owned lock. Its private detailed result remains outside the repository.
|
||||
|
||||
These checks exercise Frame Control's repository sample and generic title paths. They do not run the official Deck Shelves backend or prove its panel or Steam Home shelves in VR. The run did not include a wearer, physical controller, VR keyboard, native scrolling, or optical comfort check. A mirror capture and changed compositor pixels do not by themselves prove wearer-visible passthrough for this run. The [compatibility table](../framey-compatibility.md) retains those limits.
|
||||
@@ -0,0 +1,45 @@
|
||||
# v0.4.40 tag and post-tag Frame checks, 26 October 2026
|
||||
|
||||
The v0.4.40 annotated tag dereferences to main merge commit `a4d8073e3199671873d304b80a6acc8cdbd33866` from PR #291. The tag contains the 0.4.40 package and lockfile version. PR #291 passed all reported exact-head checks before merge, including Linux, Windows, macOS, iOS, fake Frame e2e and installer preview builds.
|
||||
|
||||
## Real Frame, tagged source
|
||||
|
||||
From an exact `v0.4.40` checkout, `scripts/frame-smoke.sh` acquired its unique `/tmp/frame-test.lock` owner before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On SteamOS 0.5.6 build `20261022.6153909`, all **13/13** reserved title steps passed: ARM64, x86_64 and Windows executable install, launch and remove, plus final cleanup. The command exited 0, which includes successful removal of its own lock. Its detailed result remains private under `tests/smoke/results/2026-10-26T035641Z-20261022.6153909.json` in the test checkout.
|
||||
|
||||
## Real Frame, next main source
|
||||
|
||||
After #289 and #290 merged, `scripts/framey-vr-check.py --exercise` ran from main commit `56bde24be1d38ac060e8ff287455182765e48564` under its own unique shared lock. The locked preflight found activity 3, proximity 6.25, battery 99%, dashboard already visible, and original SteamVR power values `false` and `5`.
|
||||
|
||||
The repository sample panel appeared in the stereo compositor; before/after compositor hashes differed and `/dev/video99` returned a mirror frame. Synthetic OpenVR pointer events were trusted by the panel. A real Decky 3.3.0 backend saved the sample setting from off to on; an independent settings-file read returned `{"enabled": true}`, and a fresh iframe reopen showed on. The quick action, slider value 50 and text entry path completed. Native scroll was attempted but its measured offset stayed 0. This exercise did not run an official store plugin.
|
||||
|
||||
The command exited 0. It removed test services and temporary profile/plugin paths, restored and read back the original power values, removed its recovery timer, reported no cleanup errors, found battery still 99%, and released its own lock. Its immediate final SteamVR activity was 0 after the temporary wake; the saved standby power behavior was restored. Private room and UI captures remain outside Git.
|
||||
|
||||
No person wore the headset. A stereo screenshot, changed pixels, trusted synthetic input and mirror frame do **not** prove wearer-visible passthrough, physical controller usability, VR keyboard behavior, native scrolling, optical legibility or comfort. The [worn checklist](../framey-vr-check.md#three-minute-worn-checklist) and hourly check remain necessary. Official TabMaster, Deck Shelves, ProtonDB Badges and other store panels were not exercised in this run.
|
||||
|
||||
## Downloaded tagged installers
|
||||
|
||||
The draft release's GitHub digest for `Frame-Control-linux-x86_64.AppImage` is `sha256:cded3c660e41eb04e835e28057da81345075c4dbe9a009f397aea1da59655b3d`. A fresh download matched it. After setting the download's executable bit, `scripts/check-appimage-launch.py` launched that exact file under isolated Xvfb: its bundled server answered HTTP 200 with `FrameControl/1`, then stopped on shutdown.
|
||||
|
||||
The draft release's GitHub digest for `Frame-Control-mac-arm64.zip` is `sha256:98f6d118d0db0cba10c3e4bfcd618953f10ba1c35246ce26041ecd39a1d3aff0`. Fresh local and physical-Mac copies matched. On a reachable physical arm64 Mac running macOS 26.5.2, a private temporary extraction reported bundle version 0.4.40; `lipo -verify_arch arm64` passed for the app executable, bundled Python, adb and Mac viewer. Its bundled Python imported `http.server`, `ssl` and `sqlite3` and reported OpenSSL 3.5.8. The temporary directory was removed and checked absent.
|
||||
|
||||
That Mac already had a person's Frame Control app running, so no tagged GUI was launched, replaced or quit. The physical Intel Air was unreachable from this worker; the tagged Intel artifact's build check and bundle-architecture CI are separate from a physical Intel runtime test. No macOS Gatekeeper browser-download path was tested.
|
||||
|
||||
## Tagged workflow and checksum gate
|
||||
|
||||
The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40266393941) ran at `a4d8073e3199671873d304b80a6acc8cdbd33866` and passed all six jobs: draft, Apple Silicon Mac, Intel Mac, Windows, Linux and checksums. Its Linux job launched the built AppImage under Xvfb; its Mac jobs checked bundled binary architectures and Python imports; its Windows job installed the Setup executable, compared installed resources and ran the bundled Python PC-library tests.
|
||||
|
||||
The draft has exactly ten installers and `SHA256SUMS`, each with a GitHub SHA-256 digest. Regenerating the checksum file from the ten asset digests produced bytes identical to the downloaded `SHA256SUMS`; its own digest matches `7457da8f292bc1b2b74cb989b4d179858055380721b26e23ccd595994701d1f8`.
|
||||
|
||||
A fresh `Frame-Control-mac-x64.zip` download matched GitHub digest `52f257fc0dfc1b41e89ab0c7a551d667af4cc46f800e4f57e2689d47c856977c`. Independent ZIP inspection found x86_64 Mach-O app, bundled Python 3.12 and Mac viewer executables. This is packaging evidence, not a physical Intel Mac launch.
|
||||
|
||||
## Publication and updater readback
|
||||
|
||||
`scripts/publish-release.sh --dry-run v0.4.40` passed the exact tag, ten installer digests and regenerated `SHA256SUMS` gate. The release script then published [v0.4.40](https://github.com/saphid/frame-control/releases/tag/v0.4.40) at 2026-10-26 04:14:35 UTC. A direct GitHub latest-release read returned `draft=false`, `tag_name=v0.4.40`, ten installers, `SHA256SUMS` and `update.json`. A fresh download of the public `update.json` parsed as version 0.4.40, its stable tag page, ten assets with SHA-256 digests and the unworn-test limits in its notes. This release contains the tagged `a4d8073` commit, not later main changes.
|
||||
|
||||
## Post-tag #292 panel regression
|
||||
|
||||
PR #292 passed all 11 hosted checks on exact head `e0115f356d3b33369a61cfba034b080a7764559f` and merged as `784118b71e3463156574988121f47354d87358c7`. From that exact main commit, `python3 scripts/framey-vr-check.py --exercise --output /tmp/framey-v0440-post292` exited 0 on the real Frame. Its unique `/tmp/frame-test.lock` owner was acquired before any read and released on exit. Preflight: battery 99%, proximity 6.25, SteamVR activity 3 standby, dashboard already visible, saved power `false/5`.
|
||||
|
||||
The temporary repository sample appeared in stereo compositor and mirror captures; compositor frame hashes changed. Trusted synthetic OpenVR pointer input saved a Decky setting to `{"enabled": true}`, independently read from the settings file and retained on a fresh iframe reopen. A quick action, slider value 50 and CDP text entry after OpenVR focus completed. Native scroll remained at offset 0. The test reported `passed=true`, no cleanup errors, removed its recovery timer, restored/read back power `false/5`, battery 99%, and released only its own lock. The private result and room captures remain under `/tmp/framey-v0440-post292/` outside Git.
|
||||
|
||||
This exercises one quick-action activation. It does not prove the new rapid duplicate-tap guard with physical controllers. No person wore the headset; no official Decky plugin panel was tested. The hourly worn-headset schedule remains enabled for wearer-visible passthrough, controller, scrolling, keyboard and comfort checks.
|
||||
@@ -0,0 +1,7 @@
|
||||
# v0.4.41 candidate: real Frame title smoke, 26 October 2026
|
||||
|
||||
Source commit `c87e5c6` bumps the app and lockfile to 0.4.41 on top of main after PRs #289, #290, #292–#295. It includes the exact official ARM64 audio evidence from #293. The release tag and installers do not exist at this candidate stage.
|
||||
|
||||
`scripts/frame-smoke.sh` acquired its unique owner in `/tmp/frame-test.lock` before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, all **13/13** steps passed: existing reserved-title cleanup, devkit identity/status, install/launch/remove for ARM64, x86_64 and Windows test executables, and final cleanup. The command exited 0, which includes successfully releasing only its own lock. Its detailed result remains private under `tests/smoke/results/2026-10-26T043229Z-20261022.6153909.json` in the candidate checkout.
|
||||
|
||||
No person wore the headset. This checks title management and process cleanup, not wearer-visible rendering, physical controllers, official plugin VR panels, audio output or installed desktop packages. After PR #294, a separate exact-main unworn Framey sample exercise passed live setting persistence, synthetic OpenVR input, stereo compositor change and cleanup; it did not prove rapid physical controller taps or native scroll. The [v0.4.40 publication evidence](v0440-release-2026-10-26.md) records the previous tagged installer gates. New v0.4.41 tagged installers and checksums must be verified before publication.
|
||||
@@ -0,0 +1,23 @@
|
||||
# v0.4.41 tagged release, 26 October 2026
|
||||
|
||||
The annotated `v0.4.41` tag dereferences to main merge commit `b5d50a8d896f790a94baf4e937db914eab475ed9` from PR #297. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40271174791) passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs. The platform jobs launched the built Linux AppImage, checked the Windows installation and bundled PC Python libraries, and checked Mac bundle architectures. These CI results do not establish a physical Windows or Mac launch.
|
||||
|
||||
## Real Frame, exact tag
|
||||
|
||||
From an exact tag checkout, `scripts/frame-smoke.sh` acquired its unique owner in `/tmp/frame-test.lock` before reading the headset. Preflight found SteamVR activity 3 (standby) and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, all **13/13 reserved title steps passed**; the command exited 0, including test title and process cleanup and release of its owned lock. ARM64 and Proton test executables launched. The x86_64 launch step recorded that optional Steam Linux Runtime 4 was **not installed**, so that executable did not actually launch. The private detailed result is `tests/smoke/results/2026-10-26T045140Z-20261022.6153909.json` in the exact-tag checkout.
|
||||
|
||||
No person wore the headset. This title smoke does not verify active passthrough pixels, wearer-visible panels, physical controller input, official Decky plugin panels, audio output, comfort or VR keyboard use. The separate [unworn Framey sample exercise](framey-scroll-probe-2026-10-26.md) established live sample setting persistence, stereo compositor changes and trusted synthetic OpenVR input; it did not establish a worn result. The hourly worn check remains enabled.
|
||||
|
||||
## Downloaded installer and checksum checks
|
||||
|
||||
The draft held ten installers plus `SHA256SUMS`, each with a GitHub SHA-256 digest. `scripts/publish-release.sh --dry-run v0.4.41` checked the exact tag and all ten installer digests, regenerated `SHA256SUMS` from those digests and matched its own GitHub digest. A fresh downloaded checksum list independently matched `e7555956aeb9ec3a464081aaafe1ed6762f65a17c04edaa6ea45286f10927109`.
|
||||
|
||||
A fresh Linux x86_64 AppImage download matched `5d1f50dc8527faf299f2058c7b5ed19057ec7c81c41449f6ccd44d68e6b4d212`. `scripts/check-appimage-launch.py` launched that exact file under isolated Xvfb: the bundled server answered HTTP 200 with `FrameControl/1`, then stopped. The first local attempt failed at Chromium sandbox startup inside the command sandbox; rerunning the same downloaded file with normal host permissions passed. The release workflow's independent Linux launch also passed.
|
||||
|
||||
Fresh Mac ZIP downloads matched GitHub digests: arm64 `84615ec8a0c62481a9f33bae1815d55ec902db9aa9d4bd767fcf9380c3d44d7a`, x64 `2800b749cda98d15207950eca4a6bd205d35f30551f16efd7720b41347edc7ca`. Both contain bundle version 0.4.41; the app executable Mach-O CPU types are arm64 and x86_64 respectively. The physical Mac was unreachable over SSH, so neither tagged ZIP was launched there. The physical Intel Air was also not tested with this tag.
|
||||
|
||||
## Publication and updater readback
|
||||
|
||||
The final dry run passed with the release notes and hardware limits in the draft. `scripts/publish-release.sh v0.4.41` published [v0.4.41](https://github.com/saphid/frame-control/releases/tag/v0.4.41) at 2026-10-26 05:01:47 UTC. GitHub's latest-release API read back `draft=false`, tag `v0.4.41`, ten installers, `SHA256SUMS` and `update.json`. A fresh public latest-download of `update.json` parsed as version 0.4.41 with the stable release page, ten SHA-256 digests and the unworn-test limits in its notes. The release contains the tagged `b5d50a8` commit; no post-tag changes are implied.
|
||||
|
||||
The exact-version official Decky catalog in this release is **3 works / 48 partial / 54 broken / 11 N/A**. Volume Mixer and Mixer are works only for the tested per-app silent-stream volume scope on real ARM64 Decky; [audio evidence](framey-audio-arm64-2026-10-26.md) describes the independent Pulse readbacks. Volume Boost remains partial. Audible playback, Bluetooth, other mixer controls and official-plugin worn VR remain unverified.
|
||||
@@ -66,7 +66,7 @@ none of these frontend controls has passed a worn-headset check yet.
|
||||
| `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium |
|
||||
| `SteamClient` subscriptions and `DFL.Router.MainRunningApp` | Explicit inert registration methods return disposable handles; no events or attached running game. Mutating Steam/power methods are unavailable. | Host contract tests; these are safe fallbacks, not live Steam functionality |
|
||||
| `DialogCheckbox`, `DialogButtonPrimary` | Native checkbox and button controls in the panel | Host unit test; Storage Cleaner empty-data browser check |
|
||||
| Optional `settings`, `frameyQuickActions` | Separate settings tab; up to eight actions with result text | Host unit test; these fields are Framey additions |
|
||||
| Optional `settings`, `frameyQuickActions` | Separate settings tab when content also exists; settings-only plugins have no empty Panel tab; up to eight actions with result text | Host unit test; these fields are Framey additions |
|
||||
| `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e; real ARM64 Notebook save/reopen across backend restart and Simple Timer settings/recents persistence via local Chromium. No worn-VR claim. |
|
||||
| API v1/v2 `addEventListener` / `removeEventListener` | Plugin-scoped real backend events while panel is open; bounded queue, about one-second polling, subscription expiry | Real ARM64 Simple Timer countdown, start/cancel and completion toast via local Chromium; scope/bounds/reconnect tests. No closed-panel notifications. |
|
||||
| `toaster.toast` | Visible toast inside the plugin panel; plain text or React nodes for title, body, icon, logo and subtext; `dismiss` and optional `onClick` | Host unit test; no Steam system notification or worn VR check |
|
||||
|
||||
@@ -4,7 +4,7 @@ Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins
|
||||
|
||||
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
|
||||
|
||||
**Works: 1 · Partial: 48 · Broken: 56 · N/A: 11.**
|
||||
**Works: 3 · Partial: 48 · Broken: 54 · N/A: 11.**
|
||||
|
||||
- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification.
|
||||
- **Partial** means only a subset or a local fixture scope was verified, often just frontend/settings initialization. The remaining backend/device functionality is unverified; read the reason for the exact scope.
|
||||
@@ -27,7 +27,7 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl
|
||||
|
||||
The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member.
|
||||
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. Other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -42,7 +42,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 12: MusicControl 1.1.6 | legacy / legacy | partial | Legacy media controls initialize; player discovery and playback are unverified. | static archive, local browser, fake-Frame |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings render locally with inert system-key subscriptions and empty detached Steam context. Live-game discovery, process pause/resume and suspend integration are unsupported/unverified; no backend actions ran. | static archive, local browser, fake-Frame |
|
||||
| 14: ProtonDB Badges 1.3.4 | 2 / 2 | partial | Settings render locally and read-only get_setting RPC returned the default through real Decky 3.3.0 on ARM64 Frame. Steam game-page badges have no patch target; live writes and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Frontend audit failed: Cannot read properties of null (reading 'm_GamepadUIAudioStore'). | static archive, local browser, fake-Frame |
|
||||
| 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel. | static archive, local browser, fake-Frame |
|
||||
| 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 19: Notebook 0.1.5 | legacy / legacy | works | Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Per-game auto-selection and background artwork unavailable; worn-VR input unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 21: PowerTools 2.0.3 | legacy / legacy | N/A | Not supported on Frame: Targets AMD Ryzen power controls; Frame uses an ARM64 SoC. | static archive, local browser, fake-Frame |
|
||||
@@ -55,9 +55,9 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 30: TunnelDeck 1.0.4 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
|
||||
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Frontend audit failed: DFL.createModuleMapping is not a function. | static archive, local browser, fake-Frame |
|
||||
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel. | static archive, local browser, fake-Frame |
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | broken | Detached app overviews are now available, but RunningApps/lifetime observations and Steam artwork helpers still lack a live mapping. Discord activity integration remains unavailable/unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | partial | Local Chromium discovers and changes a private silent stream through the actual Decky 3.3.0 Linux backend, with independent Pulse readback. Frame ARM64 libraries/audio session and Bluetooth playback controls remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame |
|
||||
| 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame |
|
||||
@@ -68,10 +68,10 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'type'). | static archive, local browser, fake-Frame |
|
||||
| 53: Shotty 0.1.0 | legacy / legacy | broken | Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Local Chromium sync/separate sliders, numeric volume entry and cache reopen pass through the actual Decky 3.3.0 backend on a private virtual stereo sink. Frame pactl/default sink, boost above 100% and physical audio remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
|
||||
| 60: DeckyFileServer 1.2.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend. | static archive, local browser, fake-Frame |
|
||||
| 61: TabMaster 2.15.1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio'). | static archive, local browser, fake-Frame |
|
||||
| 61: TabMaster 2.15.1 | 1 / 2 | partial | Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified. | static archive, local browser, fake-Frame |
|
||||
| 62: TS3 QuickAccess 1.2.0 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: ts3-qs4sd/bin/ts3-qs4sd.so. | static archive, local browser, fake-Frame |
|
||||
| 63: Free Loader 1.6.0 | 1 / 2 | broken | Unsafe plugin archive member. | static archive, local browser, fake-Frame |
|
||||
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. | static archive, local browser, fake-Frame |
|
||||
@@ -91,7 +91,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 80: MagicBlack 2.0.2 | 1 / 2 | N/A | Not supported on Frame: Targets a global Steam Deck screen-black overlay; the isolated panel cannot black out the Frame compositor. | static archive, local browser, fake-Frame |
|
||||
| 81: IsThereAnyDeal for Deck 1.1.1 | legacy / legacy | partial | Country settings render and select the real Frame Steam country through the read-only bridge. Settings writes, deal lookups and Steam page integration remain unverified; plugin backend calls were denied. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 82: MagicPods 2.0.21 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: MagicPodsDecky/bin/magicpodscore. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 84: Junk-Store 1.1.13 | legacy / legacy | broken | Route registration succeeds; rendering fails on a missing Steam ContainerBackground component. | static archive, local browser, fake-Frame |
|
||||
| 84: Junk-Store 1.1.13 | legacy / legacy | broken | Local CSS lookup fixes the loading view crash. Actual Decky 3.3.0 amd64 startup/static InitActions return a script execution/JSON error; the published scripts are not executable after extraction. Store install/login/game launch and ARM64 remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 85: Decky-Undervolt 1.0.8 | 1 / 2 | N/A | Not supported on Frame: RyzenAdj Curve Optimizer requires an AMD CPU; Frame is ARM64. | static archive, local browser, fake-Frame |
|
||||
| 87: ScreenSaver 1.1.2 | legacy / legacy | partial | Initializer succeeds; global screen overlay and idle/suspend integration have no verified VR target. | static archive, local browser, fake-Frame |
|
||||
| 88: steamdeck-input-disabler 1.0.2 | legacy / legacy | N/A | Not supported on Frame: Targets the built-in Steam Deck USB gamepad; Frame has different controllers. | static archive, local browser, fake-Frame |
|
||||
@@ -108,7 +108,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 99: Decky Zerotier 0.3.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 100: Virtual Surround Sound 0.0.5 | 1 / 2 | partial | Initializer succeeds; avatar proxy is unavailable and live audio-routing backend unverified. | static archive, local browser, fake-Frame |
|
||||
| 101: XIVOmega 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 102: DeckSP 1.1.3 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio'). | static archive, local browser, fake-Frame |
|
||||
| 102: DeckSP 1.1.3 | 1 / 2 | broken | Optional UI-audio lookup succeeds, but the published frontend raises "Cannot access PluginManager before initialization" when Steam MobX reaction lookup is unavailable. JamesDSP installation/audio routing and ARM64 remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 103: Decky Ludusavi 1.2.2 | 1 / 2 | partial | Initializer succeeds; Flatpak Ludusavi discovery and backup/restore unverified. | static archive, local browser, fake-Frame |
|
||||
| 104: Game Sync 1.0.4 | 1 / 2 | partial | Backend get_config is intentionally rejected by the audit before initialization completes; actual backend compatibility is unknown. | static archive, local browser, fake-Frame |
|
||||
| 105: WiFi Locker 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
@@ -140,7 +140,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame |
|
||||
| 133: Non-Steam Badges 0.1.0 | 1 / 2 | partial | Initializer succeeds; Non-Steam game badges have no supported Steam page injection target. | static archive, local browser, fake-Frame |
|
||||
| 134: Deck Progress Tracker 1.3.6 | 1 / 2 | partial | Initializer succeeds; Steam game/library statistics and page patches unverified. | static archive, local browser, fake-Frame |
|
||||
| 135: Deck Shelves 2.0.0 | 1 / unknown | broken | Frontend audit failed: Failed to resolve module specifier "./chunks/pluginApi-B8sV7970.js". Invalid relative url or base scheme isn't hierarchical.. | static archive, local browser, fake-Frame |
|
||||
| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified. | static archive, local browser, fake-Frame |
|
||||
| 136: Decky-Clipper 1.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: Decky-Clipper/bin/gstreamer-1.0/libgstproxy.so. | static archive, local browser, fake-Frame |
|
||||
| 137: Decky Proton Launch 0.9.0 | 1 / 2 | partial | Initializer succeeds; Proton wrapper installation and launch changes were not run. | static archive, local browser, fake-Frame |
|
||||
| 138: Docky 1.4.13 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
@@ -149,7 +149,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 141: DeckyClocker 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 142: MuteOnSleep 1.0.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 143: Desktop Apps 0.1.0 | 1 / 2 | partial | Initializer succeeds; Flatpak app discovery and Steam shortcut changes unverified. | static archive, local browser, fake-Frame |
|
||||
| 144: Mixer 0.1.0 | 1 / 2 | partial | Local Chromium numeric stream volume and filter save/reopen pass through the actual Decky 3.3.0 Linux backend; filters survive its restart. Frame pw-dump/wpctl, real audio session and worn VR remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 144: Mixer 0.1.0 | 1 / 2 | works | Per-app discovery and numeric volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, independently read back on a silent test stream. Filters persist on amd64; ARM64 filter persistence, Reset, internal audio nodes, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 145: VARTA 1.0.21 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
|
||||
## API, UI and backend inventory
|
||||
|
||||
+18
-3
@@ -70,6 +70,15 @@ countdown, completion toast and persisted recents/subtle mode worked. Simple
|
||||
Timer remains partial: its own expired-timer startup raises a missing
|
||||
`timer_task` error, sound/suspend are unsupported, and worn VR was not checked.
|
||||
|
||||
DFL module and class lookup searches a finite registry of Framey's own controls,
|
||||
CSS maps and an explicitly unsupported optional UI-audio player. It follows DFL's
|
||||
module/export filter and mapping contracts; unknown Steam modules return
|
||||
`undefined`, and unknown module mappings are empty. Returned IDs describe local
|
||||
Framey exports, not Steam webpack IDs. Sound playback throws an unsupported
|
||||
error, and Steam audio stores, function-source signatures and global patches are
|
||||
not fabricated. The [module lookup checks](evidence/framey-module-lookup-2026-10-26.md)
|
||||
record the resulting TabMaster documentation support and remaining blockers.
|
||||
|
||||
The host currently supplies sections, rows, buttons, toggles, text fields,
|
||||
sliders, grouped dropdowns,
|
||||
scroll containers, a modal, locale information, local panel routes and toasts,
|
||||
@@ -157,7 +166,10 @@ Install, Update, and Remove show the current job stage in the headset panel.
|
||||
The store verifies each downloaded archive and the headset applies it with a
|
||||
rollback if Decky cannot start. The panel remembers the current job ID for this
|
||||
browser session, so **Check result** can resume polling after a navigation or
|
||||
temporary connection error. Refresh the installed list before starting another
|
||||
temporary connection error. After a polling error, Install, Update, and Remove
|
||||
stay disabled while the saved job's result is unknown; use **Check result**
|
||||
before starting another change.
|
||||
Refresh the installed list before starting another
|
||||
operation after an error. The headset panel and desktop app send the hash of
|
||||
the version you saw; if the official store changes that version before the job
|
||||
starts, Frame Control stops before downloading or changing the headset. Refresh
|
||||
@@ -185,10 +197,13 @@ and the fake Frame flow before asking users to install it.
|
||||
|
||||
An API v2 plugin's normal `content` is its default VR panel. Framey also shows
|
||||
an optional `settings` React element under a **Settings** tab and an optional
|
||||
`titleView` above it. For one-tap headset controls, return
|
||||
`titleView` above it. A plugin with only `settings` opens that view directly;
|
||||
Framey shows tabs only when both views exist. For one-tap headset controls, return
|
||||
`frameyQuickActions: [{label: 'Refresh data', onClick: refreshData}]` from the
|
||||
plugin initializer. Framey displays up to eight actions and reports a pending,
|
||||
success, or failure state. These descriptor fields are Framey additions; Decky
|
||||
success, or failure state. It ignores extra taps while an action is pending,
|
||||
and a failed action can be retried. Invalid action descriptors do not count
|
||||
toward the eight-action limit. These descriptor fields are Framey additions; Decky
|
||||
ignores them. Use a plugin backend method through `@decky/api` for work needing
|
||||
persistent state, and keep a quick action safe to repeat.
|
||||
See the [Decky API compatibility table](frame-plugin-author.md#framey-decky-api-compatibility)
|
||||
|
||||
@@ -53,7 +53,12 @@ iframe through its own debugging target, and reads control geometry. OpenVR's
|
||||
actual gamescope overlay. The check verifies delivery to the intended element
|
||||
and a trusted pointer event, toggles the sample, reads its real Decky settings
|
||||
file, reopens a fresh iframe, and checks the saved value. It also clicks the
|
||||
Settings/Panel tabs and quick action. A smooth-scroll event is attempted and its measured scroll offset is reported.
|
||||
Settings/Panel tabs and quick action. The scroll diagnostic creates a temporary
|
||||
160-pixel-high region with more than 700 pixels of content, moves the native
|
||||
pointer over it, then attempts OpenVR smooth and discrete scroll events. It
|
||||
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
|
||||
No wheel event or offset change means this synthetic injection path remains
|
||||
unverified; it does not establish whether a physical controller can scroll.
|
||||
Temporary controls exercise the shipped
|
||||
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
|
||||
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
|
||||
|
||||
@@ -14,6 +14,9 @@ action calls the backend, so it reads the persisted setting rather than a
|
||||
browser cache. A missing, damaged, or old non-boolean value reads as the
|
||||
caller's default;
|
||||
the next save atomically replaces the sample's settings file.
|
||||
The panel also guards its own read and write calls synchronously. If two
|
||||
controller events arrive before React redraws a disabled control, the second
|
||||
event is ignored; after the request settles, a new change can be saved.
|
||||
|
||||
The **About this setting** button opens a local detail page at
|
||||
`/framey-sample/details/saved-setting`. It demonstrates `routerHook.addRoute`,
|
||||
|
||||
+11
@@ -30,7 +30,11 @@ function Content() {
|
||||
const [enabled, setEnabled] = SP_REACT.useState(false);
|
||||
const [busy, setBusy] = SP_REACT.useState(true);
|
||||
const [message, setMessage] = SP_REACT.useState('Loading setting…');
|
||||
const pending = SP_REACT.useRef(false);
|
||||
async function refresh() {
|
||||
if (pending.current)
|
||||
return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
try {
|
||||
const value = await getSetting('enabled', false);
|
||||
@@ -41,11 +45,17 @@ function Content() {
|
||||
setMessage(`Could not read setting: ${String(error)}`);
|
||||
}
|
||||
finally {
|
||||
pending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
SP_REACT.useEffect(() => { void refresh(); }, []);
|
||||
async function change(value) {
|
||||
// Two controller events can arrive before React redraws the disabled toggle.
|
||||
// Keep writes ordered so a late first reply cannot replace the final choice.
|
||||
if (pending.current)
|
||||
return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
try {
|
||||
await setSetting('enabled', value);
|
||||
@@ -58,6 +68,7 @@ function Content() {
|
||||
setMessage(`Could not save setting: ${String(error)}`);
|
||||
}
|
||||
finally {
|
||||
pending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n\n async function refresh() {\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><p role=\"status\">{message}</p></PanelSectionRow>\n </PanelSection>;\n}\n\nfunction Details() {\n const {topic} = useParams<{topic?: string}>();\n return <PanelSection title=\"Sample detail page\">\n <PanelSectionRow><p>Topic: {topic || 'overview'}</p></PanelSectionRow>\n <PanelSectionRow><p>The toggle is stored by this plugin's Decky backend in the user's settings directory.</p></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.NavigateBack()}>Back to setting</ButtonItem></PanelSectionRow>\n </PanelSection>;\n}\n\nexport default definePlugin(() => {\n routerHook.addRoute('/framey-sample/details/:topic?', Details);\n return Object.assign({\n name: 'Framey Settings Sample',\n icon: <span aria-hidden=\"true\">⚙</span>,\n content: <Content />,\n onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'),\n}, {\n // FrLine truncated
|
||||
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, useRef, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n const pending = useRef(false);\n\n async function refresh() {\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n // Two controller events can arrive before React redraws the disabled toggle.\n // Keep writes ordered so a late first reply cannot replace the final choice.\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><p role=\"status\">{message}</p></PanelSectionRow>\n </PanelSection>;\n}\n\nfunction Details() {\n const {topic} = useParams<{topic?: string}>();\n return <PanelSection title=\"Sample detail page\">\n <PanelSectionRow><p>Topic: {topic || 'overview'}</p></PanelSectionRow>\n <PanelSectionRow><p>The toggle is stored by this plugin's Decky backend in the user's settings directory.</p></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.NavigateBack()}>Back Line truncated
|
||||
@@ -1,6 +1,6 @@
|
||||
import {callable, definePlugin, routerHook, toaster} from '@decky/api';
|
||||
import {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
const getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');
|
||||
const setSetting = callable<[key: string, value: boolean], boolean>('set_setting');
|
||||
@@ -9,8 +9,11 @@ function Content() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [message, setMessage] = useState('Loading setting…');
|
||||
const pending = useRef(false);
|
||||
|
||||
async function refresh() {
|
||||
if (pending.current) return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
try {
|
||||
const value = await getSetting('enabled', false);
|
||||
@@ -19,6 +22,7 @@ function Content() {
|
||||
} catch (error) {
|
||||
setMessage(`Could not read setting: ${String(error)}`);
|
||||
} finally {
|
||||
pending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +30,10 @@ function Content() {
|
||||
useEffect(() => {void refresh();}, []);
|
||||
|
||||
async function change(value: boolean) {
|
||||
// Two controller events can arrive before React redraws the disabled toggle.
|
||||
// Keep writes ordered so a late first reply cannot replace the final choice.
|
||||
if (pending.current) return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
try {
|
||||
await setSetting('enabled', value);
|
||||
@@ -36,6 +44,7 @@ function Content() {
|
||||
} catch (error) {
|
||||
setMessage(`Could not save setting: ${String(error)}`);
|
||||
} finally {
|
||||
pending.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ def main():
|
||||
def do_GET(self):
|
||||
path = self.path.split('?')[0]
|
||||
content_type = 'text/html; charset=utf-8'
|
||||
module_asset = False
|
||||
if path == '/':
|
||||
# Only names/IDs are needed by the UI; never embed raw executable source.
|
||||
catalog = json.dumps([{'id': p['id'], 'name': p['name']} for p in plugins]).replace('<', '\\u003c')
|
||||
@@ -113,6 +114,22 @@ def main():
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
self.send_error(404, 'Verified frontend archive unavailable')
|
||||
return
|
||||
elif path.startswith('/framey/module/'):
|
||||
try:
|
||||
plugin_id, name = path.removeprefix('/framey/module/').split('/', 1)
|
||||
if not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module path')
|
||||
record = records[int(plugin_id)]
|
||||
data = (args.cache/(record['sha256']+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != record['sha256']:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
body = archive.read(record['folder']+'/dist/'+name)
|
||||
content_type = 'text/javascript; charset=utf-8'
|
||||
module_asset = True
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
self.send_error(404, 'Verified frontend module unavailable')
|
||||
return
|
||||
else:
|
||||
files = {'/framey-runtime/react.js': 'ui/vendor/react-18.3.1.min.js',
|
||||
'/framey-runtime/react-dom.js': 'ui/vendor/react-dom-18.3.1.min.js',
|
||||
@@ -124,6 +141,8 @@ def main():
|
||||
content_type = 'text/javascript'
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', content_type)
|
||||
if module_asset:
|
||||
self.send_header('Access-Control-Allow-Origin', 'null')
|
||||
self.send_header('Content-Security-Policy', "default-src 'none'; script-src 'self' 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data:; frame-src 'self'; connect-src 'none'")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
@@ -54,13 +54,16 @@ def render(data):
|
||||
'A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer '
|
||||
'through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, '
|
||||
'independent server readbacks and Mixer filter persistence across a runtime restart passed. '
|
||||
'These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. '
|
||||
'An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. '
|
||||
'A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs '
|
||||
'and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot '
|
||||
'to render Deck Settings game selection, Steamback\'s empty save panel and PlayTime statistics/settings. '
|
||||
'Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. '
|
||||
'Shotty\'s frontend now starts, but its published backend fails on missing `click`; it stays broken. '
|
||||
'Other official backends remain unexecuted. '
|
||||
'A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 '
|
||||
'through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. '
|
||||
'Its backend was denied; Steam Home shelves and worn VR remain unverified. '
|
||||
'A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. '
|
||||
'The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. '
|
||||
'No worn-VR official-plugin result is claimed. '
|
||||
'A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings '
|
||||
|
||||
@@ -260,6 +260,11 @@ print('{}')
|
||||
except Exception as exc:
|
||||
report['hiddenOverlayError'] = str(exc)
|
||||
raise RuntimeError('Frame Control overlay stayed hidden after focus retry; no input sent')
|
||||
if args.exercise:
|
||||
lines = remote(VRCMD + ' --overlays').splitlines()
|
||||
report['testOverlayListing'] = [line.strip() for index, line in enumerate(lines)
|
||||
if line.startswith("'" + KEY + "'") or
|
||||
(index and lines[index - 1].startswith("'" + KEY + "'"))]
|
||||
if args.wake or args.exercise:
|
||||
shot = remote.python((ROOT/'ui/frame_vrshot.py').read_text(), timeout=20)['path']
|
||||
try:
|
||||
|
||||
@@ -93,9 +93,11 @@ class Overlay:
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(mouse=Mouse(x, y, button)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def post_scroll(self, xdelta, ydelta):
|
||||
def post_scroll(self, xdelta, ydelta, kind=309):
|
||||
if kind not in (305, 309):
|
||||
raise ValueError('Only OpenVR discrete and smooth scroll events are allowed')
|
||||
view = self.interface(b'FnTable:IVROverlayView_003')
|
||||
event = Event(309, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def image_hash(self):
|
||||
|
||||
@@ -46,7 +46,7 @@ def exercise(profile, plugin_id, folder):
|
||||
|
||||
def text(): return panel.eval('document.body.innerText')
|
||||
|
||||
def click(selector):
|
||||
def point(selector):
|
||||
# Account for CSS pixels, iframe position, border, HiDPI scaling, and
|
||||
# OpenVR's bottom-left mouse origin; read geometry after scrolling.
|
||||
panel.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
|
||||
@@ -58,6 +58,10 @@ def exercise(profile, plugin_id, folder):
|
||||
y = sy - (outer['y'] + pos['y']) * sy / outer['h']
|
||||
if not 0 <= x <= sx or not 0 <= y <= sy:
|
||||
raise RuntimeError('Control is outside the visible viewport')
|
||||
return x, y
|
||||
|
||||
def click(selector):
|
||||
x, y = point(selector)
|
||||
count = panel.eval('__vrClickCount')
|
||||
vr.post_mouse(300, x, y)
|
||||
vr.post_mouse(301, x, y, 1)
|
||||
@@ -113,12 +117,39 @@ def exercise(profile, plugin_id, folder):
|
||||
# CDP only types text; OpenVR supplies pointer focus into the field.
|
||||
command(parent, 'Input.insertText', {'text':'framey-ci'})
|
||||
result['textValue'] = wait(lambda:panel.eval('document.querySelector("#vr-controls input[type=text]").value'), lambda v:v == 'framey-ci')
|
||||
panel.eval('window.scrollTo(0,0)')
|
||||
result['scrollBefore'] = panel.eval('scrollY')
|
||||
vr.post_scroll(0,-3)
|
||||
time.sleep(.5)
|
||||
result['scrollAfter'] = panel.eval('scrollY')
|
||||
# The sample alone may be shorter than its iframe. Use an owned,
|
||||
# visibly scrollable region so a zero offset is a meaningful result.
|
||||
panel.eval('''(()=>{
|
||||
const box=document.createElement('div');box.id='vr-scroll-probe';
|
||||
box.style.cssText='height:160px;overflow-y:scroll;border:2px solid #72baf0';
|
||||
box.innerHTML='<div style="height:700px;padding:8px">Scroll probe</div>';
|
||||
document.body.append(box);
|
||||
window.__vrWheelEvents=[];
|
||||
document.addEventListener('wheel',e=>__vrWheelEvents.push({trusted:e.isTrusted,
|
||||
deltaY:e.deltaY,target:e.target?.id||e.target?.tagName}),{capture:true,passive:true});
|
||||
return true;
|
||||
})()''')
|
||||
result['scrollGeometry'] = panel.eval('(()=>{let e=document.getElementById("vr-scroll-probe");return {height:e.clientHeight,content:e.scrollHeight}})()')
|
||||
if result['scrollGeometry']['content'] <= result['scrollGeometry']['height']:
|
||||
raise RuntimeError('Scroll diagnostic did not create overflow')
|
||||
x, y = point('#vr-scroll-probe')
|
||||
panel.eval('window.__vrScrollPointer=[];document.addEventListener("pointermove",e=>__vrScrollPointer.push({trusted:e.isTrusted,target:e.target?.id||e.target?.tagName}),{capture:true});true')
|
||||
vr.post_mouse(300, x, y)
|
||||
time.sleep(.2)
|
||||
result['scrollPointer'] = panel.eval('__vrScrollPointer')
|
||||
result['scrollBefore'] = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'] = []
|
||||
for kind in (309, 305):
|
||||
vr.post_scroll(0,-3,kind)
|
||||
time.sleep(.5)
|
||||
offset = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'].append({'eventType':kind,'offset':offset})
|
||||
if offset != result['scrollBefore']:
|
||||
break
|
||||
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
|
||||
result['scrollEvents'] = panel.eval('__vrWheelEvents')
|
||||
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
|
||||
panel.eval('document.getElementById("vr-scroll-probe").remove()')
|
||||
panel.eval('document.getElementById("vr-controls").remove()')
|
||||
# Scroll saved status into the visible iframe for compositor evidence.
|
||||
panel.eval('document.querySelector("[role=status]").scrollIntoView({block:"center"})')
|
||||
|
||||
@@ -177,6 +177,13 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
|
||||
expected = hashlib.sha256(bundle.read(plugin['folder']+'/dist/index.js')).hexdigest()
|
||||
self.assertEqual(ssh(f'sha256sum {installed}/dist/index.js').split()[0], expected)
|
||||
self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json')).get('api_version', 'legacy'), plugin['api_version'])
|
||||
if plugin['id'] == 135:
|
||||
chunk = 'chunks/pluginApi-B8sV7970.js'
|
||||
with zipfile.ZipFile(io.BytesIO(official_archive(plugin))) as bundle:
|
||||
expected_chunk = bundle.read(plugin['folder']+'/dist/'+chunk)
|
||||
self.assertEqual(
|
||||
ssh(f'sha256sum {installed}/dist/{chunk}').split()[0],
|
||||
hashlib.sha256(expected_chunk).hexdigest())
|
||||
job = finished(ok('POST', '/api/decky/plugin',
|
||||
{'action':'remove','id':plugin['id'],'acknowledged':True}))
|
||||
self.assertFalse(job.get('error'), job)
|
||||
|
||||
BIN
Binary file not shown.
+10
-1
@@ -7,20 +7,26 @@ from a local fixture so CI does not depend on the store being online.
|
||||
|
||||
| File | Official plugin | Version | Store SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `7.zip` | CSS Loader | 2.1.3 | `99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231` |
|
||||
| `12.zip` | MusicControl | 1.1.6 | `246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62` |
|
||||
| `14.zip` | ProtonDB Badges | 1.3.4 | `90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358` |
|
||||
| `123.zip` | Steam Status | 1.2.3 | `9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e` |
|
||||
| `50.zip` | Storage Cleaner | 1.4.0 | `1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591` |
|
||||
| `18.zip` | Bluetooth | 3.1.1 | `f07f7b74f31da15b5e3a8234a3f00faa01365b56b8f496aca7bb79120be7b8a5` |
|
||||
| `26.zip` | HLTB for Deck | 2.0.12 | `bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558` |
|
||||
| `36.zip` | SteamGridDB | 1.8.1 | `e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0` |
|
||||
| `135.zip` | Deck Shelves | 2.0.0 | `b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91` |
|
||||
|
||||
The matching source projects are
|
||||
[CSS Loader](https://github.com/suchmememanyskill/SDH-CssLoader),
|
||||
[MusicControl](https://github.com/nikurasu/MusicControl),
|
||||
[ProtonDB Badges](https://github.com/bschelst/protondb-decky),
|
||||
[Steam Status](https://github.com/bschelst/steamstatus-decky),
|
||||
[Storage Cleaner](https://github.com/mcarlucci/decky-storage-cleaner),
|
||||
[Bluetooth](https://github.com/Outpox/Bluetooth), and
|
||||
[HLTB for Deck](https://github.com/morwy/hltb-for-deck).
|
||||
[HLTB for Deck](https://github.com/morwy/hltb-for-deck),
|
||||
[SteamGridDB](https://github.com/SteamGridDB/decky-steamgriddb), and
|
||||
[Deck Shelves](https://github.com/santojon/Deck-Shelves).
|
||||
The test verifies the ZIP digest, archive policy, API v1/v2 manifest, installed
|
||||
frontend bytes, managed store receipt, and removal. Separate local host tests
|
||||
start Bluetooth's frontend and its initial panel without running effects or
|
||||
@@ -29,3 +35,6 @@ module lookup. They do **not** run plugin Python backends, Bluetooth actions,
|
||||
HLTB searches, or prove JavaScript panels render in the VR shell. The
|
||||
Bluetooth result and limits are in
|
||||
[the fixture evidence](../../../docs/evidence/framey-bluetooth-fixture-2026-10-25.md).
|
||||
Deck Shelves' chunked frontend renders its settings in local Chromium with
|
||||
backend/network calls denied. Its Steam Home shelves cannot be verified in the
|
||||
isolated VR panel; no worn-headset test is claimed.
|
||||
@@ -17,6 +17,8 @@ PLUGINS = (
|
||||
'folder': 'hltb-for-deck', 'sha256': 'bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558'},
|
||||
{'id': 36, 'name': 'SteamGridDB', 'version': '1.8.1', 'api_version': 1,
|
||||
'folder': 'decky-steamgriddb', 'sha256': 'e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0'},
|
||||
{'id': 135, 'name': 'Deck Shelves', 'version': '2.0.0', 'api_version': 1,
|
||||
'folder': 'Deck-Shelves', 'sha256': 'b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91'},
|
||||
)
|
||||
|
||||
# This archive has a legacy IIFE frontend with no manifest API version.
|
||||
|
||||
@@ -7,7 +7,8 @@ const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__s
|
||||
assert.notEqual(testSource,source);
|
||||
const states=[];
|
||||
const React={Fragment:'fragment',Component:class {},createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
useState:value=>[value,next=>states.push(next)], useEffect(){},isValidElement:value=>!!value?.type};
|
||||
useState:value=>[value,next=>states.push(next)], useRef:value=>({current:value}),
|
||||
useEffect(){},isValidElement:value=>!!value?.type};
|
||||
class Element {constructor(){this.children=[];this.textContent='';this.attributes={};}
|
||||
append(...children){this.children.push(...children);}setAttribute(key,value){this.attributes[key]=value;}
|
||||
remove(){this.removed=true;}}
|
||||
@@ -98,14 +99,33 @@ const plugin={content:{type:'content'},settings:{type:'settings'},titleView:{typ
|
||||
{label:'invalid'},...Array.from({length:9},(_,i)=>({label:`Action ${i}`,onClick:()=>{}}))]};
|
||||
const tree=window.__surface({plugin});
|
||||
const buttons=nodes(tree,'button');
|
||||
assert.equal(buttons.length,9); // Panel, Settings, and seven valid actions inside the first eight entries.
|
||||
assert.equal(buttons.length,10); // Panel, Settings, and eight valid actions after filtering.
|
||||
assert.equal(nodes(tree,'nav')[0].props['aria-label'],'Plugin views');
|
||||
assert.equal(nodes(tree,'section')[0].props['aria-label'],'Plugin quick actions');
|
||||
assert.equal(nodes(tree,'header').length,1);
|
||||
await buttons[2].props.onClick();
|
||||
assert.equal(called,1);
|
||||
assert.deepEqual(states.slice(-3),[0,'Refresh data…','Fresh',null].slice(-3));
|
||||
assert.equal(nodes(window.__surface({plugin:{settings:{type:'settings'}}}),'nav').length,1);
|
||||
let actionCount=0, settleAction;
|
||||
const guarded=window.__surface({plugin:{content:{type:'content'},frameyQuickActions:[{
|
||||
label:'Save setting',onClick:()=>{actionCount++;return new Promise((resolve,reject)=>{
|
||||
settleAction={resolve,reject};});}}]}});
|
||||
const saveButton=nodes(guarded,'button')[0];
|
||||
const firstSave=saveButton.props.onClick();
|
||||
await saveButton.props.onClick(); // React has not rendered disabled state yet.
|
||||
assert.equal(actionCount,1,'rapid second tap must not repeat a backend mutation');
|
||||
settleAction.reject(Error('offline'));
|
||||
await firstSave;
|
||||
const retrySave=saveButton.props.onClick();
|
||||
assert.equal(actionCount,2,'a failed action must release the synchronous lock');
|
||||
settleAction.resolve('Saved');
|
||||
await retrySave;
|
||||
const settingsOnly=window.__surface({plugin:{settings:{type:'settings'}}});
|
||||
assert.equal(nodes(settingsOnly,'nav').length,0);
|
||||
assert.equal(nodes(settingsOnly,'settings').length,1);
|
||||
const panelOnly=window.__surface({plugin:{content:{type:'content'}}});
|
||||
assert.equal(nodes(panelOnly,'nav').length,0);
|
||||
assert.equal(nodes(panelOnly,'content').length,1);
|
||||
|
||||
// API v2 backend calls remain request-scoped and errors return to the caller.
|
||||
const api=window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit.connect(2,'Test');
|
||||
@@ -165,7 +185,35 @@ const undo=api.routerHook.addRoute('/plugin/details',LocalRoute);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/details'),true);
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,1);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/decky/store'),false);
|
||||
assert.equal(window.DFL.findModuleChild(()=>true),null);
|
||||
// Bounded lookup uses DFL's module/export semantics, with only local exports.
|
||||
const dfl=window.DFL;
|
||||
const spinnerClasses=dfl.findClassModule(module=>!!module.SpinnerLoaderContainer);
|
||||
assert.equal(spinnerClasses.ContainerBackground,'framey-loading');
|
||||
assert.equal(dfl.findClass('framey/spinner','Medium'),'framey-medium');
|
||||
assert.equal(dfl.findClassByName('LoadingStatus'),'framey-status');
|
||||
assert.equal(dfl.unminifyClass('framey-loading'),'ContainerBackground');
|
||||
assert.equal(dfl.findClassModule(module=>module.SteamOnlyClass),undefined);
|
||||
assert.equal(dfl.findClass('steam/unknown','Medium'),undefined);
|
||||
assert.equal(dfl.findClassByName('SteamOnlyClass'),undefined);
|
||||
const details=dfl.findModuleDetailsByExport((value,name)=>name==='Spinner' && typeof value==='function');
|
||||
assert.equal(details[0].Spinner,dfl.Spinner);
|
||||
assert.equal(details[1],dfl.Spinner);
|
||||
assert.equal(details[2],'Spinner');assert.equal(details[3],'framey/components');
|
||||
assert.equal(dfl.findModule(module=>module.Spinner),details[0]);
|
||||
assert.equal(dfl.findModuleByExport((value,name)=>name==='Spinner'),details[0]);
|
||||
assert.equal(dfl.findModuleChild(module=>module.Spinner),dfl.Spinner);
|
||||
assert.equal(dfl.findModuleExport((value,name)=>name==='Spinner',1000),undefined);
|
||||
assert.equal(dfl.findModuleExport(value=>value.prototype.nonexistent),undefined);
|
||||
assert.equal(dfl.findModuleChild(()=>false),undefined);
|
||||
const mapping=dfl.createModuleMapping(module=>!!module.SpinnerLoaderContainer);
|
||||
assert.equal(mapping.size,1);assert.equal(mapping.get('framey/spinner'),spinnerClasses);
|
||||
mapping.clear();assert.equal(dfl.findAllModules(module=>!!module.SpinnerLoaderContainer).length,1);
|
||||
assert.equal(dfl.createModuleMapping(module=>module.SteamOnlyClass).size,0);
|
||||
const audio=dfl.findModuleExport(value=>value.GamepadUIAudio).GamepadUIAudio;
|
||||
assert.equal(audio.available,false);
|
||||
assert.throws(()=>audio.AudioPlaybackManager.PlayAudioURL('file:///sound.wav'),/not supported in Framey/);
|
||||
assert.equal(dfl.findModuleExport(value=>value.m_GamepadUIAudioStore),undefined);
|
||||
assert.equal(dfl.findModuleExport(value=>value.toString?.().includes('().LibraryContextMenu')),undefined);
|
||||
window.DFL.Navigation.CloseSideMenus();
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0);
|
||||
undo();assert.equal(window.DFL.Navigation.Navigate('/plugin/details'),false);
|
||||
|
||||
@@ -49,7 +49,7 @@ context.SP_JSX=window.SP_JSX;
|
||||
vm.runInContext(official,context,{timeout:2000});
|
||||
const plugin=window.__official();
|
||||
assert.equal(typeof window.DFL.findModuleChild,'function');
|
||||
assert.equal(window.DFL.findModuleChild(()=>{}),null); // Steam has no matching module in the VR panel.
|
||||
assert.equal(window.DFL.findModuleChild(()=>{}),undefined); // Steam has no matching module in the VR panel.
|
||||
assert.equal(plugin.content?.type?.name,'QuickAccessView');
|
||||
const panel=plugin.content.type();
|
||||
assert.ok(panel?.type); // Read-only settings render with empty local data.
|
||||
|
||||
@@ -8,7 +8,8 @@ const bundle=readFileSync(new URL('../../examples/framey-settings/dist/index.js'
|
||||
assert.match(bundle,/SP_JSX\.jsx/);
|
||||
const React={Fragment:Symbol('fragment'),Component:class {},
|
||||
createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
useState:value=>[value,()=>{}],useEffect(){},isValidElement:value=>!!value?.type};
|
||||
useState:value=>[value,()=>{}],useRef:value=>({current:value}),
|
||||
useEffect(){},isValidElement:value=>!!value?.type};
|
||||
class Element {
|
||||
constructor(){this.children=[];this.textContent='';this.attributes={};}
|
||||
append(...nodes){this.children.push(...nodes);}
|
||||
@@ -51,8 +52,12 @@ assert.equal(calls[0].method,'get_setting');
|
||||
assert.deepEqual(Array.from(calls[0].args),['enabled',false]);
|
||||
// Exercise the built sample's save path through the real Framey toast host.
|
||||
// Its JSX body must reach a React root, rather than stringify to an object.
|
||||
content.children[0][0].children[0].props.onChange(true);
|
||||
const toggle=content.children[0][0].children[0];
|
||||
toggle.props.onChange(true);
|
||||
toggle.props.onChange(false); // Before React redraws disabled, do not race the first write.
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(calls.filter(call=>call.method==='set_setting').length,1);
|
||||
assert.deepEqual(Array.from(calls.find(call=>call.method==='set_setting').args),['enabled',true]);
|
||||
const toast=elements.get('framey-toasts').children[0];
|
||||
assert.equal(toast.attributes.role,'status');
|
||||
assert.equal(toast.children[0].textContent,'Framey sample');
|
||||
@@ -62,6 +67,10 @@ assert.match(JSON.stringify(toastBody),/Setting saved/);
|
||||
assert.match(JSON.stringify(toastBody),/on/);
|
||||
toast.children[2].onclick();
|
||||
assert.equal(roots.get(toast.children[1]).unmounted,true);
|
||||
toggle.props.onChange(false);
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(calls.filter(call=>call.method==='set_setting').length,2,
|
||||
'a completed save must release the sample setting lock');
|
||||
assert.equal(window.DFL.Navigation.Navigate('/framey-sample/details/saved-setting'),true);
|
||||
assert.equal(window.DFL.useParams().topic,'saved-setting');
|
||||
assert.equal(window.DFL.Navigation.Navigate('/library/home'),false);
|
||||
|
||||
@@ -82,6 +82,38 @@ await vm.runInContext('deckyChange("update",{id:2,name:"Plugin 2",sha256:"b".rep
|
||||
assert.match(el('deckyJobStatus').textContent,/Error: archive digest mismatch/);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
|
||||
// Polling can lose the connection after the server accepted a plugin job.
|
||||
// Keep both store and installed controls disabled until its saved ID resolves.
|
||||
let changes=0;
|
||||
ctx.mockApi=async (path,body) => {
|
||||
if(path==='/api/decky/plugin'){changes++;return {job:'job-pending'};}
|
||||
if(path.startsWith('/api/job'))throw Error('connection lost');
|
||||
if(path==='/api/decky')return {managed:true,running:true,
|
||||
plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]};
|
||||
throw Error(`unexpected ${path}`);
|
||||
};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx);
|
||||
assert.equal(changes,1);
|
||||
assert.equal(session.has('frameyPluginJob'),true);
|
||||
assert.equal(el('deckyJobCheck').hidden,false);
|
||||
assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,true);
|
||||
assert.equal(el('deckyStoreList').children[1].children[1].disabled,true);
|
||||
await vm.runInContext('deckyChange("remove",{id:2,name:"Plugin 2"})',ctx);
|
||||
assert.equal(changes,1);
|
||||
assert.match(el('deckyJobStatus').textContent,/Check result before starting another/);
|
||||
ctx.mockApi=async path => {
|
||||
if(path.startsWith('/api/job'))return {done:true,message:'Installed Plugin 2'};
|
||||
if(path==='/api/decky')return {managed:true,running:true,
|
||||
plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]};
|
||||
throw Error(`unexpected ${path}`);
|
||||
};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('checkDeckyJob()',ctx);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,false);
|
||||
assert.equal(el('deckyStoreList').children[1].children[1].disabled,false);
|
||||
|
||||
// A failed refresh must remove old install buttons. A late older response
|
||||
// must not replace the newer catalog the wearer just requested.
|
||||
ctx.mockApi=async path => {assert.equal(path,'/api/decky/store');throw Error('offline');};
|
||||
@@ -117,6 +149,7 @@ await vm.runInContext('checkDeckyJob()',ctx);
|
||||
assert.match(el('deckyJobStatus').textContent,/app restarted and lost this job result/);
|
||||
assert.equal(el('deckyJobCheck').hidden,true);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
assert.equal(el('deckyStoreList').children[0].children[1].disabled,false);
|
||||
|
||||
// Known hardware blockers remain visible, cannot install, and keep their reason in the opened panel.
|
||||
ctx.hardware={id:900,name:'Hardware only',version:'1.0',description:'Fan control',root:false,
|
||||
@@ -131,3 +164,35 @@ assert.equal(el('deckyStoreList').children.length,0);
|
||||
vm.runInContext('openFrameyPanel(900,"Hardware only")',ctx);
|
||||
assert.match(el('frameyCompatibility').textContent,/Deck fan hardware/);
|
||||
assert.match(el('frameyCompatibility').textContent,/local browser/);
|
||||
|
||||
// A failed installed-status read must not leave stale Remove or Install actions.
|
||||
// Older responses must not replace a newer status, even if they arrive last.
|
||||
vm.runInContext('deckyStore=[{id:2,name:"Plugin 2",version:"1.0",description:"A panel",root:false}];' +
|
||||
'deckyState={running:true,plugins:[{storeId:2,name:"Plugin 2",version:"1.0"}]};showDeckyStore()',ctx);
|
||||
let statusCalls=[];
|
||||
ctx.mockApi=path=>{assert.equal(path,'/api/decky');
|
||||
return new Promise((resolve,reject)=>statusCalls.push({resolve,reject}));};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
const staleSuccess=vm.runInContext('refreshDecky()',ctx);
|
||||
const latestFailure=vm.runInContext('refreshDecky()',ctx);
|
||||
statusCalls[1].reject(Error('offline'));
|
||||
await latestFailure;
|
||||
assert.match(el('deckyState').textContent,/Installed plugins unavailable: offline/);
|
||||
assert.equal(el('deckyInstalled').children.length,0);
|
||||
assert.equal(el('deckyInstalled').textContent,'Refresh to check installed plugins.');
|
||||
assert.equal(el('deckyStoreList').children[0].children[1].disabled,true);
|
||||
statusCalls[0].resolve({running:true,plugins:[{storeId:2,name:'Stale',version:'1.0'}]});
|
||||
await staleSuccess;
|
||||
assert.match(el('deckyState').textContent,/offline/);
|
||||
assert.equal(el('deckyInstalled').children.length,0);
|
||||
|
||||
statusCalls=[];
|
||||
const staleFailure=vm.runInContext('refreshDecky()',ctx);
|
||||
const latestSuccess=vm.runInContext('refreshDecky()',ctx);
|
||||
statusCalls[1].resolve({managed:true,running:true,plugins:[{storeId:2,name:'Current',version:'1.0'}]});
|
||||
await latestSuccess;
|
||||
statusCalls[0].reject(Error('old offline error'));
|
||||
await staleFailure;
|
||||
assert.match(el('deckyInstalled').children[0].children[0].textContent,/Current/);
|
||||
assert.doesNotMatch(el('deckyState').textContent,/offline/);
|
||||
assert.equal(el('deckyStoreList').children[0].children[1].disabled,false);
|
||||
@@ -74,9 +74,14 @@ class OfficialBundles(unittest.TestCase):
|
||||
(folder / '.frame-control-store.json').write_text(json.dumps({'storeId': plugin['id']}))
|
||||
with patch.object(framey_panel, 'PLUGINS', plugins):
|
||||
page = framey_panel.panel(plugin['id']).decode()
|
||||
encoded = re.search(r'<script id="framey-bundle" type="application/json">([^<]+)</script>', page)
|
||||
encoded = re.search(r'<script id="framey-bundle" type="application/json"[^>]*>([^<]+)</script>', page)
|
||||
self.assertIsNotNone(encoded)
|
||||
self.assertEqual(base64.b64decode(encoded.group(1)), (folder / 'dist/index.js').read_bytes())
|
||||
if plugin['id'] == 135:
|
||||
self.assertIn('data-module="/framey/module/135/index.js"', page)
|
||||
with patch.object(framey_panel, 'PLUGINS', plugins):
|
||||
self.assertEqual(framey_panel.local_module(135, 'chunks/pluginApi-B8sV7970.js'),
|
||||
(folder/'dist/chunks/pluginApi-B8sV7970.js').read_bytes())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -187,6 +187,26 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/);
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
framey_panel.panel(14)
|
||||
|
||||
def test_chunked_frontend_serves_only_owned_javascript(self):
|
||||
(self.folder/'dist/index.js').write_text(
|
||||
"import './chunks/view.js'; export default () => ({content: 'OK'});")
|
||||
chunks = self.folder/'dist/chunks'
|
||||
chunks.mkdir()
|
||||
(chunks/'view.js').write_text('export const title = "Settings";')
|
||||
page = framey_panel.panel(14).decode()
|
||||
self.assertIn('data-module="/framey/module/14/index.js"', page)
|
||||
self.assertEqual(framey_panel.local_module(14, 'chunks/view.js'),
|
||||
b'export const title = "Settings";')
|
||||
for name in ('../plugin.json', 'chunks/../../plugin.json',
|
||||
'chunks/view.js.map', 'other.js', 'chunks/%2e%2e/view.js'):
|
||||
with self.subTest(name=name), self.assertRaises(FileNotFoundError):
|
||||
framey_panel.local_module(14, name)
|
||||
(chunks/'linked.js').symlink_to(chunks/'view.js')
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
framey_panel.local_module(14, 'chunks/linked.js')
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
framey_panel.local_module(15, 'chunks/view.js')
|
||||
|
||||
def test_legacy_keyword_rpc_is_scoped_and_validated(self):
|
||||
import shlex
|
||||
def fake_ssh(command, stdin=None, **_):
|
||||
@@ -273,6 +293,15 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/);
|
||||
self.assertIn(b'framey-bundle', body)
|
||||
self.assertEqual(get('/framey/panel/15')[0], 404)
|
||||
|
||||
(self.folder/'dist/chunks').mkdir()
|
||||
(self.folder/'dist/chunks/view.js').write_text('export const value = 1;')
|
||||
status, headers, body = get('/framey/module/14/chunks/view.js')
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(headers['Access-Control-Allow-Origin'], 'null')
|
||||
self.assertEqual(body, b'export const value = 1;')
|
||||
self.assertEqual(get('/framey/module/14/chunks/../plugin.json')[0], 404)
|
||||
self.assertEqual(get('/framey/module/15/chunks/view.js')[0], 404)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -105,6 +105,9 @@ class Safety(unittest.TestCase):
|
||||
overlay = importlib.util.module_from_spec(overlay_spec); overlay_spec.loader.exec_module(overlay)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Event), 60)
|
||||
self.assertEqual(overlay.Event.data.offset, 12)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Scroll), 16)
|
||||
with self.assertRaises(ValueError):
|
||||
object.__new__(overlay.Overlay).post_scroll(0, -3, 308)
|
||||
original = check.frame_headset.bundle()
|
||||
patched = check.debug_bundle(original, '/tmp/owned-profile')
|
||||
import tarfile
|
||||
|
||||
@@ -793,7 +793,7 @@
|
||||
"https://github.com/SteamDeckHomebrew/decky-loader"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Frontend audit failed: Cannot read properties of null (reading 'm_GamepadUIAudioStore').",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -804,7 +804,7 @@
|
||||
"id": 16,
|
||||
"name": "Audio Loader",
|
||||
"ready": false,
|
||||
"error": "Cannot read properties of null (reading 'm_GamepadUIAudioStore')",
|
||||
"error": "Cannot read properties of undefined (reading 'm_AudioPlaybackManager')",
|
||||
"logs": [
|
||||
{
|
||||
"kind": "warn",
|
||||
@@ -2784,7 +2784,7 @@
|
||||
"https://github.com/SteamGridDB/decky-steamgriddb"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: DFL.createModuleMapping is not a function.",
|
||||
"reason": "Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -2795,7 +2795,7 @@
|
||||
"id": 36,
|
||||
"name": "SteamGridDB",
|
||||
"ready": false,
|
||||
"error": "DFL.createModuleMapping is not a function",
|
||||
"error": "Cannot convert undefined or null to object",
|
||||
"logs": [],
|
||||
"methods": []
|
||||
},
|
||||
@@ -2990,14 +2990,15 @@
|
||||
"https://github.com/ldo/dbussy",
|
||||
"https://github.com/mk-fg/python-pulse-control"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium discovers and changes a private silent stream through the actual Decky 3.3.0 Linux backend, with independent Pulse readback. Frame ARM64 libraries/audio session and Bluetooth playback controls remain unverified.",
|
||||
"status": "works",
|
||||
"reason": "Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 38,
|
||||
@@ -4030,13 +4031,14 @@
|
||||
"https://github.com/saumya-banthia/volume-boost.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium sync/separate sliders, numeric volume entry and cache reopen pass through the actual Decky 3.3.0 backend on a private virtual stereo sink. Frame pactl/default sink, boost above 100% and physical audio remain unverified.",
|
||||
"reason": "Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 58,
|
||||
@@ -4403,8 +4405,8 @@
|
||||
"https://github.com/Tormak9970/TabMaster",
|
||||
"https://github.com/Tormak9970/TabMaster.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio').",
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -4414,10 +4416,17 @@
|
||||
"browser": {
|
||||
"id": 61,
|
||||
"name": "TabMaster",
|
||||
"ready": false,
|
||||
"error": "Cannot read properties of null (reading 'GamepadUIAudio')",
|
||||
"logs": [],
|
||||
"methods": []
|
||||
"ready": true,
|
||||
"error": null,
|
||||
"logs": [
|
||||
{
|
||||
"kind": "error",
|
||||
"message": "%c TabMaster %c ERROR %c background: #ff6d05; color: black; background: #c70808; color: black; background: transparent; Could not find mobx.reaction"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
"log_message"
|
||||
]
|
||||
},
|
||||
"archiveValidation": {
|
||||
"id": 61,
|
||||
@@ -6462,7 +6471,7 @@
|
||||
"https://github.com/ebenbruyns/junkstore.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Route registration succeeds; rendering fails on a missing Steam ContainerBackground component.",
|
||||
"reason": "Local CSS lookup fixes the loading view crash. Actual Decky 3.3.0 amd64 startup/static InitActions return a script execution/JSON error; the published scripts are not executable after extraction. Store install/login/game launch and ARM64 remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -6474,14 +6483,10 @@
|
||||
"name": "Junk-Store",
|
||||
"ready": true,
|
||||
"error": null,
|
||||
"logs": [
|
||||
{
|
||||
"kind": "error",
|
||||
"message": "TypeError: Cannot read properties of null (reading 'ContainerBackground')"
|
||||
}
|
||||
],
|
||||
"logs": [],
|
||||
"methods": [
|
||||
"fetch_rss_feed"
|
||||
"fetch_rss_feed",
|
||||
"execute_action"
|
||||
]
|
||||
},
|
||||
"archiveValidation": {
|
||||
@@ -8473,7 +8478,7 @@
|
||||
"https://github.com/jessebofill/DeckSP.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio').",
|
||||
"reason": "Optional UI-audio lookup succeeds, but the published frontend raises \"Cannot access PluginManager before initialization\" when Steam MobX reaction lookup is unavailable. JamesDSP installation/audio routing and ARM64 remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -8484,7 +8489,7 @@
|
||||
"id": 102,
|
||||
"name": "DeckSP",
|
||||
"ready": false,
|
||||
"error": "Cannot read properties of null (reading 'GamepadUIAudio')",
|
||||
"error": "Cannot access 'PluginManager' before initialization",
|
||||
"logs": [],
|
||||
"methods": []
|
||||
},
|
||||
@@ -11886,9 +11891,9 @@
|
||||
"archiveBytes": 862269,
|
||||
"api": 1,
|
||||
"flags": [],
|
||||
"modernBridge": false,
|
||||
"modernBridge": true,
|
||||
"frontend": true,
|
||||
"frontendApi": "unknown",
|
||||
"frontendApi": "1",
|
||||
"pythonFiles": [
|
||||
"Deck-Shelves/main.py"
|
||||
],
|
||||
@@ -11916,8 +11921,8 @@
|
||||
"https://github.com/santojon/Deck-Shelves.git",
|
||||
"https://github.com/sponsors/santojon"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Failed to resolve module specifier \"./chunks/pluginApi-B8sV7970.js\". Invalid relative url or base scheme isn't hierarchical..",
|
||||
"status": "partial",
|
||||
"reason": "The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -11951,6 +11956,12 @@
|
||||
"sha256": "b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"chunkedFollowUp": {
|
||||
"evidence": "docs/evidence/framey-deck-shelves-browser-2026-10-26.md",
|
||||
"mode": "exact archive in opaque local Chromium; backend and network denied",
|
||||
"frontendReady": true,
|
||||
"settingsRendered": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13263,14 +13274,15 @@
|
||||
"https://github.com/allmazz/decky-volume-mixer",
|
||||
"https://github.com/allmazz/decky-volume-mixer.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium numeric stream volume and filter save/reopen pass through the actual Decky 3.3.0 Linux backend; filters survive its restart. Frame pw-dump/wpctl, real audio session and worn VR remain unverified.",
|
||||
"status": "works",
|
||||
"reason": "Per-app discovery and numeric volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, independently read back on a silent test stream. Filters persist on amd64; ARM64 filter persistence, Reset, internal audio nodes, audible output and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 144,
|
||||
|
||||
+85
-12
@@ -288,6 +288,50 @@
|
||||
R.createElement('button', {type:'button',onClick:closeModal}, 'Close'), element));
|
||||
return {Close:closeModal};
|
||||
};
|
||||
// These are Framey's own styles and components, never Steam's webpack cache.
|
||||
// Keep the registry finite: an unknown Steam implementation stays unavailable.
|
||||
const classModules = new Map(Object.entries({
|
||||
'framey/spinner': {SpinnerLoaderContainer:'framey-spinner', ContainerBackground:'framey-loading',
|
||||
LoadingStatus:'framey-status', Medium:'framey-medium'},
|
||||
'framey/footer': {QuickAccessFooter:'framey-footer', BasicFooter:'framey-footer'},
|
||||
'framey/app-details': {AppDetailSectionList:'framey-sections', AppActionButton:'framey-action'},
|
||||
'framey/play-button': {PlayButtonContainer:'framey-actions', ButtonChild:'framey-button-child',
|
||||
Green:'framey-green', PlayButton:'framey-action'},
|
||||
'framey/library-image': {LongTitles:'framey-long-title', GreyBackground:'framey-muted', Image:'framey-image'},
|
||||
'framey/library': {GamepadLibrary:'framey-library'},
|
||||
'framey/focus': {FocusRingRoot:'framey-focus', FocusRing:'framey-focus'},
|
||||
}).map(([id, classes]) => [id, Object.freeze(classes)]));
|
||||
const localModules = new Map(classModules);
|
||||
const moduleVariants = module => [module?.default, module].filter(value => value != null);
|
||||
function findModule(filter) {
|
||||
for (const module of localModules.values())
|
||||
for (const value of moduleVariants(module)) if (filter(value)) return value;
|
||||
}
|
||||
function findModuleDetailsByExport(filter, minExports) {
|
||||
for (const [id, module] of localModules)
|
||||
for (const value of moduleVariants(module)) {
|
||||
if (typeof value !== 'object' || (minExports && Object.keys(value).length < minExports)) continue;
|
||||
for (const name in value) {
|
||||
if (!value[name]) continue;
|
||||
// DFL tolerates filters that dereference an incompatible export.
|
||||
try { if (filter(value[name], name)) return [value, value[name], name, id]; } catch (_) {}
|
||||
}
|
||||
}
|
||||
return [undefined, undefined, undefined, undefined];
|
||||
}
|
||||
function findModuleChild(filter) {
|
||||
for (const module of localModules.values())
|
||||
for (const value of moduleVariants(module)) {
|
||||
const result = filter(value);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
function createModuleMapping(filter) {
|
||||
const mapping = new Map();
|
||||
for (const [id, module] of localModules)
|
||||
for (const value of moduleVariants(module)) if (filter(value)) mapping.set(id, value);
|
||||
return mapping;
|
||||
}
|
||||
const DFL = window.DFL = {
|
||||
definePlugin: init => init,
|
||||
PanelSection, PanelSectionRow: passthrough('div'), Field, ToggleField, DropdownItem,
|
||||
@@ -301,6 +345,7 @@
|
||||
DialogHeader:passthrough('h2'), DialogSubHeader:passthrough('h3'), DialogFooter:passthrough('footer'),
|
||||
DialogLabel:passthrough('label'), DialogControlsSection:passthrough('section'),
|
||||
DialogControlsSectionHeader:passthrough('h3'), ModalRoot:passthrough('section'),
|
||||
ModalPosition:passthrough('div'),
|
||||
Menu:passthrough('div'), MenuGroup:passthrough('section'), MenuItem,
|
||||
ProgressBar, ProgressBarWithInfo:ProgressBar,
|
||||
gamepadDialogClasses:{}, quickAccessMenuClasses:{}, gamepadTabbedPageClasses:{},
|
||||
@@ -314,12 +359,30 @@
|
||||
showModal, showContextMenu:showModal,
|
||||
// Steam app, library, and store patches have no target inside this panel.
|
||||
afterPatch:noPatch, beforePatch:noPatch, replacePatch:noPatch, createReactTreePatcher:noPatch,
|
||||
findInReactTree: () => null, findModuleExport: () => null, findModuleChild: () => null,
|
||||
findModule:()=>null, findModuleByExport:()=>null, findClassModule:()=>null,
|
||||
findClass:()=>null, findSP:()=>null, fakeRenderComponent:()=>null,
|
||||
findInReactTree: () => null, findModule, findModuleChild, findModuleDetailsByExport,
|
||||
findModuleByExport:(filter, minExports)=>findModuleDetailsByExport(filter,minExports)[0],
|
||||
findModuleExport:(filter, minExports)=>findModuleDetailsByExport(filter,minExports)[1],
|
||||
createModuleMapping, findAllModules:filter=>[...createModuleMapping(filter).values()],
|
||||
findClassModule:filter=>[...classModules.values()].find(filter),
|
||||
findClass:(id,name)=>classModules.get(id)?.[name],
|
||||
findClassByName:name=>[...classModules.values()].find(classes=>classes[name])?.[name],
|
||||
unminifyClass:token=>{
|
||||
for(const classes of classModules.values())
|
||||
for(const name of Object.keys(classes)) if(classes[name]===token)return name;
|
||||
},
|
||||
findSP:()=>null, fakeRenderComponent:()=>null,
|
||||
appDetailsClasses: {}, appDetailsHeaderClasses: {},
|
||||
useParams: () => currentParams,
|
||||
};
|
||||
localModules.set('framey/components', Object.freeze(Object.fromEntries(
|
||||
Object.entries(DFL).filter(([name,value])=>typeof value==='function' && /^[A-Z]/.test(name)))));
|
||||
// Some panels eagerly look up the optional Steam UI sound player. Allow the
|
||||
// lookup, but never claim playback or expose Steam's mutable audio store.
|
||||
localModules.set('framey/ui-audio', Object.freeze({UIAudio:Object.freeze({
|
||||
GamepadUIAudio:Object.freeze({available:false, AudioPlaybackManager:Object.freeze({
|
||||
PlayAudioURL:()=>{throw Error('Steam UI sound playback is not supported in Framey');},
|
||||
})}),
|
||||
})}));
|
||||
// No Steam game is attached to an isolated panel. Subscriptions have a
|
||||
// disposable handle but emit no fabricated game, power, or input events.
|
||||
const subscription = () => {const dispose=()=>{};dispose.unregister=dispose;return dispose;};
|
||||
@@ -419,23 +482,27 @@
|
||||
const [route, setRoute] = R.useState(currentRoute);
|
||||
R.useEffect(() => {routeListeners.add(setRoute); return () => routeListeners.delete(setRoute);}, []);
|
||||
const [running, setRunning] = R.useState(null);
|
||||
const runningRef = R.useRef(false);
|
||||
const [message, setMessage] = R.useState('');
|
||||
const actions = Array.isArray(plugin.frameyQuickActions) ? plugin.frameyQuickActions.slice(0, 8)
|
||||
const actions = Array.isArray(plugin.frameyQuickActions) ? plugin.frameyQuickActions
|
||||
.filter(action => action && typeof action.label === 'string' && action.label.length <= 80 &&
|
||||
typeof action.onClick === 'function') : [];
|
||||
typeof action.onClick === 'function').slice(0, 8) : [];
|
||||
async function runAction(action, index) {
|
||||
if (running !== null) return;
|
||||
// React state may not render between two controller clicks. Lock before
|
||||
// awaiting a backend action so one tap cannot start a second mutation.
|
||||
if (runningRef.current) return;
|
||||
runningRef.current = true;
|
||||
setRunning(index); setMessage(`${action.label}…`);
|
||||
try {
|
||||
const result = await action.onClick();
|
||||
setMessage(typeof result === 'string' && result.length < 200 ? result : `${action.label} done`);
|
||||
} catch (failure) { setMessage(`${action.label} failed: ${failure.message || failure}`); }
|
||||
finally { setRunning(null); }
|
||||
finally { runningRef.current = false; setRunning(null); }
|
||||
}
|
||||
return R.createElement(R.Fragment, null,
|
||||
plugin.titleView && R.createElement('header', null, plugin.titleView),
|
||||
route && R.createElement('button', {type:'button', onClick:()=>navigate(null)}, 'Back to plugin'),
|
||||
plugin.settings && R.createElement('nav', {'aria-label':'Plugin views'},
|
||||
plugin.content && plugin.settings && R.createElement('nav', {'aria-label':'Plugin views'},
|
||||
R.createElement('button', {type:'button', 'aria-current':view === 'panel' ? 'page' : undefined,
|
||||
onClick:()=>setView('panel')}, 'Panel'),
|
||||
R.createElement('button', {type:'button', 'aria-current':view === 'settings' ? 'page' : undefined,
|
||||
@@ -506,7 +573,8 @@
|
||||
async function start() {
|
||||
try {
|
||||
await seeded;
|
||||
const bytes = Uint8Array.from(atob(document.getElementById('framey-bundle').textContent), c=>c.charCodeAt(0));
|
||||
const bundleNode = document.getElementById('framey-bundle');
|
||||
const bytes = Uint8Array.from(atob(bundleNode.textContent), c=>c.charCodeAt(0));
|
||||
const bundleText = new TextDecoder().decode(bytes);
|
||||
// Only panels referencing library globals need the extra read. A failed
|
||||
// read leaves them unavailable rather than fabricating an empty library.
|
||||
@@ -517,10 +585,15 @@
|
||||
// Old Decky builds are expressions returning the initializer, rather than
|
||||
// ES modules. A module wrapper preserves the sandbox and avoids eval.
|
||||
const legacy = /^\s*\(function\s*\(/.test(bundleText);
|
||||
const url = URL.createObjectURL(new Blob(legacy ?
|
||||
['const __frameyLegacy =\n', bytes, '\nexport default __frameyLegacy;'] : [bytes], {type:'text/javascript'}));
|
||||
let module;
|
||||
try { module = await import(url); } finally { URL.revokeObjectURL(url); }
|
||||
if (bundleNode.dataset.module && !legacy) {
|
||||
// A hierarchical URL lets official Vite chunks resolve relative imports.
|
||||
module = await import(new URL(bundleNode.dataset.module, location.href).href);
|
||||
} else {
|
||||
const url = URL.createObjectURL(new Blob(legacy ?
|
||||
['const __frameyLegacy =\n', bytes, '\nexport default __frameyLegacy;'] : [bytes], {type:'text/javascript'}));
|
||||
try { module = await import(url); } finally { URL.revokeObjectURL(url); }
|
||||
}
|
||||
if (typeof module.default !== 'function') throw Error('The Decky frontend has no plugin entry point');
|
||||
plugin = await module.default(legacyServerAPI);
|
||||
if (legacy && plugin?.title && !plugin.titleView) plugin.titleView = plugin.title;
|
||||
|
||||
+56
-8
@@ -11,6 +11,7 @@ import frame_decky
|
||||
METHOD = re.compile(r'[A-Za-z][A-Za-z0-9_]{0,63}\Z')
|
||||
EVENT = re.compile(r'[A-Za-z0-9_:/.-]{1,128}\Z')
|
||||
SESSION = re.compile(r'[A-Za-z0-9_.-]{1,100}\Z')
|
||||
MODULE_PATH = re.compile(r'[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*/[A-Za-z0-9_.-]+\.js\Z|index\.js\Z')
|
||||
PLUGINS = Path.home()/'.local/share/frame-control/decky/plugins'
|
||||
|
||||
|
||||
@@ -18,8 +19,8 @@ class PanelError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def local_bundle(plugin_id):
|
||||
"""Read a modern Decky bundle with a store receipt; reject linked paths."""
|
||||
def _owned_dist(plugin_id):
|
||||
"""Locate a compatible store-owned frontend without following linked paths."""
|
||||
if type(plugin_id) is not int or plugin_id <= 0 or not PLUGINS.is_dir() or PLUGINS.is_symlink():
|
||||
raise FileNotFoundError('Framey plugin unavailable')
|
||||
for folder in PLUGINS.iterdir():
|
||||
@@ -55,24 +56,71 @@ def local_bundle(plugin_id):
|
||||
legacy = re.match(rb'\s*\(function\s*\(', bundle) is not None
|
||||
if version is None and not legacy:
|
||||
raise FileNotFoundError('This legacy frontend is not a supported Decky IIFE bundle')
|
||||
if version == 1 and not legacy and b'__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit' not in bundle:
|
||||
raise FileNotFoundError('This API v1 frontend does not use the supported Decky API bridge')
|
||||
return bundle
|
||||
if version == 1 and not legacy:
|
||||
marker = b'__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit'
|
||||
supported = marker in bundle
|
||||
if not supported and b'./chunks/' in bundle:
|
||||
chunks = dist/'chunks'
|
||||
if chunks.is_symlink() or not chunks.is_dir():
|
||||
raise FileNotFoundError('Framey plugin chunks unavailable')
|
||||
files = list(chunks.iterdir())
|
||||
if len(files) > 128:
|
||||
raise FileNotFoundError('Framey plugin has too many chunks')
|
||||
for chunk in files:
|
||||
if chunk.suffix != '.js':
|
||||
continue
|
||||
if chunk.is_symlink() or not chunk.is_file() or chunk.stat().st_size > 8*1024*1024:
|
||||
raise FileNotFoundError('Framey plugin chunk unavailable')
|
||||
if marker in chunk.read_bytes():
|
||||
supported = True
|
||||
if not supported:
|
||||
raise FileNotFoundError('This API v1 frontend does not use the supported Decky API bridge')
|
||||
return dist, bundle
|
||||
except (OSError, ValueError):
|
||||
raise FileNotFoundError('Framey plugin files are invalid')
|
||||
raise FileNotFoundError('Plugin is not managed by the Frame Control Decky store')
|
||||
|
||||
|
||||
def local_bundle(plugin_id):
|
||||
"""Read a modern Decky bundle with a store receipt; reject linked paths."""
|
||||
return _owned_dist(plugin_id)[1]
|
||||
|
||||
|
||||
def local_module(plugin_id, name):
|
||||
"""Serve only JavaScript within a store-owned dist directory for module imports."""
|
||||
if not isinstance(name, str) or not MODULE_PATH.fullmatch(name):
|
||||
raise FileNotFoundError('Framey module path unavailable')
|
||||
dist, bundle = _owned_dist(plugin_id)
|
||||
if name == 'index.js':
|
||||
return bundle
|
||||
target = dist
|
||||
for part in name.split('/'):
|
||||
target = target/part
|
||||
if target.is_symlink():
|
||||
raise FileNotFoundError('Framey module path unavailable')
|
||||
if not target.is_file() or target.stat().st_size > 8*1024*1024:
|
||||
raise FileNotFoundError('Framey module unavailable')
|
||||
return target.read_bytes()
|
||||
|
||||
|
||||
def panel(plugin_id):
|
||||
"""The plugin receives no Frame UI key and is sandboxed to an opaque origin."""
|
||||
encoded = base64.b64encode(local_bundle(plugin_id)).decode('ascii')
|
||||
bundle = local_bundle(plugin_id)
|
||||
encoded = base64.b64encode(bundle).decode('ascii')
|
||||
module = f' data-module="/framey/module/{plugin_id}/index.js"' if b'./chunks/' in bundle else ''
|
||||
return ('''<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Framey plugin</title><style>body{font:21px/1.4 system-ui;background:#0d1925;color:#f3f7fa;margin:0;padding:20px}
|
||||
button,select,input{font:inherit;min-height:52px;background:#23618c;color:#fff;border:1px solid #72baf0;border-radius:9px;padding:8px}
|
||||
button:focus-visible,select:focus-visible,input:focus-visible{outline:4px solid #f8d370}button{margin:4px 8px 4px 0}button:disabled{opacity:.55}section{padding:12px;margin:12px 0;background:#1b3042;border-radius:10px}
|
||||
label{display:grid;gap:6px;margin:10px 0}small{color:#b9c9d6}</style>
|
||||
label{display:grid;gap:6px;margin:10px 0}small{color:#b9c9d6}
|
||||
.framey-spinner,.framey-loading{display:grid;place-items:center;min-height:80px}.framey-loading{background:#1b3042}
|
||||
.framey-status{color:#b9c9d6}.framey-medium{font-size:1em}.framey-footer{padding:12px 0}
|
||||
.framey-sections,.framey-library{display:flex;flex-direction:column;gap:12px}.framey-actions{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.framey-action{min-height:52px}.framey-button-child{display:inline-flex;align-items:center;gap:8px}.framey-green{background:#246b42}
|
||||
.framey-long-title{overflow-wrap:anywhere}.framey-muted{background:#1b3042}.framey-image{max-width:100%;object-fit:contain}
|
||||
.framey-focus:focus-visible{outline:4px solid #f8d370}</style>
|
||||
<p id="framey-error" role="status"></p><div id="framey-toasts" aria-live="polite"></div><div id="framey-root"></div><div id="framey-modal"></div>
|
||||
<script id="framey-bundle" type="application/json">''' + encoded + '''</script>
|
||||
<script id="framey-bundle" type="application/json"''' + module + '''>''' + encoded + '''</script>
|
||||
<script src="/framey-runtime/react.js"></script><script src="/framey-runtime/react-dom.js"></script>
|
||||
<script src="/framey-runtime/host.js"></script></html>''').encode()
|
||||
|
||||
|
||||
+21
-6
@@ -105,9 +105,13 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
|
||||
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
|
||||
}catch(error){list.textContent=error.message;}}
|
||||
function compatibilityText(plugin){const c=plugin.compatibility;return c ? `${c.status === 'na' ? 'N/A' : c.status}: ${c.reason}${c.verification?.length ? ' Verified: '+c.verification.join(', ')+'.' : ''}` : 'Unverified on Frame';}
|
||||
function deckyJobPending(){return !!sessionStorage.getItem('frameyPluginJob');}
|
||||
let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0,
|
||||
deckyStatusRequest=0,
|
||||
activeFrameyId=null, frameySession=null, deckyBusy=false;
|
||||
async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
async function refreshDecky(){const request=++deckyStatusRequest;try{const state=await api('/api/decky');
|
||||
if(request!==deckyStatusRequest)return;
|
||||
deckyState=state;
|
||||
byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.':
|
||||
deckyState.managed?'Decky is stopped. You can remove a managed plugin to recover, then start Decky from Frame Control on your computer.':
|
||||
'Decky is not running. Install it from Frame Control on your computer.';
|
||||
@@ -118,9 +122,14 @@ async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
if(plugin.storeId){const open=document.createElement('button');open.className='secondary';open.textContent='Open panel';
|
||||
open.onclick=()=>openFrameyPanel(plugin.storeId,plugin.name);row.append(open);
|
||||
const button=document.createElement('button');button.className='danger';button.textContent='Remove';
|
||||
button.disabled=deckyBusy;button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);}
|
||||
button.disabled=deckyBusy||deckyJobPending();button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);}
|
||||
list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.';
|
||||
showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}}
|
||||
showDeckyStore();}catch(error){if(request!==deckyStatusRequest)return;
|
||||
deckyState=null;
|
||||
byId('deckyState').textContent=`Installed plugins unavailable: ${error.message}`;
|
||||
byId('deckyInstalled').replaceChildren();
|
||||
byId('deckyInstalled').textContent='Refresh to check installed plugins.';
|
||||
showDeckyStore();}}
|
||||
async function loadDeckyStore(){const request=++deckyStoreRequest;byId('deckyStorePanel').hidden=false;
|
||||
deckyStore=[];deckyStoreIndex=0;showDeckyStore();
|
||||
byId('deckyStoreSummary').textContent='Loading official Decky store…';
|
||||
@@ -147,7 +156,7 @@ function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren
|
||||
const button=document.createElement('button');button.textContent='Install';
|
||||
const installed=(deckyState?.plugins||[]).find(p=>p.storeId===plugin.id);
|
||||
if(installed)button.textContent=installed.version===plugin.version?'Reinstall':'Update';
|
||||
button.disabled=deckyBusy||!deckyState?.running||plugin.root||!!plugin.compatibility?.blockInstall;
|
||||
button.disabled=deckyBusy||deckyJobPending()||!deckyState?.running||plugin.root||!!plugin.compatibility?.blockInstall;
|
||||
const note=document.createElement('p');note.textContent=compatibilityText(plugin);label.append(note);
|
||||
if(plugin.root && !plugin.compatibility?.blockInstall)label.append(' · Needs root; not supported on Frame');
|
||||
button.onclick=()=>deckyChange(installed?'update':'install',plugin);row.append(label,button);list.append(row);}}
|
||||
@@ -175,7 +184,12 @@ async function followDeckyJob(job,action,name){
|
||||
}
|
||||
throw Error('Still working. Use Check result to reconnect to this job.');
|
||||
}
|
||||
async function deckyChange(action,plugin){if(deckyBusy||!confirm(`${action} ${plugin.name}? Store plugins run code as your Frame user.`))return;
|
||||
async function deckyChange(action,plugin){if(deckyBusy)return;
|
||||
if(deckyJobPending()){
|
||||
deckyJobView('Previous plugin change','Check result before starting another plugin change.',null,true);
|
||||
return;
|
||||
}
|
||||
if(!confirm(`${action} ${plugin.name}? Store plugins run code as your Frame user.`))return;
|
||||
deckyBusy=true;showDeckyStore();
|
||||
deckyJobView(`${action} ${plugin.name}`,'Starting…',0);
|
||||
try{const started=await api('/api/decky/plugin',{action,id:plugin.id,
|
||||
@@ -188,7 +202,8 @@ async function deckyChange(action,plugin){if(deckyBusy||!confirm(`${action} ${pl
|
||||
finally{deckyBusy=false;await refreshDecky();}
|
||||
}
|
||||
async function checkDeckyJob(){let saved;try{saved=JSON.parse(sessionStorage.getItem('frameyPluginJob')||'null');}catch(_){saved=null;}
|
||||
if(!saved||typeof saved.job!=='string'){sessionStorage.removeItem('frameyPluginJob');return;}
|
||||
if(deckyBusy)return;
|
||||
if(!saved||typeof saved.job!=='string'){sessionStorage.removeItem('frameyPluginJob');showDeckyStore();return;}
|
||||
deckyBusy=true;byId('deckyJobCheck').hidden=true;
|
||||
try{await followDeckyJob(saved.job,saved.action,saved.name);sessionStorage.removeItem('frameyPluginJob');}
|
||||
catch(error){deckyJobView(`${saved.action} ${saved.name}`,`Error: ${error.message}`,null,true);}
|
||||
|
||||
@@ -3691,6 +3691,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_bytes(framey_panel.panel(plugin_id), 'text/html; charset=utf-8', framey=True)
|
||||
except (ValueError, FileNotFoundError, OSError):
|
||||
self.send_json({'error':'Framey panel unavailable'}, 404)
|
||||
elif path.startswith('/framey/module/'):
|
||||
try:
|
||||
plugin_id, name = path.removeprefix('/framey/module/').split('/', 1)
|
||||
module = framey_panel.local_module(int(plugin_id), name)
|
||||
# The plugin iframe has an opaque origin; only its module
|
||||
# imports need cross-origin access to these public JS files.
|
||||
self.send_bytes(module, 'text/javascript; charset=utf-8', framey=True,
|
||||
headers=[('Access-Control-Allow-Origin', 'null')])
|
||||
except (ValueError, FileNotFoundError, OSError):
|
||||
self.send_json({'error':'Framey module unavailable'}, 404)
|
||||
elif path.startswith('/framey-runtime/'):
|
||||
files = {'react.js':'vendor/react-18.3.1.min.js',
|
||||
'react-dom.js':'vendor/react-dom-18.3.1.min.js',
|
||||
|
||||
Reference in new issue
Block a user