Commit Graph
26 Commits
Author SHA1 Message Date
1f6b4d11ca feat(permissions): gate emulator streaming and in-browser play behind group permissions (#5185)
* feat(permissions): gate emulator streaming behind a group permission

Add a streaming permission entity. Read shows streaming and allows joining
a session; write allows starting one. Users without read get a disabled
streaming config, so every streaming surface hides.

Migration 0152 grants read and write to every existing group, so nobody
loses access on upgrade. Groups created afterwards start without it.
Owner-scoped session controls (save, exit, release) stay ungated so a
revoked user can still leave a running game without losing saves.

* fix(streaming): pr-ready code-review fixes for the streaming grant

Own-only read no longer joins other users' sessions, the config refetches on permissions:changed, the store skips the joinable request when streaming is off, and tests clean up the groups they create.

* refactor(streaming): pr-ready simplify pass on the streaming grant

Collapse the streaming permission helpers onto assert_can/can_access, return an empty joinable list instead of 403, skip the permission resolve when streaming is off, and add a stream.start action key so the UI hides Stream (but keeps Join) for users without the write grant.

* docs(streaming): pr-ready review-polish pass on the streaming grant

Trim the 0152 migration docstring to a single summary line, matching 0150 and 0151.

* docs(streaming): pr-ready humanizer pass on the streaming grant

Plainer wording for the stream.start action key comment and the Permission Groups paragraph, which now says an own-only read shows streaming without the join.

* feat(permissions): gate in-browser emulation behind a group permission

Adds an emulation entity next to streaming. Read plays in the browser
(EmulatorJS, Ruffle, js-dos, PICO-8, EasyRPG) and joins netplay rooms,
write hosts them. Migration 0152 now backfills both entities onto every
existing group.

* fix(streaming): address Greptile review on PR 5185

- Pin the claimed container in the stream player, so a permission refresh
  that turns streaming off mid-game keeps save, load and disc swap until
  the session ends.
- Empty the Join list when a refreshed config turns streaming off, and
  discard any joinable request still in flight.
- Update the installPermissionsHydration doc to name every layout that
  calls it.

* fix(migrations): renumber play permissions migration to 0153 after master's 0152

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* chore(permissions): drop redundant PermEntity comments

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* refactor(streaming): drop the live config refresh on permission changes

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Georges-Antoine Assi <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-09 14:47:11 -04:00
Sam DornanandClaude Opus 5.5 b3ad84c296 Route ROM visibility through a single RomVisibilityFilter (#5064)
Replace the hidden_platform_ids/hidden_rom_ids parameter pairs with one
RomVisibilityFilter that builds the WHERE clauses and checks loaded ROMs,
so a new visibility rule (the age-rating cap for #2013) lands in one place.
No behaviour change; the visibility-filtered queries send the same SQL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wc3NzFkv97CXavLyZg5kJE
2026-10-03 10:46:47 -04:00
Zach Clendenen cf4353e9e2 Merge branch 'feature/streaming-container-names' into streaming-patches-8
# Conflicts:
#	backend/endpoints/streaming.py
#	backend/handler/streaming/config.py
2026-09-25 18:04:31 -05:00
Zach Clendenen f45cd3d3cc feat(streaming): open a container's desktop by its name 2026-09-25 15:20:39 -05:00
Georges-Antoine AssiandClaude Opus 5.5 d99c3c5f7b fix(streaming): report a failed archive resume, stop duplicate stream entry, and fold emulator case in core checks (code-review)
- Exit-state emulators with no uploaded archive no longer send a resume slot or report resume=true.
- isCoreCompatible compares emulator ids case-insensitively, like grouping and labels.
- launch-ready skips enterStream when a status poll already entered the stream.
- A Desktop heartbeat answered after a release no longer overwrites the exited state.
- Drop the notice_in_scope alias, reuse session_is_desktop, trim over-long comments.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-24 17:43:30 -04:00
Zach Clendenen 903bb35142 fix(streaming): bind the in-session controls to their claim, move the claim resolver into access, and translate the new strings (Greptile review)
Volume, mute, save-state, load-state and swap-disc now carry the claim's container and stamp, so a replaced tab or a second pool tab cannot drive another session. The claim resolver moves from the endpoint into handler/streaming/access.py, and the draining and occupied strings get real translations in every locale.
2026-09-18 16:15:17 -05:00
Zach Clendenen 8e92711a29 docs(streaming): trim the joinable_session_rom docstring to two lines (review-polish) 2026-09-18 14:45:37 -05:00
Zach Clendenen e822ffa559 refactor(streaming): share the claim and emulator-key helpers, drop per-caller null conversions, and skip the ROM lookup for unjoinable sessions (simplify)
One forgetClaim/enterStream path in Stream.vue, one emulatorKey/emulatorLabelFrom home, the heartbeat compare-and-set via same_claim, and test helpers for drains, stubbed stops, deferred claims and 409s.
2026-09-18 14:41:25 -05:00
Zach Clendenen 99b44b52fa fix(streaming): keep a named save-and-exit to a game of its platform
Save-and-exit went through the release route's resolution, which ends whatever a named container holds, so naming a shared container could save and kill a desktop or another platform's game. A release still ends anything named; save-and-exit now matches only a game of the path's platform.
2026-09-18 12:17:01 -05:00
Zach Clendenen 11eabe6431 docs(streaming): fit the branch's comments and docstrings in two lines
The repo caps a comment at two lines and a docstring at one sentence plus Args and Returns. Several comments and docstrings this branch wrote or edited ran to three lines or more, so they are tightened, with parameter notes moved under Args.
2026-09-18 12:03:10 -05:00
Zach Clendenen 79be00ceb0 refactor(streaming): require a platform when scoping a session lookup
Every caller names the platform, so the None default and the branch it fed only widened the signature.
2026-09-18 11:23:19 -05:00
Zach Clendenen 4f2d1198f2 docs(streaming): tighten the comments this branch grew
Several notes had run to four or more lines, a couple of them recounting
how the code was reordered rather than what it does now.
2026-09-17 16:59:38 -05:00
Zach Clendenen f425fb5553 refactor(streaming): read the joinable filter from one place
The join walk and the joinable listing each spelled out what makes a
session joinable, kept in step by a comment. Both now ask access.
2026-09-17 16:56:34 -05:00
Zach Clendenen e8c96f8aa8 refactor(streaming): drop arguments no caller varies
resolve_owned_session's include_desktop was never passed, and the two
teardown helpers and stamp_launched defaulted arguments every caller
supplies. The 409 builder's holder snapshot no longer reuses the name of
the container above it.
2026-09-17 16:53:46 -05:00
Zach Clendenen b80d9af68c fix(streaming): scope the ended-session notice to the claim asked about
The tombstone is keyed per container, so the status poll walked a pool
reading any notice it found: an admin desktop's, or one from another
platform the container serves. A game tab then tore itself down with a
reason belonging to a claim it never held. The notice records the ended
claim's platform and kind, so the walk now applies the same scope the
live-session lookup uses.
2026-09-17 16:47:01 -05:00
Zach Clendenen 5233649ebd fix(streaming): join the session the caller asked for
An unnamed join stopped at the first pool member holding a live session, so a
desktop, another platform's game, the caller's own session or a ROM they cannot
see masked the session they were offered and the join answered 404 or handed
back somebody else's room.

The walk now skips a member that fails the same filter the joinable listing
applies and keeps looking, and the Join button carries the container of the row
it came from into the stream URL so the join names its session outright.
2026-09-17 15:59:57 -05:00
Zach Clendenen dd6b949b6a fix(streaming): bind release and heartbeat to the caller's claim
A claim is the holder plus the moment they took it, so a tab that missed
its own takeover could still act on the claim that replaced it: its
unload release ended the new session and its beat kept it alive. Both
routes now take the claimed_at the claim answered with, and a mismatch
reads as a session the caller no longer holds: the release is a no-op and
the heartbeat reports ended, with the termination notice the displaced
tab needs.

Admin releases from the settings and activity panels end whatever is
running and send no stamp.
2026-09-17 14:50:32 -05:00
Zach Clendenen 9984f7dd27 fix(streaming): scope session lookups to the platform and skip desktops
A container can serve several platforms while holding one session, so a
lookup that starts from the caller has to match the platform it was asked
about. Otherwise a ps2 session shadows the same container's ngc row: the
claim reads as a duplicate and 409s, status reports it active on a
platform the caller holds nothing on, and an unnamed heartbeat or release
lands on it.

A desktop session is only reachable when the caller names its container,
which is what the desktop view does, so a game tab no longer beats or
releases an admin's desktop, and a desktop no longer blocks its admin's
own game claim. Occupancy is still the reservation's job: the claim walk
just fails that container's SET NX and moves on.
2026-09-17 14:44:15 -05:00
Zach Clendenen 08f1e0001e simplify(pr-ready step 3): group dialable containers per platform in one place, point the claim comment at interchangeable_with 2026-09-17 11:46:22 -05:00
Zach Clendenen 9c1e9f4d75 fix(streaming): put every container in a pool, claim from the first
A platform's containers are now grouped into pools of interchangeable
members (same emulator, memory card sync, save picker and protocol), so a
lone container is a pool of one instead of a skipped non-member. Game claims,
status, heartbeats and joins still walk only the first pool, which matches
the containers they reached before.

The "not a pool member" warning fired on every lookup, one container at a
time. It now fires once per config and names every container a game claim
can never reach. Named lookups (desktop, admin release, named heartbeat)
already reach a container in any pool.
2026-09-17 11:42:18 -05:00
Zach Clendenen c62af75621 polish(pr-ready step 4): trim the heartbeat and named-container comments 2026-09-17 09:53:01 -05:00
Zach Clendenen e2e35835db simplify(pr-ready step 3): run a named heartbeat through the pool's lookup, reuse the store heartbeat and entry_for_platform
A named container is a one-entry candidate list, so the heartbeat has one lookup
and one status answer instead of a closure branching on the key, and it no longer
reads the session twice. The desktop view beats through the store wrapper, and
configured_emulator shares the platform match.
2026-09-17 09:41:31 -05:00
Zach Clendenen 398c088d49 polish(pr-ready step 4): wrap the fleet row's state line and actions, trim prose
At 320px the End session button ran past the row, and below md the state
line truncated away who holds the session. The actions now wrap and the
state line wraps at every width. Docstrings and the xs comment are cut to
the repo's two-line limit.
2026-09-17 08:19:22 -05:00
Zach Clendenen bdd661d30a simplify(pr-ready step 3): share the per-platform entry lookup, build the state line in one place
entry_for_platform backs both container_for_session and
resolve_named_container, and the fleet row's state line is assembled in
sessionState instead of split across template blocks.
2026-09-17 08:14:41 -05:00
Zach Clendenen 339308d586 fix(streaming): resolve a named container by key, not through the pool
An admin ending a session names the container, but the lookup went through
containers_for_platform, which drops any container that disagrees with the
head of the pool. A second container with a different emulator, card sync,
save picker or protocol then 404'd on release, surfacing as "Could not end
the session." in the fleet panel, while its session kept holding it.

The named lookup now goes through containers_by_key and only checks that the
container serves the platform. Owner checks are unchanged.
2026-09-17 07:58:07 -05:00
Georges-Antoine AssiandClaude Opus 5 f78348da0f refactor(streaming): move the endpoint's domain layer into handler/streaming
endpoints/streaming.py had grown to 3778 lines, but only about a third of
that was routes. The rest was a second copy of the layer handler/streaming
already exists to hold: broker verbs, webstation protocol translation,
state and save and memory-card transfer, session teardown.

Split it along those seams rather than by route, and give claim_session
the same treatment: its 406 lines are now four named phases.

Cross-module calls go through the module (states.pull_state_to_library,
commands.stop) instead of imported names, so one definition site is what
tests patch and what every caller reads.

No route path, scope or response model changed, so the OpenAPI schema is
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-05 18:47:18 -04:00