Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e88b081947 | |||
| 1b30c3be39 | |||
| f8ca95af3c | |||
| 4a50db0721 | |||
| 260db25803 | |||
| 80e1cc258b | |||
| bfc46723e6 | |||
| bace029a8a | |||
| 7664042193 | |||
| a604b4ad42 | |||
| fdf4c94811 | |||
| d9f30f0bfe | |||
| 1f4e9c8498 | |||
| e2b13c0c8f | |||
| bfb15c7299 |
@@ -752,3 +752,238 @@
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **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))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* 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))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
|
||||
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
|
||||
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||
* **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:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
|
||||
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
|
||||
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
|
||||
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
|
||||
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
|
||||
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
|
||||
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **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))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* 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))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
|
||||
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
|
||||
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||
* **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:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
|
||||
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
|
||||
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
|
||||
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
|
||||
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
|
||||
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
|
||||
* **official-bots:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **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))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* 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))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
|
||||
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
|
||||
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||
* **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:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
|
||||
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
|
||||
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
|
||||
* **official-bots:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
|
||||
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
|
||||
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
|
||||
* **official-bots:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **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))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* 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))
|
||||
* **official-bots:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
|
||||
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
|
||||
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||
* **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:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
|
||||
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
|
||||
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
|
||||
* **official-bots:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
|
||||
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
|
||||
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
|
||||
* **official-bots:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
## (2026-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add initialization metrics for various services ([d438e97](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d438e97f32bdde0bfc63c1b4a8cc810cdd093166))
|
||||
* add OpenTelemetry trace configuration with parentbased sampler ([3904d5a](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3904d5ad8ad4930ddee65287a7bfab785a6148f5))
|
||||
* **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))
|
||||
* **config:** update application.yml for PostgreSQL and remove staging/production configurations ([2404e61](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/2404e6164c3b50ffccbea5238d636060d6abe4d6))
|
||||
* **config:** update application.yml for staging and production environments ([6113432](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/6113432a14c476a3a0dfc0d449e17d023697f2ba))
|
||||
* configure logging and add OpenTelemetry support ([#49](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/49)) ([d57c488](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/d57c4886612d1d92da0e1b79209fc83e6ef537a1))
|
||||
* **docker:** add .dockerignore and .gitignore files for build exclusions ([c987d8e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/c987d8e258c0e6c4cfbdaa8381c64c410d7a2b83))
|
||||
* **docker:** add Dockerfiles for building Quarkus application in native and JVM modes ([3f2d2bb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3f2d2bb4c97fa8cddba66e1da4427c54236dfeed))
|
||||
* **docker:** add Dockerfiles for Quarkus application in JVM and native modes ([34b9933](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/34b993304670cf2aa62cd2f6460cee7b9864b08e))
|
||||
* **events:** migrate game-creation and bot flows to Redis Streams NCS-89 ([#62](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/62)) ([a24924c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/a24924c23057db3d700a75dbc4333557789cd991))
|
||||
* NCS-78 Add Traceability to the Applications ([#46](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/46)) ([649566e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/649566eb3fcf38f91c8896a739f74ea318af312d))
|
||||
* NCS-78 Add Traceability to the Applications ([#47](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/47)) ([87dfc6c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/87dfc6c2bcce7f7d58fc641bd8d468a2e584c108))
|
||||
* 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))
|
||||
* **official-bots:** activate opening book in expert bot (native-safe) ([260db25](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/260db25803ec55ce99e55782791eabdc190dfed4))
|
||||
* **official-bots:** consume GameOver stream for bot cleanup ([#67](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/67)) ([db9d153](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/db9d1533912f4b41c4d1ca80ccffdde5d23d6ff6))
|
||||
* **official-bots:** make HybridBot veto actionable and use it for expert ([1df29cf](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1df29cf3a6e21af3f396b2b7a6da67d978f941ae))
|
||||
* **official-bots:** park expert bot on tournament server at startup ([#75](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/75)) ([30295a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/30295a4bb95855ee8261c92278bb9ebc80ee12ee))
|
||||
* **official-bots:** resolve tournament bot token from Redis and account service ([386ddc5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/386ddc5c19f8f893b16c6422aa5393b54c872e45))
|
||||
* **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:** seed external server registry from env var on startup ([845dc9c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/845dc9c2935c8bc1be42541dfaf31c9a861d3272))
|
||||
* true-microservices ([#40](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/40)) ([5909242](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/590924254e8a2754de661a57a03e43f89ceb6299))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable official bots to connect to external tournament server ([#71](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/71)) ([688d30e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/688d30e2b10026923372be5fca3c63eaaee2de2a))
|
||||
* **official-bots:** configure JWT verification ([#72](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/72)) ([98c64fc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/98c64fc0d56dc542beb31c75f4b9056d91de03cd))
|
||||
* **official-bots:** correct parkOn path from /api/bots to /api/account/bots ([1be9949](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1be9949c0b5c6a1db535696620d77735050d6c93))
|
||||
* **official-bots:** derive tournament game color from game endpoint ([#79](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/79)) ([bfc4672](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfc46723e615bb9b65f7f9bba5f53877c4f079a7))
|
||||
* **official-bots:** discover tournament games by polling, not just the stream ([10113fd](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/10113fd0579b614d15870798d933bc9c495d2049))
|
||||
* **official-bots:** make botToken optional, fall back to env, fix 502 status ([f43d193](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f43d1930d80670d810c57b54eaa3789854fa082c))
|
||||
* **official-bots:** NCS-70-auto-register official bots with account service ([#59](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/59)) ([7117a93](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7117a93376272094d0b1a6abf2121254ce396684))
|
||||
* **official-bots:** park on external tournament servers using correct endpoint and token ([3188241](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/31882417377468b41bbe3ff94506aa4928024450))
|
||||
* **official-bots:** play games by polling state instead of NDJSON stream ([bfb15c7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/bfb15c7299bd471d5e064a577ed10af98e2ea90a))
|
||||
* **official-bots:** play only own tournament games with correct color ([4651bb7](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/4651bb796f07a21bd013d9521b2dfe2e1078cebb))
|
||||
* **official-bots:** prioritize Redis token over stale env var in joinTournament ([83dd2d4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/83dd2d4335ca48eb3e5aa234a75367574276ba63))
|
||||
* **official-bots:** register with tournament server directly to get correct token ([64b5d55](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/64b5d5567f110c2fe152558c7de275a1e0b30e21))
|
||||
* **official-bots:** resolve per-difficulty bot token on tournament join ([fdf4c94](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fdf4c94811d086996447bb4657fac1d9bd6e5a93))
|
||||
* **official-bots:** resume tournaments already joined after restart ([285b73e](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/285b73efbd6dd98cec410ade9eead9881d693a8f))
|
||||
* **official-bots:** sync bots before token fetch on first startup after DB wipe ([b0ddb27](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0ddb274d23bca8b1b3f691ce0d643f33e0b54cd))
|
||||
* **official-bots:** use ThreadLocalRandom in PolyglotBook for native image ([1b30c3b](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1b30c3be393d25712c8743d3d9057207f8bbb67c))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "refactor: update metrics paths formatting in application.yml for clarity" ([3870566](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/38705663498d5f47c40dafe2f26198589ede8656))
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.bot.bots.{ClassicalBot, HybridBot}
|
||||
import de.nowchess.bot.util.PolyglotBook
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import org.jboss.logging.Logger
|
||||
|
||||
object BotController:
|
||||
private val log = Logger.getLogger(classOf[BotController])
|
||||
|
||||
private val openingBook = PolyglotBook.fromResource("/opening_book.bin")
|
||||
|
||||
private val bots: Map[String, Bot] = Map(
|
||||
"easy" -> ClassicalBot(BotDifficulty.Easy),
|
||||
"medium" -> ClassicalBot(BotDifficulty.Medium),
|
||||
"hard" -> ClassicalBot(BotDifficulty.Hard),
|
||||
"expert" -> HybridBot(BotDifficulty.Expert, vetoReporter = log.debug(_)),
|
||||
"expert" -> HybridBot(BotDifficulty.Expert, vetoReporter = log.debug(_), book = Some(openingBook)),
|
||||
)
|
||||
|
||||
def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase)
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package de.nowchess.bot.config
|
||||
|
||||
import de.nowchess.bot.resource.{JoinTournamentRequest, JoinTournamentResponse}
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[JoinTournamentRequest],
|
||||
classOf[JoinTournamentResponse],
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
+82
-82
@@ -28,10 +28,10 @@ class TournamentBotGamePlayer:
|
||||
private val log = Logger.getLogger(classOf[TournamentBotGamePlayer])
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var botController: BotController = uninitialized
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var botController: BotController = uninitialized
|
||||
@Inject var redis: RedisDataSource = uninitialized
|
||||
@Inject var redisConfig: RedisConfig = uninitialized
|
||||
@Inject @RestClient var accountServiceClient: AccountServiceClient = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
@@ -43,6 +43,9 @@ class TournamentBotGamePlayer:
|
||||
private val hardestDifficulty = "expert"
|
||||
private val autoJoinIntervalMs = 15000L
|
||||
|
||||
private val gameTerminalStatuses =
|
||||
Set("checkmate", "stalemate", "draw", "resigned", "timeout", "aborted", "finished")
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@volatile private var running = true
|
||||
@volatile private var autoJoinToken: Option[String] = None
|
||||
@@ -87,8 +90,7 @@ class TournamentBotGamePlayer:
|
||||
open.foreach { tournamentId =>
|
||||
if joinedTournaments.add(tournamentId) then
|
||||
val cfg = TournamentBotConfig(autoJoinServerUrl, tournamentId, token, botId, hardestDifficulty)
|
||||
if joinedOrParticipating(cfg) then startAsync(cfg)
|
||||
else joinedTournaments.remove(tournamentId)
|
||||
if !joinedOrParticipating(cfg) then joinedTournaments.remove(tournamentId)
|
||||
}
|
||||
playPendingGames(token, botId)
|
||||
}
|
||||
@@ -117,7 +119,9 @@ class TournamentBotGamePlayer:
|
||||
yield result
|
||||
|
||||
private def findBotGame(pairings: JsonNode, botId: String): Option[(String, String)] =
|
||||
pairings.elements().asScala
|
||||
pairings
|
||||
.elements()
|
||||
.asScala
|
||||
.flatMap { p =>
|
||||
val whiteId = p.path("white").path("id").asText()
|
||||
val blackId = p.path("black").path("id").asText()
|
||||
@@ -127,7 +131,9 @@ class TournamentBotGamePlayer:
|
||||
.nextOption()
|
||||
|
||||
private def activeMatch(matches: JsonNode): Option[String] =
|
||||
matches.elements().asScala
|
||||
matches
|
||||
.elements()
|
||||
.asScala
|
||||
.find(m => m.path("gameId").asText().nonEmpty && !(m.has("outcome") && !m.path("outcome").isNull))
|
||||
.map(_.path("gameId").asText())
|
||||
|
||||
@@ -149,9 +155,12 @@ class TournamentBotGamePlayer:
|
||||
|
||||
private def openTournaments(): List[String] =
|
||||
Try {
|
||||
val response = client.target(autoJoinServerUrl)
|
||||
.path("api").path("tournament")
|
||||
.request(MediaType.APPLICATION_JSON).get()
|
||||
val response = client
|
||||
.target(autoJoinServerUrl)
|
||||
.path("api")
|
||||
.path("tournament")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.get()
|
||||
if response.getStatus == 200 then
|
||||
val node = objectMapper.readTree(response.readEntity(classOf[String]))
|
||||
response.close()
|
||||
@@ -162,8 +171,8 @@ class TournamentBotGamePlayer:
|
||||
private def resolveToken(difficulty: String): Option[String] =
|
||||
val name = botName(difficulty)
|
||||
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:$name"
|
||||
registerWithServer(tournamentServiceUrl, name)
|
||||
.orElse(fetchTokenFromAccountService(name))
|
||||
fetchTokenFromAccountService(name)
|
||||
.orElse(registerWithServer(tournamentServiceUrl, name))
|
||||
.map { token =>
|
||||
redis.value(classOf[String]).set(redisKey, token)
|
||||
log.infof("Refreshed bot token for %s — stored in Redis", name)
|
||||
@@ -185,8 +194,11 @@ class TournamentBotGamePlayer:
|
||||
private def registerWithServer(serverUrl: String, name: String): Option[String] =
|
||||
Try {
|
||||
val body = s"""{"name":"${name.replace("\"", "\\\"")}","isBot":true}"""
|
||||
val response = client.target(serverUrl)
|
||||
.path("api").path("auth").path("register")
|
||||
val response = client
|
||||
.target(serverUrl)
|
||||
.path("api")
|
||||
.path("auth")
|
||||
.path("register")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
|
||||
val status = response.getStatus
|
||||
@@ -199,11 +211,11 @@ class TournamentBotGamePlayer:
|
||||
log.warnf("Register %s on %s returned status %d: %s", name, serverUrl, status, errBody)
|
||||
response.close()
|
||||
None
|
||||
}.recover { case ex => log.warnf(ex, "Register %s on %s failed", name, serverUrl); None }
|
||||
.toOption.flatten
|
||||
}.recover { case ex => log.warnf(ex, "Register %s on %s failed", name, serverUrl); None }.toOption.flatten
|
||||
|
||||
private def fetchTokenFromAccountService(name: String): Option[String] =
|
||||
Try(accountServiceClient.getBotToken(name).token).toOption.filter(_.nonEmpty)
|
||||
Try(accountServiceClient.getBotToken(name).token).toOption
|
||||
.filter(_.nonEmpty)
|
||||
.orElse {
|
||||
Try {
|
||||
val allNames = BotController.listBots.map(botName)
|
||||
@@ -229,9 +241,13 @@ class TournamentBotGamePlayer:
|
||||
|
||||
private def fetchRemoteServers(): List[String] =
|
||||
Try {
|
||||
val response = client.target(tournamentServiceUrl)
|
||||
.path("api").path("tournament").path("servers")
|
||||
.request(MediaType.APPLICATION_JSON).get()
|
||||
val response = client
|
||||
.target(tournamentServiceUrl)
|
||||
.path("api")
|
||||
.path("tournament")
|
||||
.path("servers")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.get()
|
||||
if response.getStatus == 200 then
|
||||
val node = objectMapper.readTree(response.readEntity(classOf[String]))
|
||||
response.close()
|
||||
@@ -242,7 +258,11 @@ class TournamentBotGamePlayer:
|
||||
private def parkOnAccountService(serverUrl: String, difficulty: String, token: String): Unit =
|
||||
Try {
|
||||
val body = s"""{"name":"${botName(difficulty)}"}"""
|
||||
val response = client.target(serverUrl).path("api").path("account").path("bots")
|
||||
val response = client
|
||||
.target(serverUrl)
|
||||
.path("api")
|
||||
.path("account")
|
||||
.path("bots")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", s"Bearer $token")
|
||||
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
|
||||
@@ -256,7 +276,10 @@ class TournamentBotGamePlayer:
|
||||
private def parkOnTournamentServer(serverUrl: String, name: String, token: String): Unit =
|
||||
Try {
|
||||
val body = s"""{"name":"${name.replace("\"", "\\\"")}"}"""
|
||||
val response = client.target(serverUrl).path("api").path("bots")
|
||||
val response = client
|
||||
.target(serverUrl)
|
||||
.path("api")
|
||||
.path("bots")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", s"Bearer $token")
|
||||
.post(Entity.entity(body, MediaType.APPLICATION_JSON))
|
||||
@@ -274,10 +297,11 @@ class TournamentBotGamePlayer:
|
||||
botToken: Option[String],
|
||||
difficulty: String,
|
||||
): Either[String, String] =
|
||||
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:${botName(difficulty)}"
|
||||
val resolvedToken = botToken.filter(_.nonEmpty)
|
||||
val redisKey = s"${redisConfig.prefix}:tournament-bot:token:${botName(difficulty)}"
|
||||
val resolvedToken = botToken
|
||||
.filter(_.nonEmpty)
|
||||
.orElse(Option(redis.value(classOf[String]).get(redisKey)).filter(_.nonEmpty))
|
||||
.orElse(System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty))
|
||||
.orElse(resolveToken(difficulty))
|
||||
resolvedToken match
|
||||
case None => Left("No bot token provided and TOURNAMENT_BOT_TOKEN not configured")
|
||||
case Some(token) =>
|
||||
@@ -334,8 +358,7 @@ class TournamentBotGamePlayer:
|
||||
log.infof("Listening to tournament %s event stream", cfg.tournamentId)
|
||||
forEachLine(response.readEntity(classOf[InputStream])): line =>
|
||||
parse(line).foreach: node =>
|
||||
if node.path("type").asText() == "gameStart" then
|
||||
onGameStart(cfg, node.path("gameId").asText())
|
||||
if node.path("type").asText() == "gameStart" then onGameStart(cfg, node.path("gameId").asText())
|
||||
|
||||
private def onGameStart(cfg: TournamentBotConfig, gameId: String): Unit =
|
||||
if gameId.isEmpty then ()
|
||||
@@ -370,58 +393,44 @@ class TournamentBotGamePlayer:
|
||||
private def playGame(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
|
||||
Try {
|
||||
log.infof("Playing game %s as %s", gameId, color)
|
||||
openGameStream(cfg, gameId).foreach(consumeGameStream(cfg, gameId, color, _))
|
||||
pollGameLoop(cfg, gameId, color)
|
||||
activeGames.remove(gameId)
|
||||
} match
|
||||
case Failure(ex) => log.errorf(ex, "Game %s crashed", gameId); activeGames.remove(gameId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def consumeGameStream(cfg: TournamentBotConfig, gameId: String, color: String, stream: InputStream): Unit =
|
||||
val reader = new BufferedReader(new InputStreamReader(stream))
|
||||
// The native JAX-RS client buffers streaming responses, so reading the NDJSON game stream blocks
|
||||
// forever. Poll the game state with plain GETs (which work) and move when it is our turn.
|
||||
private def pollGameLoop(cfg: TournamentBotConfig, gameId: String, color: String): Unit =
|
||||
// scalafix:off DisableSyntax.var
|
||||
var done = false
|
||||
var done = false
|
||||
var lastFen = ""
|
||||
// scalafix:on DisableSyntax.var
|
||||
Iterator
|
||||
.continually(reader.readLine())
|
||||
.map(Option(_))
|
||||
.takeWhile(opt => opt.isDefined && running && !done)
|
||||
.flatten
|
||||
.foreach { line =>
|
||||
parse(line).foreach: node =>
|
||||
node.path("type").asText() match
|
||||
case "gameState" =>
|
||||
maybeMove(
|
||||
cfg,
|
||||
gameId,
|
||||
color,
|
||||
node.path("turn").asText(),
|
||||
node.path("status").asText(),
|
||||
node.path("fen").asText(),
|
||||
)
|
||||
case "move" =>
|
||||
maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText())
|
||||
case "gameEnd" =>
|
||||
log.infof(
|
||||
"Game %s ended — status=%s winner=%s",
|
||||
gameId,
|
||||
node.path("status").asText(),
|
||||
node.path("winner").asText(),
|
||||
); done = true
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
private def maybeMove(
|
||||
cfg: TournamentBotConfig,
|
||||
gameId: String,
|
||||
color: String,
|
||||
turn: String,
|
||||
status: String,
|
||||
fen: String,
|
||||
): Unit =
|
||||
if turn == color && status == "ongoing" && fen.nonEmpty then
|
||||
computeUci(cfg, fen) match
|
||||
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
||||
case Some(uci) => submitMove(cfg, gameId, uci)
|
||||
while running && !done do
|
||||
fetchJson(cfg, target(cfg).path("game").path(gameId)) match
|
||||
case None => sleep(2000)
|
||||
case Some(node) =>
|
||||
val status = node.path("status").asText()
|
||||
if gameTerminalStatuses.contains(status) then
|
||||
log.infof("Game %s ended — status=%s", gameId, status); done = true
|
||||
else
|
||||
// TEMP: tournament-server reports wrong color in pairings (everyone white).
|
||||
// The game endpoint white/black ids are correct, so derive our color from it.
|
||||
val whiteId = node.path("white").path("id").asText()
|
||||
val blackId = node.path("black").path("id").asText()
|
||||
val myColor =
|
||||
if whiteId == cfg.botId then "white"
|
||||
else if blackId == cfg.botId then "black"
|
||||
else color
|
||||
val turn = node.path("turn").asText()
|
||||
val fen = node.path("fen").asText()
|
||||
if turn == myColor && status == "ongoing" && fen.nonEmpty && fen != lastFen then
|
||||
lastFen = fen
|
||||
log.infof("Our turn in game %s — computing move (fen=%s)", gameId, fen)
|
||||
computeUci(cfg, fen) match
|
||||
case None => log.warnf("No move found for game %s (fen=%s)", gameId, fen)
|
||||
case Some(uci) => submitMove(cfg, gameId, uci)
|
||||
sleep(1000)
|
||||
|
||||
private def computeUci(cfg: TournamentBotConfig, fen: String): Option[String] =
|
||||
FenParser.parseFen(fen) match
|
||||
@@ -439,15 +448,6 @@ class TournamentBotGamePlayer:
|
||||
case Failure(ex) => log.errorf(ex, "Error submitting move %s in game %s", uci, gameId)
|
||||
case Success(_) => ()
|
||||
|
||||
private def openGameStream(cfg: TournamentBotConfig, gameId: String): Option[InputStream] =
|
||||
Try {
|
||||
val response = authed(cfg, target(cfg).path("game").path(gameId).path("stream"))
|
||||
.header("Accept", "application/x-ndjson")
|
||||
.get()
|
||||
if response.getStatus == 200 then Some(response.readEntity(classOf[InputStream]))
|
||||
else { log.warnf("Game stream %s returned status %d", gameId, response.getStatus); response.close(); None }
|
||||
}.getOrElse(None)
|
||||
|
||||
private def engine(cfg: TournamentBotConfig): Bot =
|
||||
botController.getBot(cfg.difficulty).orElse(botController.getBot("medium")).get
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
|
||||
import java.io.{DataInputStream, FileInputStream}
|
||||
import java.io.{DataInputStream, FileInputStream, InputStream}
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
|
||||
/** Reads a Polyglot opening book (.bin file) and probes it for moves.
|
||||
*
|
||||
@@ -16,24 +16,11 @@ import scala.util.Random
|
||||
* - weight: 2 bytes (Short) — move weight (higher = preferred)
|
||||
* - learn: 4 bytes (Int) — learning data (unused)
|
||||
*/
|
||||
final class PolyglotBook(path: String):
|
||||
|
||||
private val entries: Map[Long, Vector[BookEntry]] =
|
||||
try {
|
||||
val r = loadBookFile(path)
|
||||
println(s"Book loaded successfully. ${r.size} entries found.")
|
||||
r
|
||||
} catch
|
||||
case e: Exception =>
|
||||
println(s"Error loading book: $e")
|
||||
// Gracefully fail: return empty map if book cannot be loaded
|
||||
// This allows the bot to work even if the book file is missing
|
||||
scala.collection.immutable.Map.empty
|
||||
final class PolyglotBook private (entries: Map[Long, Vector[BookEntry]]):
|
||||
|
||||
/** Probe the book for a move in the given position. Returns a weighted random move, or None if not in book. */
|
||||
def probe(context: GameContext): Option[Move] =
|
||||
val hash = PolyglotHash.hash(context)
|
||||
println(f"0x$hash%016X")
|
||||
entries.get(hash).flatMap { bookEntries =>
|
||||
if bookEntries.isEmpty then None
|
||||
else
|
||||
@@ -41,24 +28,6 @@ final class PolyglotBook(path: String):
|
||||
decodeMove(entry.move, context)
|
||||
}
|
||||
|
||||
private def loadBookFile(path: String): Map[Long, Vector[BookEntry]] =
|
||||
val input = DataInputStream(FileInputStream(path))
|
||||
try
|
||||
val result = mutable.Map[Long, Vector[BookEntry]]()
|
||||
while input.available() > 0 do
|
||||
val key = input.readLong()
|
||||
val move = input.readShort()
|
||||
val weight = input.readShort()
|
||||
input.readInt() // learning data (unused)
|
||||
|
||||
val entry = BookEntry(key, move, weight)
|
||||
result.updateWith(key) {
|
||||
case Some(entries) => Some(entries :+ entry)
|
||||
case None => Some(Vector(entry))
|
||||
}
|
||||
result.toMap
|
||||
finally input.close()
|
||||
|
||||
/** Decode a packed Polyglot move short into an Option[Move].
|
||||
*
|
||||
* Bit layout of the move Short:
|
||||
@@ -124,7 +93,7 @@ final class PolyglotBook(path: String):
|
||||
if entries.length == 1 then entries.head
|
||||
else
|
||||
val totalWeight = entries.map(_.weight).sum
|
||||
val pick = Random.nextInt(totalWeight.max(1)) // NOSONAR
|
||||
val pick = ThreadLocalRandom.current().nextInt(totalWeight.max(1)) // NOSONAR
|
||||
|
||||
@scala.annotation.tailrec
|
||||
def select(remaining: Int, idx: Int): BookEntry =
|
||||
@@ -134,4 +103,48 @@ final class PolyglotBook(path: String):
|
||||
|
||||
select(pick, 0)
|
||||
|
||||
object PolyglotBook:
|
||||
|
||||
/** Load a book from a filesystem path. Fails gracefully to an empty book. */
|
||||
def apply(path: String): PolyglotBook =
|
||||
safeLoad(s"file $path")(FileInputStream(path))
|
||||
|
||||
/** Load a book from a classpath resource (native-image safe: the resource is embedded in the binary, so no file must
|
||||
* be mounted into the pod).
|
||||
*/
|
||||
def fromResource(name: String): PolyglotBook =
|
||||
Option(getClass.getResourceAsStream(name)) match
|
||||
case Some(stream) => safeLoad(s"resource $name")(stream)
|
||||
case None =>
|
||||
println(s"Error loading book: resource $name not found on classpath")
|
||||
new PolyglotBook(Map.empty)
|
||||
|
||||
private def safeLoad(source: String)(stream: => InputStream): PolyglotBook =
|
||||
try
|
||||
val entries = parse(stream)
|
||||
println(s"Book loaded successfully from $source. ${entries.size} entries found.")
|
||||
new PolyglotBook(entries)
|
||||
catch
|
||||
case e: Exception =>
|
||||
println(s"Error loading book from $source: $e")
|
||||
new PolyglotBook(Map.empty)
|
||||
|
||||
private def parse(stream: InputStream): Map[Long, Vector[BookEntry]] =
|
||||
val input = DataInputStream(stream)
|
||||
try
|
||||
val result = mutable.Map[Long, Vector[BookEntry]]()
|
||||
while input.available() > 0 do
|
||||
val key = input.readLong()
|
||||
val move = input.readShort()
|
||||
val weight = input.readShort()
|
||||
input.readInt() // learning data (unused)
|
||||
|
||||
val entry = BookEntry(key, move, weight)
|
||||
result.updateWith(key) {
|
||||
case Some(entries) => Some(entries :+ entry)
|
||||
case None => Some(Vector(entry))
|
||||
}
|
||||
result.toMap
|
||||
finally input.close()
|
||||
|
||||
private case class BookEntry(key: Long, move: Short, weight: Int)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=31
|
||||
MINOR=36
|
||||
PATCH=0
|
||||
|
||||
@@ -94,3 +94,46 @@
|
||||
* **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:** 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))
|
||||
## (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))
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.time.Instant
|
||||
@Entity
|
||||
@Table(name = "tournaments")
|
||||
class Tournament:
|
||||
// scalafix:off DisableSyntax.var
|
||||
// scalafix:off
|
||||
@Id
|
||||
var id: String = uninitialized
|
||||
|
||||
@@ -33,4 +33,7 @@ class Tournament:
|
||||
|
||||
@Column(nullable = true)
|
||||
var originServerUrl: String = null
|
||||
|
||||
@Column(nullable = true)
|
||||
var nativeTournamentId: String = null
|
||||
// scalafix:on
|
||||
|
||||
+88
-27
@@ -52,9 +52,9 @@ class TournamentResource:
|
||||
@PermitAll
|
||||
def list(): Response =
|
||||
val (created, started, finished) = tournamentService.list()
|
||||
val internalCreated = created.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
||||
val internalStarted = started.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
||||
val internalFinished = finished.map(t => objectMapper.valueToTree[JsonNode](tournamentService.toDto(t)))
|
||||
val internalCreated = created.map(nativeOverlay)
|
||||
val internalStarted = started.map(nativeOverlay)
|
||||
val internalFinished = finished.map(nativeOverlay)
|
||||
|
||||
val (extCreated, extStarted, extFinished) = registry
|
||||
.serverUrls()
|
||||
@@ -96,7 +96,7 @@ class TournamentResource:
|
||||
val form = CreateTournamentForm(name, nbRounds, clockLimit, clockIncrement, rated)
|
||||
val t = tournamentService.create(userId, form)
|
||||
selfUrl.ifPresent { url =>
|
||||
registry.serverUrls().foreach { remoteUrl =>
|
||||
registry.serverUrls().filterNot(externalClient.isNativeServer).foreach { remoteUrl =>
|
||||
if !externalClient.replicateTournament(remoteUrl, toReplicateRequest(t), url) then
|
||||
log.warnf("Failed to replicate tournament %s to %s", t.id, remoteUrl)
|
||||
}
|
||||
@@ -115,8 +115,52 @@ class TournamentResource:
|
||||
"rated" -> t.rated.toString,
|
||||
),
|
||||
)
|
||||
if !externalClient.publishNative(nativeServerUrl, directorName, form) then
|
||||
log.warnf("Failed to publish tournament %s to native server %s", t.id, nativeServerUrl)
|
||||
externalClient.publishNative(nativeServerUrl, directorName, form) match
|
||||
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] =
|
||||
if nativeServerUrl.isEmpty then None
|
||||
else
|
||||
Option(t.nativeTournamentId).filter(_.nonEmpty).orElse {
|
||||
val found = externalClient
|
||||
.fetchList(nativeServerUrl)
|
||||
.flatMap { node =>
|
||||
Seq("created", "started", "finished").iterator
|
||||
.flatMap(k => node.path(k).elements().asScala)
|
||||
.find(_.path("fullName").asText() == t.fullName)
|
||||
.map(_.path("id").asText())
|
||||
.filter(_.nonEmpty)
|
||||
}
|
||||
found.foreach(id => tournamentService.setNativeTournamentId(t.id, id))
|
||||
found
|
||||
}
|
||||
|
||||
// Overlay live participant/standings/status fields from the native twin onto a local DTO so
|
||||
// bots that joined directly on the native server are reflected in NowChess.
|
||||
private def nativeOverlay(t: de.nowchess.tournament.domain.Tournament): JsonNode =
|
||||
val standings = tournamentService.getStandings(t.id)
|
||||
val dto = objectMapper.valueToTree[JsonNode](tournamentService.toDto(t, standings))
|
||||
nativeIdFor(t).flatMap(nid => externalClient.fetch(nativeServerUrl, nid)) match
|
||||
case Some(native) =>
|
||||
val merged = dto.deepCopy[com.fasterxml.jackson.databind.node.ObjectNode]()
|
||||
Seq("nbPlayers", "standing", "status", "round", "winner").foreach { field =>
|
||||
if native.has(field) then merged.set(field, native.get(field))
|
||||
}
|
||||
merged
|
||||
case None => dto
|
||||
|
||||
private def encodeForm(params: Map[String, String]): String =
|
||||
params
|
||||
@@ -132,8 +176,7 @@ class TournamentResource:
|
||||
def get(@PathParam("id") id: String): Response =
|
||||
tournamentService.get(id) match
|
||||
case Some(t) =>
|
||||
val standings = tournamentService.getStandings(id)
|
||||
Response.ok(tournamentService.toDto(t, standings)).build()
|
||||
Response.ok(nativeOverlay(t)).build()
|
||||
case None =>
|
||||
resolveServer(id)
|
||||
.flatMap(url => externalClient.fetch(url, id).map(node => Response.ok(node).build()))
|
||||
@@ -179,19 +222,26 @@ class TournamentResource:
|
||||
val (status, body) = externalClient.proxyPost(originUrl, s"api/tournament/$id/start", auth)
|
||||
Response.status(status).entity(body).build()
|
||||
case None =>
|
||||
tournamentService.start(id, userId) match
|
||||
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
|
||||
case Left(error) =>
|
||||
error match
|
||||
case TournamentError.NotFound(_) =>
|
||||
val auth = Option(headers.getHeaderString("Authorization"))
|
||||
resolveServer(id)
|
||||
.map { url =>
|
||||
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
|
||||
Response.status(status).entity(body).build()
|
||||
}
|
||||
.getOrElse(errorResponse(error))
|
||||
case _ => errorResponse(error)
|
||||
tournamentService.get(id).flatMap(nativeIdFor) match
|
||||
case Some(nativeId) =>
|
||||
val auth = Option(headers.getHeaderString("Authorization"))
|
||||
val (status, body) = externalClient.proxyPost(nativeServerUrl, s"api/tournament/$nativeId/start", auth)
|
||||
if status / 100 == 2 then tournamentService.markStatus(id, "started")
|
||||
Response.status(status).entity(body).build()
|
||||
case None =>
|
||||
tournamentService.start(id, userId) match
|
||||
case Right(t) => Response.ok(tournamentService.toDto(t)).build()
|
||||
case Left(error) =>
|
||||
error match
|
||||
case TournamentError.NotFound(_) =>
|
||||
val auth = Option(headers.getHeaderString("Authorization"))
|
||||
resolveServer(id)
|
||||
.map { url =>
|
||||
val (status, body) = externalClient.proxyPost(url, s"api/tournament/$id/start", auth)
|
||||
Response.status(status).entity(body).build()
|
||||
}
|
||||
.getOrElse(errorResponse(error))
|
||||
case _ => errorResponse(error)
|
||||
|
||||
@POST
|
||||
@Path("/{id}/join")
|
||||
@@ -210,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(_) =>
|
||||
@@ -317,7 +374,8 @@ class TournamentResource:
|
||||
tournamentService.get(id) match
|
||||
case Some(t) if Option(t.originServerUrl).isDefined =>
|
||||
val auth = Option(headers.getHeaderString("Authorization"))
|
||||
externalClient.proxyGetStream(t.originServerUrl, s"api/tournament/$id/stream", auth)
|
||||
externalClient
|
||||
.proxyGetStream(t.originServerUrl, s"api/tournament/$id/stream", auth)
|
||||
.map { inputStream =>
|
||||
Response
|
||||
.ok(new StreamingOutput {
|
||||
@@ -334,10 +392,12 @@ class TournamentResource:
|
||||
.`type`("application/x-ndjson")
|
||||
.build()
|
||||
}
|
||||
.getOrElse(Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id stream unavailable")).build())
|
||||
.getOrElse(
|
||||
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(s"Tournament $id stream unavailable")).build(),
|
||||
)
|
||||
case Some(_) =>
|
||||
val botId = Option(jwt.getSubject).getOrElse("")
|
||||
val queue = new java.util.concurrent.LinkedBlockingQueue[Option[String]]()
|
||||
val queue = new java.util.concurrent.LinkedBlockingQueue[Option[String]]()
|
||||
val emitter = new io.smallrye.mutiny.subscription.MultiEmitter[String] {
|
||||
def emit(item: String): io.smallrye.mutiny.subscription.MultiEmitter[String] =
|
||||
queue.put(Some(item)); this
|
||||
@@ -358,7 +418,7 @@ class TournamentResource:
|
||||
var cont = true
|
||||
while cont do
|
||||
queue.take() match
|
||||
case None => cont = false
|
||||
case None => cont = false
|
||||
case Some(line) =>
|
||||
output.write((line + "\n").getBytes("UTF-8"))
|
||||
output.flush()
|
||||
@@ -440,7 +500,8 @@ class TournamentResource:
|
||||
.getOrElse(Response.status(Response.Status.NOT_FOUND).build())
|
||||
|
||||
private def resolveServer(tournamentId: String): Option[String] =
|
||||
tournamentService.get(tournamentId)
|
||||
tournamentService
|
||||
.get(tournamentId)
|
||||
.flatMap(t => Option(t.originServerUrl))
|
||||
.orElse(registry.findServerUrl(tournamentId))
|
||||
.orElse {
|
||||
|
||||
+37
-13
@@ -14,7 +14,7 @@ import scala.util.Try
|
||||
class ExternalTournamentClient:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@volatile private var directorToken: Option[String] = None
|
||||
|
||||
@ConfigProperty(name = "nowchess.tournament.native-server-url", defaultValue = "http://141.37.123.132:8086")
|
||||
@@ -30,7 +30,7 @@ class ExternalTournamentClient:
|
||||
// RS256 user token to it — swap in the director token registered on that server.
|
||||
private def normalize(url: String): String = url.stripSuffix("/")
|
||||
|
||||
private def isNativeServer(serverUrl: String): Boolean =
|
||||
def isNativeServer(serverUrl: String): Boolean =
|
||||
nativeServerUrl.nonEmpty && normalize(serverUrl) == normalize(nativeServerUrl)
|
||||
|
||||
private def directorBearer(): Option[String] =
|
||||
@@ -45,24 +45,27 @@ class ExternalTournamentClient:
|
||||
private def authFor(serverUrl: String, userAuth: Option[String]): Option[String] =
|
||||
if isNativeServer(serverUrl) then directorBearer() else userAuth
|
||||
|
||||
def publishNative(serverUrl: String, directorName: String, form: String): Boolean =
|
||||
def publishNative(serverUrl: String, directorName: String, form: String): Option[String] =
|
||||
val token = directorToken.orElse {
|
||||
val fresh = registerDirector(serverUrl, directorName)
|
||||
directorToken = fresh
|
||||
fresh
|
||||
}
|
||||
token.exists { tok =>
|
||||
if createNative(serverUrl, tok, form) then true
|
||||
else
|
||||
token.flatMap { tok =>
|
||||
createNative(serverUrl, tok, form).orElse {
|
||||
val refreshed = registerDirector(serverUrl, directorName)
|
||||
directorToken = refreshed
|
||||
refreshed.exists(createNative(serverUrl, _, form))
|
||||
refreshed.flatMap(createNative(serverUrl, _, form))
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +79,25 @@ class ExternalTournamentClient:
|
||||
client.close()
|
||||
}.getOrElse(None)
|
||||
|
||||
private def createNative(serverUrl: String, token: String, form: String): Boolean =
|
||||
// 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()
|
||||
val response = client
|
||||
@@ -84,11 +105,14 @@ class ExternalTournamentClient:
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
.header("Authorization", s"Bearer $token")
|
||||
.post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED))
|
||||
try response.getStatus / 100 == 2
|
||||
try
|
||||
if response.getStatus / 100 == 2 then
|
||||
Option(objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText()).filter(_.nonEmpty)
|
||||
else None
|
||||
finally
|
||||
response.close()
|
||||
client.close()
|
||||
}.getOrElse(false)
|
||||
}.getOrElse(None)
|
||||
|
||||
def fetchList(serverUrl: String): Option[JsonNode] =
|
||||
Try {
|
||||
@@ -141,8 +165,8 @@ class ExternalTournamentClient:
|
||||
|
||||
def replicateTournament(serverUrl: String, req: ReplicateTournamentRequest, selfUrl: String): Boolean =
|
||||
Try {
|
||||
val client = buildClient()
|
||||
val body = objectMapper.writeValueAsString(req)
|
||||
val client = buildClient()
|
||||
val body = objectMapper.writeValueAsString(req)
|
||||
val response = client
|
||||
.target(s"$serverUrl/api/tournament/replicate")
|
||||
.request(MediaType.APPLICATION_JSON)
|
||||
|
||||
+8
@@ -82,6 +82,14 @@ class TournamentService:
|
||||
def get(id: String): Option[Tournament] =
|
||||
tournamentRepository.findOptById(id)
|
||||
|
||||
@Transactional
|
||||
def setNativeTournamentId(id: String, nativeId: String): Unit =
|
||||
tournamentRepository.findOptById(id).foreach(_.nativeTournamentId = nativeId)
|
||||
|
||||
@Transactional
|
||||
def markStatus(id: String, status: String): Unit =
|
||||
tournamentRepository.findOptById(id).foreach(_.status = status)
|
||||
|
||||
def list(): (List[Tournament], List[Tournament], List[Tournament]) =
|
||||
(
|
||||
tournamentRepository.findByStatus("created"),
|
||||
|
||||
@@ -30,3 +30,7 @@ nowchess:
|
||||
secret: test-secret
|
||||
auth:
|
||||
enabled: false
|
||||
tournament:
|
||||
self-url: ""
|
||||
external-servers: ""
|
||||
native-server-url: "http://localhost:1"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=7
|
||||
MINOR=9
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user