Let anyone with the game URL watch as a read-only spectator
CI / Build and push docker image (push) Successful in 3m0s
CI / Build and push docker image (push) Successful in 3m0s
Anonymous users and logged-in non-players can now open /game/<id> and
follow the match: they see the table, the score and each player's
captured cards and scope, but never a hand and never the join code.
Spectator actions are rejected with a forbidden error.
Backend (tavolo-platform / tavolo-scopone):
- /ws/games/{id} accepts anonymous and non-seated connections as
spectators; only unknown games are refused. The state payload carries
spectator: true and withholds join_code unless the viewer is seated.
- GET /api/games/{id} is public and returns the same personalized
(player) or read-only (spectator) view.
- GameEngine.view_for/game_over_view take Optional[str] (None = spectator)
and are documented to hide privileged information for non-seated
viewers.
Frontend (Sycamore):
- Parse the spectator flag and render a spectator table: card backs plus
captured/scope for every seat, no clickable hand, "Spectating" badge.
- A pre-start spectator sees a "game not started yet" waiting room with
the watch link and no join code.
- "Copy watch link" buttons in the waiting room and the game HUD.
- The hand-end summary carries no acknowledge button for spectators and
the game-over overlay hides "My matches".
Tests and docs updated.
This commit is contained in:
22 files changed
+453
-185
No files matched your search
@@ -44,10 +44,24 @@ class GamesRouteTest(unittest.TestCase):
|
||||
self.assertEqual(game_id, snap["id"])
|
||||
self.assertEqual("dummy", snap["game_type"])
|
||||
self.assertTrue(snap["viewer_seated"])
|
||||
self.assertFalse(snap["spectator"])
|
||||
self.assertIn("join_code", snap)
|
||||
|
||||
# A logged-in non-player is a read-only spectator: the view
|
||||
# marks it and the join code is withheld.
|
||||
with oidc_user(platform.oidc, "mallory"):
|
||||
forbidden = await client.get(f"/api/games/{game_id}")
|
||||
self.assertEqual(403, forbidden.status_code)
|
||||
watched = await client.get(f"/api/games/{game_id}")
|
||||
self.assertEqual(200, watched.status_code)
|
||||
spect = watched.json()
|
||||
self.assertFalse(spect["viewer_seated"])
|
||||
self.assertTrue(spect["spectator"])
|
||||
self.assertNotIn("join_code", spect)
|
||||
|
||||
# Anonymous users may watch too.
|
||||
anonymous = await client.get(f"/api/games/{game_id}")
|
||||
self.assertEqual(200, anonymous.status_code)
|
||||
self.assertTrue(anonymous.json()["spectator"])
|
||||
self.assertNotIn("join_code", anonymous.json())
|
||||
|
||||
@async_test
|
||||
async def test_join_starts_game(self) -> None:
|
||||
|
||||
Reference in new issue
Block a user