Add the games, runtime, and engine-version pages over the existing lobby,
runtime, and engine-version services (no new business logic).
- GET/POST /_gm/games list + create public game
- GET /_gm/games/{id} detail incl. runtime snapshot
- POST /_gm/games/{id}/force-start|stop game state actions
- POST /_gm/games/{id}/ban-member ban a member (uuid + reason)
- POST /_gm/games/{id}/runtime/restart|patch|force-next-turn
- GET/POST /_gm/engine-versions registry + register
- POST /_gm/engine-versions/{ver}/disable disable a version
Console depends on GameAdmin / RuntimeAdmin / EngineVersionAdmin interfaces
(satisfied by the concrete services) so the pages render in tests without a
database. Collection-mutating POSTs are mounted on the collection path to avoid
a static-vs-param route conflict in gin. Writes flow through the CSRF guard and
redirect back; the create form parses datetime-local as UTC.
Tests: list/detail (with and without a runtime), create (visibility/owner/time
assertions), force-start (+ bad-CSRF), ban-member (+ bad uuid), runtime patch
(+ missing version), engine-version list/register/disable, and unavailable.
Docs: backend/docs/admin-console.md page inventory extended.
This commit is contained in:
@@ -98,3 +98,5 @@ button {
|
||||
button:hover { filter: brightness(1.1); }
|
||||
button.danger { background: var(--danger); color: #1a0606; }
|
||||
code { background: var(--bg); padding: 0.05rem 0.3rem; border-radius: 4px; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0.8rem 0; }
|
||||
.actions form { margin: 0; }
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package adminconsole
|
||||
|
||||
// GameRow is one line in the games list table.
|
||||
type GameRow struct {
|
||||
GameID string
|
||||
GameName string
|
||||
Visibility string
|
||||
Status string
|
||||
Owner string
|
||||
Players string
|
||||
TurnSchedule string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
// GamesListData is the view model for the paginated games list.
|
||||
type GamesListData struct {
|
||||
Items []GameRow
|
||||
Page int
|
||||
PageSize int
|
||||
Total int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
PrevPage int
|
||||
NextPage int
|
||||
}
|
||||
|
||||
// GameDetailData is the view model for a single game, combining the lobby
|
||||
// record with the runtime snapshot and the available actions.
|
||||
type GameDetailData struct {
|
||||
GameID string
|
||||
GameName string
|
||||
Description string
|
||||
Visibility string
|
||||
Status string
|
||||
Owner string
|
||||
MinPlayers int32
|
||||
MaxPlayers int32
|
||||
StartGapHours int32
|
||||
StartGapPlayers int32
|
||||
TurnSchedule string
|
||||
TargetEngineVersion string
|
||||
EnrollmentEndsAt string
|
||||
CreatedAt string
|
||||
StartedAt string
|
||||
FinishedAt string
|
||||
|
||||
HasRuntime bool
|
||||
RuntimeStatus string
|
||||
CurrentEngineVersion string
|
||||
EngineHealth string
|
||||
CurrentTurn int32
|
||||
NextGenerationAt string
|
||||
Paused bool
|
||||
}
|
||||
|
||||
// EngineVersionRow is one line in the engine-version registry table.
|
||||
type EngineVersionRow struct {
|
||||
Version string
|
||||
ImageRef string
|
||||
Enabled bool
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
// EngineVersionsData is the view model for the engine-version registry page.
|
||||
type EngineVersionsData struct {
|
||||
Items []EngineVersionRow
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{define "content" -}}
|
||||
{{$csrf := .CSRFToken}}
|
||||
<h1>Engine versions</h1>
|
||||
{{with .Data}}
|
||||
<table class="list">
|
||||
<thead><tr><th>Version</th><th>Image</th><th>Enabled</th><th>Created</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td>{{.Version}}</td>
|
||||
<td><code>{{.ImageRef}}</code></td>
|
||||
<td>{{if .Enabled}}<span class="ok">yes</span>{{else}}<span class="bad">no</span>{{end}}</td>
|
||||
<td>{{.CreatedAt}}</td>
|
||||
<td>{{if .Enabled}}<form method="post" action="/_gm/engine-versions/{{.Version}}/disable" onsubmit="return confirm('Disable {{.Version}}?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit" class="danger">Disable</button></form>{{end}}</td>
|
||||
</tr>
|
||||
{{else}}<tr><td colspan="5"><span class="note">no engine versions</span></td></tr>{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
<section class="panel">
|
||||
<h2>Register version</h2>
|
||||
<form method="post" action="/_gm/engine-versions" class="form">
|
||||
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
||||
<label>Version <input type="text" name="version" placeholder="semver e.g. 0.1.0" required></label>
|
||||
<label>Image ref <input type="text" name="image_ref" required></label>
|
||||
<label>Enabled <input type="checkbox" name="enabled" value="true" checked></label>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
</section>
|
||||
{{- end}}
|
||||
@@ -0,0 +1,65 @@
|
||||
{{define "content" -}}
|
||||
{{$csrf := .CSRFToken}}
|
||||
{{with .Data}}
|
||||
<p><a href="/_gm/games">« all games</a></p>
|
||||
<h1>{{if .GameName}}{{.GameName}}{{else}}(unnamed){{end}}</h1>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Game</h2>
|
||||
<ul class="kv">
|
||||
<li>Game ID: <code>{{.GameID}}</code></li>
|
||||
<li>Visibility: {{.Visibility}}</li>
|
||||
<li>Status: {{.Status}}</li>
|
||||
<li>Owner: {{.Owner}}</li>
|
||||
<li>Players: {{.MinPlayers}}–{{.MaxPlayers}}</li>
|
||||
<li>Start gap: {{.StartGapHours}}h / {{.StartGapPlayers}} players</li>
|
||||
<li>Turn schedule: {{.TurnSchedule}}</li>
|
||||
<li>Target engine: {{.TargetEngineVersion}}</li>
|
||||
<li>Enrollment ends: {{.EnrollmentEndsAt}}</li>
|
||||
<li>Created: {{.CreatedAt}}</li>
|
||||
<li>Started: {{if .StartedAt}}{{.StartedAt}}{{else}}—{{end}}</li>
|
||||
<li>Finished: {{if .FinishedAt}}{{.FinishedAt}}{{else}}—{{end}}</li>
|
||||
</ul>
|
||||
{{if .Description}}<p>{{.Description}}</p>{{end}}
|
||||
<div class="actions">
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/force-start" onsubmit="return confirm('Force-start this game?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit">Force start</button></form>
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/force-stop" onsubmit="return confirm('Force-stop this game?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit" class="danger">Force stop</button></form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Runtime</h2>
|
||||
{{if .HasRuntime}}
|
||||
<ul class="kv">
|
||||
<li>Status: {{.RuntimeStatus}}</li>
|
||||
<li>Engine version: {{.CurrentEngineVersion}}</li>
|
||||
<li>Engine health: {{.EngineHealth}}</li>
|
||||
<li>Current turn: {{.CurrentTurn}}</li>
|
||||
<li>Next generation: {{if .NextGenerationAt}}{{.NextGenerationAt}}{{else}}—{{end}}</li>
|
||||
<li>Paused: {{if .Paused}}yes{{else}}no{{end}}</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/runtime/restart" onsubmit="return confirm('Restart the engine container?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit">Restart</button></form>
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/runtime/force-next-turn" onsubmit="return confirm('Force the next turn now?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit">Force next turn</button></form>
|
||||
</div>
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/runtime/patch" class="form">
|
||||
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
||||
<label>Patch to version <input type="text" name="target_version" placeholder="e.g. 0.1.1" required></label>
|
||||
<button type="submit">Patch</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<p class="note">No runtime record for this game yet.</p>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Ban member</h2>
|
||||
<form method="post" action="/_gm/games/{{.GameID}}/ban-member" class="form" onsubmit="return confirm('Ban this member from the game?');">
|
||||
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
||||
<label>User ID <input type="text" name="user_id" required></label>
|
||||
<label>Reason <input type="text" name="reason"></label>
|
||||
<button type="submit" class="danger">Ban member</button>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{define "content" -}}
|
||||
{{$csrf := .CSRFToken}}
|
||||
<h1>Games</h1>
|
||||
{{with .Data}}
|
||||
<table class="list">
|
||||
<thead><tr><th>Name</th><th>Visibility</th><th>Status</th><th>Owner</th><th>Players</th><th>Schedule</th><th>Created</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td><a href="/_gm/games/{{.GameID}}">{{if .GameName}}{{.GameName}}{{else}}(unnamed){{end}}</a></td>
|
||||
<td>{{.Visibility}}</td>
|
||||
<td>{{.Status}}</td>
|
||||
<td>{{.Owner}}</td>
|
||||
<td>{{.Players}}</td>
|
||||
<td>{{.TurnSchedule}}</td>
|
||||
<td>{{.CreatedAt}}</td>
|
||||
</tr>
|
||||
{{else}}<tr><td colspan="7"><span class="note">no games</span></td></tr>{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<nav class="pager">
|
||||
{{if .HasPrev}}<a href="/_gm/games?page={{.PrevPage}}&page_size={{.PageSize}}">« prev</a>{{end}}
|
||||
<span>page {{.Page}} · {{.Total}} total</span>
|
||||
{{if .HasNext}}<a href="/_gm/games?page={{.NextPage}}&page_size={{.PageSize}}">next »</a>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
<section class="panel">
|
||||
<h2>Create public game</h2>
|
||||
<form method="post" action="/_gm/games" class="form">
|
||||
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
||||
<label>Name <input type="text" name="game_name" required></label>
|
||||
<label>Description <input type="text" name="description"></label>
|
||||
<label>Min players <input type="number" name="min_players" value="2" min="1"></label>
|
||||
<label>Max players <input type="number" name="max_players" value="8" min="1"></label>
|
||||
<label>Start gap hours <input type="number" name="start_gap_hours" value="0" min="0"></label>
|
||||
<label>Start gap players <input type="number" name="start_gap_players" value="0" min="0"></label>
|
||||
<label>Enrollment ends <input type="datetime-local" name="enrollment_ends_at" required></label>
|
||||
<label>Turn schedule <input type="text" name="turn_schedule" placeholder="e.g. @every 24h" required></label>
|
||||
<label>Engine version <input type="text" name="target_engine_version" placeholder="e.g. 0.1.0" required></label>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</section>
|
||||
{{- end}}
|
||||
Reference in New Issue
Block a user