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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:05:57 -05:00
parent 7ea0453a6c
commit e4991f0118
3 changed files with 20 additions and 25 deletions

View File

@@ -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);

View File

@@ -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 '<div>' + 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') : '') + '</div>';
};
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 '<tr><td class="text-body-secondary pe-2">' + esc(k) + '</td><td>' + v + '</td></tr>'; };
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 || '') + '<hr class="my-2">' + html;
body.innerHTML = '<div class="mb-1">' + fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '</div><table class="table table-sm table-borderless mb-0"><tbody>' + queue + '</tbody></table>';
return;
}
var u = s.usage || {}, l = s.limits || {}, cores = u.cores || 1;
var row = function (k, v) { return '<tr><td class="text-body-secondary pe-2">' + esc(k) + '</td><td>' + v + '</td></tr>'; };
html = '<div class="mb-1">' + fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused', 'info') : '') + '</div>' +
'<table class="table table-sm table-borderless mb-1"><tbody>' +
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') +
'</tbody></table>' + html;
var recent = (s.recent || []).slice(0, 5);
if (recent.length) html += '<div class="mt-1 text-body-secondary">Last: ' + recent.map(function (r) {
return esc(r.kind) + ' ' + esc(r.id) + (r.ok ? ' ' + esc(ms(r.ms)) : ' failed');
}).join(' · ') + '</div>';
body.innerHTML = html;
body.innerHTML = '<div class="mb-1">' + fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused', 'info') : '') + '</div>' +
'<table class="table table-sm table-borderless mb-0"><tbody>' + 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)) : '')) +
'</tbody></table>';
}).catch(function () {});
}
loadUgc();