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>
81 lines
1.9 KiB
Python
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}
|