Compare commits

...
Author SHA1 Message Date
SAPHID 657b4449f7 Document v0.4.40 release and post-tag Frame verification 2026-10-26 04:16:43 +00:00
Alex Southwell 784118b71e Merge pull request #292 from saphid/fix/framey-quick-action-guard
Guard Framey quick actions against repeated taps
2026-10-26 04:13:32 +00:00
SAPHID e0115f356d Guard Framey quick actions against repeated taps 2026-10-26 04:04:53 +00:00
Alex Southwell 56bde24be1 Merge pull request #290 from saphid/fix/framey-pending-job
Hold Framey plugin changes until saved job resolves
2026-10-26 03:56:02 +00:00
Alex Southwell 59c270a7ad Merge pull request #289 from saphid/feat/framey-module-exports
Enable local Decky module lookups and TabMaster docs
2026-10-26 03:55:43 +00:00
Alex Southwell a4d8073e31 Merge pull request #291 from saphid/release/v0.4.40
Prepare Frame Control v0.4.40
2026-10-26 03:54:16 +00:00
SAPHID c0275b483a Record combined main regression checks for Framey module lookup 2026-10-26 03:39:24 +00:00
SAPHID ff49b44350 Record v0.4.40 candidate real Frame gates 2026-10-26 03:39:09 +00:00
SAPHID 7e6fb7e342 Keep Framey store locked while job result is unknown 2026-10-26 03:38:51 +00:00
SAPHID 57c1c4564b Merge remote-tracking branch 'origin/main' into feat/framey-module-exports
# Conflicts:
#	docs/framey-compatibility.md
#	scripts/framey-compatibility-doc.py
2026-10-26 03:35:18 +00:00
SAPHID 9aa26a8322 Merge remote-tracking branch 'origin/main' into release/v0.4.40 2026-10-26 03:34:08 +00:00
Alex Southwell 38c388f3f3 Merge pull request #288 from saphid/fix/framey-stale-status
Clear stale Framey store status after refresh failures
2026-10-26 03:33:52 +00:00
Alex Southwell 27e3c42ed0 Merge pull request #286 from saphid/feat/framey-chunked-frontend
Load chunked Decky frontends in Framey
2026-10-26 03:33:21 +00:00
SAPHID c2be12fa75 Merge remote-tracking branch 'origin/main' into feat/framey-module-exports 2026-10-26 03:32:53 +00:00
SAPHID c164606fe3 Support bounded Framey-local Decky module and CSS lookup 2026-10-26 03:32:53 +00:00
SAPHID 8d9d4807c3 Prepare Frame Control v0.4.40 version 2026-10-26 03:26:00 +00:00
SAPHID 7f8a743cb5 Clarify real Frame gate scope in Deck Shelves evidence 2026-10-26 03:21:40 +00:00
SAPHID 40df801cdb Clear stale Framey store state after status failure 2026-10-26 03:21:16 +00:00
SAPHID 35d53e6010 Merge remote-tracking branch 'origin/main' into feat/framey-chunked-frontend 2026-10-26 03:21:10 +00:00
SAPHID 1b02d7f558 Record exact Frame gates and verify installed chunk on fake Frame 2026-10-26 03:21:09 +00:00
Alex Southwell edfa48d1f5 Merge pull request #287 from saphid/fix/framey-settings-only-panel
Framey: hide empty Panel tab for settings-only plugins
2026-10-26 03:20:12 +00:00
Alex Southwell c89ddffcbd Merge pull request #285 from saphid/docs/mac-v0439-runtime
Document official Intel Mac runtime test and x64 download
2026-10-26 03:12:42 +00:00
Alex Southwell 5183f30068 Merge pull request #284 from saphid/fix/mac-server-shutdown
Fix server cleanup interrupted by late SIGTERM on Mac quit
2026-10-26 03:12:06 +00:00
SAPHID 37e2faacc7 Hide empty Panel tab for settings-only Framey plugins 2026-10-26 03:10:40 +00:00
SAPHID b0c4647b85 Support chunked official Decky frontends in Framey 2026-10-26 03:09:57 +00:00
Alex Southwell 2b6b05a175 Let SIGTERM request server shutdown without interrupting cleanup 2026-10-26 13:59:17 +11:00
Alex Southwell 5c9ca110ed Merge pull request #283 from saphid/docs/v0439-release-evidence
Document v0.4.39 tagged release evidence
2026-10-26 02:57:01 +00:00
SAPHID e3540cab5e Separate released and post-release Framey coverage 2026-10-26 02:41:33 +00:00
SAPHID 0eaa9f2088 Record post-release Framey hardware checks 2026-10-26 02:39:39 +00:00
SAPHID 59f7348aa5 Merge remote-tracking branch 'origin/main' into docs/v0439-release-evidence 2026-10-26 02:39:07 +00:00
SAPHID 67a95d4f4c Document v0.4.39 tagged release verification 2026-10-26 02:31:10 +00:00
32 changed files with 1109 additions and 86 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.39",
"version": "0.4.40",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.39",
"version": "0.4.40",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.39",
"version": "0.4.40",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
+6 -6
View File
@@ -1,11 +1,11 @@
# 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.40**, 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): 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 has physical-Mac regression coverage, not a released fix | 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 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.40 tagged source against the real Frame; all 13 reserved native/Proton title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.40 tag `a4d8073`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0440-release-2026-10-26.md); [earlier candidate smoke](evidence/v0440-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) |
@@ -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.40 classifies the 116 exact-version plugins as 1 works (Notebook default flow only), 49 partial, 55 broken and 11 not applicable. Post-tag PR #289 changes TabMaster to partial, giving 1/50/54/11 on current main; #293 audio evidence is still in CI. 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. 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.40: [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), [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-#292 unworn sample evidence](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [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,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.
+212
View File
@@ -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
}
}
}
+25
View File
@@ -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.
+45
View File
@@ -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.
+1 -1
View File
@@ -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 |
+8 -8
View File
@@ -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: 1 · Partial: 50 · 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. 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. 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,7 +55,7 @@ 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 |
| 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 |
@@ -71,7 +71,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 |
| 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 |
+18 -3
View File
@@ -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)
+19
View File
@@ -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)
+4 -1
View File
@@ -60,7 +60,10 @@ def render(data):
'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 '
+7
View File
@@ -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)
Binary file not shown.
+10 -1
View File
@@ -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.
+2
View File
@@ -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.
+52 -4
View File
@@ -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);
+1 -1
View File
@@ -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.
+65
View File
@@ -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);
+6 -1
View File
@@ -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__':
+29
View File
@@ -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()
+20 -3
View File
@@ -426,10 +426,27 @@ class OneServer(unittest.TestCase):
# Stopped as the app stops it: by closing stdin (the only way on Windows,
# where terminate() is a hard kill) and, elsewhere, by SIGTERM too.
for stop in ["stdin"] + (["sigterm"] if os.name != "nt" else []):
for stop in ["stdin"] + (["sigterm", "stdin+late-sigterm"] if os.name != "nt" else []):
with self.subTest(stop=stop):
left_by_dead = [staged(tmp, "frame-vr-", dead.pid), staged(index_dir, ".download-", dead.pid)]
proc = subprocess.Popen([sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0", "--exit-on-eof"],
command = [sys.executable, str(ROOT / "ui" / "server.py"), "--port", "0", "--exit-on-eof"]
if stop == "stdin+late-sigterm":
# Deliver the app's second stop mechanism exactly as cleanup
# replaces the signal handler: the race observed on a real Mac.
wrapper = (
"import os, runpy, signal, sys; from pathlib import Path\n"
"original = signal.signal\n"
"def install(sig, handler):\n"
" if sig == signal.SIGTERM and handler == signal.SIG_IGN:\n"
" os.kill(os.getpid(), signal.SIGTERM)\n"
" return original(sig, handler)\n"
"signal.signal = install\n"
"sys.argv = sys.argv[1:]\n"
"sys.path.insert(0, str(Path(sys.argv[0]).parent))\n"
"runpy.run_path(sys.argv[0], run_name='__main__')\n"
)
command = [sys.executable, "-c", wrapper, *command[1:]]
proc = subprocess.Popen(command,
env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
try:
@@ -437,7 +454,7 @@ class OneServer(unittest.TestCase):
self.assertEqual([d for d in left_by_dead if d.exists()], [])
in_flight = [staged(tmp, "frame-vr-", proc.pid), staged(tmp, "frame-agent-", proc.pid),
staged(index_dir, ".download-", proc.pid)]
if stop == "stdin":
if stop.startswith("stdin"):
proc.stdin.close()
else:
proc.terminate()
+33 -24
View File
@@ -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": []
},
@@ -4403,8 +4403,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 +4414,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 +6469,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 +6481,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 +8476,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 +8487,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 +11889,9 @@
"archiveBytes": 862269,
"api": 1,
"flags": [],
"modernBridge": false,
"modernBridge": true,
"frontend": true,
"frontendApi": "unknown",
"frontendApi": "1",
"pythonFiles": [
"Deck-Shelves/main.py"
],
@@ -11916,8 +11919,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 +11954,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
}
},
{
+85 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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);}
+17 -4
View File
@@ -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',
@@ -4172,8 +4182,12 @@ def main():
LINK.start()
if not PRIVATE: # one worker per user, in the app's server
threading.Thread(target=autosync.run, args=(_stop_sync,), daemon=True).start()
def request_shutdown(*_):
# shutdown() must run outside the serve_forever thread. Do not raise
# from SIGTERM: the app can send it after stdin EOF has begun cleanup.
threading.Thread(target=httpd.shutdown, daemon=True).start()
if not frame_host.WINDOWS:
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))
signal.signal(signal.SIGTERM, request_shutdown)
if args.exit_on_eof:
def watch_stdin():
# os.read, not sys.stdin.buffer.read: a buffered read holds stdin's lock,
@@ -4181,7 +4195,7 @@ def main():
# when it can't take that lock back from this thread.
while os.read(0, 4096):
pass
threading.Thread(target=httpd.shutdown, daemon=True).start()
request_shutdown()
threading.Thread(target=watch_stdin, daemon=True).start()
try:
# The real port, which --port 0 leaves to the system (the iPhone app reads it from here).
@@ -4190,8 +4204,7 @@ def main():
except KeyboardInterrupt:
pass
finally:
# The app both closes stdin and sends SIGTERM on quit; a second signal
# mid-cleanup would abort it and leave the SSH master running.
# Cleanup is already underway; later stop signals need no more workers.
if not frame_host.WINDOWS:
signal.signal(signal.SIGTERM, signal.SIG_IGN)
_power.shutdown()