diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt index e5dadda72..a52b1fd23 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListBotGamesResponse.kt @@ -23,6 +23,7 @@ data class ListBotGamesResponse( val status: GameEventType, val outcome: Outcome?, val index: Int, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, val variant: Variant?, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt index b177144c9..3ccf20d1b 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/admin/ListGamesResponse.kt @@ -20,6 +20,7 @@ data class ListGamesResponse(val entries: List) { val timeControlIncrement: Int?, val status: GameEventType, val index: Int, + val isPreAnalyzed: Boolean, val winnerUserId: String?, val created: Long, val lastUpdated: Long, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt index 8917c489f..ef725a4c8 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/botgame/ListUserBotGamesDto.kt @@ -18,6 +18,7 @@ data class ListUserBotGamesDto(val entries: List) { val status: GameEventType, val outcome: Outcome?, val moveIndex: Int, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, val variant: Variant, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt index b9db02a20..1895298c2 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/game/ListUserGamesResponse.kt @@ -26,6 +26,7 @@ data class ListUserGamesResponse(val entries: List) { val outcome: UserOutcome?, val ratingFrom: Int?, val ratingTo: Int?, + val isPreAnalyzed: Boolean, val created: Long, val lastUpdated: Long, val numberOfMessages: Int, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt index 3c26060e9..efb481986 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsBotGameService.kt @@ -15,6 +15,7 @@ import io.elephantchess.db.utils.nextMove import io.elephantchess.db.utils.plusSeconds import io.elephantchess.engines.EnginePool import io.elephantchess.engines.protocol.model.InfoLinesResult +import io.elephantchess.model.AnalysisStatus import io.elephantchess.model.BotGameMoveType import io.elephantchess.model.Engine import io.elephantchess.model.GameEventType.* @@ -231,6 +232,7 @@ class PlayerVsBotGameService( status = record.gameStatus, outcome = record.outcome, moveIndex = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), variant = record.variant ?: Variant.XIANGQI, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt index aae05f6ef..126fc63c6 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameService.kt @@ -523,6 +523,7 @@ class PlayerVsPlayerGameService( outcome = gameRecord.userOutcome(userId), ratingFrom = ratingFrom, ratingTo = ratingTo, + isPreAnalyzed = gameRecord.analysisStatus == AnalysisStatus.COMPLETED, created = gameRecord.created.toEpochMilliseconds(), lastUpdated = gameRecord.lastUpdated.toEpochMilliseconds(), numberOfMessages = numberOfMessages, diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt index bd353f6cb..3aee025c8 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/services/admin/AdminFeedService.kt @@ -9,6 +9,7 @@ import io.elephantchess.db.services.PlayerVsBotGameDaoService import io.elephantchess.db.services.PlayerVsPlayerGameDaoService import io.elephantchess.db.services.PuzzleResultDaoService import io.elephantchess.db.utils.winnerUserId +import io.elephantchess.model.AnalysisStatus import io.elephantchess.model.GameEventType.AUTO_CANCELED import io.elephantchess.model.UserType import io.elephantchess.servicelayer.dto.admin.ListBotGamesResponse @@ -91,6 +92,7 @@ class AdminFeedService( timeControlIncrement = record.timeControlIncrement, status = record.gameStatus, index = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, winnerUserId = record.winnerUserId(), created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), @@ -125,6 +127,7 @@ class AdminFeedService( status = record.gameStatus, outcome = record.outcome, index = record.currentHalfMoveIndex, + isPreAnalyzed = record.analysisStatus == AnalysisStatus.COMPLETED, created = record.created.toEpochMilliseconds(), lastUpdated = record.lastUpdated.toEpochMilliseconds(), variant = record.variant, diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt index f31dde48b..55aa572b5 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/PlayerVsPlayerGameServiceTest.kt @@ -3,6 +3,8 @@ package io.elephantchess.servicelayer.services import io.elephantchess.db.dao.codegen.Tables.* import io.elephantchess.db.utils.awaitExecute import io.elephantchess.db.utils.awaitSingleValue +import io.elephantchess.model.AnalysisStatus.COMPLETED +import io.elephantchess.model.AnalysisStatus.PARTIALLY_COMPLETED import io.elephantchess.model.GameEventType import io.elephantchess.model.GameEventType.* import io.elephantchess.model.Outcome @@ -659,6 +661,50 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { assertEquals(PERPETUAL_CHECKING, response.gameEventType) } + @Test + fun listUserGamesPreAnalyzedStatusTest() = runTest { + val gameId = createAndJoinGame(userId1, userId2, inviterColor = RED) + + val beforeUpdateEntry = + pvpGameService + .listUserGames(userId1.id, beforeTs = null) + .entries + .find { it.gameId == gameId } + ?: throw AssertionError("Expected game $gameId in list") + + assertFalse(beforeUpdateEntry.isPreAnalyzed) + + dslContext + .update(GAME) + .set(GAME.ANALYSIS_STATUS, PARTIALLY_COMPLETED) + .where(GAME.ID.eq(gameId)) + .awaitExecute() + + val afterPartialUpdateEntry = + pvpGameService + .listUserGames(userId1.id, beforeTs = null) + .entries + .find { it.gameId == gameId } + ?: throw AssertionError("Expected game $gameId in list") + + assertFalse(afterPartialUpdateEntry.isPreAnalyzed) + + dslContext + .update(GAME) + .set(GAME.ANALYSIS_STATUS, COMPLETED) + .where(GAME.ID.eq(gameId)) + .awaitExecute() + + val afterUpdateEntry = + pvpGameService + .listUserGames(userId1.id, beforeTs = null) + .entries + .find { it.gameId == gameId } + ?: throw AssertionError("Expected game $gameId in list") + + assertTrue(afterUpdateEntry.isPreAnalyzed) + } + /** * User should not be able to create more than 3 CREATED PvP games with the same settings. */ @@ -716,7 +762,6 @@ class PlayerVsPlayerGameServiceTest : ServiceTest() { // different time category (BLITZ vs RAPID): should succeed pvpGameService.createGame(userId1, request.copy(timeControlBase = 3.minutes.inWholeSeconds.toInt())) - assertEquals(4, countGameByStatus(CREATED)) } diff --git a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt index 4f1b04491..8a11573d5 100644 --- a/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt +++ b/webapp-service-layer/src/test/kotlin/io/elephantchess/servicelayer/services/ServiceTest.kt @@ -71,9 +71,10 @@ abstract class ServiceTest : PostgresTest(), KoinComponent { guestUserId: String? = null, ): Pair { val password = insecure().nextAlphanumeric(10) + val testUserIdentifier = "$i-${insecure().nextAlphanumeric(6)}" val request = SignUpRequest( - username = "test$i", - email = "test$i@gmail.com", + username = "test$testUserIdentifier", + email = "test$testUserIdentifier@gmail.com", password = password, transferGuestData = transferGuestData, ) diff --git a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js index 0b7ef8471..e10bf5a5b 100644 --- a/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js +++ b/webapp/src/main/resources/public/js/admin/admin-feeds-dto.js @@ -31,6 +31,7 @@ class GameAnalyticsDto { #timeControl; #status; #index; + #isPreAnalyzed; #winnerUserId; #created; #lastUpdated; @@ -50,6 +51,7 @@ class GameAnalyticsDto { this.#timeControl = TimeControl.fromJson(json); this.#status = json.status; this.#index = json.index; + this.#isPreAnalyzed = json.isPreAnalyzed; this.#winnerUserId = json.winnerUserId; this.#created = json.created; this.#lastUpdated = json.lastUpdated; @@ -153,6 +155,13 @@ class GameAnalyticsDto { return Number(this.#index); } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + /** * @return {string} */ @@ -246,6 +255,7 @@ class BotGameAnalyticsDto { #status; #outcome; #index; + #isPreAnalyzed; #created; #lastUpdated; #variant; @@ -262,6 +272,7 @@ class BotGameAnalyticsDto { this.#status = json.status; this.#outcome = json.outcome; this.#index = Number(json.index); + this.#isPreAnalyzed = json.isPreAnalyzed; this.#created = Number(json.created); this.#lastUpdated = Number(json.lastUpdated); this.#variant = json.variant ?? Variant.XIANGQI; @@ -351,6 +362,13 @@ class BotGameAnalyticsDto { return this.#index; } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + /** * @return {number} */ diff --git a/webapp/src/main/resources/public/js/admin/admin-feeds.js b/webapp/src/main/resources/public/js/admin/admin-feeds.js index 7a623df3d..d5c471749 100644 --- a/webapp/src/main/resources/public/js/admin/admin-feeds.js +++ b/webapp/src/main/resources/public/js/admin/admin-feeds.js @@ -106,6 +106,7 @@ class AdminFeedsPage extends BasePage { row.insertCell().innerText = entry.formattedStatus; row.insertCell().innerText = entry.index.toString(); + row.insertCell().innerText = entry.isPreAnalyzed ? '[v]' : '[ ]'; row.insertCell().innerText = entry.formattedCreated; row.insertCell().innerText = entry.formattedLastUpdated; row.insertCell().innerText = entry.formattedSourceType; @@ -182,6 +183,10 @@ class AdminFeedsPage extends BasePage { indexCell.className = 'value-cell'; indexCell.innerText = entry.index.toString(); + const preAnalyzedCell = row.insertCell(); + preAnalyzedCell.className = 'value-cell'; + preAnalyzedCell.innerText = entry.isPreAnalyzed ? '[v]' : '[ ]'; + const createdCell = row.insertCell(); createdCell.className = 'label-cell'; createdCell.innerText = entry.formattedCreated; diff --git a/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js b/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js index dc3f167a6..c04aabfcd 100644 --- a/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js +++ b/webapp/src/main/resources/public/js/userdata/my-bot-games-dto.js @@ -28,6 +28,7 @@ class BotGameEntryDto { #status; #outcome; #moveIndex; + #isPreAnalyzed; #variant; /** @@ -50,6 +51,7 @@ class BotGameEntryDto { this.#status = json.status; this.#outcome = json.outcome; this.#moveIndex = json.moveIndex; + this.#isPreAnalyzed = json.isPreAnalyzed; this.#created = json.created; this.#lastUpdated = json.lastUpdated; this.#variant = json.variant ?? Variant.XIANGQI; @@ -158,6 +160,13 @@ class BotGameEntryDto { return currentMoveIndexToFullMove(this.#moveIndex); } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + get created() { return this.#created; } diff --git a/webapp/src/main/resources/public/js/userdata/my-bot-games.js b/webapp/src/main/resources/public/js/userdata/my-bot-games.js index 55b5a728d..5692538cd 100644 --- a/webapp/src/main/resources/public/js/userdata/my-bot-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-bot-games.js @@ -90,6 +90,9 @@ class MyBotGamesPage extends InfiniteScrollPage { const customFenIndicatorPane = document.createElement('div'); customFenIndicatorPane.className = 'indicator-pane'; + const preAnalysisIndicatorPane = document.createElement('div'); + preAnalysisIndicatorPane.className = 'indicator-pane'; + const outcomeIndicatorPane = document.createElement('div'); outcomeIndicatorPane.className = 'indicator-pane'; @@ -105,6 +108,7 @@ class MyBotGamesPage extends InfiniteScrollPage { leftPane, middlePane, customFenIndicatorPane, + preAnalysisIndicatorPane, outcomeIndicatorPane, rightPane ); @@ -131,6 +135,9 @@ class MyBotGamesPage extends InfiniteScrollPage { if (entry.hasCustomStartFen) { customFenIndicatorPane.append(buildCustomFenIcon(entry)); } + if (entry.isPreAnalyzed) { + preAnalysisIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId)); + } // outcome indicator pane const outcomeDiv = buildUserOutcomeDiv(entry); diff --git a/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js new file mode 100644 index 000000000..f9614af64 --- /dev/null +++ b/webapp/src/main/resources/public/js/userdata/my-game-item-utils.js @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 Encelade SRL + * Copyright (C) 2026 elephantchess.io + * Copyright (C) 2026 BenoƮt Vleminckx (benckx) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @param gameId {string} + * @returns {HTMLDivElement} + */ +function buildPreAnalyzedIcon(gameId) { + const iconImg = buildImg('/images/icons/stats.png', 'icon'); + iconImg.alt = 'This game is pre-analyzed'; + iconImg.style.opacity = '75%'; + + const div = wrapInDiv(iconImg); + div.id = `pre-analyzed-${gameId}`; + addToolTip(div, 'This game is pre-analyzed'); + return div; +} diff --git a/webapp/src/main/resources/public/js/userdata/my-games-dto.js b/webapp/src/main/resources/public/js/userdata/my-games-dto.js index 5a8d37717..a73a16abc 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games-dto.js +++ b/webapp/src/main/resources/public/js/userdata/my-games-dto.js @@ -35,6 +35,7 @@ class GameEntryDto { #ratingFrom; #ratingTo; #numberOfMessages; + #isPreAnalyzed; #variant; /** @@ -70,6 +71,7 @@ class GameEntryDto { this.#created = json.created; this.#lastUpdated = json.lastUpdated; this.#numberOfMessages = json.numberOfMessages; + this.#isPreAnalyzed = json.isPreAnalyzed; this.#variant = json.variant ?? Variant.XIANGQI; } @@ -251,6 +253,13 @@ class GameEntryDto { return Number(this.#numberOfMessages); } + /** + * @return {boolean} + */ + get isPreAnalyzed() { + return this.#isPreAnalyzed; + } + /** * @return {string} */ diff --git a/webapp/src/main/resources/public/js/userdata/my-games.js b/webapp/src/main/resources/public/js/userdata/my-games.js index 12852762b..8cf00d802 100644 --- a/webapp/src/main/resources/public/js/userdata/my-games.js +++ b/webapp/src/main/resources/public/js/userdata/my-games.js @@ -276,6 +276,7 @@ class MyGamesPage extends InfiniteScrollPage { const chatIndicatorPane = buildDivWithClass('indicator-pane'); const outcomeIndicatorPane = buildDivWithClass('indicator-pane'); const ratingDeltaIndicatorPane = buildDivWithClass('indicator-pane'); + const preAnalyzedIndicatorPane = buildDivWithClass('indicator-pane'); const rightPane = buildDivWithClass('right-pane'); const item = buildAnchorWithClass(entry.gameUrl,null, 'my-game-item'); @@ -283,6 +284,7 @@ class MyGamesPage extends InfiniteScrollPage { variantPane, timeControlPane, middlePane, + preAnalyzedIndicatorPane, chatIndicatorPane, ratingDeltaIndicatorPane, outcomeIndicatorPane, @@ -330,6 +332,11 @@ class MyGamesPage extends InfiniteScrollPage { } ratingDeltaIndicatorPane.append(buildRatingModeDiv(entry)); + // pre-analysis indicator pane + if (entry.isPreAnalyzed) { + preAnalyzedIndicatorPane.append(buildPreAnalyzedIcon(entry.gameId)); + } + // outcome indicator pane const outcomeDiv = buildUserOutcomeDiv(entry); if (outcomeDiv != null) { diff --git a/webapp/src/main/resources/templates/admin/admin_feeds.html b/webapp/src/main/resources/templates/admin/admin_feeds.html index eeaa17e65..936939f5d 100644 --- a/webapp/src/main/resources/templates/admin/admin_feeds.html +++ b/webapp/src/main/resources/templates/admin/admin_feeds.html @@ -27,6 +27,7 @@

Last Game Updates

time status index + pre-analyzed created updated source @@ -51,6 +52,7 @@

Last Bot Game Updates

status outcome index + pre-analyzed created updated diff --git a/webapp/src/main/resources/templates/userdata/my_bot_games.html b/webapp/src/main/resources/templates/userdata/my_bot_games.html index 18e708240..cdcf22fb0 100644 --- a/webapp/src/main/resources/templates/userdata/my_bot_games.html +++ b/webapp/src/main/resources/templates/userdata/my_bot_games.html @@ -7,6 +7,7 @@ + diff --git a/webapp/src/main/resources/templates/userdata/my_games.html b/webapp/src/main/resources/templates/userdata/my_games.html index 1bc961087..668338885 100644 --- a/webapp/src/main/resources/templates/userdata/my_games.html +++ b/webapp/src/main/resources/templates/userdata/my_games.html @@ -7,6 +7,7 @@ +