Each player has 5 minutes; the server keeps the time and a scheduled service ends games whose running clock reaches zero. Replaces the offline turn and inactivity rules.
Requirements, design decisions and tasks for the Keycloak login, written
after the implementation; mobile login and turning off legacy userKey
login remain open.
- Backend README: DATABASE_URL, VALKEY_URL, the new routes and how the
backend behaves when Postgres or Valkey is down
- TODO: drop the fixed spectator and memory bugs, add game history on
mobile, a page with all games, single-instance limits, backups and the
new .env variables
- Spec marked as implemented, all tasks ticked
- Opening a finished game shows its final position, result, score, date
and duration, with Watch from start
- First / previous / play-pause / next / last, speed 1x, 2x, 4x, a
timeline, the move list and arrow keys and space
- Stepping forward animates the move like a live game; jumps show the
position directly
- ?move=n links to a move and follows the shown move
- Players see the board from their own side
- PitAnimator takes its step interval as a parameter
- Move list under the board with player, pit and what happened (extra
turn, capture, stones to the store, game over); collapsed on phones
- Selecting a move or using the arrow keys shows that position with a
banner and Back to live; pits are locked meanwhile and the banner says
when it is your turn
- Countdown while the opponent is offline on their turn
- on_game_end is handled, so games that end by timeout or inactivity end
on the page with the reason
- Banner for the user's game in progress with a Continue button
- Live games as cards with the real board, updated over WebSocket; the
most watched finished games when nothing is live
- The user's last 3 games, from their side of the board, with the result
as they would say it
- The lobby offers to continue the game in progress when a new one is
refused
- Spectators register once connected and identified, and again after a
reconnect, so they are counted
- The backend includes games that ended but are not saved yet in /game/:id
and the lists
Board components size themselves from a --board-unit CSS variable instead
of vw. By default it is 1vw, so the game page looks the same; with the new
width prop the same board renders small, e.g. in previews.
- GET /games/me/active and /games/me (Bearer token), /games/popular (live
games, or the most spectated finished ones), and /game/:id now also
returns saved records of finished games
- Lists are cached in Valkey for 10 seconds, cleared when a game is saved,
and empty instead of failing while the database is down
- Live game previews over WebSocket: subscribers get the top 5 on
subscribe and on change, at most once per second, without counting as
spectators; ids are public ids
- RTMT supports several connection listeners
- Games record start and move times, spectator peak and distinct count
- Games end as completed, left (the leaver loses), timeout (5 minutes
offline on your turn while the opponent is online) or inactive (both
offline for 30 minutes); everyone gets on_game_end
- Connection info carries offlineSince for the countdown
- A new game is rejected while the user has one in progress
- Live games are written to Valkey after every change and restored on
startup; finished games are saved to Postgres with public ids, and
saves that fail are kept and retried every 30 seconds
- Clients get a trimmed game without spectator ids
- Startup waits for Valkey, restores games, then accepts connections;
migrations are retried while Postgres is unreachable
- Valkey client that fails commands at once while Valkey is down instead
of queueing them, and logs connection errors once
- ValkeyLiveGameStore: live game snapshots and finished games waiting to
be saved, loaded with SCAN on startup
- ValkeyCache: JSON values with a TTL; errors count as cache misses
- Integration tests against a real Valkey, including an unreachable one
- Connection pool and a small migration runner (schema_migrations, one
transaction per migration, advisory lock)
- games and game_players tables with indexes for a user's games and for
popular games
- PostgresGameHistoryRepository: idempotent save, get, recent games of a
user, popular games
- Integration tests against a real Postgres via testcontainers; a custom
Jest environment gives tests Node's web globals, which Jest 27 leaves out
- GameRecord carries the number of distinct spectators
- Spectators are removed from games when they disconnect; the lookup
iterated an iterator with for...in and never found anything
- Finished and left games are removed from memory, not only from the
player mappings
- The connection info is sent once when a game starts
- PlayerSummary, MoveRecord, live and finished game summaries, GameRecord,
timeout constants, startedAt/moveTimes on Game and offlineSince on
UserConnectionInfo
- Channels for live game previews and for rejecting a new game while one
is in progress
- getInitialBoard, getBoardAfterMove, describeMove and createGameAtMove,
with tests against games played with the engine
- English and Turkish strings for the new screens
- postgres replaces keycloak-db and holds a keycloak and a mancala database,
each with its own user, created by apps/postgres/init
- valkey with an append-only file for live game state and caches
- backend gets DATABASE_URL and VALKEY_URL
- First visit logs in as a new guest through Keycloak without a form;
the session, including an offline refresh token, is kept in local storage
- Account menu on the home page: guests can log in or sign up, users can
log out (which starts a new guest session)
- The WebSocket client in core authenticates with the access token after
connecting, reports "connected" once the server accepts it, and re-sends
renewed tokens on the open connection
- Game page shows player names; guests appear as "Guest 4F2A"
- Auth channels, AuthUser and player names in UserConnectionInfo move to
core and the backend uses them from there
- Mobile keeps using userKey login for now
- Apps and @mancala/core depend on the packages in this repo through
link: dependencies instead of the npm releases, so changes no longer
need a publish
- Root package.json gets a build:packages script that installs and
builds both packages
- Backend and frontend Docker images are built from the repo root so
they can include the packages; compose builds them that way
- Metro watches the packages folder and resolves their imports from the
app's node_modules
- New auth module verifies Keycloak access tokens offline against the
realm's signing keys (issuer, RS256, typ=Bearer, allowed azp); guests
are recognised by the guest realm role
- Clients send an auth message after connecting and get on_auth or
on_auth_error; unauthenticated sockets are closed after 5 seconds and
their messages ignored; a socket can refresh its token but not switch user
- A user can be connected from several sockets; a late close of an old
socket no longer drops the new one
- Malformed messages are ignored instead of throwing
- Games store player names and guest flags and send them with the
connection info
- Legacy ?userKey= clients and /register stay available behind
LEGACY_USERKEY_AUTH; legacy keys must be UUIDs so they cannot take a
Keycloak user's kc: id
- TypeScript target raised to es2020 for async/await
- Custom authenticator creates and logs in a guest user when the
authorization request has guest=true; other requests get the login form
- Scheduled task removes guests with no remaining sessions
- Realm export with mancala-web PKCE client, guest role and guest-aware
browser flow (verify profile disabled so guests are not asked for details)
- Dockerfile building the extension into a Keycloak 26.7.5 image
- Keycloak and Postgres in compose; nginx proxies auth.playmancala.app
and rate limits the auth endpoint
- Integration tests run the extension and realm in a Keycloak container