From 8b64bf08c64fbd4cfd2c705d032c72fc4a1e2a10 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 21:20:20 -0500 Subject: [PATCH] feat(dashboard): the UGC server on the Diagnostics page When the UGC server is enabled, the Diagnostics page shows it: online or unreachable, busy workers and queue, CPU against its limit, memory and the jobs' estimated memory against its budget, stored files, what it made and failed since it started, the last makes, and how many models and cars/rockets are made, waiting, failed or empty. Refreshed every 10 seconds from /api/diagnostics/ugc (health_view). Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/UgcRoutes.cpp | 24 ++++++++++++ dDashboardServer/templates/diagnostics.jinja2 | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 5d3017a2e..0e9d81c3e 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -7,6 +7,7 @@ #include #include "CDClientDatabase.h" +#include "ServerState.h" #include "Database.h" #include "UgcIconParams.h" #include "NifFile.h" @@ -433,6 +434,29 @@ namespace UgcRoutes { }, WorkerPool::ePriority::URGENT); }); + Route(eHTTPMethod::GET, "/api/diagnostics/ugc", Perm("health_view"), + "The UGC server for the Diagnostics page: {enabled, online, status (its /status: queue, workers, CPU, memory, limits, " + "throttling, recent makes) or error, counts: {model, modular} per state}", + [](HTTPReply& reply, const HTTPContext& context) { + bool enabled = false, online = false; + { + std::lock_guard lock(ServerState::g_StatusMutex); + enabled = ServerState::g_UgcEnabled; + online = ServerState::g_UgcStatus.online; + } + const auto base = InternalUrl(); + Workers::Reply(reply, context, false, [base, enabled, online](HTTPReply& out) { + nlohmann::json body = { { "success", true }, { "enabled", enabled }, { "online", online }, + { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } } }; + if (enabled) { + const auto fetched = CachedGet(base + "/status", eCache::STATUS); + if (fetched->status == 200) body["status"] = nlohmann::json::parse(fetched->body, nullptr, false); + else body["error"] = fetched->status == 0 ? fetched->error : "answered " + std::to_string(fetched->status); + } + JsonReply(out, eHTTPStatusCode::OK, body); + }, WorkerPool::ePriority::URGENT); + }); + Route(eHTTPMethod::GET, "/api/ugc/files/:kind/:id/:file", Perm("properties_view"), "A file the UGC server made (fetched from ugc_internal_url): icon.png, model.nif, model.noao.nif (before the lighting bake), " "stats.json, or previous. (the version before it was made again). 408 while it's being made", diff --git a/dDashboardServer/templates/diagnostics.jinja2 b/dDashboardServer/templates/diagnostics.jinja2 index 88c13b6d3..90fc636af 100644 --- a/dDashboardServer/templates/diagnostics.jinja2 +++ b/dDashboardServer/templates/diagnostics.jinja2 @@ -61,6 +61,7 @@
+
UGC server
UGC page
Asking the UGC server…
Servers (averages over the range; click to filter)
Busiest message types
@@ -81,6 +82,42 @@ var direction = 'in'; var data = null; + // The UGC server (when it's enabled): its work, what it uses, and what it has made + function loadUgc() { + api.get('/api/diagnostics/ugc').then(function (d) { + var card = document.getElementById('ugcCard'), body = document.getElementById('ugcBody'); + card.classList.toggle('d-none', !d.enabled); + if (!d.enabled) return; + var counts = function (title, c) { + return '
' + esc(title) + ': ' + fmt.badge(nf.format(c.done || 0) + ' made', 'success') + ' ' + fmt.badge(nf.format(c.pending || 0) + ' waiting', 'secondary') + ' ' + + fmt.badge(nf.format(c.failed || 0) + ' failed', c.failed ? 'danger' : 'secondary') + (c.empty ? ' ' + fmt.badge(nf.format(c.empty) + ' empty', 'light') : '') + '
'; + }; + var html = counts('Models', d.counts.model) + counts('Cars and rockets', d.counts.modular); + var s = d.status; + if (!s) { + body.innerHTML = fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '
' + html; + return; + } + var u = s.usage || {}, l = s.limits || {}, cores = u.cores || 1; + var row = function (k, v) { return '' + esc(k) + '' + v + ''; }; + html = '
' + fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused', 'info') : '') + '
' + + '' + + row('Work', esc(s.active) + ' of ' + esc(s.workers) + ' workers busy, ' + esc(nf.format(s.queued)) + ' queued') + + row('CPU', esc(((u.cpuPercent || 0) / cores).toFixed(0)) + '% of ' + esc(cores) + ' cores' + (l.maxCpus ? ' (limit ' + esc((l.maxCpus / cores * 100).toFixed(0)) + '%)' : '')) + + row('Memory', esc(bytes(u.residentBytes || 0)) + ' in use; jobs ~' + esc(bytes(s.jobMemoryBytes || 0)) + (l.maxMemoryBytes ? ' of ' + esc(bytes(l.maxMemoryBytes)) : '') + (s.memoryWaits ? ', ' + esc(s.memoryWaits) + ' waits' : '')) + + row('Files', esc(bytes(s.storedBytes || 0)) + (s.maxStorageBytes ? ' of ' + esc(bytes(s.maxStorageBytes)) : '') + (s.evicted ? ', ' + esc(s.evicted) + ' evicted' : '')) + + row('Since start', esc(nf.format(s.made || 0)) + ' made, ' + esc(nf.format(s.failed || 0)) + ' failed attempts') + + '
' + html; + var recent = (s.recent || []).slice(0, 5); + if (recent.length) html += '
Last: ' + recent.map(function (r) { + return esc(r.kind) + ' ' + esc(r.id) + (r.ok ? ' ' + esc(ms(r.ms)) : ' failed'); + }).join(' · ') + '
'; + body.innerHTML = html; + }).catch(function () {}); + } + loadUgc(); + setInterval(loadUgc, 10000); + function bytes(v) { if (v >= 1073741824) return (v / 1073741824).toFixed(2) + ' GB'; if (v >= 1048576) return (v / 1048576).toFixed(1) + ' MB';