diff --git a/src/Prospect.Server.Api/Controllers/AdminController.cs b/src/Prospect.Server.Api/Controllers/AdminController.cs
index 9c70312..ccd0b86 100644
--- a/src/Prospect.Server.Api/Controllers/AdminController.cs
+++ b/src/Prospect.Server.Api/Controllers/AdminController.cs
@@ -14,6 +14,8 @@ namespace Prospect.Server.Api.Controllers;
// Read-only back-office to visualise everything relevant in the database.
// Served at /admin, restricted to LAN / loopback callers (an admin panel must not be
// reachable from the public internet even though :8443 is port-forwarded).
+// List pages (players, catalog) are driven by a small self-contained client-side table
+// engine (sort / filter / search / paging) fed by the /admin/api/* JSON endpoints.
[Route("admin")]
public class AdminController : ControllerBase
{
@@ -51,58 +53,45 @@ public class AdminController : ControllerBase
var sb = new StringBuilder();
sb.Append("
");
sb.Append(Card("Joueurs", users.Count.ToString()));
- sb.Append(Card("Entités (title_player)", entities.Count.ToString()));
+ sb.Append(Card("Entités", entities.Count.ToString()));
sb.Append(Card("Docs UserData", dataCount.ToString()));
sb.Append(Card("Clés TitleData", titleKeys.Count.ToString()));
+ sb.Append(Card("Catalogue", _gameRef.All.Count.ToString()));
sb.Append("
");
-
- sb.Append("Joueurs récents
");
- sb.Append("| DisplayName | PlayFab Id | Auth | |
");
- foreach (var u in users.Take(15))
- {
- var auth = u.Auth != null ? string.Join(", ", u.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
- sb.Append($"| {Esc(u.DisplayName)} | {Esc(u.Id)} | {Esc(auth)} | " +
- $"Voir |
");
- }
- sb.Append("
");
- sb.Append("Tous les joueurs → TitleData →
");
-
+ sb.Append("Joueurs → Catalogue → TitleData →
");
return Html(Layout("Dashboard", sb.ToString()));
}
[HttpGet("players")]
- public async Task Players([FromQuery] string? q)
+ public IActionResult Players()
{
if (!IsLan()) return Denied();
+ var body =
+ "Joueurs
" +
+ "⟳ Backfill pseudos Steam
" +
+ "" +
+ "";
+ return Html(Layout("Joueurs", body));
+ }
- var users = await _userService.GetAllAsync();
- var entities = (await _entityService.GetAllAsync())
- .GroupBy(e => e.UserId)
- .ToDictionary(g => g.Key, g => g.First().Id);
-
- if (!string.IsNullOrWhiteSpace(q))
- {
- var needle = q.Trim().ToLowerInvariant();
- users = users.Where(u =>
- (u.DisplayName ?? "").ToLowerInvariant().Contains(needle) ||
- (u.Id ?? "").ToLowerInvariant().Contains(needle) ||
- (u.Auth != null && u.Auth.Any(a => (a.Key ?? "").ToLowerInvariant().Contains(needle)))).ToList();
- }
-
- var sb = new StringBuilder();
- sb.Append($"");
- sb.Append($"{users.Count} joueur(s)
");
- sb.Append("| DisplayName | PlayFab Id | Entity Id | Auth | |
");
- foreach (var u in users.OrderBy(u => u.DisplayName))
- {
- var auth = u.Auth != null ? string.Join(", ", u.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
- var ent = entities.TryGetValue(u.Id, out var eid) ? eid : "";
- sb.Append($"| {Esc(u.DisplayName)} | {Esc(u.Id)} | {Esc(ent)} | " +
- $"{Esc(auth)} | Voir |
");
- }
- sb.Append("
");
- return Html(Layout("Joueurs", sb.ToString()));
+ [HttpGet("catalog")]
+ public IActionResult Catalog()
+ {
+ if (!IsLan()) return Denied();
+ var body =
+ "Catalogue
" +
+ "Référence dataminée (TCF-Wiki) — " + _gameRef.All.Count + " entrées.
" +
+ "" +
+ "";
+ return Html(Layout("Catalogue", body));
}
[HttpGet("players/{id}")]
@@ -119,10 +108,10 @@ public class AdminController : ControllerBase
var sb = new StringBuilder();
var auth = user.Auth != null ? string.Join(", ", user.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
+ sb.Append($"← Joueurs
");
sb.Append($"{Esc(user.DisplayName)}
");
sb.Append($"PlayFabId {Esc(user.Id)} · Entity {Esc(entity?.Id ?? "-")} · {Esc(auth)}
");
- // Summary cards
sb.Append("");
if (map.TryGetValue("Balance", out var bal))
{
@@ -138,14 +127,12 @@ public class AdminController : ControllerBase
sb.Append(Card($"Fortuna S{s} XP", (xp.Value ?? "0").Trim()));
sb.Append("
");
- // Inventory table
if (map.TryGetValue("Inventory", out var inv))
{
sb.Append("Inventaire
");
sb.Append(InventoryTable(inv.Value));
}
- // All keys
sb.Append("Toutes les données
");
foreach (var d in data)
{
@@ -153,7 +140,6 @@ public class AdminController : ControllerBase
$" · {d.Value?.Length ?? 0} car · maj {d.LastUpdated:yyyy-MM-dd HH:mm}{(d.Public ? " · public" : "")}");
sb.Append($"{Esc(Pretty(d.Value))}");
}
-
return Html(Layout("Joueur · " + user.DisplayName, sb.ToString()));
}
@@ -164,6 +150,7 @@ public class AdminController : ControllerBase
var all = _titleDataService.Find(new List());
var sb = new StringBuilder();
+ sb.Append("TitleData
");
sb.Append("| Clé | Taille | |
");
foreach (var kv in all.OrderBy(k => k.Key))
sb.Append($"| {Esc(kv.Key)} | {kv.Value?.Length ?? 0} car | " +
@@ -181,11 +168,10 @@ public class AdminController : ControllerBase
if (!all.TryGetValue(key, out var value))
return Html(Layout("Clé introuvable", "Aucune clé TitleData avec ce nom.
"));
- // Large values (Blueprints, Contracts…) can be huge — pretty-print only if reasonable.
var body = value != null && value.Length > 300_000
? $"Valeur volumineuse ({value.Length} car) — affichage brut.
{Esc(value)}"
: $"{Esc(Pretty(value))}";
- return Html(Layout("TitleData · " + key, $"{Esc(key)}
" + body));
+ return Html(Layout("TitleData · " + key, $"← TitleData
{Esc(key)}
" + body));
}
[HttpGet("backfill-names")]
@@ -220,35 +206,37 @@ public class AdminController : ControllerBase
return Html(Layout("Backfill", body));
}
- [HttpGet("catalog")]
- public IActionResult Catalog([FromQuery] string? q, [FromQuery] string? cat)
+ // ---------- JSON API (consumed by the client-side table engine) ----------
+
+ [HttpGet("api/players")]
+ public async Task ApiPlayers()
{
if (!IsLan()) return Denied();
-
- var all = _gameRef.All;
- IEnumerable> items = all;
- if (!string.IsNullOrWhiteSpace(cat))
- items = items.Where(kv => kv.Value.Category == cat);
- if (!string.IsNullOrWhiteSpace(q))
+ var users = await _userService.GetAllAsync();
+ var entities = (await _entityService.GetAllAsync())
+ .GroupBy(e => e.UserId).ToDictionary(g => g.Key, g => g.First().Id);
+ var list = users.Select(u => new
{
- var n = q.Trim().ToLowerInvariant();
- items = items.Where(kv => kv.Key.ToLowerInvariant().Contains(n) || (kv.Value.Name ?? "").ToLowerInvariant().Contains(n));
- }
- var cats = all.Values.Select(v => v.Category).Distinct().OrderBy(x => x).ToList();
- var list = items.OrderBy(kv => kv.Value.Category).ThenBy(kv => kv.Value.Name).Take(1000).ToList();
+ name = string.IsNullOrEmpty(u.DisplayName) ? u.Id : u.DisplayName,
+ id = u.Id,
+ entity = entities.TryGetValue(u.Id, out var e) ? e : "",
+ auth = u.Auth != null ? string.Join(", ", u.Auth.Select(a => $"{a.Type}:{a.Key}")) : ""
+ });
+ return Json(list);
+ }
- var sb = new StringBuilder();
- sb.Append($"Référence dataminée (TCF-Wiki) — {all.Count} entrées connues.
");
- sb.Append($"");
- sb.Append($"{list.Count} résultat(s){(list.Count >= 1000 ? " (tronqué)" : "")}
");
- sb.Append("| Nom | Rareté | Catégorie | id |
");
- foreach (var kv in list)
- sb.Append($"| {Esc(kv.Value.Name)} | {Rarity(kv.Value.Rarity)} | {Esc(kv.Value.Category)} | {Esc(kv.Key)} |
");
- sb.Append("
");
- return Html(Layout("Catalogue", sb.ToString()));
+ [HttpGet("api/catalog")]
+ public IActionResult ApiCatalog()
+ {
+ if (!IsLan()) return Denied();
+ var list = _gameRef.All.Select(kv => new
+ {
+ id = kv.Key,
+ name = kv.Value.Name,
+ rarity = kv.Value.Rarity,
+ category = kv.Value.Category
+ });
+ return Json(list);
}
// ---------- helpers ----------
@@ -262,13 +250,12 @@ public class AdminController : ControllerBase
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
var b = ip.GetAddressBytes();
- if (b[0] == 10) return true; // 10.0.0.0/8
- if (b[0] == 192 && b[1] == 168) return true; // 192.168.0.0/16
- if (b[0] == 172 && b[1] >= 16 && b[1] <= 31) return true; // 172.16.0.0/12
+ if (b[0] == 10) return true;
+ if (b[0] == 192 && b[1] == 168) return true;
+ if (b[0] == 172 && b[1] >= 16 && b[1] <= 31) return true;
if (b[0] == 127) return true;
return false;
}
- // IPv6 unique-local (fc00::/7) or link-local (fe80::/10)
var s = ip.GetAddressBytes();
if ((s[0] & 0xFE) == 0xFC) return true;
if (s[0] == 0xFE && (s[1] & 0xC0) == 0x80) return true;
@@ -280,6 +267,9 @@ public class AdminController : ControllerBase
private IActionResult Html(string html) => Content(html, "text/html; charset=utf-8");
+ private IActionResult Json(object data) =>
+ Content(JsonSerializer.Serialize(data), "application/json; charset=utf-8");
+
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
private static string Pretty(string? json)
@@ -333,7 +323,6 @@ public class AdminController : ControllerBase
catch { return $"{Esc(Pretty(json))}"; }
}
- // Small coloured rarity chip.
private static string Rarity(string? r)
{
if (string.IsNullOrEmpty(r)) return "";
@@ -352,31 +341,88 @@ public class AdminController : ControllerBase
private static string Card(string label, string value) =>
$"";
- private static string Layout(string title, string body) => $@"
+ private static string Layout(string title, string body) =>
+ Head.Replace("__TITLE__", Esc(title)) + body + Foot;
+
+ // Static shell (no C# interpolation → JS braces/quotes stay literal). Single quotes only.
+ private const string Head = @"
-{Esc(title)} · The Cycle Admin
+__TITLE__ · The Cycle Admin
-{body}";
+";
+
+ private const string Foot = @"
+";
}