mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 09:01:47 +01:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8abe32c6 | ||
|
|
f50139fe6a | ||
|
|
4ac59f1a44 | ||
|
|
c91cfff797 | ||
|
|
d5e62f643d | ||
|
|
f3f574666f | ||
|
|
40ece4fef0 | ||
|
|
f52c020310 | ||
|
|
efe0c508f4 | ||
|
|
1af2bb45d9 | ||
|
|
daee952873 | ||
|
|
98909cb543 | ||
|
|
7172be17fc | ||
|
|
761e952d8f | ||
|
|
0237e342d2 | ||
|
|
10b196beaa | ||
|
|
3f74ff4bb0 |
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,101 @@
|
||||
# Running-game context and Discord Status, 2026-10-26
|
||||
|
||||
Framey now projects running apps and the selected main app from Steam CEF into
|
||||
detached `SteamUIStore` / `DFL.Router` getters. It relays actual Steam lifetime
|
||||
callbacks while the panel is open. Public Steam CDN capsule URLs are projected
|
||||
for running apps; local artwork paths and URL query data never cross the bridge.
|
||||
The [machine-readable evidence](framey-running-context-2026-10-26.json) records
|
||||
the exact archives, fixtures, browser observations and cleanup results.
|
||||
|
||||
| Plugin / scope | Result | Verification |
|
||||
| --- | --- | --- |
|
||||
| Discord Status 1.5.0 | **Broken → partial.** Disconnected panel renders; activity construction accepts the synthetic running game without the previous missing-store/artwork errors. | Exact unmodified official frontend in opaque Chromium iframe; backend/network denied. Actual official `is_connected` returns false in real Decky 3.3.0 ARM64, with no Discord IPC present. |
|
||||
| Pause Games | **Partial.** Running-list initialization reaches `pid_from_appid` for the synthetic game. | Local Chromium fixture; all backend calls, including settings writes and process actions, denied. |
|
||||
| Notebook | **Works for its previously verified default notebook scope.** Per-game context is now supplied, but real per-game selection remains unverified. | Synthetic game causes the official frontend to request `loadPage`; backend calls denied in this pass. Earlier default save/reopen ARM64 evidence remains separate. |
|
||||
| PlayTime | **Partial.** Running snapshots do not satisfy its private MobX observation. | Local Chromium explicitly logs that its running-app observer is unavailable. No automatic tracking claim. |
|
||||
| 14 other focused panels | Labels retained. Initialization alone does not establish backend functionality. | Exact frontends with synthetic nonempty running/library snapshots and backend/network denied; Game Sync still stops on the denied `get_config` read. |
|
||||
|
||||
The bridge addresses a shared dependency detected in 34 official frontend
|
||||
bundles, including hardware-inapplicable plugins whose installation remains
|
||||
blocked. This is infrastructure coverage, not 34 working plugins. Catalog after
|
||||
this pass: **3 works / 49 partial / 53 broken / 11 N/A**.
|
||||
|
||||
## Real Frame checks and cleanup
|
||||
|
||||
Every device probe acquired the atomic `/tmp/frame-test.lock` and released only
|
||||
its matching owner. Read-only probes verified the actual RunningApps and
|
||||
MainRunningApp getter implementations, the lifetime API's object with an
|
||||
`unregister()` method, and a public `https://shared.steamstatic.com` capsule URL.
|
||||
|
||||
A private removable user service ran the published Decky 3.3.0 ARM64 executable
|
||||
and the exact unmodified Discord Status 1.5.0 store archive. Production
|
||||
`framey_panel.call_method` returned false from `is_connected`. Production
|
||||
`poll_running` verified two empty running-list reads, an actual lifetime
|
||||
subscription and explicit close. The device had no running apps: **nonempty
|
||||
reads and launch/exit callbacks were not observed on hardware**. Decky startup
|
||||
reinjected its frontend/restarted Steam CEF. No plugin game, power, audio or process-control
|
||||
action was invoked, and no Discord connection/activity mutation was allowed.
|
||||
|
||||
After close, CEF reported zero sessions and a null timer. The test service,
|
||||
unit and private base were removed. A fresh independently locked audit read
|
||||
`LoadState=not-found`, `ActiveState=inactive`, `MainPID=0`, absent paths and zero
|
||||
CEF sessions/timers. Battery stayed **99 → 99**; normal Decky stayed failed and
|
||||
the normal panel service stayed inactive. No headset wipe or system service
|
||||
installation was used.
|
||||
|
||||
## Local verification and limits
|
||||
|
||||
- Full current official store fetch: 116 IDs/versions/digests unchanged at
|
||||
2026-10-26 05:18:22 UTC.
|
||||
- Fake Frame: 116 transport cases, 60 install/remove cycles and 56 expected
|
||||
blockers; private Compose project and volumes removed. Plugin execution was
|
||||
stubbed.
|
||||
- Full local Chromium sweep: 103 of 116 initializer-ready with explicitly
|
||||
synthetic library/running data. No backend/network request was allowed.
|
||||
The focused Discord Status observation lasted eight seconds and constructed
|
||||
`Test Running Game` activity; the visible UI correctly remained disconnected.
|
||||
- Node contract tests cover actual-event queue projection, retry/deduplication,
|
||||
bounds, expiry, unregister, reconnect, update-before-callback ordering,
|
||||
invalid data, public-artwork filtering, stale sessions, overlapping reads,
|
||||
failure clearing stale state and late replies after close. Nonempty app rows
|
||||
and launch/exit notifications in these tests are explicit synthetic fixtures.
|
||||
|
||||
The queue retains at most 64 real notifications per session; overflow can lose
|
||||
old callbacks. Polling runs about every two seconds with no overlap. At most four
|
||||
CEF sessions survive; unreachable cleanup expires after 15 seconds, checked
|
||||
every five seconds. The bridge does not implement private MobX internals,
|
||||
suspend/input events, game actions or closed-panel automation.
|
||||
|
||||
Discord shortcut detection, real IPC connection/activity, local custom artwork,
|
||||
actual running-game transitions, worn VR and physical controller/keyboard use
|
||||
remain unverified. None of the new fixture checks promotes a plugin to works.
|
||||
|
||||
After #301 merged, running polls and both close paths adopted its pinned SHA-256
|
||||
receipt guard. Local Python/Node tests verify the parent carries that receipt,
|
||||
missing/stale receipts are rejected before CEF after plugin replacement, and a
|
||||
reopened panel can poll and close with the new receipt. Rejected stale closes
|
||||
retain the tested finite-lease cleanup. This authorization-only follow-up did
|
||||
not repeat the device checks above; their recorded hardware scope is unchanged.
|
||||
|
||||
## Post-merge unworn panel exercise
|
||||
|
||||
On merged main `daee952873255b86018cce1d2a4d3bd070ec4341`,
|
||||
`scripts/framey-vr-check.py --exercise --output /tmp/framey-post304-exercise`
|
||||
acquired its own `/tmp/frame-test.lock` before any headset read. At 05:54 UTC,
|
||||
preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%.
|
||||
The temporary sample panel passed the real Decky setting save and fresh-iframe
|
||||
reopen, a trusted synthetic OpenVR pointer, quick action, slider value 50 and
|
||||
CDP text entry after OpenVR focus. Stereo compositor SHA-256 changed from
|
||||
`50fcab1ee25e6d9398106cc0c24ec6616e07bcf4573b52be1774c715b756060b`
|
||||
to `98241806fb1f775e7f4c5ef0b0564bfd3c552ee896ff225b4d0cb5b7b53f70d1`;
|
||||
the `/dev/video99` mirror capture hash was
|
||||
`c284c05f86b60362168c1d1f58fe775974b70ac54bbfd57834802b892eb4509e`.
|
||||
The command exited 0 with `passed=true`. The original power values (`false` and
|
||||
`5.000000`) were read back, battery remained 99%, the recovery timer was removed,
|
||||
cleanup errors were empty and the run released only its own lock. The private
|
||||
full report is `/tmp/framey-post304-exercise/result.json` on the test host.
|
||||
|
||||
This exercise covered the sample panel path after the running-context merge. It
|
||||
did not observe a nonempty running-game list, a game lifetime callback, Discord
|
||||
activity, a worn display or physical controller input. Synthetic native scroll
|
||||
again did not move (`scrollTop=0`, no wheel event).
|
||||
@@ -0,0 +1,405 @@
|
||||
{
|
||||
"baseCommit": "daee952873255b86018cce1d2a4d3bd070ec4341",
|
||||
"plugin": {
|
||||
"id": 58,
|
||||
"name": "Volume Boost",
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc"
|
||||
},
|
||||
"runtime": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6",
|
||||
"os": "Frame OS 0.5.6",
|
||||
"arch": "aarch64"
|
||||
},
|
||||
"verificationLimits": {
|
||||
"fakeFrame": "Prior exact archive HTTP/SSH install/receipt/removal; plugin execution stubbed, not repeated",
|
||||
"localBrowser": "Actual headset.html, React and Framey host, exact unmodified official frontend in opaque iframe; direct RPC relay to actual ARM64 backend",
|
||||
"realFrameSSH": "Production receipt-bound framey_panel.call_method over SSH and live Steam CEF DeckyBackend.call into actual Decky 3.3.0 ARM64 plugin; default speaker sink remained muted throughout",
|
||||
"wornVR": "Not tested",
|
||||
"audibleOutput": "Not tested; gain writes and readbacks only",
|
||||
"deployedHTTP": "Not tested; outer status/install/check responses were fixtures, methods used production call_method with exact current receipt"
|
||||
},
|
||||
"catalogRefresh": {
|
||||
"checkedAtUtc": "2026-10-26T06:10:58.537029+00:00",
|
||||
"plugins": 116,
|
||||
"changedIdsVersionsDigests": 0
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"timeUtc": "2026-10-26T06:07:19.370988+00:00",
|
||||
"locale": "LC_ALL=C for the private Decky service",
|
||||
"originalSink": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 0,
|
||||
"front-right": 0
|
||||
}
|
||||
},
|
||||
"writes": [
|
||||
{
|
||||
"request": {
|
||||
"value": 125,
|
||||
"channel": "both"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 81920,
|
||||
"front-right": 81920
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 125,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 125,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"value": 75,
|
||||
"channel": "both"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 49152,
|
||||
"front-right": 49152
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 75,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 75,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"value": 74,
|
||||
"channel": "both"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 48496,
|
||||
"front-right": 48496
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 74,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 74,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"value": 33,
|
||||
"channel": "left"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 21626,
|
||||
"front-right": 0
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 33,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"value": 67,
|
||||
"channel": "right"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 0,
|
||||
"front-right": 43909
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 67,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"value": 33,
|
||||
"channel": "right"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 0,
|
||||
"front-right": 21626
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 33,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
}
|
||||
],
|
||||
"reads": [
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"reopen": "Fresh query/session, ready; UI returned to actual restored zero; no write on mount",
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "inactive",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "inactive",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true
|
||||
},
|
||||
"exactSinkRestored": true,
|
||||
"lockReleased": true
|
||||
},
|
||||
{
|
||||
"timeUtc": "2026-10-26T06:12:04.889613+00:00",
|
||||
"locale": "Inherited user-service environment, matching production unit; no LANG or LC_ALL override",
|
||||
"originalSink": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 0,
|
||||
"front-right": 0
|
||||
}
|
||||
},
|
||||
"writes": [
|
||||
{
|
||||
"request": {
|
||||
"value": 150,
|
||||
"channel": "both"
|
||||
},
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentPulseReadback": {
|
||||
"mute": true,
|
||||
"volumes": {
|
||||
"front-left": 98304,
|
||||
"front-right": 98304
|
||||
}
|
||||
},
|
||||
"backendReadback": {
|
||||
"left": {
|
||||
"result": 150,
|
||||
"success": true
|
||||
},
|
||||
"right": {
|
||||
"result": 150,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"guardArmedBeforeWrite": true,
|
||||
"exactOriginalRestored": true
|
||||
}
|
||||
],
|
||||
"reads": [
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"reopen": "Fresh query/session, ready; UI returned to actual restored zero; no write on mount",
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "inactive",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "inactive",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true
|
||||
},
|
||||
"exactSinkRestored": true,
|
||||
"lockReleased": true
|
||||
}
|
||||
],
|
||||
"guard": {
|
||||
"scope": "Private test helper only; not a shipped feature or plugin patch",
|
||||
"timeoutSeconds": 60,
|
||||
"runtimeMaxSec": 120,
|
||||
"sequence": [
|
||||
"Capture exact default sink identity, native per-channel volume integers and original mute",
|
||||
"Persist recovery state; start bounded device-side systemd watchdog before mute/write",
|
||||
"Require stereo front-left/front-right and mute readback before plugin call",
|
||||
"Only official get_volume_state/set_volume_state; value 0..150, known channels",
|
||||
"Read native Pulse volume and mute independently; read both channels through actual plugin",
|
||||
"Restore exact native volume while muted, verify, then restore original mute and verify exact snapshot",
|
||||
"Remove recovery state, stop only own watchdog; cleanup own service/base and release matching lock"
|
||||
],
|
||||
"automaticTimeoutExercised": false,
|
||||
"explicitRestoreExercised": true
|
||||
},
|
||||
"freshCleanupAudit": {
|
||||
"exactSinkRestored": true,
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"services": {
|
||||
"framey-boost-kp0z857q58-b4126054.service": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n",
|
||||
"framey-boost-kp0z857q58-b4126054-guard.service": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n"
|
||||
},
|
||||
"battery": [
|
||||
100
|
||||
]
|
||||
},
|
||||
"remaining": [
|
||||
"Audible sound/quality, perceived loudness and physical speaker gain are unverified",
|
||||
"Worn VR, headset controller input and Bluetooth sinks are unverified",
|
||||
"No default route change, playback or power-setting action was invoked",
|
||||
"Decky startup reinjects its frontend and can restart Steam CEF; normal managed services stayed inactive"
|
||||
],
|
||||
"repositoryChecks": {
|
||||
"python": "1,626 tests, 16 skips, no failures",
|
||||
"site": "18 passed",
|
||||
"catalogDocCheck": "passed",
|
||||
"diffCheck": "passed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
# Volume Boost sink controls on the ARM64 Frame
|
||||
|
||||
Volume Boost 0.0.7 (official store ID 58) now qualifies as **works for sink
|
||||
controls**. The exact official, unmodified Python backend ran in Decky 3.3.0
|
||||
ARM64 on Frame OS 0.5.6. Local Chromium rendered its official frontend through
|
||||
Framey's existing legacy bridge. Actual control changes reached the Frame's
|
||||
default speaker sink, including above-100% gain, with independent Pulse
|
||||
readbacks. The sink stayed muted; no audible-output or worn-VR claim is made.
|
||||
|
||||
[Digest-pinned observations and cleanup](framey-volume-boost-arm64-2026-10-26.json)
|
||||
record both runs. This supersedes the Volume Boost read-only limitation in the
|
||||
[previous ARM64 audio check](framey-audio-arm64-2026-10-26.md). No plugin patch,
|
||||
native rebuild or new compatibility shim was necessary. Volume Boost's backend
|
||||
uses Python and the Frame's existing ARM64 `pactl`; its hard-coded UID 1000
|
||||
matches the actual Frame user.
|
||||
|
||||
| Local Chromium action | Actual ARM64 result, independently read back |
|
||||
| --- | --- |
|
||||
| Linked numeric input 125% | Native Pulse channel values 81920 / 81920; plugin reads 125 / 125 |
|
||||
| Linked slider click, then ArrowDown | 75% then 74%; native values 49152 / 49152 then 48496 / 48496 |
|
||||
| Unlink, set left to 33% | Native values 21626 / 0; plugin reads 33 / 0 |
|
||||
| Set right to 67% | Native values 0 / 43909; plugin reads 0 / 67 |
|
||||
| Re-link | Official frontend issues a right-only 33% write; native values 0 / 21626 |
|
||||
| Close and reopen | New document/session, ready; backend reads and displayed linked value return to the restored 0%; no mount write |
|
||||
| Repeat linked numeric 150% with inherited service locale | Native values 98304 / 98304; plugin reads 150 / 150; reopen again reads restored zero |
|
||||
|
||||
The test restores the original sink state after **each** write. Therefore the
|
||||
separate-channel checks intentionally begin from 0% / 0%, rather than from the
|
||||
previous UI value. A right-only re-link request is the published plugin's
|
||||
behavior. These checks verify channel selection and preservation of the other
|
||||
channel; they do not claim a continuous unmuted listening session. Pulse's
|
||||
integer conversion truncates fractional native values; the evidence retains
|
||||
those integers rather than replacing them with rounded percentages.
|
||||
|
||||
## Runtime and transport
|
||||
|
||||
Each run separately acquired the atomic `/tmp/frame-test.lock`; neither stole
|
||||
an existing lock. A unique private Decky user service used its own plugin,
|
||||
settings and runtime directories, was never enabled, and had an eight-minute
|
||||
runtime limit. The upstream ARM64 loader matched GitHub's published SHA-256
|
||||
and ELF machine 183. The plugin ZIP matched the official digest.
|
||||
|
||||
The local relay served actual `ui/headset.html`, vendored React and
|
||||
`framey-host.js`, with exact official frontend bytes in an opaque
|
||||
`allow-scripts` iframe. Outer status/install/check responses were fixtures.
|
||||
Plugin calls used production `framey_panel.call_method`, including the current
|
||||
store receipt, SSH through lxso2, live Steam CEF and `DeckyBackend.call` into the
|
||||
actual plugin subprocess. Only the temporary install paths were redirected.
|
||||
This does not verify the deployed headset HTTP endpoint or a VR compositor.
|
||||
|
||||
The first private service explicitly used `LC_ALL=C`. The second inherited the
|
||||
user-service locale without a `LANG` or `LC_ALL` override, matching the
|
||||
production service configuration, and repeated actual reads, a 150% write and
|
||||
reopen. Zero alone was not accepted as success: upstream returns zero on
|
||||
command failure. Every write returned true, both backend channel reads agreed,
|
||||
and independent native Pulse readbacks confirmed nonzero changes.
|
||||
|
||||
## Bounded muted writes and restoration
|
||||
|
||||
The default sink was already muted at native channel values 0 / 0. Before each
|
||||
plugin write, a private device-side helper captured the exact sink identity,
|
||||
channel values and mute state; persisted recovery state; and started a bounded
|
||||
60-second systemd watchdog. It then required a successful mute readback before
|
||||
allowing the plugin call. The relay allowed only `get_volume_state` and
|
||||
`set_volume_state`, known channels and values from 0 through 150.
|
||||
|
||||
After the call, independent `pactl --format=json list sinks` and both plugin
|
||||
reads checked the result. In `finally`, the helper restored exact native
|
||||
channel values while muted, checked them, restored the original mute state,
|
||||
and verified the entire snapshot before removing recovery state and stopping
|
||||
its watchdog. No default route, playback, existing stream or power setting was
|
||||
changed. Automatic watchdog expiry was armed but not deliberately exercised;
|
||||
explicit restoration passed after all seven writes.
|
||||
|
||||
Both private Decky services/bases were removed and their locks released.
|
||||
Fresh-lock cleanup audits found their Decky and guard units unloaded/inactive
|
||||
with PID 0, paths absent, and the exact original sink/mute restored. Battery
|
||||
stayed 100 → 100; normal managed Decky and headset-panel services stayed
|
||||
inactive. Decky startup reinjects its frontend and can restart Steam CEF.
|
||||
|
||||
## Catalog and verification limits
|
||||
|
||||
The canonical store catalog and generated table report **4 works / 48 partial /
|
||||
53 broken / 11 N/A**. A fresh full official-store fetch at 06:10:58 UTC found
|
||||
all 116 IDs, versions and digests unchanged. Historical fake-Frame checks cover
|
||||
this exact archive's install/receipt/removal with backend execution stubbed;
|
||||
that test was not repeated or relabelled as real-backend evidence.
|
||||
|
||||
Audible gain, sound quality, physical speaker output, Bluetooth sinks,
|
||||
headset-controller input and worn VR remain unverified. The store row includes
|
||||
these limits. This change records new real-backend evidence and updates its
|
||||
compatibility classification; it changes no shipped audio implementation.
|
||||
|
||||
The full server suite completed 1,626 tests with no failures (16 skips); all
|
||||
18 site tests passed. Generated-document consistency and `git diff --check`
|
||||
also passed. No implementation change followed those tests.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Official Web Browser 1.4.3 frontend follow-up
|
||||
|
||||
The exact official Decky store archive for ID 59 is 91,859 bytes and matched
|
||||
SHA-256 `1192864fddc5d72550346b8dff02c21d71f0c40a06ac41c4e67bb6220d267f9d`.
|
||||
Its legacy bundle calls `deckyFrontendLib.getReactRoot` when attempting to patch
|
||||
Steam's main menu and search bar. The prior full-catalog audit recorded
|
||||
`getReactRoot is not a function` and initializer failure.
|
||||
|
||||
Framey's isolated iframe has no Steam React root. The host now exposes
|
||||
`getReactRoot()` with a **null** result, consistent with its existing inert
|
||||
`findInReactTree()` lookup. It never returns a fabricated Steam tree or permits
|
||||
patching the user's Steam UI. The focused Node host test checks this contract.
|
||||
|
||||
A focused local Chromium run used the verified archive through
|
||||
`scripts/framey-audit-browser.py`, with every backend method, fetch and
|
||||
WebSocket denied. The exact frontend reported `ready=true` with no initializer
|
||||
error after 2.5 seconds, and again during a longer ten-second observation.
|
||||
It attempted `set_main_tab` and `load`; the audit denied both. Console output
|
||||
reported that no Steam window or main-menu root was available. The panel did
|
||||
not show a usable browser view in the audited state.
|
||||
|
||||
Static inspection of this archive's published Python backend shows
|
||||
`set_main_tab()` requesting a Steam GamepadUI CDP tab, then returning a key and
|
||||
port for its own WebSocket server. That Steam tab and browser injection path
|
||||
are outside Framey's isolated panel. No real plugin backend was executed, no
|
||||
browser tab or navigation was verified, and no real Frame or worn-VR check was
|
||||
performed for this change. The catalog retains **broken** for this digest; its
|
||||
reason and focused observation now distinguish successful initialization from
|
||||
the unresolved core browser dependency. The full 116-plugin browser baseline
|
||||
still refers to its earlier host commit; this is a one-plugin follow-up.
|
||||
|
||||
## Unworn Framey regression gate
|
||||
|
||||
The exact code commit `d5e62f643d462b7070f27c2c47b11b4aa9982094` ran
|
||||
`scripts/framey-vr-check.py --exercise` under its own shared-lock owner at
|
||||
07:26 UTC. Preflight found SteamVR activity 3 (standby), proximity 6.25 and
|
||||
battery 100%. The real Decky sample setting saved `enabled=true`, survived a
|
||||
fresh iframe, and the quick action read the saved value. A trusted synthetic
|
||||
OpenVR pointer, slider value 50, CDP text entry after focus and changed stereo
|
||||
compositor hashes passed. The mirror capture SHA-256 was
|
||||
`4f0d977fab7c144a3490b12ebefddacf3126db585057eb022006ffdf844a7498`.
|
||||
The command exited 0 with `passed=true`; original power settings (`false` and
|
||||
`5.000000`) were read back, battery ended at 99%, the recovery timer was
|
||||
removed, cleanup errors were empty and only its own lock was released. The
|
||||
private full report is `/tmp/framey-web-browser-vr-d5e62f6/result.json` on the
|
||||
test host. This verifies the sample-panel regression path, not the official
|
||||
Web Browser plugin or a worn headset. Synthetic native scroll still did not
|
||||
move.
|
||||
@@ -0,0 +1,62 @@
|
||||
# v0.4.42 tagged release, 26 October 2026
|
||||
|
||||
The annotated `v0.4.42` tag dereferences to main merge commit
|
||||
`98909cb54374a7bd7ec1f665f50eda20fa196f32` from PR #302. The app and
|
||||
lockfile both report 0.4.42. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40276685171)
|
||||
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
|
||||
The Linux job launched its built AppImage under isolated Xvfb. CI does not prove
|
||||
a physical Windows or Mac launch.
|
||||
|
||||
## Real Frame, exact tag
|
||||
|
||||
From an exact tag checkout, `scripts/frame-smoke.sh` acquired its own unique
|
||||
owner in `/tmp/frame-test.lock` before reading the headset. Preflight found
|
||||
SteamVR activity 3 (standby), proximity 6.25 and battery 100%. On Frame OS 0.5.6
|
||||
build `20261022.6153909`, it recorded **12 passed / 1 skipped / 0 failed** and
|
||||
exited 0. ARM64 and Proton test executables launched. The x86_64 title was
|
||||
installed and removed, but its launch was skipped because optional Steam Linux
|
||||
Runtime 4 was absent. Test title/process cleanup passed and only the run's lock
|
||||
was released. The private result is
|
||||
`tests/smoke/results/2026-10-26T055940Z-20261022.6153909.json` in the exact tag
|
||||
checkout.
|
||||
|
||||
The separate [unworn Framey candidate exercise](v0442-candidate-2026-10-26.md)
|
||||
passed real sample setting save/reopen, synthetic OpenVR pointer delivery and
|
||||
stereo compositor change, with power restored and cleanup complete. It was run
|
||||
on the release branch's code before the evidence-only final commit. No person
|
||||
wore the Frame. Neither run verifies official plugin VR panels, physical
|
||||
controller usability, native scrolling, VR keyboard or optical comfort. The
|
||||
hourly worn check remains enabled.
|
||||
|
||||
## Installer, checksum and updater checks
|
||||
|
||||
The draft held ten installers plus `SHA256SUMS`, each with a GitHub SHA-256
|
||||
digest. `scripts/publish-release.sh --dry-run v0.4.42` checked the exact remote
|
||||
tag and every installer digest, regenerated the checksum list from those
|
||||
metadata digests, and matched the uploaded `SHA256SUMS` digest. A fresh
|
||||
checksum-list download independently matched
|
||||
`e5c85f5a46d789dccafae5259bd00b9b048dd10ca4f2a6cdc52d1f432a6db989`.
|
||||
|
||||
A fresh Linux x86_64 AppImage download matched
|
||||
`16fc9fbe448bccbe59aa4206bc171c6bfc741973d2b4da0a79652a5c11d837b3`.
|
||||
`scripts/check-appimage-launch.py` launched those exact bytes under isolated
|
||||
Xvfb: the bundled server answered HTTP 200 with `FrameControl/1` and then
|
||||
stopped. Fresh Mac ZIP downloads matched GitHub digests: arm64
|
||||
`394d6088e732a19289b7b92366dd247f14f7293a345d57ed5c4e9c83a50abad3`
|
||||
and x64 `66d4606e38210d02af59dc87e4d1216282ad9e968056d7096f247fcc51e72395`.
|
||||
Both bundles report version 0.4.42; their main Mach-O CPU types are arm64 and
|
||||
x86_64 respectively. Neither tagged Mac ZIP was launched on a physical Mac.
|
||||
|
||||
`scripts/publish-release.sh v0.4.42` published
|
||||
[v0.4.42](https://github.com/saphid/frame-control/releases/tag/v0.4.42) at
|
||||
2026-10-26 06:02:20 UTC. GitHub's latest-release API read back `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.42 with the stable release page and ten SHA-256 installer digests.
|
||||
The tag contains #299, #300, #301 and #303 through #302's release merge.
|
||||
#304 merged after the tag and is not in this release.
|
||||
|
||||
The exact-version official Decky table for v0.4.42 is **3 works / 48 partial /
|
||||
54 broken / 11 N/A**. The two additional works labels are restricted to tested
|
||||
per-app silent-stream volume control. Audible playback and official-plugin
|
||||
worn VR remain unverified.
|
||||
@@ -64,7 +64,9 @@ none of these frontend controls has passed a worn-headset check yet.
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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. |
|
||||
| Other `SteamClient` subscriptions | Explicit inert registration methods return disposable handles; no fabricated power/input/game-action events. Mutating Steam/power methods are unavailable. | Host contract tests; safe fallbacks only |
|
||||
| `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 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. |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Official Decky store compatibility with Framey
|
||||
|
||||
Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins from https://plugins.deckbrew.xyz/plugins.
|
||||
Snapshot: 2026-10-26T06:10:58.537029+00:00; 116 current visible official plugins from https://plugins.deckbrew.xyz/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: 3 · Partial: 48 · Broken: 54 · N/A: 11.**
|
||||
**Works: 4 · Partial: 48 · Broken: 53 · 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. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
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. 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. A [Web Browser frontend follow-up](evidence/framey-web-browser-2026-10-26.md) verified its exact archive now initializes with an inert null Steam React-root lookup. Its GamepadUI CDP/browser backend paLine truncated
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -40,11 +40,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 10: vibrantDeck 2.0.1 | legacy / legacy | N/A | Not supported on Frame: Changes X11 gamescope color properties, not the Frame SteamVR compositor. | static archive, local browser, fake-Frame |
|
||||
| 11: Fantastic 0.5.1 | legacy / legacy | N/A | Not supported on Frame: Targets Steam Deck fan hardware; no Frame fan-control mapping is provided. | static archive, local browser, fake-Frame |
|
||||
| 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 |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied. | 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. 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 |
|
||||
| 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. Running-game context is now supplied; per-game selection was attempted with a synthetic game and backend calls denied. Real per-game selection, background artwork and worn-VR input remain 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 |
|
||||
| 22: Memory Deck 0.1.8 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: memory-deck/bin/libscanmem.so.1.0.0. | static archive, local browser, fake-Frame |
|
||||
| 23: Animation Changer 1.3.2 | legacy / legacy | partial | Initializer succeeds; forced backend failure prevents loading animations. Boot-video changes untested. | static archive, local browser, fake-Frame |
|
||||
@@ -56,7 +56,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 | 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 |
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | partial | Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame |
|
||||
@@ -68,15 +68,15 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'type'). | static archive, local browser, fake-Frame |
|
||||
| 53: Shotty 0.1.0 | legacy / legacy | broken | Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | works | Default-sink controls pass in local Chromium through real Decky 3.3.0 ARM64: linked slider/keyboard and numeric writes, separate channels, re-link and reopen, with independent Pulse readback including 125%/150% while muted. Exact original volume/mute restored. Audible output, Bluetooth sinks and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | The exact frontend now initializes with a null Steam React-root lookup. The browser itself still requires Steam GamepadUI CDP injection and a backend WebSocket unavailable in Framey; no working browser tab was verified. | static archive, local browser, fake-Frame, focused local browser |
|
||||
| 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 | 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 |
|
||||
| 65: Decky Terminal 0.5.3 | 1 / 2 | partial | Local routes initialize after parameter-route support; no shell commands were run. | static archive, local browser, fake-Frame |
|
||||
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. ARM64 backend, live MobX/running-game observations and automatic recording remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 67: Battery Tracker 0.2.0 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 68: Wine Cellar 0.2.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-wine-cellar/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 70: MicroSDeck 0.11.2 | legacy / 2 | broken | Plugin contains a non-ARM64 Linux binary: MicroSDeck/bin/backend. | static archive, local browser, fake-Frame |
|
||||
|
||||
+34
-7
@@ -115,9 +115,9 @@ Its cleanup actions were not tested; they may remove local game saves.
|
||||
|
||||
Framey also supplies local tabs/sidebar pages, spinners, dialog/menu wrappers
|
||||
and progress bars. Its explicit SteamClient subscription fallbacks return
|
||||
disposable handles without emitting fabricated events. There is no attached
|
||||
running game in `DFL.Router`, and power, app launch and hardware mutation methods
|
||||
are not supplied. Plugins that need live Steam stores still require integration
|
||||
disposable handles without emitting fabricated events. Running-game reads and
|
||||
lifetime notifications use the scoped bridge described below; power, app launch
|
||||
and hardware mutation methods are not supplied. Other live Steam stores require integration
|
||||
work. IndexedDB is hidden because the browser exposes it but denies access in
|
||||
an opaque iframe; localForage can then select Framey's bounded localStorage.
|
||||
Direct IndexedDB users remain unsupported.
|
||||
@@ -125,13 +125,40 @@ Direct IndexedDB users remain unsupported.
|
||||
`SuspensefulImage` renders a native image. SteamClient `Storage.GetJSON` and
|
||||
`SetObject` use the same bounded per-plugin cache, including panel reopen
|
||||
persistence; they do not read Steam's account/global JSON store. Missing keys
|
||||
reject with `Not found`. Detached `SteamUIStore.RunningApps` is a frozen empty
|
||||
list, and the suspend store has no methods. Optional Steam app/account globals
|
||||
are undefined. These fallbacks let settings render without inventing live
|
||||
reject with `Not found`. The suspend store has no methods. Optional Steam app/account globals
|
||||
are undefined until a successful library read. These fallbacks let settings render without inventing live
|
||||
games or account identity. [The exact-bundle follow-up](evidence/framey-ui-fallbacks-2026-10-26.json)
|
||||
verifies Pause Games and Tailscale Control settings in local Chromium; their
|
||||
primary backend actions remain unverified. PlayTime still requires Steam's
|
||||
real app store and game observers.
|
||||
MobX game observers for automatic tracking.
|
||||
|
||||
Panels referencing running-game APIs now receive a detached, read-only
|
||||
`SteamUIStore.RunningApps` / `MainRunningApp` snapshot, also exposed through
|
||||
`DFL.Router`. The authenticated headset-only `/api/framey/running` endpoint
|
||||
checks installed store ownership; the parent checks the active panel session.
|
||||
Poll and close requests also carry the panel's pinned SHA-256 receipt. A missing
|
||||
or changed installed receipt is rejected before any Steam CEF request; a rejected
|
||||
stale close relies on the existing finite subscription lease for cleanup.
|
||||
Fixed bundled code runs in Steam CEF. It copies at most 64 running app overviews, a main app and
|
||||
public Steam CDN capsule URLs; it never copies local artwork paths, launch
|
||||
commands or Steam objects. The host refreshes about every two seconds without
|
||||
overlapping requests. Failure clears stale context and sets
|
||||
`SteamUIStore.FrameyRunningAvailable` to false.
|
||||
|
||||
`GameSessions.RegisterForAppLifetimeNotifications` relays actual Steam
|
||||
`{unAppID, bRunning}` callbacks through a bounded 64-event queue. Snapshot
|
||||
changes never fabricate launch/exit callbacks. Handles support `.unregister()`;
|
||||
panel close/replacement removes the CEF subscription, with a 15-second lease
|
||||
(checked every five seconds) if cleanup cannot reach Steam. At most four panel
|
||||
sessions are retained. These are open-panel observations, not closed-panel
|
||||
automation or a MobX implementation. Queue overflow can lose old callbacks.
|
||||
`appStore` capsule helpers expose only projected public URLs for running apps;
|
||||
unavailable or local custom artwork returns an empty value.
|
||||
|
||||
[Running-context evidence](evidence/framey-running-context-2026-10-26.md) separates
|
||||
real Frame empty-list/subscription/cleanup reads, the exact Discord Status
|
||||
ARM64 backend connection check, and synthetic nonempty/transition fixtures.
|
||||
Real game transitions, Discord activity and worn-VR operation remain unverified.
|
||||
|
||||
Three parameterless SteamClient reads use Steam's actual CEF context:
|
||||
`User.GetIPCountry`, `InstallFolder.GetInstallFolders` and `System.GetSystemInfo`.
|
||||
|
||||
@@ -36,7 +36,7 @@ Ready means initializer completion, not useful plugin behavior. No plugin backen
|
||||
<button id=run>Run catalog</button> <button id=download>Download observations</button><p id=progress></p>
|
||||
<iframe id=panel sandbox="allow-scripts" style="width:1100px;height:740px"></iframe>
|
||||
<script>
|
||||
const plugins=CATALOG_DATA, librarySnapshot=LIBRARY_DATA;
|
||||
const plugins=CATALOG_DATA, librarySnapshot=LIBRARY_DATA, runningSnapshot=RUNNING_DATA;
|
||||
window.results=[];let active=null,running=false;
|
||||
addEventListener('message',event=>{
|
||||
if(event.source!==panel.contentWindow||!active)return;
|
||||
@@ -45,6 +45,8 @@ addEventListener('message',event=>{
|
||||
if(data.type==='framey:error')active.error=String(data.message).slice(0,500);
|
||||
if(data.type==='framey:ready')active.ready=true;
|
||||
if(data.type==='framey:events')event.source.postMessage({type:'framey:reply',id:data.id,result:{events:[],cursor:0}},'*');
|
||||
if(data.type==='framey:running')event.source.postMessage({type:'framey:reply',id:data.id,
|
||||
...(runningSnapshot?{result:runningSnapshot}:{error:'Read-only compatibility audit: live running context blocked'})},'*');
|
||||
if(data.type==='framey:steam')event.source.postMessage({type:'framey:reply',id:data.id,
|
||||
...(data.method==='GetLibrarySnapshot'&&librarySnapshot?
|
||||
{result:librarySnapshot}:{error:'Read-only compatibility audit: live Steam API blocked'})},'*');
|
||||
@@ -53,13 +55,14 @@ addEventListener('message',event=>{
|
||||
event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: backend method blocked'},'*');}
|
||||
if(data.type==='framey:cache:init')event.source.postMessage({type:'framey:cache:seed',session:data.session,entries:{}},'*');
|
||||
});
|
||||
window.runAudit=async(ids)=>{
|
||||
window.runAudit=async(ids,settleMs=1200)=>{
|
||||
if(!Number.isInteger(settleMs)||settleMs<1200||settleMs>10000)throw Error('Invalid audit settle time');
|
||||
if(running)throw Error('Audit already running');running=true;run.disabled=true;
|
||||
try{for(const plugin of plugins.filter(plugin=>!ids||ids.includes(plugin.id))){
|
||||
active={id:plugin.id,name:plugin.name,ready:false,error:null,logs:[],methods:[],
|
||||
library:librarySnapshot?'snapshot replay':'unavailable'};
|
||||
library:librarySnapshot?'snapshot replay':'unavailable',running:runningSnapshot?'snapshot replay':'unavailable'};
|
||||
progress.textContent=plugin.id+' '+plugin.name;panel.src='/panel/'+plugin.id;
|
||||
await new Promise(resolve=>setTimeout(resolve,1200));results.push(active);active=null;
|
||||
await new Promise(resolve=>setTimeout(resolve,settleMs));results.push(active);active=null;
|
||||
panel.src='about:blank';await new Promise(resolve=>setTimeout(resolve,30));
|
||||
}}finally{running=false;run.disabled=false;progress.textContent='Done: '+results.length;}
|
||||
return results;
|
||||
@@ -77,6 +80,8 @@ def main():
|
||||
parser.add_argument('--port', type=int, default=18765)
|
||||
parser.add_argument('--library-snapshot', type=Path,
|
||||
help='Optional detached Steam library JSON; replay only, not live Steam/backend evidence')
|
||||
parser.add_argument('--running-snapshot', type=Path,
|
||||
help='Optional detached running-app JSON; replay only, not live Steam/backend evidence')
|
||||
args = parser.parse_args()
|
||||
plugins = json.loads(args.catalog.read_text())['plugins']
|
||||
records = {p['id']: p for p in plugins}
|
||||
@@ -86,6 +91,12 @@ def main():
|
||||
if len(raw) > 2*1024*1024:
|
||||
parser.error('library snapshot exceeds 2 MiB')
|
||||
library = json.loads(raw)
|
||||
running = None
|
||||
if args.running_snapshot:
|
||||
raw = args.running_snapshot.read_bytes()
|
||||
if len(raw) > 256*1024:
|
||||
parser.error('running snapshot exceeds 256 KiB')
|
||||
running = json.loads(raw)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
@@ -99,7 +110,8 @@ def main():
|
||||
# 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')
|
||||
body = PAGE.replace('CATALOG_DATA', catalog).replace(
|
||||
'LIBRARY_DATA', json.dumps(library).replace('<', '\\u003c')).encode()
|
||||
'LIBRARY_DATA', json.dumps(library).replace('<', '\\u003c')).replace(
|
||||
'RUNNING_DATA', json.dumps(running).replace('<', '\\u003c')).encode()
|
||||
elif path.startswith('/panel/'):
|
||||
try:
|
||||
record = records[int(path.split('/')[-1])]
|
||||
|
||||
@@ -54,7 +54,8 @@ def render(data):
|
||||
'A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer '
|
||||
'through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, '
|
||||
'independent server readbacks and Mixer filter persistence across a runtime restart passed. '
|
||||
'An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. '
|
||||
'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. '
|
||||
@@ -71,6 +72,7 @@ def render(data):
|
||||
'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. '
|
||||
'A [Web Browser frontend follow-up](evidence/framey-web-browser-2026-10-26.md) verified its exact archive now initializes with an inert null Steam React-root lookup. Its GamepadUI CDP/browser backend path remains unavailable, so it stays broken. '
|
||||
'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 '
|
||||
|
||||
@@ -3,7 +3,7 @@ import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source=readFileSync(new URL('../../ui/framey-host.js',import.meta.url),'utf8');
|
||||
const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__surface=PluginSurface; window.__legacy=legacyServerAPI; window.__library=setLibrarySnapshot;\n})();');
|
||||
const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__surface=PluginSurface; window.__legacy=legacyServerAPI; window.__library=setLibrarySnapshot; window.__running=setRunningSnapshot; window.__refreshRunning=refreshRunning;\n})();');
|
||||
assert.notEqual(testSource,source);
|
||||
const states=[];
|
||||
const React={Fragment:'fragment',Component:class {},createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
@@ -24,6 +24,9 @@ const context=vm.createContext({window,document:{getElementById:id=>{
|
||||
navigator:{language:'en'}, parent,DOMException,addEventListener:(type,handler)=>listeners.push({type,handler}),
|
||||
setTimeout:()=>1,clearTimeout(){}});
|
||||
vm.runInContext(testSource,context);
|
||||
// No Steam React tree exists inside the isolated Framey iframe.
|
||||
assert.equal(window.DFL.getReactRoot({}),null);
|
||||
assert.equal(window.DFL.findInReactTree(window.DFL.getReactRoot(null),()=>true),null);
|
||||
// Official @decky/rollup bundles use the automatic JSX runtime, including keyed lists.
|
||||
const jsx=window.SP_JSX.jsx('p',{id:'message',children:'Saved'},'message-key');
|
||||
assert.equal(jsx.type,'p');
|
||||
@@ -254,6 +257,7 @@ assert.equal(window.SteamClient.System.ShutdownPC,undefined);
|
||||
assert.equal(window.DFL.Router.MainRunningApp,undefined);
|
||||
assert.equal(window.DFL.Router.RunningApps.length,0);
|
||||
assert.equal(window.DFL.Router.RunningApps,window.SteamUIStore.RunningApps);
|
||||
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false);
|
||||
assert.ok(Object.isFrozen(window.SteamUIStore));
|
||||
assert.ok(Object.isFrozen(window.SuspendResumeStore));
|
||||
assert.equal(window.SuspendResumeStore.InitiateSleep,undefined);
|
||||
@@ -283,6 +287,46 @@ for(const invalid of [null,{apps:[],gameIds:[10]},
|
||||
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 lifetimes=[];
|
||||
const lifetimeHandle=window.SteamClient.GameSessions.RegisterForAppLifetimeNotifications(event=>{
|
||||
lifetimes.push({...event,running:Array.from(window.DFL.Router.RunningApps,app=>app.appid)});
|
||||
});
|
||||
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.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);
|
||||
window.DFL.Router.MainRunningApp.display_name='Detached';
|
||||
assert.equal(running.apps[0][1],'Game Ten');
|
||||
const launch={...running,events:[{sequence:1,unAppID:10,bRunning:true}],cursor:1};
|
||||
window.__running(launch);await Promise.resolve();
|
||||
assert.equal(lifetimes.length,1);assert.deepEqual(lifetimes[0].running,[10]);
|
||||
window.__running(launch);await Promise.resolve();assert.equal(lifetimes.length,1);
|
||||
for(const invalid of [null,{...running,runningIds:[99]}, {...running,mainAppId:99},
|
||||
{...running,runningIds:[10,10]}, {...running,artwork:[[10,'file:///private',[]]]}, {...running,cursor:-1},
|
||||
{...running,events:[{sequence:1,unAppID:10,bRunning:'yes'}],cursor:1}]){
|
||||
assert.throws(()=>window.__running(invalid),/Invalid Steam/);
|
||||
assert.equal(window.DFL.Router.RunningApps.length,1);
|
||||
}
|
||||
const exited={...running,artwork:[],apps:[],runningIds:[],mainAppId:null,
|
||||
events:[{sequence:2,unAppID:10,bRunning:false}],cursor:2};
|
||||
window.__running(exited);await Promise.resolve();
|
||||
assert.deepEqual(lifetimes[1].running,[]);assert.equal(lifetimes[1].bRunning,false);
|
||||
lifetimeHandle.unregister();lifetimeHandle();
|
||||
window.__running({...launch,cursor:3,events:[{sequence:3,unAppID:10,bRunning:true}]});
|
||||
await Promise.resolve();assert.equal(lifetimes.length,2);
|
||||
// A failed live read clears stale running context without inventing an exit.
|
||||
const refresh=window.__refreshRunning();
|
||||
const runningRequest=messages.at(-1);assert.equal(runningRequest.type,'framey:running');
|
||||
listeners.find(l=>l.type==='message').handler({source:parent,data:{type:'framey:reply',id:runningRequest.id,error:'offline'}});
|
||||
await refresh;
|
||||
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false);
|
||||
assert.equal(window.DFL.Router.RunningApps.length,0);
|
||||
assert.equal(window.DFL.Router.MainRunningApp,undefined);
|
||||
assert.equal(lifetimes.length,2);
|
||||
for(const register of [window.SteamClient.System.UI.RegisterForSystemKeyEvents,
|
||||
window.SteamClient.Apps.RegisterForGameActionUserRequest]){
|
||||
const dispose=register(()=>eventCount++);assert.equal(dispose.unregister,dispose);dispose();
|
||||
@@ -359,3 +403,13 @@ assert.throws(()=>api.addEventListener('bad event',callback),/Invalid Decky even
|
||||
assert.throws(()=>api.addEventListener('tick',null),/Invalid Decky event/);
|
||||
listeners.find(x=>x.type==='unload').handler();
|
||||
assert.equal(messages.at(-1).type,'framey:events:close');
|
||||
const finalRefresh=window.__refreshRunning();
|
||||
assert.equal(window.__refreshRunning(),finalRefresh,'Running reads must not overlap');
|
||||
const finalRequest=messages.at(-1);
|
||||
listeners.find(x=>x.type==='pagehide').handler();
|
||||
assert.equal(messages.at(-1).type,'framey:running:close');
|
||||
onMessage({source:parent,data:{type:'framey:reply',id:finalRequest.id,result:launch}});
|
||||
await finalRefresh;
|
||||
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false,'Closed panels ignore late snapshots');
|
||||
const messageCount=messages.length;
|
||||
await window.__refreshRunning();assert.equal(messages.length,messageCount);
|
||||
@@ -69,3 +69,30 @@ 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]};
|
||||
const beforeRunning=requests.length;
|
||||
for(const change of [{session:'old'},{args:[-1]},{args:[true]},{args:[0,'extra']}])
|
||||
await send({...running,...change});
|
||||
await send(running,{});assert.equal(requests.length,beforeRunning);
|
||||
const pendingRunning=send(running);
|
||||
assert.equal(requests.at(-1).path,'/api/framey/running');
|
||||
assert.equal(requests.at(-1).body.active,true);
|
||||
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
|
||||
const oldRunning=requests.at(-1);
|
||||
vm.runInContext('openFrameyPanel(96,"Timer")',context);
|
||||
assert.equal(requests.at(-1).body.active,false);
|
||||
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
|
||||
assert.equal(requests.at(-1).body.session,session);
|
||||
const beforeStaleReply=replies.length;
|
||||
oldRunning.resolve({ok:true,json:async()=>({result:{apps:[],events:[],cursor:0}})});
|
||||
await pendingRunning;assert.equal(replies.length,beforeStaleReply);
|
||||
session=vm.runInContext('frameySession',context);
|
||||
const finalRunning=send({...running,id:21,session});
|
||||
requests.at(-1).resolve({ok:true,json:async()=>({result:{apps:[],events:[],cursor:0}})});
|
||||
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);
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
// Real Frame getter source and unregister-handle shape were probed over SSH.
|
||||
// Nonempty rows and launch/exit notifications here are explicit test fixtures.
|
||||
const source=readFileSync(new URL('../../ui/framey-running.js',import.meta.url),'utf8');
|
||||
const listeners=new Set(),timers=new Map();let now=1000,nextTimer=0;
|
||||
const api={RegisterForAppLifetimeNotifications(listener){
|
||||
listeners.add(listener);return {unregister:()=>listeners.delete(listener)};
|
||||
}};
|
||||
const app={appid:10,display_name:'Test game',sort_as:'test',app_type:1,gameid:'10',
|
||||
minutes_playtime_forever:30,minutes_playtime_last_two_weeks:5,rt_last_time_played:123,
|
||||
private_path:'PRIVATE',owner:'PRIVATE'};
|
||||
const window={SteamClient:{GameSessions:api},SteamUIStore:{RunningApps:[app],MainRunningApp:app}};
|
||||
window.appStore={GetVerticalCapsuleURLForApp:()=> 'https://shared.steamstatic.com/steam/apps/10/library.jpg?t=123',
|
||||
GetCustomVerticalCapsuleURLs:()=>['file:///PRIVATE/path','https://example.com/PRIVATE','https://shared.steamstatic.com/steam/apps/10/custom.jpg']};
|
||||
const context=vm.createContext({window,URL,Date:{now:()=>now},
|
||||
setInterval:fn=>{timers.set(++nextTimer,fn);return nextTimer;},clearInterval:id=>timers.delete(id)});
|
||||
const poll=(session='one',pluginId=13,active=true,cursor=0)=>
|
||||
JSON.parse(JSON.stringify(vm.runInContext(source,context)({session,pluginId,active,cursor})));
|
||||
const emit=event=>{for(const listener of listeners)listener(event);};
|
||||
let data=poll();
|
||||
assert.deepEqual(data.apps,[[10,'Test game','test',1,'10',30,5,123]]);
|
||||
assert.deepEqual(data.artwork,[[10,'https://shared.steamstatic.com/steam/apps/10/library.jpg',['https://shared.steamstatic.com/steam/apps/10/custom.jpg']]]);
|
||||
assert.deepEqual(data.runningIds,[10]);assert.equal(data.mainAppId,10);
|
||||
assert.equal(data.lifetimeAvailable,true);assert.deepEqual(data.events,[]);
|
||||
assert.equal(listeners.size,1);
|
||||
// A nonempty snapshot does not invent a game launch notification.
|
||||
assert.equal(data.cursor,0);assert.deepEqual(poll().events,[]);
|
||||
emit({unAppID:10,bRunning:true,private_token:'PRIVATE'});
|
||||
assert.deepEqual(poll().events,[{sequence:1,unAppID:10,bRunning:true}]);
|
||||
assert.deepEqual(poll('one',13,true,1).events,[]);
|
||||
assert.equal(poll().events.length,1); // Retry a dropped HTTP response.
|
||||
assert.equal(listeners.size,1); // Polls never duplicate registration.
|
||||
poll('two',37);assert.equal(listeners.size,2);
|
||||
assert.throws(()=>poll('one',37),/another plugin/);
|
||||
for(const event of [{unAppID:'10',bRunning:true},{unAppID:10,bRunning:1},
|
||||
{unAppID:NaN,bRunning:true},{unAppID:-1,bRunning:false}])emit(event);
|
||||
assert.equal(poll().cursor,1);
|
||||
for(let n=0;n<100;n++)emit({unAppID:10,bRunning:!!(n%2)});
|
||||
assert.equal(poll().events.length,64);
|
||||
assert.equal(poll().cursor,101);
|
||||
window.SteamUIStore.RunningApps=[];window.SteamUIStore.MainRunningApp=undefined;
|
||||
assert.deepEqual(poll().runningIds,[]);assert.equal(poll().mainAppId,null);
|
||||
assert.equal(poll().cursor,101); // Reading a closed game invents no exit event.
|
||||
assert.equal(poll('one',13,false),null);assert.equal(listeners.size,1);
|
||||
now+=16000;for(const timer of [...timers.values()])timer();
|
||||
assert.equal(listeners.size,0);assert.equal(timers.size,0);
|
||||
poll();assert.equal(listeners.size,1);
|
||||
window.SteamClient.GameSessions={...api};poll();assert.equal(listeners.size,1);
|
||||
poll('one',13,false);assert.equal(listeners.size,0);
|
||||
window.SteamUIStore.RunningApps=Array(65).fill(app);
|
||||
assert.throws(()=>poll(),/Too many/);assert.equal(listeners.size,0);
|
||||
window.SteamUIStore=undefined;
|
||||
assert.throws(()=>poll(),/unavailable/);assert.equal(listeners.size,0);
|
||||
@@ -22,6 +22,54 @@ RECEIPT = 'a' * 64
|
||||
|
||||
|
||||
class Panel(unittest.TestCase):
|
||||
def test_running_queue_and_host_contract(self):
|
||||
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_running.mjs')],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_running_bridge_is_installed_scoped_bounded_and_headset_only(self):
|
||||
def fake_ssh(command, stdin, **_):
|
||||
self.assertEqual(command, 'python3 -')
|
||||
compile(stdin, '<running read>', 'exec')
|
||||
self.assertIn('RegisterForAppLifetimeNotifications', stdin)
|
||||
self.assertIn('262144', stdin)
|
||||
self.assertNotIn('__FRAMEY_STEAM_', stdin)
|
||||
return '{"result":{"apps":[],"events":[],"cursor":0}}'
|
||||
with patch.object(framey_panel.frame_decky, 'status', return_value={'plugins':[{'storeId':13,'sha256':'a'*64}]}):
|
||||
self.assertEqual(framey_panel.poll_running(fake_ssh,13,'test-1',True,receipt='a'*64)['result']['cursor'],0)
|
||||
for plugin_id,session,active,cursor in [(True,'test',True,0),(0,'test',True,0),
|
||||
(14,'test',True,0),(13,'bad session',True,0),(13,'test',1,0),
|
||||
(13,'test',True,True),(13,'test',True,-1),(13,'test',True,2**53)]:
|
||||
with self.subTest(args=(plugin_id,session,active,cursor)):
|
||||
with self.assertRaises(framey_panel.PanelError):
|
||||
framey_panel.poll_running(lambda *a,**k:self.fail('Unexpected SSH'),
|
||||
plugin_id,session,active,cursor,receipt='a'*64)
|
||||
with patch.object(server, 'HEADSET_RUNTIME', False), patch.dict(server.os.environ, {'FRAME_E2E_CONTAINER':'0'}):
|
||||
with self.assertRaises(server.Failure):
|
||||
server.framey_running_action({'id':13,'session':'test','active':True})
|
||||
with patch.object(server, 'HEADSET_RUNTIME', True), patch.object(framey_panel, 'poll_running',return_value={'result':None}) as poll:
|
||||
self.assertEqual(server.framey_running_action({'id':13,'session':'test','active':False,'receipt':'a'*64}),{'result':None})
|
||||
poll.assert_called_once_with(server.ssh,13,'test',False,0,'a'*64)
|
||||
|
||||
def test_running_rejects_stale_or_missing_receipt_after_plugin_replacement(self):
|
||||
installed = {'storeId':13,'sha256':'a'*64}
|
||||
def safe_ssh(command, stdin, **_):
|
||||
self.assertEqual(command,'python3 -')
|
||||
compile(stdin,'<running read>','exec')
|
||||
return '{"result":null}'
|
||||
with patch.object(framey_panel.frame_decky,'status',side_effect=lambda _: {'plugins':[installed]}):
|
||||
framey_panel.poll_running(safe_ssh,13,'old-panel',True,receipt='a'*64)
|
||||
installed['sha256'] = 'b'*64 # Another manager replaces the installed plugin.
|
||||
for active in (True,False):
|
||||
for receipt in (None,'','bad','a'*64):
|
||||
with self.subTest(active=active,receipt=receipt):
|
||||
with self.assertRaisesRegex(framey_panel.PanelError,'receipt|changed'):
|
||||
framey_panel.poll_running(lambda *a,**k:self.fail('Stale running request reached CEF'),
|
||||
13,'old-panel',active,receipt=receipt)
|
||||
# A reopened panel uses the new receipt for both polling and cleanup.
|
||||
framey_panel.poll_running(safe_ssh,13,'new-panel',True,receipt='b'*64)
|
||||
framey_panel.poll_running(safe_ssh,13,'new-panel',False,receipt='b'*64)
|
||||
|
||||
def test_steam_reads_are_allowlisted_and_installed_only(self):
|
||||
with patch.object(framey_panel.frame_decky, 'status', return_value={
|
||||
'plugins':[{'storeId':81,'name':'IsThereAnyDeal for Deck','sha256':RECEIPT}]}):
|
||||
@@ -281,7 +329,7 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/);
|
||||
with tarfile.open(fileobj=io.BytesIO(gzip.decompress(packed))) as archive:
|
||||
names = set(archive.getnames())
|
||||
self.assertTrue({'ui/framey_panel.py','ui/framey-host.js',
|
||||
'ui/framey-events.js',
|
||||
'ui/framey-events.js','ui/framey-running.js',
|
||||
'ui/vendor/react-18.3.1.min.js','ui/vendor/react-dom-18.3.1.min.js',
|
||||
'ui/vendor/REACT-LICENSE.txt','ui/vendor/REACT-DOM-LICENSE.txt'} <= names)
|
||||
|
||||
|
||||
+112
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"checkedAtUtc": "2026-10-25T23:19:48.854504+00:00",
|
||||
"checkedAtUtc": "2026-10-26T06:10:58.537029+00:00",
|
||||
"source": "https://plugins.deckbrew.xyz/plugins",
|
||||
"hostCommit": "779f9c5",
|
||||
"method": "Exact SHA-256 verified archives. All frontends attempted in opaque-iframe Chromium for 1.2 s with backend calls and network denied; ready means initializer only. Backend calls were denied in the baseline browser audit. Scoped live backend follow-ups are recorded separately in docs/framey-compatibility.md.",
|
||||
@@ -580,7 +580,7 @@
|
||||
"https://github.com/wynn1212/SDH-PauseGames.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "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.",
|
||||
"reason": "Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -613,6 +613,11 @@
|
||||
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1006,7 +1011,7 @@
|
||||
"https://github.com/popsUlfr/SDH-Notebook.git"
|
||||
],
|
||||
"status": "works",
|
||||
"reason": "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.",
|
||||
"reason": "Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Running-game context is now supplied; per-game selection was attempted with a synthetic game and backend calls denied. Real per-game selection, background artwork and worn-VR input remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -1080,6 +1085,11 @@
|
||||
"existingNotesTouched": false
|
||||
},
|
||||
"method": "local Chromium + real Frame SSH; private actual Decky 3.3.0 ARM64 service"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2870,8 +2880,8 @@
|
||||
"https://github.com/andrewburgess/steamdeck-discord-status",
|
||||
"https://github.com/andrewburgess/steamdeck-discord-status.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "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.",
|
||||
"status": "partial",
|
||||
"reason": "Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -2917,6 +2927,11 @@
|
||||
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
|
||||
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
|
||||
"frontendReady": true
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3404,6 +3419,11 @@
|
||||
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
|
||||
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
|
||||
"frontendReady": true
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3985,6 +4005,11 @@
|
||||
"sha256": "71dc21ce5f9f4ee0573bd927e9b444d45dc47327888ca78ed063c7bcc57bfac1",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4030,8 +4055,8 @@
|
||||
"https://github.com/saumya-banthia/volume-boost",
|
||||
"https://github.com/saumya-banthia/volume-boost.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified.",
|
||||
"status": "works",
|
||||
"reason": "Default-sink controls pass in local Chromium through real Decky 3.3.0 ARM64: linked slider/keyboard and numeric writes, separate channels, re-link and reopen, with independent Pulse readback including 125%/150% while muted. Exact original volume/mute restored. Audible output, Bluetooth sinks and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -4159,12 +4184,13 @@
|
||||
"https://github.com/jessebofill/DeckWebBrowser.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: deckyFrontendLib.getReactRoot is not a function.",
|
||||
"reason": "The exact frontend now initializes with a null Steam React-root lookup. The browser itself still requires Steam GamepadUI CDP injection and a backend WebSocket unavailable in Framey; no working browser tab was verified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"focused local browser"
|
||||
],
|
||||
"browser": {
|
||||
"id": 59,
|
||||
@@ -4216,6 +4242,17 @@
|
||||
"sha256": "1192864fddc5d72550346b8dff02c21d71f0c40a06ac41c4e67bb6220d267f9d",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"focusedBrowser": {
|
||||
"hostChange": "DFL.getReactRoot returns null without exposing a Steam tree",
|
||||
"ready": true,
|
||||
"error": null,
|
||||
"methods": [
|
||||
"set_main_tab",
|
||||
"load"
|
||||
],
|
||||
"backendAndNetwork": "denied",
|
||||
"scope": "initializer only; no usable browser tab or real backend verified"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4447,6 +4484,11 @@
|
||||
"sha256": "e08333859a32b0c177b8e9a7959c5eeeafe38f946578f7f1d161c721f0e05da5",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4964,7 +5006,7 @@
|
||||
"https://github.com/your-username/PlayTime"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. ARM64 backend, live MobX/running-game observations and automatic recording remain unverified/unsupported.",
|
||||
"reason": "Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -5016,6 +5058,11 @@
|
||||
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
|
||||
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
|
||||
"frontendReady": true
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6920,6 +6967,11 @@
|
||||
"sha256": "2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -7289,6 +7341,11 @@
|
||||
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
|
||||
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
|
||||
"frontendReady": true
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -7719,6 +7776,11 @@
|
||||
"sha256": "49309a02fa028d26c77230cc9f70fec9bcce91d59e887bfe8f9d3e42f8a19139",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -7906,6 +7968,11 @@
|
||||
"sha256": "b6c514d147cc46bad5331ca9da36628eb9c330976e13735687499a4a9f17f6d4",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -8607,6 +8674,11 @@
|
||||
"sha256": "8a69f45aadce763406b5c27bfef194761006d5e01c5e8841badeb13a27dd0d88",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -8716,6 +8788,11 @@
|
||||
"sha256": "65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -9710,6 +9787,11 @@
|
||||
"sha256": "ec59fc8aceb4e1abe2066b3a0707ed6313cabc4421b7c17765397903e2f8fd26",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -10845,6 +10927,11 @@
|
||||
"sha256": "17d4166a9e98979df5fdb07c7eae42317acd54a453d3b82757508f971193c0fe",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11207,6 +11294,11 @@
|
||||
"sha256": "3def99993c412b669933376b6420e44c68cd1fe9d9baf14b77a1f231242bd7e8",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11691,6 +11783,11 @@
|
||||
"sha256": "248294b5d6b9d5a5d635ce35f4b5185ce59d4e4fd59353e24df4b29d74c2ade4",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13228,6 +13325,11 @@
|
||||
"sha256": "33fb674993c9b2bd119f0c6118c465e379f166ec1539c8183fcf7580f1d89577",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"runningFollowUp": {
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+104
-14
@@ -15,7 +15,14 @@
|
||||
// user identity or suspend state in this isolated document.
|
||||
for (const name of ['App','appStore','appInfoStore','collectionStore']) window[name] = undefined;
|
||||
window.SuspendResumeStore = Object.freeze({});
|
||||
window.SteamUIStore = Object.freeze({RunningApps:Object.freeze([]), MainRunningApp:undefined});
|
||||
let runningApps = Object.freeze([]), mainRunningApp, runningAvailable = false;
|
||||
let runningArtwork = new Map();
|
||||
const runningListeners = new Set();
|
||||
let runningCursor = 0, runningRequest = null, runningTimer = null, runningClosed = false;
|
||||
window.SteamUIStore = Object.freeze({
|
||||
get RunningApps() {return runningApps;}, get MainRunningApp() {return mainRunningApp;},
|
||||
get FrameyRunningAvailable() {return runningAvailable;},
|
||||
});
|
||||
// Steam normally fills these token maps. Official plugins can safely use
|
||||
// their own fallback strings when the isolated panel has no Steam tokens.
|
||||
window.LocalizationManager = {m_rgLocalesToUse:[navigator.language || 'en'],
|
||||
@@ -359,7 +366,10 @@
|
||||
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, findModule, findModuleChild, findModuleDetailsByExport,
|
||||
// The isolated panel has no Steam React root. Legacy plugins can inspect
|
||||
// this null result and keep their own controls without patching Steam UI.
|
||||
getReactRoot: () => 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()],
|
||||
@@ -383,8 +393,8 @@
|
||||
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.
|
||||
// Unsupported subscriptions remain inert. Lifetime callbacks below relay
|
||||
// actual Steam notifications; no power, input or launch events are invented.
|
||||
const subscription = () => {const dispose=()=>{};dispose.unregister=dispose;return dispose;};
|
||||
const register = names => Object.fromEntries(names.map(name=>[name,subscription]));
|
||||
const steamLanguage = () => {
|
||||
@@ -403,7 +413,14 @@
|
||||
...register(['RegisterForSettingsChanges'])},
|
||||
Apps:register(['RegisterForAppDetails','RegisterForGameActionTaskChange',
|
||||
'RegisterForGameActionStart','RegisterForGameActionEnd','RegisterForGameActionUserRequest']),
|
||||
GameSessions:register(['RegisterForAppLifetimeNotifications','RegisterForScreenshotNotification']),
|
||||
GameSessions:{...register(['RegisterForScreenshotNotification']),
|
||||
RegisterForAppLifetimeNotifications:listener=>{
|
||||
if (typeof listener !== 'function') throw Error('Invalid Steam lifetime listener');
|
||||
if (runningListeners.size >= 32) throw Error('Too many Steam lifetime listeners');
|
||||
runningListeners.add(listener);
|
||||
const dispose=()=>runningListeners.delete(listener);
|
||||
dispose.unregister=dispose; return dispose;
|
||||
}},
|
||||
System:{...register(['RegisterForOnResumeFromSuspend','RegisterForOnSuspendRequest']),
|
||||
GetSystemInfo:()=>request('GetSystemInfo',[],false,'framey:steam'),
|
||||
UI:register(['RegisterForFocusChangeEvents','RegisterForSystemKeyEvents'])},
|
||||
@@ -428,7 +445,9 @@
|
||||
},
|
||||
},
|
||||
};
|
||||
DFL.Router = {...DFL.Navigation, ...window.SteamUIStore};
|
||||
DFL.Router = {...DFL.Navigation,
|
||||
get RunningApps() {return runningApps;}, get MainRunningApp() {return mainRunningApp;},
|
||||
};
|
||||
DFL.DialogButton.render = () => ({type: DFL.DialogButton});
|
||||
const toaster = {toast: ({title, body, subtext, icon, logo, duration, showToast=true, onClick}={}) => {
|
||||
const data = {title, body, subtext, icon, logo};
|
||||
@@ -541,14 +560,9 @@
|
||||
},
|
||||
fetchNoCors: () => Promise.reject(Error('Decky proxy fetch is unavailable in Framey')),
|
||||
};
|
||||
function setLibrarySnapshot(snapshot) {
|
||||
// These are detached plugin-local values. Patching an overview or its Map
|
||||
// must never mutate Steam's library. Live MobX observations are unsupported.
|
||||
if (!snapshot || !Array.isArray(snapshot.apps) || snapshot.apps.length > 10000 ||
|
||||
!Array.isArray(snapshot.gameIds) || snapshot.gameIds.length > 10000 ||
|
||||
(snapshot.steamId != null && (typeof snapshot.steamId !== 'string' || !/^\d{17}$/.test(snapshot.steamId))))
|
||||
throw Error('Invalid Steam library snapshot');
|
||||
const apps = snapshot.apps.map(row => {
|
||||
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 ||
|
||||
typeof row[1] !== 'string' || row[1].length > 2048 ||
|
||||
(row[2] != null && typeof row[2] !== 'string') || !Number.isSafeInteger(row[3]) ||
|
||||
@@ -560,16 +574,90 @@
|
||||
return {appid,display_name,sort_as,app_type,gameid,
|
||||
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played};
|
||||
});
|
||||
}
|
||||
function setLibrarySnapshot(snapshot) {
|
||||
// These are detached plugin-local values. Patching an overview or its Map
|
||||
// must never mutate Steam's library. Live MobX observations are unsupported.
|
||||
if (!snapshot || !Array.isArray(snapshot.apps) || snapshot.apps.length > 10000 ||
|
||||
!Array.isArray(snapshot.gameIds) || snapshot.gameIds.length > 10000 ||
|
||||
(snapshot.steamId != null && (typeof snapshot.steamId !== 'string' || !/^\d{17}$/.test(snapshot.steamId))))
|
||||
throw Error('Invalid Steam library snapshot');
|
||||
const apps = projectApps(snapshot.apps, 10000);
|
||||
const map = new Map(apps.map(app=>[app.appid,app]));
|
||||
if (map.size !== apps.length || snapshot.gameIds.some(id=>!Number.isSafeInteger(id) || !map.has(id)))
|
||||
throw Error('Invalid Steam game collection');
|
||||
window.appStore = {allApps:apps, m_mapApps:map,
|
||||
GetAppOverviewByAppID:id=>map.get(Number(id)),
|
||||
GetAppOverviewByGameID:id=>apps.find(app=>app.gameid === String(id)),
|
||||
GetVerticalCapsuleURLForApp:app=>runningArtwork.get(Number(app?.appid))?.[0] || '',
|
||||
GetCustomVerticalCapsuleURLs:app=>[...(runningArtwork.get(Number(app?.appid))?.[1] || [])],
|
||||
};
|
||||
window.collectionStore = {allGamesCollection:{allApps:snapshot.gameIds.map(id=>map.get(id))}};
|
||||
window.App = snapshot.steamId ? {m_CurrentUser:{strSteamID:snapshot.steamId}} : undefined;
|
||||
}
|
||||
function setRunningSnapshot(data) {
|
||||
if (!data || !Array.isArray(data.runningIds) || data.runningIds.length > 64 ||
|
||||
!Array.isArray(data.events) || data.events.length > 64 ||
|
||||
!Array.isArray(data.artwork) || data.artwork.length > 65 ||
|
||||
!Number.isSafeInteger(data.cursor) || data.cursor < 0 || typeof data.lifetimeAvailable !== 'boolean')
|
||||
throw Error('Invalid Steam running snapshot');
|
||||
const apps = projectApps(data.apps, 65), map = new Map(apps.map(app=>[app.appid,app]));
|
||||
if (map.size !== apps.length || new Set(data.runningIds).size !== data.runningIds.length ||
|
||||
data.runningIds.some(id=>!Number.isSafeInteger(id) || !map.has(id)) ||
|
||||
(data.mainAppId !== null && (!Number.isSafeInteger(data.mainAppId) || !map.has(data.mainAppId))))
|
||||
throw Error('Invalid Steam running apps');
|
||||
let sequence = 0;
|
||||
const publicArt = value => typeof value === 'string' && value.length <= 2048 &&
|
||||
/^https:\/\/(?:[a-z0-9-]+\.)*steamstatic\.com\/steam\/apps\/[^?#\s\\]+$/.test(value);
|
||||
const artwork = new Map();
|
||||
for (const row of data.artwork) {
|
||||
if (!Array.isArray(row) || row.length !== 3 || !map.has(row[0]) || artwork.has(row[0]) ||
|
||||
(row[1] !== null && !publicArt(row[1])) || !Array.isArray(row[2]) || row[2].length > 8 ||
|
||||
row[2].some(url=>!publicArt(url))) throw Error('Invalid Steam public artwork');
|
||||
artwork.set(row[0],[row[1],[...row[2]]]);
|
||||
}
|
||||
for (const event of data.events) {
|
||||
if (!Number.isSafeInteger(event?.sequence) || event.sequence <= sequence || event.sequence > data.cursor ||
|
||||
!Number.isSafeInteger(event.unAppID) || event.unAppID <= 0 || typeof event.bRunning !== 'boolean')
|
||||
throw Error('Invalid Steam lifetime event');
|
||||
sequence = event.sequence;
|
||||
}
|
||||
// Update detached stores before callbacks read Router.RunningApps.
|
||||
runningApps = Object.freeze(data.runningIds.map(id=>map.get(id)));
|
||||
mainRunningApp = data.mainAppId === null ? undefined : map.get(data.mainAppId);
|
||||
runningAvailable = true;
|
||||
runningArtwork = artwork;
|
||||
if (window.appStore) for (const app of apps) {
|
||||
const existing = window.appStore.m_mapApps.get(app.appid);
|
||||
if (existing) Object.assign(existing,app);
|
||||
else {window.appStore.m_mapApps.set(app.appid,app);window.appStore.allApps.push(app);}
|
||||
}
|
||||
const previous = runningCursor;
|
||||
runningCursor = data.cursor;
|
||||
for (const event of data.events) if (event.sequence > previous)
|
||||
for (const listener of runningListeners)
|
||||
Promise.resolve().then(()=>runningListeners.has(listener) && listener({unAppID:event.unAppID,bRunning:event.bRunning}))
|
||||
.catch(failure=>console.warn('Steam lifetime listener failed',failure.message));
|
||||
}
|
||||
function refreshRunning() {
|
||||
if (runningClosed) return Promise.resolve();
|
||||
if (runningRequest) return runningRequest;
|
||||
runningRequest = request('running',[runningCursor],false,'framey:running')
|
||||
.then(data=>{if (!runningClosed) setRunningSnapshot(data);})
|
||||
.catch(failure=>{
|
||||
runningAvailable = false;
|
||||
runningApps = Object.freeze([]); mainRunningApp = undefined; runningArtwork = new Map();
|
||||
console.warn('Framey Steam running context unavailable',failure.message);
|
||||
}).finally(()=>{
|
||||
runningRequest = null;
|
||||
if (!runningClosed) runningTimer = setTimeout(refreshRunning,2000);
|
||||
});
|
||||
return runningRequest;
|
||||
}
|
||||
addEventListener('pagehide',()=>{
|
||||
runningClosed = true; clearTimeout(runningTimer); runningListeners.clear();
|
||||
parent.postMessage({type:'framey:running:close',session:cacheSession},'*');
|
||||
});
|
||||
async function start() {
|
||||
try {
|
||||
await seeded;
|
||||
@@ -582,6 +670,8 @@
|
||||
try {setLibrarySnapshot(await request('GetLibrarySnapshot',[],false,'framey:steam'));}
|
||||
catch (failure) {console.warn('Framey Steam library unavailable', failure.message);}
|
||||
}
|
||||
if (/\b(?:RunningApps|MainRunningApp|RegisterForAppLifetimeNotifications)\b/.test(bundleText))
|
||||
await refreshRunning();
|
||||
// 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);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/* Fixed, read-only projection executed in Steam CEF. No plugin source runs here. */
|
||||
(function(payload) {
|
||||
const api = window.SteamClient?.GameSessions;
|
||||
let bridge = window.__frameyRunningBridgeV1;
|
||||
if (bridge && bridge.api !== api) {
|
||||
for (const key of [...bridge.sessions.keys()]) bridge.remove(key);
|
||||
bridge = null;
|
||||
}
|
||||
if (!bridge) {
|
||||
const sessions = new Map();
|
||||
const remove = key => {
|
||||
const item = sessions.get(key);
|
||||
item?.handle?.unregister();
|
||||
sessions.delete(key);
|
||||
if (!sessions.size) {clearInterval(bridge.timer); bridge.timer = null;}
|
||||
};
|
||||
bridge = window.__frameyRunningBridgeV1 = {api, sessions, remove, timer:null};
|
||||
}
|
||||
let item = bridge.sessions.get(payload.session);
|
||||
if (item && item.pluginId !== payload.pluginId) throw Error('Framey running session belongs to another plugin');
|
||||
if (!payload.active) {bridge.remove(payload.session); return null;}
|
||||
const store = window.SteamUIStore;
|
||||
if (!Array.isArray(store?.RunningApps)) throw Error('Steam running apps are unavailable');
|
||||
const apps = store.RunningApps;
|
||||
if (apps.length > 64) throw Error('Too many running Steam apps');
|
||||
const main = store.MainRunningApp;
|
||||
const project = app => [app.appid, app.display_name, app.sort_as, app.app_type, String(app.gameid),
|
||||
app.minutes_playtime_forever, app.minutes_playtime_last_two_weeks, app.rt_last_time_played];
|
||||
// Include a selected main app even if Steam's list changes between the reads.
|
||||
const rows = apps.map(project);
|
||||
if (main && !apps.some(app => app.appid === main.appid)) rows.push(project(main));
|
||||
const publicArt = value => {
|
||||
if (typeof value !== 'string' || value.length > 2048) return null;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.port ||
|
||||
!/(^|\.)steamstatic\.com$/.test(url.hostname) || !url.pathname.startsWith('/steam/apps/')) return null;
|
||||
return url.origin + url.pathname; // Never copy local artwork paths or query data.
|
||||
} catch (_) {return null;}
|
||||
};
|
||||
const artwork = [...apps, ...(main && !apps.some(app=>app.appid === main.appid) ? [main] : [])].map(app => {
|
||||
const art = window.appStore;
|
||||
const vertical = publicArt(art?.GetVerticalCapsuleURLForApp?.(app));
|
||||
const custom = art?.GetCustomVerticalCapsuleURLs?.(app);
|
||||
return [app.appid, vertical, Array.isArray(custom) ? custom.slice(0,8).map(publicArt).filter(Boolean) : []];
|
||||
});
|
||||
if (!item) {
|
||||
if (bridge.sessions.size >= 4) throw Error('Too many Framey running sessions');
|
||||
item = {pluginId:payload.pluginId, queue:[], sequence:0, touched:Date.now(), handle:null};
|
||||
if (typeof api?.RegisterForAppLifetimeNotifications === 'function') {
|
||||
item.handle = api.RegisterForAppLifetimeNotifications(event => {
|
||||
// This queue contains actual Steam notifications, never snapshot diffs.
|
||||
if (!Number.isSafeInteger(event?.unAppID) || event.unAppID <= 0 || typeof event.bRunning !== 'boolean') return;
|
||||
item.queue.push({sequence:++item.sequence, unAppID:event.unAppID, bRunning:event.bRunning});
|
||||
if (item.queue.length > 64) item.queue.shift();
|
||||
});
|
||||
}
|
||||
bridge.sessions.set(payload.session, item);
|
||||
if (!bridge.timer) bridge.timer = setInterval(() => {
|
||||
for (const [key, state] of bridge.sessions)
|
||||
if (Date.now() - state.touched > 15000) bridge.remove(key);
|
||||
}, 5000);
|
||||
}
|
||||
item.touched = Date.now();
|
||||
return {apps:rows, artwork, runningIds:apps.map(app => app.appid), mainAppId:main?.appid ?? null,
|
||||
events:item.queue.filter(event => event.sequence > payload.cursor), cursor:item.sequence,
|
||||
lifetimeAvailable:!!item.handle};
|
||||
})
|
||||
@@ -244,3 +244,20 @@ def poll_events(ssh, plugin_id, session, events, cursor=0, receipt=None):
|
||||
source = Path(__file__).with_name('framey-events.js').read_text(encoding='utf-8')
|
||||
script = EVENT_SCRIPT.replace('__FRAMEY_EVENTS_SOURCE__', repr(source))
|
||||
return json.loads(ssh('python3 - ' + shlex.quote(json.dumps(payload)), stdin=script, timeout=25))
|
||||
|
||||
|
||||
def poll_running(ssh, plugin_id, session, active, cursor=0, receipt=None):
|
||||
"""Project running apps and actual lifetime events for one installed panel."""
|
||||
if type(plugin_id) is not int or plugin_id <= 0:
|
||||
raise PanelError('Choose an installed Decky store plugin')
|
||||
if not isinstance(session, str) or not SESSION.fullmatch(session):
|
||||
raise PanelError('Invalid Framey running session')
|
||||
if type(active) is not bool or type(cursor) is not int or not 0 <= cursor <= 9007199254740991:
|
||||
raise PanelError('Invalid Framey running request')
|
||||
_current_plugin(ssh, plugin_id, receipt)
|
||||
payload = {'pluginId':plugin_id, 'session':session, 'active':active, 'cursor':cursor}
|
||||
source = Path(__file__).with_name('framey-running.js').read_text(encoding='utf-8')
|
||||
expression = '(' + source + ')(' + json.dumps(payload) + ')'
|
||||
script = STEAM_READ_SCRIPT.replace('__FRAMEY_STEAM_READ__', repr(expression)).replace(
|
||||
'__FRAMEY_STEAM_LIMIT__', str(256*1024))
|
||||
return json.loads(ssh('python3 -', stdin=script, timeout=25))
|
||||
+24
-1
@@ -237,8 +237,12 @@ function openFrameyPanel(id,name){
|
||||
setTimeout(()=>{if(activeFrameyId===id&&byId('frameyPanelStatus').textContent==='Loading plugin panel…')
|
||||
byId('frameyPanelStatus').textContent='No compatible panel loaded. This plugin may need Steam UI features Framey cannot provide.';},12000);
|
||||
}
|
||||
let frameyEventsActive=false;
|
||||
let frameyEventsActive=false,frameyRunningActive=false;
|
||||
function closeFrameyEvents(){
|
||||
if(frameyRunningActive&&activeFrameyId!==null&&frameySession)
|
||||
api('/api/framey/running',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
|
||||
session:frameySession,active:false}).catch(()=>{});
|
||||
frameyRunningActive=false;
|
||||
if(frameyEventsActive&&activeFrameyId!==null&&frameySession)
|
||||
api('/api/framey/events',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
|
||||
session:frameySession,events:[],cursor:0}).catch(()=>{});
|
||||
@@ -279,6 +283,25 @@ window.addEventListener('message',async event=>{
|
||||
api('/api/framey/check',{kind:'panel',pluginId:activeFrameyId}).catch(()=>{});return;}
|
||||
if(req.type==='framey:input'){api('/api/framey/check',{kind:'input'}).catch(()=>{});return;}
|
||||
if(req.type==='framey:error'){byId('frameyPanelStatus').textContent=`Plugin panel unavailable: ${String(req.message).slice(0,200)}`;return;}
|
||||
if(req.type==='framey:running:close'){
|
||||
if(req.session===frameySession&&frameyRunningActive){
|
||||
frameyRunningActive=false;
|
||||
api('/api/framey/running',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
|
||||
session:frameySession,active:false}).catch(()=>{});
|
||||
}return;
|
||||
}
|
||||
if(req.type==='framey:running'){
|
||||
if(req.session!==frameySession||!Number.isSafeInteger(req.id)||req.id<1||
|
||||
!Array.isArray(req.args)||req.args.length!==1||!Number.isSafeInteger(req.args[0])||req.args[0]<0)return;
|
||||
const pluginId=activeFrameyId,session=frameySession,receipt=activeFrameyReceipt?.sha256,
|
||||
reply={type:'framey:reply',id:req.id};
|
||||
frameyRunningActive=true;
|
||||
try{const data=await api('/api/framey/running',{id:pluginId,receipt,session,active:true,cursor:req.args[0]});reply.result=data.result;}
|
||||
catch(error){reply.error=error.message;}
|
||||
if(pluginId===activeFrameyId&&session===frameySession&&event.source===byId('frameyFrame').contentWindow)
|
||||
event.source.postMessage(reply,'*');
|
||||
return;
|
||||
}
|
||||
if(req.type==='framey:events:close'){
|
||||
if(req.session===frameySession)closeFrameyEvents();return;
|
||||
}
|
||||
|
||||
@@ -606,6 +606,16 @@ def framey_events_action(body):
|
||||
raise Failure(str(exc), 400) from exc
|
||||
|
||||
|
||||
def framey_running_action(body):
|
||||
if not HEADSET_RUNTIME and os.environ.get('FRAME_E2E_CONTAINER') != '1':
|
||||
raise Failure('Decky plugin panels run in the headset service', 400)
|
||||
try:
|
||||
return framey_panel.poll_running(ssh, body.get('id'), body.get('session'),
|
||||
body.get('active'), body.get('cursor', 0), body.get('receipt'))
|
||||
except framey_panel.PanelError as exc:
|
||||
raise Failure(str(exc), 400) from exc
|
||||
|
||||
|
||||
def framey_check_action(body):
|
||||
"""Record only a live, explicitly started worn-headset check."""
|
||||
if not HEADSET_RUNTIME or body.get('kind') not in ('input', 'panel'):
|
||||
@@ -3474,6 +3484,7 @@ POST = {
|
||||
"/api/framey/method": framey_method_action,
|
||||
"/api/framey/steam": framey_steam_action,
|
||||
"/api/framey/events": framey_events_action,
|
||||
"/api/framey/running": framey_running_action,
|
||||
"/api/framey/check": framey_check_action,
|
||||
"/api/headset": headset_action,
|
||||
"/api/headset/quick": headset_quick,
|
||||
|
||||
Reference in new issue
Block a user