From e4991f0118544af22280244db1f3cc9ba828aea3 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 06:05:57 -0500 Subject: [PATCH] feat(dashboard): a simpler UGC card on Diagnostics The card had the UGC page's counts per state for both kinds, totals since the server started and the last makes, which the UGC page's lists cover. It now says whether the UGC server is up (throttled, paused), how many items wait and failed (linking to the failed ones), its busy workers, CPU, memory and stored files. /api/diagnostics/ugc read the database on a worker thread; the counts are now read on the web thread and the worker only fetches the UGC server's status. Check: Diagnostics with the UGC server enabled: the card is short, reads right while it's up, throttled and stopped, and the failed link opens the failed ones. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/UgcRoutes.cpp | 7 ++-- dDashboardServer/templates/diagnostics.jinja2 | 34 +++++++------------ docs/Dashboard.md | 4 ++- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index 67be52882..c5b97ec2f 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -458,9 +458,10 @@ namespace UgcRoutes { 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()) } } } }; + // The database only on this thread; the worker only fetches + nlohmann::json body = { { "success", true }, { "enabled", enabled }, { "online", online }, + { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } } }; + Workers::Reply(reply, context, false, [base, enabled, body = std::move(body)](HTTPReply& out) mutable { if (enabled) { const auto fetched = CachedGet(base + "/status", eCache::STATUS); if (fetched->status == 200) body["status"] = nlohmann::json::parse(fetched->body, nullptr, false); diff --git a/dDashboardServer/templates/diagnostics.jinja2 b/dDashboardServer/templates/diagnostics.jinja2 index 83d236285..abafdb3d7 100644 --- a/dDashboardServer/templates/diagnostics.jinja2 +++ b/dDashboardServer/templates/diagnostics.jinja2 @@ -83,37 +83,29 @@ var direction = 'in'; var data = null; - // The UGC server (when it's enabled): its work, what it uses, and what it has made + // The UGC server (when it's enabled): whether it's up, what it's doing and uses, and what waits or failed. What it + // made, and how long each took, is on the UGC page. 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 waiting = (d.counts.model.pending || 0) + (d.counts.modular.pending || 0), failed = (d.counts.model.failed || 0) + (d.counts.modular.failed || 0); + var row = function (k, v) { return '' + esc(k) + '' + v + ''; }; + var queue = row('Waiting', esc(nf.format(waiting)) + (failed ? ', ' + fmt.link('/ugc?view=list&state=failed', nf.format(failed) + ' failed') : '')); var s = d.status; if (!s) { - body.innerHTML = fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '
' + html; + body.innerHTML = '
' + fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '
' + queue + '
'; 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; + body.innerHTML = '
' + fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused', 'info') : '') + '
' + + '' + queue + + row('Workers', esc(s.active) + ' of ' + esc(s.workers) + ' busy') + + row('CPU', esc(((u.cpuPercent || 0) / cores).toFixed(0)) + '%' + (l.maxCpus ? ' of ' + esc((l.maxCpus / cores * 100).toFixed(0)) + '% allowed' : '')) + + row('Memory', esc(bytes(u.residentBytes || 0))) + + row('Files', esc(bytes(s.storedBytes || 0)) + (s.maxStorageBytes ? ' of ' + esc(bytes(s.maxStorageBytes)) : '')) + + '
'; }).catch(function () {}); } loadUgc(); diff --git a/docs/Dashboard.md b/docs/Dashboard.md index 4ca0b9209..1f5ac74fb 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -698,7 +698,9 @@ second of each server, HTTP requests per second of the dashboard and the UGC ser their request counts, errors and latency. **Live** shows the last 5 minutes at one second and updates as reports arrive (the `traffic` WebSocket topic); **1 hour** is at 10 seconds; **24 hours** (5 minutes) and **7 days** (30 minutes) come from the database. The servers table also shows each server's RakNet connections, average ping and resent -messages, and the worker threads of the dashboard and the UGC server. +messages, and the worker threads of the dashboard and the UGC server. While the UGC server is enabled, its card says +whether it is up (throttled, paused), how many items wait and failed (a link to the failed ones), its busy workers, CPU, +memory and stored files; what it made and how long each took is on the UGC page. How it is counted: