From 0a51c61f6624e1b6f290ecec77d5302034d5e80a Mon Sep 17 00:00:00 2001
From: Alexa <53471801+Alexa-RR@users.noreply.github.com>
Date: Tue, 28 Jul 2026 17:21:49 +0300
Subject: [PATCH] feat(admin-ui): sub-room caps and test-case issue linking
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two admin screens for work that previously had server support but no way to
reach it.
Rooms → a room → Sub-rooms: per-sub-room max players, with the room's own
advertised capacity shown alongside so the two are not mistaken for each other.
Each row also shows whether the sub-room is a matchmaking target and which blob
it loads.
Activity → Test cases: file a GitHub issue against a case, follow the link, or
unlink it, plus a reconcile button. Filing is idempotent server-side, so the
button is safe to press twice. When GitHub is unconfigured the actions disable
themselves and the page explains why rather than failing on click — a server
with no token is an ordinary deployment.
Both go in as tabs on the existing hub pages rather than new sidebar entries,
matching how the nav is organised.
Server side adds GET api/admin/v1/testcases (the list the SPA renders, with
each case's issue number) and mirrors the three issue routes under
/api/admin/v1/testcases/... because the SPA's client speaks that prefix
exclusively.
I had told the user this UI was not possible on this branch because
wwwroot/admin holds a built bundle. That was wrong — the source is in
DorkNet.Server/admin-ui and builds to it.
---
.../TestCaseIssuesController.cs | 44 ++++
.../admin-ui/src/pages/Activity.tsx | 13 +-
DorkNet.Server/admin-ui/src/pages/Room.tsx | 113 +++++++++-
.../admin-ui/src/pages/TestCases.tsx | 193 ++++++++++++++++++
docs/admin.md | 13 +-
5 files changed, 369 insertions(+), 7 deletions(-)
create mode 100644 DorkNet.Server/admin-ui/src/pages/TestCases.tsx
diff --git a/DorkNet.Server/Controllers/API/TestCaseManagement/TestCaseIssuesController.cs b/DorkNet.Server/Controllers/API/TestCaseManagement/TestCaseIssuesController.cs
index f1c7ad7..78c81b2 100644
--- a/DorkNet.Server/Controllers/API/TestCaseManagement/TestCaseIssuesController.cs
+++ b/DorkNet.Server/Controllers/API/TestCaseManagement/TestCaseIssuesController.cs
@@ -36,6 +36,47 @@ public class TestCaseIssuesController(
private const int StatusFailed = 2;
private const int StatusPassed = 3;
+
+ /// GET api/admin/v1/testcases — every test case with its
+ /// issue link, newest pass first. Optional ?passId= narrows to one
+ /// test pass and ?status= to one status.
+ ///
+ /// Exists because the admin SPA speaks /api/admin/v1 exclusively and
+ /// the client-facing test-case routes are shaped for the in-game QA tab
+ /// (per-pass, no issue fields).
+ [HttpGet("api/admin/v1/testcases")]
+ public async Task ListCases(
+ [FromQuery] uint? passId, [FromQuery] int? status, CancellationToken ct)
+ {
+ var query = db.TestCases.AsQueryable();
+ if (passId is uint pass) query = query.Where(c => c.TestPassId == pass);
+ if (status is int s) query = query.Where(c => c.Status == s);
+
+ var cases = await query
+ .OrderByDescending(c => c.TestPassId)
+ .ThenBy(c => c.Key)
+ .Take(500)
+ .ToListAsync(ct);
+
+ return Ok(new
+ {
+ githubConfigured = github.IsConfigured,
+ repository = github.Repository,
+ cases = cases.Select(c => new
+ {
+ c.Id,
+ c.Key,
+ c.Title,
+ c.RoomName,
+ c.Status,
+ c.TestPassId,
+ issueUrl = c.JiraBugUrl,
+ issueNumber = GitHubIssueService.IssueNumberFromUrl(c.JiraBugUrl),
+ c.UpdatedAt,
+ }).ToList(),
+ });
+ }
+
/// POST api/testcasemanagement/v1/testcase/{id}/issue —
/// file a GitHub issue for a test case and link it.
///
@@ -44,6 +85,7 @@ public class TestCaseIssuesController(
/// pass is therefore safe, which is the whole point — the alternative is a
/// tester filing the same bug on every run.
[HttpPost("api/testcasemanagement/v1/testcase/{id}/issue")]
+ [HttpPost("api/admin/v1/testcases/{id}/issue")]
public async Task CreateIssue(string id, CancellationToken ct)
{
if (!github.IsConfigured)
@@ -81,6 +123,7 @@ public class TestCaseIssuesController(
/// unlink, leaving the issue itself alone. Closing someone's issue as a
/// side effect of tidying a QA link would be the wrong call.
[HttpDelete("api/testcasemanagement/v1/testcase/{id}/issue")]
+ [HttpDelete("api/admin/v1/testcases/{id}/issue")]
public async Task UnlinkIssue(string id, CancellationToken ct)
{
var tc = await db.TestCases.FirstOrDefaultAsync(c => c.Id == id, ct);
@@ -112,6 +155,7 @@ public class TestCaseIssuesController(
/// change, so a case a tester has Claimed, or one nobody has run yet, is
/// never rewritten underneath them.
[HttpPost("api/testcasemanagement/v1/issues/sync")]
+ [HttpPost("api/admin/v1/testcases/issues/sync")]
public async Task Sync(CancellationToken ct)
{
if (!github.IsConfigured)
diff --git a/DorkNet.Server/admin-ui/src/pages/Activity.tsx b/DorkNet.Server/admin-ui/src/pages/Activity.tsx
index 9063dc9..d67aff8 100644
--- a/DorkNet.Server/admin-ui/src/pages/Activity.tsx
+++ b/DorkNet.Server/admin-ui/src/pages/Activity.tsx
@@ -1,25 +1,30 @@
import { Audit } from './Audit';
import { PlayerLogs } from './PlayerLogs';
+import { TestCases } from './TestCases';
import { PageHeader } from '../components/PageHeader';
import { Tabs, useTabParam } from '../components/Tabs';
-// Both log views in one place: the admin audit trail and the per-player
-// HTTP request ring buffer. Old /audit and /logs routes redirect here.
+// Internal operations views in one place: the admin audit trail, the
+// per-player HTTP request ring buffer, and QA test cases with the GitHub
+// issues filed against them. Old /audit and /logs routes redirect here.
export function Activity() {
- const [tab, setTab] = useTabParam<'audit' | 'logs'>('audit', ['audit', 'logs']);
+ const [tab, setTab] = useTabParam<'audit' | 'logs' | 'testcases'>(
+ 'audit', ['audit', 'logs', 'testcases']);
return (