Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 f074c7ac73 Optimize GetGameHistory to not load entries when only count is needed
When the admin panel requests game history with limit=0, it only needs
the total count for pagination. Previously this would load all entries
and serialize each one to JSON, causing timeouts on games with many
actions.

Now when limit <= 0, we return just the count with empty entries,
making the count request fast regardless of history size.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:42:35 -08:00
adminandClaude Opus 4.5 198e6ed3c0 Fix game ID parsing in download handler
The download handler was using ParseInt which fails for game IDs that
have the high bit set (appear as negative when formatted as signed).
The gameID is already parsed and validated in handleGameRoutes using
ParseUint, so just pass it through instead of re-parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:39:14 -08:00
2 changed files with 64 additions and 62 deletions
@@ -440,7 +440,7 @@ func handleGameRoutes(w http.ResponseWriter, r *http.Request) {
handleRewind(w, r, gameIDHex, gameID)
} else if parts[1] == "download" {
// /games/{id}/download - download game save as zip
handleGameDownload(w, r, gameIDHex)
handleGameDownload(w, r, gameIDHex, gameID)
} else if parts[1] == "state" && len(parts) >= 3 {
// /games/{id}/state/{index} - game state at action (htmx partial)
actionIndex, err := strconv.Atoi(parts[2])
@@ -680,20 +680,12 @@ func handleRewind(w http.ResponseWriter, r *http.Request, gameIDHex string, game
}
}
func handleGameDownload(w http.ResponseWriter, r *http.Request, gameIDHex string) {
func handleGameDownload(w http.ResponseWriter, r *http.Request, gameIDHex string, gameID int64) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse game ID from hex string
gameID, err := strconv.ParseInt(gameIDHex, 16, 64)
if err != nil {
log.Printf("Invalid game ID format: %s", gameIDHex)
http.Error(w, "Invalid game ID format", http.StatusBadRequest)
return
}
// Call gRPC streaming endpoint to download the zip
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
@@ -660,60 +660,70 @@ class EagleServiceImpl(
val totalCount = history.count
val startIndex = if request.startIndex > 0 then request.startIndex else 0
val limit = if request.limit > 0 then request.limit else totalCount
val limit = request.limit
implicit val formats: DefaultFormats.type = DefaultFormats
val monthNames = Vector(
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
)
// Use since(startIndex) for efficient partial loading instead of loading all results
val entries = history
.since(startIndex)
.take(limit)
.zipWithIndex
.map {
case (actionWithState, localIndex) =>
val index = startIndex + localIndex
val ar = actionWithState.actionResult
val gs = actionWithState.gameState
val actionJson = writePretty(JsonFormat.toJson(ar))
val factionName = ar.player
.flatMap(fid => gs.factions.get(fid))
.map(_.name)
.getOrElse("")
val gameDate = gs.currentDate
.map(d => s"${monthNames(d.month)} ${d.year}")
.getOrElse("")
GameHistoryEntry(
index = index,
actionType = ar.`type`.name,
roundId = gs.currentRoundId,
actingFactionId = ar.player,
provinceId = ar.province,
summary = s"${ar.`type`.name} action",
actionJson = actionJson,
factionName = factionName,
gameDate = gameDate
)
}
// If limit is 0, return just the count without loading any entries
if limit <= 0 then
GetGameHistoryResponse(
gameId = request.gameId,
totalActionCount = totalCount,
entries = Seq.empty
)
else {
implicit val formats: DefaultFormats.type = DefaultFormats
val monthNames = Vector(
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
)
// Use since(startIndex) for efficient partial loading instead of loading all results
val entries = history
.since(startIndex)
.take(limit)
.zipWithIndex
.map {
case (actionWithState, localIndex) =>
val index = startIndex + localIndex
val ar = actionWithState.actionResult
val gs = actionWithState.gameState
val actionJson = writePretty(JsonFormat.toJson(ar))
val factionName = ar.player
.flatMap(fid => gs.factions.get(fid))
.map(_.name)
.getOrElse("")
val gameDate = gs.currentDate
.map(d => s"${monthNames(d.month)} ${d.year}")
.getOrElse("")
GameHistoryEntry(
index = index,
actionType = ar.`type`.name,
roundId = gs.currentRoundId,
actingFactionId = ar.player,
provinceId = ar.province,
summary = s"${ar.`type`.name} action",
actionJson = actionJson,
factionName = factionName,
gameDate = gameDate
)
}
GetGameHistoryResponse(
gameId = request.gameId,
totalActionCount = totalCount,
entries = entries
)
GetGameHistoryResponse(
gameId = request.gameId,
totalActionCount = totalCount,
entries = entries
)
}
end if
case None =>
GetGameHistoryResponse(