Compare commits

..

2 Commits

Author SHA1 Message Date
TeamCity bace029a8a ci: bump version with Build-148 2026-06-23 20:31:27 +00:00
Janis Eccarius 7664042193 fix(tournament): mirror bot join onto native twin
Build & Test (NowChessSystems) TeamCity build finished
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>
2026-06-23 22:20:56 +02:00
4 changed files with 63 additions and 3 deletions
+22
View File
@@ -115,3 +115,25 @@
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
## (2026-06-23)
### Features
* **analytics:** add Spark batch analytics module ([#70](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/70)) ([39f1657](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/39f1657e1db6e84889af338c43be8cb5c03c3ec3))
* NCS-121 pipeline for tournament ([#68](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/68)) ([145f467](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/145f4676483f92bfe6f2d9ca40e2cb4200982e87))
* NCS-82 add Swiss-system tournament module ([#55](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/55)) ([c5661de](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c5661de4a0ebf4b33211f5a391840dcf744656b7))
* **reflection:** add GameWritebackEventDto to native reflection configuration ([1aee39c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1aee39c1ad286984501ac4b47da2b72d60b58a6f))
* **reflection:** add native reflection configuration for tournament classes ([65bc6a7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/65bc6a759937543df2d29905688bfa9e68d0c9d4))
* **tournament:** auto-join external tournaments and publish created ones ([#77](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/77)) ([9978b7e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/9978b7ea78eb658a225a461b9cd339386c0c14f3))
* **tournament:** federate tournaments across clusters with DB replication ([5b000a6](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5b000a6e5f04ea6770d1c7ab6bfdaded77a99172))
* **tournament:** remove dynamic server add/remove endpoints ([6d06edd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6d06edda69a50de65cd9efa27f26a4cc6b437f9d))
* **tournament:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
### Bug Fixes
* **tournament:** mirror bot join onto native twin ([7664042](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76640421930c26a9260da002c90e2966b97a57a4))
* **tournament:** replace scala.util.Random singleton with UUID for native image ([a50884a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a50884a11b1de500e74c18fd08d2d102d53cc3e9))
* **tournament:** sync native-server participants and route start ([#78](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/78)) ([1f4e9c8](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1f4e9c8498f55d95ab48758df60c7618445bf6ca))
* **tournament:** use HS256 director token for native tournament-server calls ([b98bdd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b98bdd2a64eb6c8279bd3cfe15d70628025ef0e5))
* **tournament:** use Optional[String] for selfUrl ConfigProperty to avoid startup failure ([28cbc2e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/28cbc2e18447aa8a04a5868889a49b555075d0c6))
* wrap server list response in ExternalTournamentServerList ([f2d79e4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f2d79e4952aea6bde762c294eb202474b7827054))
@@ -119,6 +119,16 @@ class TournamentResource:
case Some(nativeId) => tournamentService.setNativeTournamentId(t.id, nativeId)
case None => log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
// Mirror a bot join onto the native twin so it surfaces in the UI, which reads participant and
// standings fields from the native server (see nativeOverlay).
private def joinNativeTwin(id: String, botName: String): Unit =
if nativeServerUrl.nonEmpty then
tournamentService.get(id).flatMap(nativeIdFor).foreach { nativeId =>
if externalClient.joinNativeAsBot(nativeServerUrl, nativeId, botName) then
log.infof("Joined bot %s on native twin %s of tournament %s", botName, nativeId, id)
else log.warnf("Failed to join bot %s on native twin %s of tournament %s", botName, nativeId, id)
}
// Resolve the native-server twin of a local tournament. Backfills the stored id by matching
// fullName against the native list for tournaments created before the id was captured.
private def nativeIdFor(t: de.nowchess.tournament.domain.Tournament): Option[String] =
@@ -250,7 +260,14 @@ class TournamentResource:
val botId = Option(jwt.getSubject).getOrElse("")
val botName = Option(jwt.getClaim[AnyRef]("name")).map(_.toString).getOrElse(botId)
tournamentService.join(id, botId, botName) match
case Right(_) => Response.ok(OkDto()).build()
case Right(_) =>
joinNativeTwin(id, botName)
Response.ok(OkDto()).build()
// Already in the NowChess participant list but possibly never mirrored onto the native
// twin (where the UI reads participants from) — make the native join idempotent.
case Left(TournamentError.AlreadyJoined) =>
joinNativeTwin(id, botName)
Response.ok(OkDto()).build()
case Left(error) =>
error match
case TournamentError.NotFound(_) =>
@@ -60,9 +60,12 @@ class ExternalTournamentClient:
}
private def registerDirector(serverUrl: String, name: String): Option[String] =
registerAccount(serverUrl, name, isBot = false)
private def registerAccount(serverUrl: String, name: String, isBot: Boolean): Option[String] =
Try {
val client = buildClient()
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":false}"""
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":$isBot}"""
val response = client
.target(s"$serverUrl/api/auth/register")
.request(MediaType.APPLICATION_JSON)
@@ -76,6 +79,24 @@ class ExternalTournamentClient:
client.close()
}.getOrElse(None)
// The tournament server holds only the director token, which cannot join as a bot. Register the
// bot on the native server by name to mint a bot token, then join the native twin as that bot.
def joinNativeAsBot(serverUrl: String, tournamentId: String, botName: String): Boolean =
registerAccount(serverUrl, botName, isBot = true).exists { token =>
Try {
val client = buildClient()
val response = client
.target(s"$serverUrl/api/tournament/$tournamentId/join")
.request(MediaType.APPLICATION_JSON)
.header("Authorization", s"Bearer $token")
.post(Entity.json(""))
try response.getStatus / 100 == 2 || response.getStatus == 409
finally
response.close()
client.close()
}.getOrElse(false)
}
private def createNative(serverUrl: String, token: String, form: String): Option[String] =
Try {
val client = buildClient()
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=8
MINOR=9
PATCH=0