The UI reads participant/standings fields from the native-server twin
(nativeOverlay), but bot join only wrote the NowChess participant list,
so bots never appeared in replicated/native-published tournaments. On
join, register the bot on the native server by name and join the twin
as that bot. Also run this for the AlreadyJoined case so bots stuck in
the NowChess list (but missing on native) get reconciled, and return
200 instead of 409 for it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joinTournament only ever had a token for the startup difficulty
(default medium); other difficulties fell back to the single shared
TOURNAMENT_BOT_TOKEN, which our tournament server rejects (401),
surfacing as 400 "Failed to join tournament" in the UI. Resolve and
cache a token for the requested difficulty instead.
Prefer the account-service token over anonymous register in
resolveToken so the bot joins as its canonical identity rather than a
throwaway account (medium joined but never appeared as a participant).
Add NativeReflectionConfig for JoinTournamentRequest/Response so the
success path serializes in native image instead of returning an empty
200 body.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bots joining a published tournament directly on the native server were not
reflected in NowChess (0 players) and the tournament could not be started,
because create() kept a local copy plus a separate native copy whose id was
discarded — leaving the two records disconnected.
- Capture the native tournament id: createNative/publishNative now return the
id instead of Boolean; persist it on Tournament.nativeTournamentId.
- Reverse-sync on read: get()/list() overlay nbPlayers/standing/status/round/
winner from the native twin (with a fullName backfill for tournaments created
before the id was captured).
- start(): proxy to the native twin (director token via authFor) so the native
participants are used; mirror the started status locally.
- Skip the native server in the replicate loop (it has no /replicate endpoint),
removing the per-create "Failed to replicate" warning.
- Isolate native integration in tournament unit tests (native-server-url no
longer defaults to the live server).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Janis Eccarius <eccariusjanis@gmail.com>
Reviewed-on: #78
In the native image the JAX-RS client buffers streaming responses, so reading
the NDJSON game stream blocks forever — the bot discovered its game ("Playing
game …") but never saw its turn and never moved, with no error. Replace the
game-stream consumer with a poll loop over plain GET game-state calls (which
work natively): when it is our turn, compute and submit. Drop the now-unused
stream consumer, move helper, and game-stream opener. Auto-join no longer
spawns per-tournament event-stream threads; polling handles discovery + play.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tournament-server does not replay gameStart to late subscribers — a
subscriber that connects after a game activates receives only heartbeats.
The bot relied solely on live gameStart events, so any reconnect or restart
after activation left it blind and it never played (games recorded with no
moves, losing on both colors).
Now each scan polls every joined tournament's current-round pairings, finds
the bot's own non-finished game and color, and starts playing it. The game
stream still drives moves once a game is discovered. Verified end-to-end
against the live server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tournament-server only accepts HS256 tokens it issued; forwarding a
NowChessSystems RS256 user token caused "unsupported algorithm". Proxied
calls (start/join/withdraw/stream) targeting the native server now swap in
the director token registered on that server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 409 on join means the bot is already a participant (in-memory join set is
empty after a pod restart). Treat 409 as success and start playing instead of
dropping the tournament and spamming errors every scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tournament stream broadcasts a gameStart per color for every pairing in
the round, without a player id. The bot latched the first color it saw and
played games it was not part of, submitting moves for the wrong color that
the server rejected. Now it fetches game detail and matches its botId against
white/black to resolve its real color, skipping games it is not in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When classical and NNUE evals diverge above the veto threshold, HybridBot
now re-searches excluding the suspect move and switches to NNUE's preferred
alternative instead of merely logging. BotController maps the expert bot to
HybridBot so tournament auto-join uses it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Official bots now poll the external tournament server and auto-join every
created tournament with the hardest bot (expert). Tournaments created in
NowChessSystems are forwarded to the native tournament server so the bots
can see and join them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #77
The env var TOURNAMENT_BOT_TOKEN was checked before Redis, so a stale
token set in the k8s secret always won over the freshly-registered token
stored in Redis at startup. Swap order: request param → Redis → env var.
Also add WARN-level logging when registerWithServer fails (non-2xx or
exception), making the failure visible in the log stream since INFO is
filtered in production.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
External tournament servers expose POST /api/bots (registry) not
POST /api/account/bots. They also require their own HMAC-HS256 token,
not the NowChessSystems RS256 account-service token.
parkOnStartup now:
- Parks on the local NowChessSystems account service via /api/account/bots
using the resolved NowChessSystems token (unchanged)
- For each remote server from fetchRemoteServers(), calls
registerWithServer(serverUrl, name) to obtain a server-specific token
via POST /api/auth/register (public endpoint), then parks via
POST /api/bots using that token
registerWithTournamentServer extracted into registerWithServer(url, name)
so it can be reused for both the primary tournament server (resolveToken)
and all remote servers (parkOnStartup).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The TOURNAMENT_SERVICE_URL points to the NowChessTools tournament server
which uses its own HMAC-HS256 JWTs issued by POST /api/auth/register.
Tokens from the NowChessSystems account service (RS256) are rejected
with 401 by that server.
resolveToken now first calls POST {tournamentServiceUrl}/api/auth/register
(public endpoint, idempotent — finds existing identity by name or creates).
This returns the correct HMAC-HS256 token for the target server and is
stored in Redis. Falls back to the account service path for deployments
where TOURNAMENT_SERVICE_URL points to the NowChessSystems tournament module.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OfficialBotService.onStart fires on StartupEvent (after all @PostConstruct),
so official bot accounts do not exist in the account service DB yet when
TournamentBotGamePlayer.initialize() runs on a fresh DB. This caused
getBotToken to 404, falling back to the stale TOURNAMENT_BOT_TOKEN env
var which uses the old signing key and is rejected with 401.
fetchTokenFromAccountService now retries after syncing all official bot
accounts (creating them if missing), ensuring a fresh token with the
current signing key is always available on startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On startup, TournamentBotGamePlayer now resolves the bot token via
a three-tier fallback: account service (fresh, validates against current
DB) → Redis cache (shared across pod instances) → TOURNAMENT_BOT_TOKEN
env var. Fetched tokens are written to Redis so sibling pods skip the
account service call.
joinTournament gains the same Redis fallback so join calls succeed even
when TOURNAMENT_BOT_TOKEN is not set in the environment.
Adds GET /api/account/official-bots/{name}/token (InternalOnly) to the
account service, backed by AccountService.getOfficialBotTokenByName.
AccountServiceClient gains a matching getBotToken method.
TournamentBotConfig.fromEnvWithToken accepts a pre-resolved token so the
env var is no longer required when a token can be sourced elsewhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty string config value caused DeploymentException when injected as String.
Optional[String] handles absent/empty cleanly without defaultValue workaround.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Servers are now configured via TOURNAMENT_EXTERNAL_SERVERS env var.
POST /api/tournament/servers and DELETE /api/tournament/servers/{id}
are removed; only GET (list) remains for the bot's fetchRemoteServers call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TOURNAMENT_EXTERNAL_SERVERS (comma-separated URLs) is loaded into
TournamentServerRegistry at startup so the bot can park on all servers
and replication targets are known without manual API calls after each restart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replicate newly created tournaments to all registered remote servers,
persisting them with originServerUrl so the remote can proxy mutations back
- Route all mutation endpoints (join/start/terminate/withdraw) through
originServerUrl when set, instead of trying local state first
- Fix tournament event stream to proxy remote tournaments (was 404 before)
- Official bot now routes all calls through TOURNAMENT_SERVICE_URL (local
tournament service) instead of calling remote cluster directly
- Bot parks on local account service + all registered remote servers on startup
- Add TOURNAMENT_SELF_URL env var so each cluster knows its own public URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SparkFiles.get() on the driver returns a driver-local path. When this was
passed to spark.read.text() the executor tried to open that path on its own
filesystem (separate pod), silently reading 0 rows.
Fix: download and decompress the Lichess PGN to NOWCHESS_PGN_CACHE_DIR
(default /tmp) which must be a filesystem shared between driver and executor
pods. In the k8s deployment this is the spark-analytics-output PVC mounted
at /spark-output, so set NOWCHESS_PGN_CACHE_DIR=/spark-output/.pgn-cache.
Also caches the decompressed file across runs — skips download if already
present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
botToken in JoinTournamentRequest is now Option[String]. When absent the
service resolves it from TOURNAMENT_BOT_TOKEN env var so official-bot
join requests no longer need a token in the body.
Response status on join failure changed from BAD_GATEWAY (502) to
BAD_REQUEST (400).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GameLengthJob, ColorAdvantageJob, EloDistributionJob, TimeControlJob,
DailyActivityJob, RatingMismatchJob, and TerminationStatsJob bringing total
batch pipelines to 11 (+ 1 streaming).
Extends GameSource with loadExtended() / fromLichessPgnExtended() extracting
WhiteElo, BlackElo, TimeControl, UTCDate, UTCTime, Termination, ECO from PGN
headers; JDBC path returns nulls for extended columns, keeping all existing
jobs unaffected.
PlayerStatsJob gains a CSV output alongside the existing Parquet write so
the analytics webview can display player statistics without pyarrow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns a FEN string for every ply of the game (initial position + one
per move) by replaying moves via ruleSet.applyMove and exporting each
GameContext to FEN. Enables full per-move engine analysis from clients
without requiring a chess library on their side.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace header-based auth (not possible with browser WebSocket API) with a
first-message auth protocol: client sends {"type":"auth","token":"<JWT>"}
as the first text frame; server validates and proceeds or closes the connection.
Both GameWebSocketResource and UserWebSocketResource now hold incoming
connections in a pendingAuth set until the auth frame arrives, preventing
any game or event messages from being processed before identity is established.
Also removes the broken Bearer-prefix handling that caused header-based auth
to silently fail even for non-browser clients.
---------
Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #73
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
Park the expert bot on the configured tournament server (default
http://141.37.123.132:8086) on startup, reusing a fixed
TOURNAMENT_BOT_TOKEN when present instead of minting a new identity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #75