mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-06 04:43:43 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <set>
|
||||
|
||||
#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.<one of those> (the version before it was made again). 408 while it's being made",
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4 d-flex flex-column gap-3 mt-3 mt-xl-0">
|
||||
<div class="card panel-fill d-none" id="ugcCard"><div class="card-header py-2 d-flex justify-content-between align-items-center"><h5 class="mb-0 fs-6">UGC server</h5><a class="small" href="/ugc">UGC page</a></div><div class="card-body py-2 small" id="ugcBody">Asking the UGC server…</div></div>
|
||||
<div class="card panel-fill" style="flex: 1 1 0; min-height: 10rem"><div class="card-header py-2"><h5 class="mb-0 fs-6">Servers <span class="text-body-secondary fw-normal small">(averages over the range; click to filter)</span></h5></div><div class="card-body p-0" id="servers"></div></div>
|
||||
<div class="card panel-fill" style="flex: 1 1 0; min-height: 10rem"><div class="card-header py-2 d-flex justify-content-between align-items-center"><h5 class="mb-0 fs-6">Busiest message types</h5>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Direction"><input type="radio" class="btn-check" name="msgDir" id="dirIn" value="in" checked><label class="btn btn-outline-secondary py-0" for="dirIn">In</label><input type="radio" class="btn-check" name="msgDir" id="dirOut" value="out"><label class="btn btn-outline-secondary py-0" for="dirOut">Out</label></div></div>
|
||||
@@ -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 '<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 s = d.status;
|
||||
if (!s) {
|
||||
body.innerHTML = fmt.badge('Unreachable', 'warning') + ' ' + esc(d.error || '') + '<hr class="my-2">' + html;
|
||||
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;
|
||||
}).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';
|
||||
|
||||
Reference in New Issue
Block a user