mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 12:02:05 +01:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e526cb4b | ||
|
|
8230fdb19d | ||
|
|
2937a12864 | ||
|
|
71fe4a3ebb | ||
|
|
42f3b2e1e7 | ||
|
|
ca4c370b33 | ||
|
|
c50f96510f | ||
|
|
43e801091a | ||
|
|
d84491c0ec | ||
|
|
83b8d13846 |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.43",
|
||||
"version": "0.4.44",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.43",
|
||||
"version": "0.4.44",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.43",
|
||||
"version": "0.4.44",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"checkedAtUtc": "2026-10-26T08:14:37.777131+00:00",
|
||||
"plugin": {
|
||||
"id": 84,
|
||||
"name": "Junk-Store",
|
||||
"version": "1.1.13",
|
||||
"sha256": "f7ad2a0103c85bf308929b2e96ec9aeeb0d76bcc6ae97eb1e4b5f31ffba0cbb1"
|
||||
},
|
||||
"projection": {
|
||||
"field": "local_per_client_data.installed",
|
||||
"wire": "optional ninth scalar: boolean or null",
|
||||
"unknown": "null creates an empty detached metadata object; installed stays undefined",
|
||||
"privateFieldsCopied": false,
|
||||
"oldEightScalarRows": "accepted; running updates preserve existing library metadata",
|
||||
"liveObservation": false
|
||||
},
|
||||
"realFrame": {
|
||||
"os": "Frame OS 0.5.6",
|
||||
"arch": "aarch64",
|
||||
"uid": 1000,
|
||||
"decky": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"transport": "trusted SSH through lxso2",
|
||||
"sharedLock": true,
|
||||
"runtime": "private bounded removable user service; not enabled",
|
||||
"library": {
|
||||
"apps": 1106,
|
||||
"games": 895,
|
||||
"installedTrue": 41,
|
||||
"installedFalse": 0,
|
||||
"installedUnknown": 1065
|
||||
},
|
||||
"protonRuntimeInstalledFields": "undefined for both EasyAntiCheat and BattlEye; no false value invented",
|
||||
"legendaryInstalled": false,
|
||||
"allowedMethods": [
|
||||
"execute_action",
|
||||
"get_websocket_port"
|
||||
],
|
||||
"staticActions": [
|
||||
"MainMenu/GetContent",
|
||||
"init/InitActions"
|
||||
],
|
||||
"noMutationActions": true
|
||||
},
|
||||
"localBrowser": {
|
||||
"host": "actual headset.html/framey-host.js, vendored React, opaque iframe",
|
||||
"backend": "exact official ARM64 plugin through production receipt-bound bridge",
|
||||
"library": "production receipt-bound snapshot from real Frame CEF",
|
||||
"outerStatusInstallCheck": "fixture responses",
|
||||
"passed": [
|
||||
"main menu",
|
||||
"About content and tab list",
|
||||
"Dependencies tab controls render; no buttons invoked",
|
||||
"Back to main menu",
|
||||
"close/reopen and About again"
|
||||
],
|
||||
"limits": [
|
||||
"optional localhost websocket blocked by test relay CSP",
|
||||
"RSS calls denied",
|
||||
"running-context relay returns fixture/unavailable data; no lifetime claims",
|
||||
"outer extension/status fixture is incomplete; unrelated outer status/extension errors excluded from plugin claim",
|
||||
"deployed headset HTTP endpoint unverified",
|
||||
"worn VR unverified"
|
||||
]
|
||||
},
|
||||
"cleanup": {
|
||||
"initialSshStop": "timed out while test unit deactivating",
|
||||
"recovery": "SIGKILL to exact owned unit only after matching lock owner; private files removed",
|
||||
"result": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"archiveAbsent": true,
|
||||
"state": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n",
|
||||
"normal": {
|
||||
"frame-control-decky.service": "inactive",
|
||||
"frame-control-headset.service": "inactive"
|
||||
},
|
||||
"battery": [
|
||||
99
|
||||
]
|
||||
},
|
||||
"freshLockedAudit": {
|
||||
"services": 1,
|
||||
"allArtifactsAbsent": true,
|
||||
"allUnloadedInactivePidZero": true
|
||||
},
|
||||
"lockReleased": true,
|
||||
"batteryBeforeAfter": [
|
||||
[
|
||||
99
|
||||
],
|
||||
[
|
||||
99
|
||||
]
|
||||
],
|
||||
"powerSettingChanged": false,
|
||||
"localRelayAndBrowserClosed": true
|
||||
},
|
||||
"verification": {
|
||||
"python": {
|
||||
"run": 1628,
|
||||
"skipped": 16,
|
||||
"failures": 0
|
||||
},
|
||||
"panelFocused": {
|
||||
"run": 22,
|
||||
"failures": 0
|
||||
},
|
||||
"nodeContracts": [
|
||||
"framey_actions.mjs",
|
||||
"framey_running.mjs"
|
||||
],
|
||||
"siteTestFiles": {
|
||||
"passed": 2,
|
||||
"failures": 0
|
||||
},
|
||||
"fakeFrame": {
|
||||
"blocked": 56,
|
||||
"installed-removed": 60
|
||||
},
|
||||
"officialStore": {
|
||||
"plugins": 116,
|
||||
"errors": 0,
|
||||
"identitySetUnchanged": true,
|
||||
"checkedAtUtc": "2026-10-26T08:21:01.653768+00:00"
|
||||
}
|
||||
},
|
||||
"status": "partial",
|
||||
"unverified": [
|
||||
"dependency/runtime installation",
|
||||
"login",
|
||||
"game listing/download/launch",
|
||||
"RSS",
|
||||
"optional websocket operations",
|
||||
"Steam shortcut mutation",
|
||||
"worn VR"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Junk-Store About and read-only installed metadata
|
||||
|
||||
Junk-Store 1.1.13 (official ID 84) now renders its About and Dependencies tabs
|
||||
without the `local_per_client_data.installed` crash recorded in the
|
||||
[script-entrypoint audit](framey-script-entrypoints-2026-10-26.md). It remains
|
||||
**partial**. The catalog and both store views use the same updated reason.
|
||||
|
||||
The library snapshot omitted the entire `local_per_client_data` object. The
|
||||
official frontend dereferences it to display its dependency controls. Under the
|
||||
shared headset lock, a read-only probe confirmed that the real Frame exposes
|
||||
this object for the two Proton runtimes, but neither has an `installed` boolean.
|
||||
Across 1,106 app overviews, 41 had `installed: true`, none had `false`, and 1,065
|
||||
had no installed boolean. Those absent values must not be relabelled as false.
|
||||
|
||||
The snapshot adds one bounded scalar per app: a Steam-supplied boolean or null.
|
||||
Framey's detached overview creates a minimal metadata object, copying `installed`
|
||||
only for a boolean. Null leaves that property undefined. No client IDs, paths,
|
||||
other metadata, live Steam objects or methods cross the iframe boundary. Eight
|
||||
scalar rows remain accepted, and running-app updates preserve library metadata.
|
||||
The snapshot still refreshes at panel open; it is not a live installation feed.
|
||||
|
||||
## Actual ARM64 backend and local Chromium
|
||||
|
||||
The unmodified, hash-verified official archive ran in published Decky 3.3.0 ARM64
|
||||
on Frame OS 0.5.6 using a private, bounded, removable user service. Legendary was
|
||||
absent; no dependency installer, runtime install, login, game operation or custom
|
||||
backend download was invoked. The exact plugin and loader digests are in the
|
||||
[redacted results](framey-installed-context-2026-10-26.json).
|
||||
|
||||
Local Chromium ran the actual Framey host and official frontend in an opaque
|
||||
iframe. The production receipt-bound bridges read the real Steam library and
|
||||
called the official ARM64 backend's static menu actions. About content and its
|
||||
tab list, Dependencies controls, Back, close/reopen, and About again passed.
|
||||
No dependency buttons were pressed. The real library counts matched the probe.
|
||||
|
||||
The optional localhost websocket was blocked by the test relay CSP; RSS was
|
||||
deliberately denied. Outer status/install/check replies were fixtures; incomplete
|
||||
outer extension/status and running-context replies are excluded from the plugin
|
||||
claim. The screenshot still used the catalog reason from before this follow-up.
|
||||
Dependency installation, login, game operations, RSS, websocket operations,
|
||||
shortcut changes, deployed headset HTTP and worn VR remain unverified.
|
||||
|
||||
## Cleanup and regression checks
|
||||
|
||||
The initial cleanup SSH timed out while the private Decky unit was deactivating.
|
||||
Recovery checked the matching lock owner, killed only that exact test unit, and
|
||||
removed its private base, unit file and archive. A fresh locked audit independently
|
||||
confirmed unloaded/inactive/PID 0 and no artifacts, then released its lock. Normal
|
||||
managed Decky/panel services stayed inactive; battery was 99 → 99. No power-setting
|
||||
action ran. Local relay and owned browser tab are closed.
|
||||
|
||||
- Full Python suite: 1,628 tests, 16 skipped, no failures; focused panel: 22 passed.
|
||||
- Framey action/running Node contracts passed, including true/false/unknown,
|
||||
malformed metadata, detached values, old rows and running-update preservation.
|
||||
- Both site test files passed. Generated docs and diff checks passed.
|
||||
- All 116 exact official archives passed the fake-Frame audit: 60 installed/removed,
|
||||
56 expected blocks; disposable project removed. Backend execution is stubbed in
|
||||
that harness. The fresh official store has 116 entries, no audit errors, and an
|
||||
unchanged ID/version/digest set.
|
||||
|
||||
No new works claim is made. Catalog totals remain **4 works / 49 partial / 52 broken
|
||||
/ 11 N/A**; only Junk-Store's verified scope and remaining limits change.
|
||||
@@ -0,0 +1,79 @@
|
||||
# v0.4.43 tagged release, 26 October 2026
|
||||
|
||||
The annotated `v0.4.43` tag dereferences to main merge commit
|
||||
`b4e632b218eed0761db488061e3fe6c503b06a0a` from PR #311. Both the app
|
||||
and lockfile report 0.4.43. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40292211650)
|
||||
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
|
||||
The [tagged main checks](https://github.com/saphid/frame-control/actions/runs/40292128084)
|
||||
passed all seven jobs, including iOS and fake-Frame end-to-end tests. The Linux
|
||||
builder launched its AppImage under isolated Xvfb; that does not prove a physical
|
||||
display, FUSE mount or headset connection.
|
||||
|
||||
## Frame and package gates
|
||||
|
||||
The [combined-head candidate](v0443-candidate-2026-10-26.md) passed the real
|
||||
Frame smoke before tagging: 12 passed, one optional x86 launch skipped, zero
|
||||
failed. The only later changes before the tag were the Windows wireless CI
|
||||
test and version metadata. Its locked preflight found SteamVR activity 3
|
||||
(standby), proximity 6.25 and battery 99%.
|
||||
|
||||
The draft had ten installers plus `SHA256SUMS`. Each installer had a GitHub
|
||||
SHA-256 digest. A fresh downloaded checksum list contained exactly those ten
|
||||
names and digests; its own downloaded bytes matched GitHub's digest
|
||||
`57f211d8bbf76db8aa96cc4a5ae31410ed7e3075ce2e8c0c17394feb9426d01c`.
|
||||
`scripts/publish-release.sh --dry-run v0.4.43` passed the exact remote tag,
|
||||
installer and checksum gates after the notes were attached.
|
||||
|
||||
Freshly downloaded packages passed these independent checks:
|
||||
|
||||
| Package | Result |
|
||||
| --- | --- |
|
||||
| Linux x86_64 AppImage | SHA-256 `d5e6c2272a14a2aa77aa932db43f3ce887e09eb78c87bf2de410832b5df4827d`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, and stopped on exit. |
|
||||
| Mac arm64 ZIP | SHA-256 `c6c4a9c74644c3479d1af112ec60d4c2770201fc366f21bca23a4e2fcc86194a`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU arm64. |
|
||||
| Mac x64 ZIP | SHA-256 `58dfc88bb71f1733add7477f9cd888417e4827e608fe910017837ba466a72a40`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU x86_64. |
|
||||
| Windows x64 Setup | SHA-256 `8c17522b0e96d54f53949f4df56a3e1ec1fe2da8df595482a358a3d014b6f1a9`; PE/Nullsoft installer format. The tagged Windows CI also installed and checked its packaged files and Python libraries. |
|
||||
| Linux arm64 DEB | SHA-256 `c00f141b6b63344c629bbd659a366a74705c52061d83572ae235a75ac6618df0`; package version 0.4.43 and architecture arm64. |
|
||||
|
||||
Tagged Mac and Windows packages were not launched on physical machines in this
|
||||
release gate. Earlier Intel Mac acceptance covers an older version, not these
|
||||
tagged bytes.
|
||||
|
||||
## Publication and later main
|
||||
|
||||
`scripts/publish-release.sh v0.4.43` published
|
||||
[v0.4.43](https://github.com/saphid/frame-control/releases/tag/v0.4.43) at
|
||||
2026-10-26 08:39:09 UTC. GitHub's latest-release API returned `draft=false`,
|
||||
`prerelease=false`, the expected tag, ten installers, `SHA256SUMS` and
|
||||
`update.json`. A fresh public latest-download of `update.json` parsed as
|
||||
version 0.4.43 with the stable release page, ten hashed assets and the worn-VR
|
||||
limits in its notes. The release catalog is **4 works / 49 partial / 52 broken /
|
||||
11 N/A**. Volume Boost's works label covers tested muted sink controls only.
|
||||
|
||||
PRs #312 and #313 merged **after** the tag and are absent from v0.4.43. On their
|
||||
combined main commit `c50f96510fdb42a2aee227436a04c0cfe8817919`, a second
|
||||
locked `scripts/frame-smoke.sh` run at 08:37 UTC found activity 3, proximity 6.25
|
||||
and battery 99%, then passed 12 checks with the same optional x86 launch skip
|
||||
and zero failures. Test title and process cleanup passed; the command exited 0
|
||||
and released only its owned lock. The private result is
|
||||
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` in the test
|
||||
checkout. This post-tag smoke does not test the new Framey scroll buttons or
|
||||
Junk-Store's deployed headset panel.
|
||||
|
||||
The guarded `scripts/framey-vr-check.py --exercise` then ran on the same code
|
||||
commit, using the existing direct `frame` SSH alias and its own shared-lock owner.
|
||||
It began at 08:45 UTC in SteamVR standby (activity 3, proximity 6.3125,
|
||||
battery 99%). The temporary sample panel passed setting save/reopen, quick
|
||||
action, slider and text checks. Synthetic native OpenVR clicks on the visible
|
||||
**Scroll plugin down/up** buttons moved the 160-pixel test region 0 → 120 → 0;
|
||||
the compositor screenshot hash changed. Native synthetic wheel attempts still
|
||||
produced no wheel event or movement. The private report is
|
||||
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
|
||||
The command exited 0, removed its test services and power watchdog, read back
|
||||
the original `false` / `5` power settings, found battery 99%, and released its
|
||||
owned lock with no cleanup errors. It did not exercise an official plugin or a
|
||||
physical controller.
|
||||
|
||||
No wearer confirmation was available, so no worn check, wake or input exercise
|
||||
ran. Physical controller use, optical readability, official-plugin VR panels,
|
||||
audible output and VR keyboard remain unverified. The hourly worn-check schedule
|
||||
stays enabled.
|
||||
@@ -0,0 +1,56 @@
|
||||
# v0.4.44 candidate gate, 26 October 2026
|
||||
|
||||
This candidate starts from main commit
|
||||
`c50f96510fdb42a2aee227436a04c0cfe8817919`, which contains PR #312's
|
||||
Framey scroll buttons and PR #313's read-only installed-app metadata projection.
|
||||
It also carries the documentation-only v0.4.43 release evidence from PR #314.
|
||||
The app and lockfile are bumped to 0.4.44. No compatibility label changes:
|
||||
the exact-version official Decky catalog remains **4 works / 49 partial /
|
||||
52 broken / 11 N/A**. Junk-Store's About and Dependencies pages now render in
|
||||
the local Chromium test with its actual ARM64 backend; the plugin remains partial.
|
||||
|
||||
## Real Frame checks on the combined code
|
||||
|
||||
At 08:37 UTC, `scripts/frame-smoke.sh` ran on that main commit under its own
|
||||
`/tmp/frame-test.lock` owner. Locked preflight found SteamVR activity 3
|
||||
(standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build
|
||||
`20261022.6153909`, ARM64 and Proton executables launched; the optional x86_64
|
||||
launch was skipped because Steam Linux Runtime 4 is absent. The result was
|
||||
**12 passed / 1 skipped / 0 failed**. It removed its test titles, exited 0 and
|
||||
released its owned lock. The private result is
|
||||
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` on the test
|
||||
host.
|
||||
|
||||
At 08:45 UTC, `scripts/framey-vr-check.py --exercise` ran the same code under
|
||||
a separate unique shared-lock owner, beginning in standby (activity 3,
|
||||
proximity 6.3125, battery 99%). Synthetic native OpenVR clicks on the new
|
||||
**Scroll plugin down/up** buttons moved a temporary sample region 0 → 120 → 0.
|
||||
The sample setting save/reopen, quick action, slider, text and compositor-change
|
||||
checks passed. Native synthetic wheel attempts still produced no event or
|
||||
movement. Cleanup removed test services and its power watchdog, restored the
|
||||
original `false` / `5` power values, read battery 99%, and released its lock
|
||||
with no errors. The private report is
|
||||
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
|
||||
This is an unworn sample panel check. It does not prove physical controller
|
||||
scrolling, optical readability or an official-plugin panel in VR.
|
||||
|
||||
## Publication boundary
|
||||
|
||||
The version-bumped release branch also ran `scripts/frame-smoke.sh` after its
|
||||
normal merge of PR #314's documentation. At 08:59 UTC, its locked preflight
|
||||
found activity 3, proximity 6.3125 and battery 97%. The result was again
|
||||
**12 passed / 1 skipped / 0 failed** on the same Frame OS build; ARM64 and
|
||||
Proton launches passed, while the optional x86 launch was skipped for the
|
||||
missing Steam Linux Runtime 4. Owned test title/process cleanup passed, the
|
||||
command exited 0, and its lock was released. The private result is
|
||||
`tests/smoke/results/2026-10-26T085951Z-20261022.6153909.json` on the test
|
||||
host. This evidence-only paragraph is the sole change after that smoke.
|
||||
|
||||
PR #313's own actual ARM64 backend/library test is documented in
|
||||
[installed-context evidence](framey-installed-context-2026-10-26.md). Its
|
||||
dependency installation, login, game operations, RSS and optional websocket
|
||||
remain unverified. The candidate still needs exact-head CI, tagged builds,
|
||||
ten installer digests with `SHA256SUMS`, downloaded package checks, publisher
|
||||
dry run and public updater readback before publication. A worn check requires
|
||||
activity, proximity and reliable wearer confirmation; none was available, so
|
||||
the hourly schedule stays enabled.
|
||||
@@ -63,6 +63,7 @@ none of these frontend controls has passed a worn-headset check yet.
|
||||
| Official `@decky/rollup` JSX (`SP_JSX.jsx`, `jsxs`, `Fragment`) | Creates React elements in the isolated panel | Sample bundle smoke test; host unit test |
|
||||
| `PanelSection`, rows, buttons, toggles, dropdowns | Local touch-sized controls; disabled controls stay inert during a backend call | Local browser and host unit test |
|
||||
| `TextField`, `SliderField`, grouped `Dropdown` options | Native text, range and select controls in the panel; disabled state, numeric slider callback and basic input types | Host unit test; worn VR use unverified |
|
||||
| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Plugins may still use ordinary CSS overflow containers. | Parent/iframe message tests; physical controller scroll remains unverified |
|
||||
| `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 |
|
||||
| `SteamUIStore` / `DFL.Router.RunningApps`, `MainRunningApp` | Bounded detached Steam CEF reads while the panel is open; refresh about every two seconds; no MobX observation. Failed reads clear stale context. | Locked real Frame empty-list reads; nonempty local Chromium fixtures. Real running-game selection unverified. |
|
||||
| `GameSessions.RegisterForAppLifetimeNotifications` | Actual Steam callbacks, bounded queue and disposable `.unregister()` handle; no events inferred from snapshot changes; lease cleanup after lost panels | Real Frame registration/poll/close; launch/exit, retry, expiry and duplicate tests use explicit fixtures. Real game transitions unverified. |
|
||||
|
||||
@@ -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. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. 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. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. 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 followLine truncated
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. 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. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional Line truncated
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -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 | partial | Executable script extraction repaired. Official Decky 3.3.0 ARM64 static menu/backend calls and local Chromium Games/Epic navigation, back and reopen pass. About still fails on missing installed-app metadata; Legendary is absent. Dependency install, login, game operations, RSS and worn VR remain unverified. Existing installs need update/reinstall. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 84: Junk-Store 1.1.13 | legacy / legacy | partial | Official Decky 3.3.0 ARM64 static menu/backend calls pass; local Chromium Games/Epic navigation, About/Dependencies tabs, Back and reopen render with real Frame library metadata. Legendary is absent; dependency install, login, game operations, RSS, optional websocket and worn VR remain unverified. Existing installs need update/reinstall for script permissions. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 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 |
|
||||
|
||||
@@ -16,6 +16,10 @@ plugins → Open official store**. Search the catalogue, confirm an install, and
|
||||
choose **Open panel** from the installed plugin list. The panel contains the
|
||||
plugin's own controls and settings. Update and Remove affect only plugin
|
||||
folders that Frame Control's store manager installed.
|
||||
Use **Scroll plugin up** and **Scroll plugin down** above an open panel when
|
||||
wheel scrolling is unavailable. Point at a scrollable part of the plugin first
|
||||
to move that section; otherwise the buttons move the panel document. Each
|
||||
press moves about three quarters of the visible section and stops at its edge.
|
||||
Removing a plugin keeps its Decky settings. Removing the Frame Control headset
|
||||
panel or Decky backend keeps plugin folders until each is removed separately.
|
||||
|
||||
@@ -82,7 +86,11 @@ 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,
|
||||
and backend calls for the plugin that owns the panel. Steam library/store page
|
||||
and backend calls for the plugin that owns the panel. The headset's scroll
|
||||
buttons send only a direction to the isolated iframe. They target the last
|
||||
scrollable region reached by a trusted pointer move and fall back to the iframe
|
||||
document when that region disappears. A result message reports whether content
|
||||
moved; this bridge has no backend or Steam privilege. Steam library/store page
|
||||
patches, global Steam UI components, file pickers, and privileged root methods
|
||||
have no VR target. A plugin that depends on them can render only part of its UI
|
||||
or show an incompatibility message. A Decky plugin's Python backend still runs as
|
||||
|
||||
@@ -59,6 +59,10 @@ pointer over it, then attempts OpenVR smooth and discrete scroll events. It
|
||||
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
|
||||
No wheel event or offset change means this synthetic injection path remains
|
||||
unverified; it does not establish whether a physical controller can scroll.
|
||||
The exercise then clicks the visible **Scroll plugin down/up** buttons through
|
||||
the same native OpenVR pointer transport and requires the test region to move
|
||||
down and return to the top. This proves the button path only; it does not
|
||||
identify a physical controller or repair native wheel delivery.
|
||||
Temporary controls exercise the shipped
|
||||
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
|
||||
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
|
||||
|
||||
@@ -64,7 +64,7 @@ def render(data):
|
||||
'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. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. Beyond the documented checks, official backends remain unexecuted. '
|
||||
'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. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional websocket and worn VR are unverified. 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 '
|
||||
|
||||
@@ -71,10 +71,26 @@ def exercise(profile, plugin_id, folder):
|
||||
if not hit:
|
||||
raise RuntimeError('Native click hit another element: ' + str(panel.eval('({tag:__vrLastTarget?.tagName,text:__vrLastTarget?.textContent?.slice(0,100)})')))
|
||||
|
||||
def click_parent(selector):
|
||||
parent.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
|
||||
time.sleep(.3)
|
||||
pos = parent.eval(f'''(()=>{{let r=document.querySelector({json.dumps(selector)}).getBoundingClientRect();
|
||||
return {{x:r.x+r.width/2,y:r.y+r.height/2,w:innerWidth,h:innerHeight}}}})()''')
|
||||
sx, sy = vr.snapshot()['input']['scale']
|
||||
x, y = pos['x'] * sx / pos['w'], sy - pos['y'] * sy / pos['h']
|
||||
count = parent.eval('window.__vrParentClickCount')
|
||||
vr.post_mouse(300, x, y)
|
||||
vr.post_mouse(301, x, y, 1)
|
||||
vr.post_mouse(302, x, y, 1)
|
||||
wait(lambda:parent.eval('window.__vrParentClickCount'), lambda n:n > count, seconds=3)
|
||||
if not parent.eval(f'!!window.__vrParentLastTarget?.closest({json.dumps(selector)})'):
|
||||
raise RuntimeError('Native click missed parent control ' + selector)
|
||||
|
||||
try:
|
||||
parent.eval(f'(async()=>{{await refreshDecky();openFrameyPanel({plugin_id},"Framey VR Check");return true}})()')
|
||||
wait(lambda:parent.eval('byId("frameyPanelStatus").innerText'), lambda s:s == 'Plugin panel ready')
|
||||
panel = connect_panel()
|
||||
parent.eval('window.__vrParentClickCount=0;addEventListener("pointerup",e=>{window.__vrParentClickCount++;window.__vrParentLastTarget=e.target;});true')
|
||||
initial = wait(text, lambda s:'Saved setting: off' in s or 'Could not read setting:' in s)
|
||||
if 'Could not read setting:' in initial: raise RuntimeError(initial)
|
||||
result['initialText'] = initial
|
||||
@@ -149,6 +165,13 @@ def exercise(profile, plugin_id, folder):
|
||||
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
|
||||
result['scrollEvents'] = panel.eval('__vrWheelEvents')
|
||||
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
|
||||
click_parent('#frameyScrollDown')
|
||||
result['buttonScrollAfterDown'] = wait(
|
||||
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n > 0)
|
||||
click_parent('#frameyScrollUp')
|
||||
result['buttonScrollAfterUp'] = wait(
|
||||
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n == 0)
|
||||
result['buttonScrollMoved'] = result['buttonScrollAfterDown'] > 0 and result['buttonScrollAfterUp'] == 0
|
||||
panel.eval('document.getElementById("vr-scroll-probe").remove()')
|
||||
panel.eval('document.getElementById("vr-controls").remove()')
|
||||
# Scroll saved status into the visible iframe for compositor evidence.
|
||||
@@ -156,7 +179,7 @@ def exercise(profile, plugin_id, folder):
|
||||
result['finalOverlay'] = vr.snapshot()
|
||||
result['passed'] = (result['trustedOverlayPointer'] and result['diskSetting'] == {'enabled':True}
|
||||
and result['reopenedChecked'] and result['sliderValue'] != '10'
|
||||
and result['textValue'] == 'framey-ci')
|
||||
and result['textValue'] == 'framey-ci' and result['buttonScrollMoved'])
|
||||
return result
|
||||
except Exception as exc:
|
||||
result['error'] = str(exc)
|
||||
|
||||
@@ -21,7 +21,8 @@ const window={React,ReactDOM:{createRoot:node=>{
|
||||
const listeners=[];const messages=[];const parent={postMessage:message=>messages.push(message)};
|
||||
const context=vm.createContext({window,document:{getElementById:id=>{
|
||||
if(!elements.has(id))elements.set(id,new Element());return elements.get(id);},createElement:()=>new Element()},
|
||||
navigator:{language:'en'}, parent,DOMException,addEventListener:(type,handler)=>listeners.push({type,handler}),
|
||||
navigator:{language:'en'}, parent,DOMException,getComputedStyle:node=>({overflowY:node.overflowY||'visible'}),
|
||||
addEventListener:(type,handler)=>listeners.push({type,handler}),
|
||||
setTimeout:()=>1,clearTimeout(){}});
|
||||
vm.runInContext(testSource,context);
|
||||
// No Steam React tree exists inside the isolated Framey iframe.
|
||||
@@ -265,6 +266,17 @@ for(const name of ['App','appStore','appInfoStore','collectionStore'])assert.equ
|
||||
const snapshot={apps:[[10,'Game Ten','ten',1,'10',180,60,1234],
|
||||
[20,'Shortcut','shortcut',1073741824,'85899345920',0,0,0]],gameIds:[10]};
|
||||
window.__library(snapshot);
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data,undefined);
|
||||
snapshot.apps[0].push(true);snapshot.apps[1].push(null);
|
||||
window.__library(snapshot);
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,true);
|
||||
assert.deepEqual(Object.keys(window.appStore.GetAppOverviewByAppID(10).local_per_client_data),['installed']);
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID(20).local_per_client_data.installed,undefined);
|
||||
window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed=false;
|
||||
assert.equal(snapshot.apps[0][8],true); // Detached from the wire data.
|
||||
window.__library({...snapshot,apps:[snapshot.apps[0].slice(0,8).concat(false),snapshot.apps[1]]});
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,false);
|
||||
window.__library(snapshot);
|
||||
assert.equal(window.appStore.allApps.length,2);
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID('10').display_name,'Game Ten');
|
||||
assert.equal(window.appStore.GetAppOverviewByGameID('85899345920').appid,20);
|
||||
@@ -283,11 +295,12 @@ for(const invalid of [null,{apps:[],gameIds:[10]},
|
||||
{...snapshot,apps:[snapshot.apps[0],snapshot.apps[0]]},
|
||||
{...snapshot,apps:[[10,'Name','sort',1,'undefined',0,0,0]]},
|
||||
{...snapshot,apps:[[10,'Name','sort',1,'10',-1,0,0]]},
|
||||
...['true',1,{},undefined].map(installed=>({...snapshot,apps:[[10,'Name','sort',1,'10',0,0,0,installed]]})),
|
||||
{apps:Array(10001).fill(snapshot.apps[0]),gameIds:[]}]){
|
||||
assert.throws(()=>window.__library(invalid),/Invalid Steam/);
|
||||
assert.equal(window.appStore.allApps[0].display_name,'Plugin-local patch');
|
||||
}
|
||||
const running={artwork:[[10,'https://shared.steamstatic.com/steam/apps/10/library.jpg',[]]],apps:[snapshot.apps[0]],runningIds:[10],mainAppId:10,events:[],cursor:0,lifetimeAvailable:true};
|
||||
const running={artwork:[[10,'https://shared.steamstatic.com/steam/apps/10/library.jpg',[]]],apps:[snapshot.apps[0].slice(0,8)],runningIds:[10],mainAppId:10,events:[],cursor:0,lifetimeAvailable:true};
|
||||
const lifetimes=[];
|
||||
const lifetimeHandle=window.SteamClient.GameSessions.RegisterForAppLifetimeNotifications(event=>{
|
||||
lifetimes.push({...event,running:Array.from(window.DFL.Router.RunningApps,app=>app.appid)});
|
||||
@@ -296,6 +309,7 @@ window.__running(running);await Promise.resolve();
|
||||
assert.equal(lifetimes.length,0); // Initial snapshots are not launch events.
|
||||
assert.equal(window.SteamUIStore.FrameyRunningAvailable,true);
|
||||
assert.equal(window.DFL.Router.MainRunningApp.display_name,'Game Ten');
|
||||
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,true);
|
||||
assert.equal(window.appStore.GetVerticalCapsuleURLForApp({appid:10}),'https://shared.steamstatic.com/steam/apps/10/library.jpg');
|
||||
assert.equal(window.appStore.GetCustomVerticalCapsuleURLs({appid:10}).length,0);
|
||||
assert.equal(window.DFL.Router.RunningApps,window.SteamUIStore.RunningApps);
|
||||
@@ -374,6 +388,35 @@ assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
|
||||
|
||||
// Event subscription is acknowledged before a method that immediately emits.
|
||||
const onMessage=listeners.find(x=>x.type==='message').handler;
|
||||
// The headset's two scroll buttons target the last trusted pointer's nested
|
||||
// scroll region, with a document fallback after that region is removed.
|
||||
const documentScroll={scrollTop:0,clientHeight:500,scrollHeight:900,isConnected:true,overflowY:'auto'};
|
||||
const nestedScroll={scrollTop:0,clientHeight:160,scrollHeight:716,isConnected:true,
|
||||
overflowY:'scroll',parentElement:documentScroll};
|
||||
const child={scrollTop:0,clientHeight:0,scrollHeight:0,parentElement:nestedScroll};
|
||||
context.document.scrollingElement=documentScroll;
|
||||
const pointerMove=listeners.find(x=>x.type==='pointermove').handler;
|
||||
pointerMove({isTrusted:true,target:child});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(nestedScroll.scrollTop,120);
|
||||
assert.equal(messages.at(-1).moved,true);
|
||||
pointerMove({isTrusted:false,target:documentScroll});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'up'}});
|
||||
assert.equal(nestedScroll.scrollTop,0,'Untrusted movement cannot retarget scroll');
|
||||
nestedScroll.scrollTop=550;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(nestedScroll.scrollTop,556,'Scroll stops at the content end');
|
||||
assert.equal(messages.at(-1).moved,true);
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(messages.at(-1).moved,false);
|
||||
const scrollReplies=messages.length;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'stale',direction:'up'}});
|
||||
onMessage({source:{},data:{type:'framey:scroll',session:'',direction:'up'}});
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'sideways'}});
|
||||
assert.equal(messages.length,scrollReplies);
|
||||
nestedScroll.isConnected=false;
|
||||
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
|
||||
assert.equal(documentScroll.scrollTop,375);
|
||||
const eventValues=[];
|
||||
const callback=(...args)=>eventValues.push(args);
|
||||
assert.equal(api.addEventListener('timer:tick',callback),callback);
|
||||
|
||||
@@ -69,7 +69,6 @@ vm.runInContext('openFrameyPanel(96,"Timer")',context);
|
||||
const priorReplies=replies.length;
|
||||
requests.at(-1).resolve({ok:true,json:async()=>({result:'AU'})});
|
||||
await staleRead;assert.equal(replies.length,priorReplies);
|
||||
|
||||
// Running reads are tied to the current iframe and session, with no method path.
|
||||
session=vm.runInContext('frameySession',context);
|
||||
const running={type:'framey:running',id:20,session,args:[0]};
|
||||
@@ -96,3 +95,19 @@ await finalRunning;
|
||||
await send({type:'framey:running:close',session});
|
||||
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
|
||||
assert.equal(requests.at(-1).body.active,false);
|
||||
|
||||
const scrollSession=vm.runInContext('frameySession',context);
|
||||
el('frameyScrollDown').onclick();
|
||||
assert.equal(replies.at(-1).type,'framey:scroll');
|
||||
assert.equal(replies.at(-1).session,scrollSession);
|
||||
assert.equal(replies.at(-1).direction,'down');
|
||||
await send({type:'framey:scroll:result',session:'old',direction:'down',moved:true});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'Loading plugin panel…');
|
||||
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:true});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'Scrolled plugin down');
|
||||
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:false});
|
||||
assert.equal(el('frameyPanelStatus').textContent,'No more plugin content in that direction');
|
||||
el('frameyClose').onclick();
|
||||
const replyCount=replies.length;
|
||||
el('frameyScrollUp').onclick();
|
||||
assert.equal(replies.length,replyCount);
|
||||
@@ -112,12 +112,22 @@ class Panel(unittest.TestCase):
|
||||
script = '''const assert=require('node:assert/strict');
|
||||
const app={appid:10,display_name:'Game',sort_as:'game',app_type:1,gameid:'10',
|
||||
minutes_playtime_forever:60,minutes_playtime_last_two_weeks:20,rt_last_time_played:123,
|
||||
owner_account_id:'PRIVATE',per_client_data:{path:'PRIVATE'}};
|
||||
owner_account_id:'PRIVATE',per_client_data:{path:'PRIVATE'},
|
||||
local_per_client_data:{installed:true,clientid:'PRIVATE',install_path:'PRIVATE'}};
|
||||
global.window=global;global.appStore={allApps:[app]};
|
||||
global.App={m_CurrentUser:{strSteamID:'76561198000000001',strAccountBalance:'PRIVATE'}};
|
||||
global.collectionStore={allGamesCollection:{allApps:[app]}};
|
||||
const value=eval(EXPRESSION);
|
||||
assert.deepEqual(value,{apps:[[10,'Game','game',1,'10',60,20,123]],gameIds:[10],steamId:'76561198000000001'});
|
||||
assert.deepEqual(value,{apps:[[10,'Game','game',1,'10',60,20,123,true]],gameIds:[10],steamId:'76561198000000001'});
|
||||
assert.equal(JSON.stringify(value).includes('PRIVATE'),false);
|
||||
app.local_per_client_data.installed=false;
|
||||
assert.equal(eval(EXPRESSION).apps[0][8],false);
|
||||
delete app.local_per_client_data.installed;
|
||||
assert.equal(eval(EXPRESSION).apps[0][8],null);
|
||||
app.local_per_client_data=undefined;
|
||||
assert.equal(eval(EXPRESSION).apps[0][8],null);
|
||||
app.local_per_client_data={installed:'true'};
|
||||
assert.equal(eval(EXPRESSION).apps[0][8],null);
|
||||
appStore.allApps=Array(10001).fill(app);
|
||||
assert.throws(()=>eval(EXPRESSION),/snapshot limit/);
|
||||
appStore=undefined;
|
||||
|
||||
@@ -6518,7 +6518,7 @@
|
||||
"https://github.com/ebenbruyns/junkstore.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Executable script extraction repaired. Official Decky 3.3.0 ARM64 static menu/backend calls and local Chromium Games/Epic navigation, back and reopen pass. About still fails on missing installed-app metadata; Legendary is absent. Dependency install, login, game operations, RSS and worn VR remain unverified. Existing installs need update/reinstall.",
|
||||
"reason": "Official Decky 3.3.0 ARM64 static menu/backend calls pass; local Chromium Games/Epic navigation, About/Dependencies tabs, Back and reopen render with real Frame library metadata. Legendary is absent; dependency install, login, game operations, RSS, optional websocket and worn VR remain unverified. Existing installs need update/reinstall for script permissions.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -6575,7 +6575,12 @@
|
||||
"init/InitActions"
|
||||
],
|
||||
"browser": "main menu; Games/Epic static tab; back; close/reopen",
|
||||
"remainingFailure": "About: Cannot read properties of undefined (reading 'installed'); detached app overview lacks local_per_client_data.installed"
|
||||
"installedContext": {
|
||||
"evidence": "docs/evidence/framey-installed-context-2026-10-26.json",
|
||||
"field": "local_per_client_data.installed",
|
||||
"browser": "About/Dependencies, Back, close/reopen; real library projection; static ARM64 backend calls",
|
||||
"installedUnknown": "preserved as undefined; no fabricated false"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+38
-3
@@ -34,6 +34,25 @@
|
||||
const root = document.getElementById('framey-root');
|
||||
const error = document.getElementById('framey-error');
|
||||
const toastRoot = document.getElementById('framey-toasts');
|
||||
let lastScrollTarget = null;
|
||||
function scrollTarget(element) {
|
||||
for (let node = element; node && node !== document; node = node.parentElement) {
|
||||
if (node.scrollHeight <= node.clientHeight + 1) continue;
|
||||
if (node === document.scrollingElement || /^(auto|scroll|overlay)$/.test(getComputedStyle(node).overflowY))
|
||||
return node;
|
||||
}
|
||||
return document.scrollingElement;
|
||||
}
|
||||
function scrollPlugin(direction) {
|
||||
if (direction !== 'up' && direction !== 'down') return false;
|
||||
const target = lastScrollTarget?.isConnected ? lastScrollTarget : document.scrollingElement;
|
||||
if (!target) return false;
|
||||
const before = target.scrollTop;
|
||||
const maximum = Math.max(0, target.scrollHeight - target.clientHeight);
|
||||
const step = Math.max(120, Math.round(target.clientHeight * 0.75));
|
||||
target.scrollTop = Math.max(0, Math.min(maximum, before + (direction === 'down' ? step : -step)));
|
||||
return target.scrollTop !== before;
|
||||
}
|
||||
const pending = new Map();
|
||||
const routes = new Map();
|
||||
const routeListeners = new Set();
|
||||
@@ -139,6 +158,14 @@
|
||||
return request(method, args, legacy);
|
||||
}
|
||||
addEventListener('message', event => {
|
||||
if (event.source === parent && event.data?.type === 'framey:scroll' &&
|
||||
event.data.session === cacheSession) {
|
||||
if (event.data.direction !== 'up' && event.data.direction !== 'down') return;
|
||||
const moved = scrollPlugin(event.data.direction);
|
||||
parent.postMessage({type:'framey:scroll:result', session:cacheSession,
|
||||
direction:event.data.direction, moved}, '*');
|
||||
return;
|
||||
}
|
||||
if (event.source === parent && event.data?.type === 'framey:cache:seed' &&
|
||||
event.data.session === cacheSession && acceptSeed) {
|
||||
const entries = event.data.entries;
|
||||
@@ -165,6 +192,9 @@
|
||||
addEventListener('pointerup', event => {
|
||||
if (event.isTrusted) parent.postMessage({type:'framey:input'}, '*');
|
||||
}, {passive:true});
|
||||
addEventListener('pointermove', event => {
|
||||
if (event.isTrusted) lastScrollTarget = scrollTarget(event.target);
|
||||
}, {passive:true});
|
||||
|
||||
const passthrough = tag => ({children, ...props}) => R.createElement(tag, props, children);
|
||||
const Field = ({label, description, children}) => R.createElement('label', null,
|
||||
@@ -563,16 +593,21 @@
|
||||
function projectApps(rows, limit) {
|
||||
if (!Array.isArray(rows) || rows.length > limit) throw Error('Invalid Steam app overview');
|
||||
return rows.map(row => {
|
||||
if (!Array.isArray(row) || row.length !== 8 || !Number.isSafeInteger(row[0]) || row[0] <= 0 ||
|
||||
if (!Array.isArray(row) || (row.length !== 8 && row.length !== 9) || !Number.isSafeInteger(row[0]) || row[0] <= 0 ||
|
||||
typeof row[1] !== 'string' || row[1].length > 2048 ||
|
||||
(row[2] != null && typeof row[2] !== 'string') || !Number.isSafeInteger(row[3]) ||
|
||||
typeof row[4] !== 'string' || !/^\d{1,20}$/.test(row[4]) ||
|
||||
row.slice(5).some(value=>value != null && (!Number.isFinite(value) || value < 0)))
|
||||
row.slice(5,8).some(value=>value != null && (!Number.isFinite(value) || value < 0)) ||
|
||||
(row.length === 9 && row[8] !== null && typeof row[8] !== 'boolean'))
|
||||
throw Error('Invalid Steam app overview');
|
||||
const [appid,display_name,sort_as,app_type,gameid,
|
||||
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played] = row;
|
||||
return {appid,display_name,sort_as,app_type,gameid,
|
||||
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played};
|
||||
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played,
|
||||
// null means Steam supplied no installed boolean, not false. Keep the
|
||||
// detached metadata object for legacy readers; never copy paths/client IDs.
|
||||
// Older/running snapshots omit this field and must not erase library data.
|
||||
...(row.length === 9 ? {local_per_client_data:row[8] === null ? {} : {installed:row[8]}} : {})};
|
||||
});
|
||||
}
|
||||
function setLibrarySnapshot(snapshot) {
|
||||
|
||||
+2
-1
@@ -183,7 +183,8 @@ STEAM_READS = {
|
||||
const apps = Array.from(appStore.allApps);
|
||||
if (apps.length > 10000) throw Error('Steam library exceeds the Framey snapshot limit');
|
||||
return {apps:apps.map(a=>[a.appid,a.display_name,a.sort_as,a.app_type,String(a.gameid),
|
||||
a.minutes_playtime_forever,a.minutes_playtime_last_two_weeks,a.rt_last_time_played]),
|
||||
a.minutes_playtime_forever,a.minutes_playtime_last_two_weeks,a.rt_last_time_played,
|
||||
typeof a.local_per_client_data?.installed === 'boolean' ? a.local_per_client_data.installed : null]),
|
||||
gameIds:Array.from(collectionStore.allGamesCollection.allApps,a=>a.appid),
|
||||
steamId:window.App?.m_CurrentUser?.strSteamID || null};
|
||||
})()''',
|
||||
|
||||
+14
-1
@@ -53,7 +53,8 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
|
||||
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">No known install blocker</option><option value="installed">Installed plugins</option></select></label></div>
|
||||
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
|
||||
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button>
|
||||
<button id="frameyScrollUp" class="secondary">Scroll plugin up</button><button id="frameyScrollDown" class="secondary">Scroll plugin down</button></div>
|
||||
<p id="frameyCompatibility"></p>
|
||||
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
|
||||
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
|
||||
@@ -251,6 +252,12 @@ function closeFrameyEvents(){
|
||||
function closeFrameyPanel(){closeFrameyEvents();activeFrameyId=null;activeFrameyReceipt=null;frameySession=null;
|
||||
byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;}
|
||||
byId('frameyClose').onclick=closeFrameyPanel;
|
||||
function scrollFramey(direction){
|
||||
if(activeFrameyId===null||!frameySession||!['up','down'].includes(direction))return;
|
||||
byId('frameyFrame').contentWindow?.postMessage({type:'framey:scroll',session:frameySession,direction},'*');
|
||||
}
|
||||
byId('frameyScrollUp').onclick=()=>scrollFramey('up');
|
||||
byId('frameyScrollDown').onclick=()=>scrollFramey('down');
|
||||
function frameyCacheKey(id){return `framey-plugin-cache-v1:${id}`;}
|
||||
function frameyCacheRead(id){
|
||||
try{const data=JSON.parse(localStorage.getItem(frameyCacheKey(id))||'{}');
|
||||
@@ -271,6 +278,12 @@ function frameyCacheSave(id,entries){
|
||||
window.addEventListener('message',async event=>{
|
||||
if(activeFrameyId===null||event.source!==byId('frameyFrame').contentWindow)return;
|
||||
const req=event.data;if(!req||typeof req!=='object')return;
|
||||
if(req.type==='framey:scroll:result'&&req.session===frameySession){
|
||||
if(['up','down'].includes(req.direction)&&typeof req.moved==='boolean')
|
||||
byId('frameyPanelStatus').textContent=req.moved?'Scrolled plugin '+req.direction:
|
||||
'No more plugin content in that direction';
|
||||
return;
|
||||
}
|
||||
if(req.type==='framey:cache:init'&&req.session===frameySession){
|
||||
event.source.postMessage({type:'framey:cache:seed',session:frameySession,
|
||||
entries:frameyCacheRead(activeFrameyId)},'*');return;
|
||||
|
||||
Reference in new issue
Block a user