Frontend:
- Dark green/gold "velvet table" visual redesign across the whole app
(Auth, Lobby, GameList, GameTable, History, GameOver, modals), with
Playfair Display/DM Sans typography and a centralized Tailwind palette.
- Desktop game table fit-scales to fill the window; mobile gets
overlapping hand/trick layouts and larger touch-friendly cards.
- Standings sidebar now groups completed rounds by series with a
per-series subtotal row, struck-through tips on missed bids.
- History page rewritten into a scoreboard-style detail view (player
totals beside names, series grouped 2-up on desktop / stacked on
mobile) and gained game names, completed/abandoned status, and a
button to reopen a prematurely-ended game back into the lobby.
Backend:
- Fix started games being deleted from memory (and vanishing from
everyone's lobby) when all players disconnect; only `end_game` tears
down a started game now.
- Fix a crash writing a timezone-aware datetime into the naive
`ended_at` Postgres column.
- Add `reopen_game`/`restore_game` to un-end a prematurely-ended game
from history and resume it from the lobby.
- Let any seated player end an abandoned game once the host is
offline, not just the host, so the game isn't stuck forever.
- Expose SERIES_PER_GAME/ROUNDS_PER_SERIES as named constants on the
engine so the persistence layer derives game-completion rules from
bridzik.py instead of re-encoding them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- db/ package: async SQLAlchemy engine + Player/Game/Guess models
- api/auth.py: passwordless TOTP login (pyotp), session token via socket auth
- api/history.py: record guesses/points, DB-backed standings, restore
unfinished games on startup, host-only end_game
- api/__init__.py: auth-gated handlers, accounts map, rejoin via account
- frontend: Auth (QR + code) and History pages, resume/end-game in lobby/table
- docker-compose: real PostgreSQL service wired via DATABASE_URL
- tests_history.py for the persistence/auth layer; refresh CLAUDE.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace Flask-SocketIO + eventlet with python-socketio AsyncServer on an
ASGI app served by uvicorn (Python 3.14). The server is no longer started
as an import side-effect; `python -m app` runs uvicorn for dev and the
Docker image runs `uvicorn api:app`.
Bug fixes:
- create_game now mints a real uuid gid and returns it to the creator
(was hardcoded 'a').
- play_card resolves the player's hand and plays the selected Card (was
indexing a method and crashing).
Hardening:
- Identity binding: every action derives the seat from the connection
(sid -> {gid, order}); clients no longer pass a player number, closing
the hidden-cards cheat where any client could request any hand.
- Secure token-based reconnect (per-player secret token).
- disconnect handler marks players offline and drops empty games (no
more leaked games), notifying the room via player_connection.
- Guards for unknown gid, double start_game, and bad input; engine
exception messages are forwarded instead of swallowed.
- Lobby payload is public-only (no sids/tokens); game_status carries a
completed flag.
- /health endpoint via other_asgi_app; env-driven CORS and logging.
Infra:
- Dockerfile -> python:3.14-slim, uvicorn CMD, drop dead venv lines.
- requirements.txt -> python-socketio/engineio + uvicorn; drop eventlet,
Flask-SocketIO, Flask-Session.
- docker-compose: drop unused debugpy port and obsolete version key.
- Remove redundant start.py; gitignore /.venv.
Tests: test_socket.py drives the handlers (identity binding, lobby
privacy, reconnect, disconnect cleanup, error handling, play flow).
Full suite: 29 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>