* 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]>
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
- 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]>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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]>