Files
Wizard/wizard_server/app/protocol.py
T
marvinandClaude Opus 5.5 90176e1cca Serve API and website behind Caddy path prefixes
Caddy forwards /wizard/api/* and /wizard/website/* with the prefix
stripped, so the server received /ws instead of /api/ws and the website
connected to /api/ws on the domain root.

- Server routes are now /ws and /health (was /api/ws, /api/health)
- Website derives the websocket URL from its own location (../api/ws),
  giving /wizard/api/ws in production and /api/ws in development
- Vite dev proxy strips /api like the production proxy
- nginx back to the plain SPA config since Caddy strips the prefix
- /internal/ai rejects requests that came through the reverse proxy,
  in addition to the shared secret
- Docs updated for the prefixed deployment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:00:29 +02:00

81 lines
1.9 KiB
Python

"""Messages exchanged with web clients over the ``/ws`` websocket.
Client -> server messages are validated with pydantic. Server -> client
messages are plain dicts:
- ``{"type": "welcome", "player_id", "token", "name"}``
- ``{"type": "state", "lobby": LobbyView | null, "game": GameView | null, "events": [...]}``
- ``{"type": "error", "message"}``
"""
from __future__ import annotations
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, StringConstraints, TypeAdapter
PlayerName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=20)]
LobbyCode = Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, min_length=4, max_length=12)]
class Hello(BaseModel):
type: Literal["hello"]
# Token of a previous session, used to reconnect after a page reload.
token: str | None = None
class CreateLobby(BaseModel):
type: Literal["create_lobby"]
name: PlayerName
class JoinLobby(BaseModel):
type: Literal["join_lobby"]
code: LobbyCode
name: PlayerName
class LeaveLobby(BaseModel):
type: Literal["leave_lobby"]
class AddAI(BaseModel):
type: Literal["add_ai"]
class RemovePlayer(BaseModel):
type: Literal["remove_player"]
player_id: str
class SetOptions(BaseModel):
type: Literal["set_options"]
not_equal: bool
hidden_bids: bool
class StartGame(BaseModel):
type: Literal["start_game"]
class GameAction(BaseModel):
type: Literal["action"]
action: dict[str, Any]
class ReturnToLobby(BaseModel):
type: Literal["return_to_lobby"]
ClientMessage = Annotated[
Hello | CreateLobby | JoinLobby | LeaveLobby | AddAI | RemovePlayer
| SetOptions | StartGame | GameAction | ReturnToLobby,
Field(discriminator="type"),
]
client_message_adapter: TypeAdapter[ClientMessage] = TypeAdapter(ClientMessage)
def error(message: str) -> dict:
return {"type": "error", "message": message}