Add action JSON and build info to admin server (#4927)

- Add action_json, faction_name, and game_date fields to GameHistoryEntry proto
- Serialize ActionResult to JSON in EagleServiceImpl.getGameHistory()
- Display faction name and date columns in history table
- Show full JSON inline when clicking action rows (no separate AJAX call)
- Add build timestamp and git commit to admin header on all pages
- Create workspace_status.sh for Bazel build stamping
- Enable --stamp in .bazelrc

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 07:18:45 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent 906da781d5
commit ddea05514c
12 changed files with 155 additions and 37 deletions
+4
View File
@@ -36,3 +36,7 @@ common --java_language_version=17
common --java_runtime_version=remotejdk_17
common --tool_java_language_version=17
common --tool_java_runtime_version=remotejdk_17
# Workspace status for build stamping (git commit, timestamp)
common --workspace_status_command=tools/workspace_status.sh
common --stamp
@@ -21,6 +21,10 @@ go_binary(
embed = [":admin_server_lib"],
pure = "on",
visibility = ["//visibility:public"],
x_defs = {
"main.buildTime": "{BUILD_TIMESTAMP}",
"main.gitCommit": "{STABLE_GIT_COMMIT}",
},
)
# Linux x86_64 target for Docker deployment
@@ -31,4 +35,8 @@ go_binary(
goos = "linux",
pure = "on",
visibility = ["//visibility:public"],
x_defs = {
"main.buildTime": "{BUILD_TIMESTAMP}",
"main.gitCommit": "{STABLE_GIT_COMMIT}",
},
)
@@ -28,6 +28,25 @@ var templatesFS embed.FS
//go:embed static/*
var staticFS embed.FS
// Build info variables - set via ldflags
var (
buildTime = "unknown"
gitCommit = "unknown"
)
// BuildInfo holds version information displayed in the UI
type BuildInfo struct {
BuildTime string
GitCommit string
}
func getBuildInfo() BuildInfo {
return BuildInfo{
BuildTime: buildTime,
GitCommit: gitCommit,
}
}
var (
eagleAddr = flag.String("eagle-addr", "localhost:40032", "Eagle gRPC server address")
jfrSidecarAddr = flag.String("jfr-sidecar-addr", "localhost:8081", "JFR sidecar HTTP address")
@@ -291,19 +310,24 @@ func fetchGames(ctx context.Context) ([]GameInfo, error) {
// Page data structures
type GamesPageData struct {
Title string
Games []GameInfo
Title string
Games []GameInfo
BuildInfo BuildInfo
}
type GameDetailPageData struct {
Title string
Game GameInfo
Title string
Game GameInfo
BuildInfo BuildInfo
}
type HistoryEntry struct {
Index int32
ActionType string
RoundId int32
Index int32
ActionType string
RoundId int32
FactionName string
GameDate string
ActionJSON string
}
type HistoryRowsData struct {
@@ -338,8 +362,9 @@ func handleGamesPage(w http.ResponseWriter, r *http.Request) {
}
data := GamesPageData{
Title: "Games",
Games: games,
Title: "Games",
Games: games,
BuildInfo: getBuildInfo(),
}
w.Header().Set("Content-Type", "text/html")
@@ -426,8 +451,9 @@ func handleGameDetailPage(w http.ResponseWriter, r *http.Request, gameIDHex stri
}
data := GameDetailPageData{
Title: fmt.Sprintf("Game %s", gameIDHex),
Game: *game,
Title: fmt.Sprintf("Game %s", gameIDHex),
Game: *game,
BuildInfo: getBuildInfo(),
}
w.Header().Set("Content-Type", "text/html")
@@ -489,9 +515,12 @@ func handleHistoryRows(w http.ResponseWriter, r *http.Request, gameIDHex string,
entries := make([]HistoryEntry, len(resp.Entries))
for i, e := range resp.Entries {
entries[len(resp.Entries)-1-i] = HistoryEntry{
Index: e.Index,
ActionType: e.ActionType,
RoundId: e.RoundId,
Index: e.Index,
ActionType: e.ActionType,
RoundId: e.RoundId,
FactionName: e.FactionName,
GameDate: e.GameDate,
ActionJSON: e.ActionJson,
}
}
@@ -681,9 +710,10 @@ type SettingInfo struct {
}
type SettingsPageData struct {
Title string
Settings []SettingInfo
Filter string
Title string
Settings []SettingInfo
Filter string
BuildInfo BuildInfo
}
type SettingsRowsData struct {
@@ -738,9 +768,10 @@ func handleSettingsPage(w http.ResponseWriter, r *http.Request) {
}
data := SettingsPageData{
Title: "Settings",
Settings: settings,
Filter: filter,
Title: "Settings",
Settings: settings,
Filter: filter,
BuildInfo: getBuildInfo(),
}
w.Header().Set("Content-Type", "text/html")
@@ -548,3 +548,10 @@ dialog .modal-actions button {
.rewind-result:empty {
display: none;
}
/* Build info in header */
.build-info {
font-size: 0.7rem;
font-weight: normal;
color: var(--pico-muted-color);
}
@@ -1,5 +1,5 @@
<tr class="action-detail-row">
<td colspan="3">
<td colspan="6">
<div class="action-detail">
<div class="action-detail-header">
<strong>Action #{{.Index}}</strong> - {{.ActionType}} (Round {{.RoundId}})
@@ -171,10 +171,12 @@ function submitReassign() {
<table class="history-table">
<thead>
<tr>
<th style="width: 80px">#</th>
<th style="width: 60px">#</th>
<th>Action Type</th>
<th style="width: 120px">Round</th>
<th style="width: 100px">Actions</th>
<th>Faction</th>
<th style="width: 120px">Date</th>
<th style="width: 80px">Round</th>
<th style="width: 80px">Actions</th>
</tr>
</thead>
<tbody id="history-body"
@@ -182,7 +184,7 @@ function submitReassign() {
hx-trigger="load"
hx-swap="innerHTML">
<tr>
<td colspan="4" class="htmx-indicator">Loading history...</td>
<td colspan="6" class="htmx-indicator">Loading history...</td>
</tr>
</tbody>
</table>
@@ -1,11 +1,9 @@
{{range .Entries}}
<tr class="action-row"
hx-get="/games/{{$.GameID}}/action/{{.Index}}"
hx-trigger="click"
hx-target="this"
hx-swap="afterend">
<tr class="action-row" onclick="toggleActionDetail(this)">
<td>{{.Index}}</td>
<td class="action-type">{{.ActionType}}</td>
<td>{{.FactionName}}</td>
<td>{{.GameDate}}</td>
<td>{{.RoundId}}</td>
<td class="rewind-cell">
<button class="btn-rewind"
@@ -19,16 +17,35 @@
</button>
</td>
</tr>
<tr class="action-detail-row" style="display: none;">
<td colspan="6">
<div class="action-detail">
<div class="action-detail-header">
<strong>Action #{{.Index}}</strong> - {{.ActionType}} (Round {{.RoundId}})
<button class="close-btn" onclick="event.stopPropagation(); this.closest('tr').style.display='none'">&times;</button>
</div>
<pre class="action-json">{{.ActionJSON}}</pre>
</div>
</td>
</tr>
{{end}}
{{if .HasMore}}
<tr hx-get="/games/{{.GameID}}/history?start={{.NextStart}}&limit=50"
hx-trigger="revealed"
hx-swap="outerHTML">
<td colspan="4" class="htmx-indicator">Loading more...</td>
<td colspan="6" class="htmx-indicator">Loading more...</td>
</tr>
{{end}}
{{if and (not .Entries) (not .HasMore)}}
<tr>
<td colspan="4" class="empty-state">No actions recorded yet.</td>
<td colspan="6" class="empty-state">No actions recorded yet.</td>
</tr>
{{end}}
<script>
function toggleActionDetail(row) {
const detailRow = row.nextElementSibling;
if (detailRow && detailRow.classList.contains('action-detail-row')) {
detailRow.style.display = detailRow.style.display === 'none' ? '' : 'none';
}
}
</script>
@@ -11,7 +11,7 @@
<body>
<nav>
<ul>
<li class="brand">Eagle Admin</li>
<li class="brand">Eagle Admin <span class="build-info">{{if .BuildInfo.GitCommit}}({{.BuildInfo.GitCommit}}{{if .BuildInfo.BuildTime}}, {{.BuildInfo.BuildTime}}{{end}}){{end}}</span></li>
<li><a href="/">Games</a></li>
<li><a href="/settings">Settings</a></li>
<li><a href="/health">Health</a></li>
@@ -422,6 +422,9 @@ message GameHistoryEntry {
.google.protobuf.Int32Value acting_faction_id = 4;
.google.protobuf.Int32Value province_id = 5;
string summary = 6; // human-readable summary of the action
string action_json = 7; // full action result as JSON
string faction_name = 8; // name of the acting faction, if any
string game_date = 9; // in-game date (e.g. "March 453")
}
message GetActionDetailRequest {
@@ -54,6 +54,10 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update_receiver",
"@maven//:com_thesamet_scalapb_scalapb_json4s_3",
"@maven//:org_json4s_json4s_ast_3",
"@maven//:org_json4s_json4s_core_3",
"@maven//:org_json4s_json4s_native_3",
],
)
@@ -19,6 +19,9 @@ import net.eagle0.eagle.library.settings.MaxSupportedPlayers
import net.eagle0.eagle.library.EagleClientException
import net.eagle0.eagle.service.new_game_creation.GameParametersUtils
import net.eagle0.eagle.service.CustomBattleManager.CreateCustomBattleResponse
import org.json4s.*
import org.json4s.native.Serialization.writePretty
import scalapb.json4s.JsonFormat
class EagleServiceImpl(
gamesManager: GamesManager,
@@ -652,19 +655,47 @@ class EagleServiceImpl(
val startIndex = if request.startIndex > 0 then request.startIndex else 0
val limit = if request.limit > 0 then request.limit else allResults.length
val entries = allResults.zipWithIndex
implicit val formats: DefaultFormats.type = DefaultFormats
val monthNames = Vector(
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
)
val entries = allResults.zipWithIndex
.drop(startIndex)
.take(limit)
.map {
case (actionWithState, index) =>
val ar = actionWithState.actionResult
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 = actionWithState.gameState.currentRoundId,
roundId = gs.currentRoundId,
actingFactionId = ar.player,
provinceId = ar.province,
summary = s"${ar.`type`.name} action"
summary = s"${ar.`type`.name} action",
actionJson = actionJson,
factionName = factionName,
gameDate = gameDate
)
}
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Workspace status script for Bazel stamping
# Outputs key-value pairs used by x_defs in BUILD files
# Get short git commit (7 characters)
GIT_COMMIT=$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown")
echo "STABLE_GIT_COMMIT ${GIT_COMMIT}"
# Get build timestamp in ISO format
BUILD_TIME=$(date -u '+%Y-%m-%d %H:%M UTC')
echo "BUILD_TIMESTAMP ${BUILD_TIME}"