mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): show the UGC server on the home page and Server Health
Server Status gets a UGC row while master starts it, and staff with health_view a UGC Server card: up time, waiting/made/failed, busy workers and storage, linking to /ugc. Server Health adds UGC to the uptime history and a Servers table with every server's process, memory, CPU and last report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,12 @@ namespace {
|
||||
pid = ServerState::g_UgcPid;
|
||||
}
|
||||
out["pid"] = pid;
|
||||
// Up since its process started when this machine runs it (the dashboard may have started after it)
|
||||
if (pid && out.value("online", false)) {
|
||||
for (const auto& process : ServerRoutes::Processes()) {
|
||||
if (process.pid == pid && process.startedAt) out["since"] = process.startedAt;
|
||||
}
|
||||
}
|
||||
// Its traffic report (every few seconds, via master) carries its workers, totals and storage
|
||||
const auto traffic = Traffic::Server("ugc");
|
||||
out["gauges"] = traffic.value("gauges", nlohmann::json::object());
|
||||
|
||||
@@ -123,6 +123,21 @@
|
||||
// Formatting helpers used by table renderers
|
||||
window.fmt = {
|
||||
unix: function (ts) { return ts ? new Date(ts * 1000).toLocaleString() : '-'; },
|
||||
// 1536 -> "1.5 KB"
|
||||
bytes: function (n) {
|
||||
n = Number(n) || 0;
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0;
|
||||
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
||||
return (i === 0 ? n : n.toFixed(n < 10 ? 1 : 0)) + ' ' + units[i];
|
||||
},
|
||||
// Seconds -> "3d 4h", "2h 5m", "7m", "12s"
|
||||
duration: function (s) {
|
||||
s = Math.max(0, Math.floor(Number(s) || 0));
|
||||
var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
|
||||
if (d) return d + 'd ' + h + 'h';
|
||||
if (h) return h + 'h ' + m + 'm';
|
||||
return m ? m + 'm' : s + 's';
|
||||
},
|
||||
date: function (s) {
|
||||
if (!s) return '-';
|
||||
var d = new Date(String(s).replace(' ', 'T') + (String(s).indexOf('Z') === -1 && String(s).indexOf('T') === -1 ? 'Z' : ''));
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
<div class="col-lg-6"><div class="card"><div class="card-header"><h5 class="mb-0 fs-6">Players online (highest)</h5></div><div class="card-body"><svg class="health-chart" id="chartPlayers" role="img" aria-label="Players online over time"></svg></div></div></div>
|
||||
<div class="col-lg-6"><div class="card"><div class="card-header"><h5 class="mb-0 fs-6">Worlds running</h5></div><div class="card-body"><svg class="health-chart" id="chartWorlds" role="img" aria-label="Worlds running over time"></svg></div></div></div>
|
||||
<div class="col-lg-6"><div class="card"><div class="card-header"><h5 class="mb-0 fs-6">Memory (all servers)</h5></div><div class="card-body"><svg class="health-chart" id="chartMemory" role="img" aria-label="Memory over time"></svg></div></div></div>
|
||||
<div class="col-lg-6"><div class="card"><div class="card-header"><h5 class="mb-0 fs-6">Auth and chat servers</h5></div><div class="card-body" id="uptime"></div></div></div>
|
||||
<div class="col-lg-6"><div class="card"><div class="card-header"><h5 class="mb-0 fs-6">Auth, chat and UGC servers</h5></div><div class="card-body" id="uptime"></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0">Servers</h5><span class="small text-body-secondary">Memory and CPU from this machine (Linux); CPU is of one core since the last refresh</span></div>
|
||||
<div class="card-body table-responsive" id="servers"><p class="text-body-secondary mb-0">Loading…</p></div>
|
||||
</div>
|
||||
|
||||
{% if can.logs_system %}
|
||||
@@ -91,13 +96,18 @@
|
||||
}
|
||||
|
||||
function uptime(samples, from, to) {
|
||||
var rows = [['auth', 'Auth server'], ['chat', 'Chat server']].map(function (s) {
|
||||
var up = samples.filter(function (p) { return p[s[0]]; }).length;
|
||||
var percent = samples.length ? (up / samples.length * 100) : 0;
|
||||
var servers = [['auth', 'Auth server'], ['chat', 'Chat server']];
|
||||
// The UGC server only counts while master was set to start it
|
||||
if (samples.some(function (p) { return p.ugc_enabled; })) servers.push(['ugc', 'UGC server', 'ugc_enabled']);
|
||||
var rows = servers.map(function (s) {
|
||||
var counted = s[2] ? samples.filter(function (p) { return p[s[2]]; }) : samples;
|
||||
var up = counted.filter(function (p) { return p[s[0]]; }).length;
|
||||
var percent = counted.length ? (up / counted.length * 100) : 0;
|
||||
var bars = samples.map(function (p) {
|
||||
return '<span title="' + esc(new Date(p.time * 1000).toLocaleString()) + '" style="flex: 1; min-width: 1px; background: var(--bs-' + (p[s[0]] ? 'success' : 'danger') + ')"></span>';
|
||||
var color = s[2] && !p[s[2]] ? 'secondary-bg' : p[s[0]] ? 'success' : 'danger';
|
||||
return '<span title="' + esc(new Date(p.time * 1000).toLocaleString()) + '" style="flex: 1; min-width: 1px; background: var(--bs-' + color + ')"></span>';
|
||||
}).join('');
|
||||
return '<div class="mb-3"><div class="d-flex justify-content-between small mb-1"><span>' + s[1] + '</span><span>' + (samples.length ? percent.toFixed(1) + '% up' : 'no samples') + '</span></div>' +
|
||||
return '<div class="mb-3"><div class="d-flex justify-content-between small mb-1"><span>' + s[1] + '</span><span>' + (counted.length ? percent.toFixed(1) + '% up' : 'no samples') + '</span></div>' +
|
||||
'<div class="d-flex rounded overflow-hidden" style="height: 18px">' + bars + '</div></div>';
|
||||
});
|
||||
document.getElementById('uptime').innerHTML = rows.join('') + '<p class="small text-body-secondary mb-0">Red means the server was not connected when the sample was taken.</p>';
|
||||
@@ -109,7 +119,8 @@
|
||||
var c = d.current;
|
||||
document.getElementById('healthNow').innerHTML = [
|
||||
['Players online', nf.format(c.players)], ['Worlds running', nf.format(c.worlds)],
|
||||
['Auth / chat', (c.auth ? 'up' : 'DOWN') + ' / ' + (c.chat ? 'up' : 'DOWN')], ['Memory', c.memory_kb ? size(c.memory_kb) : 'n/a']
|
||||
[c.ugc_enabled ? 'Auth / chat / UGC' : 'Auth / chat', [c.auth, c.chat].concat(c.ugc_enabled ? [c.ugc] : []).map(function (up) { return up ? 'up' : 'DOWN'; }).join(' / ')],
|
||||
['Memory', c.memory_kb ? size(c.memory_kb) : 'n/a']
|
||||
].map(function (s) {
|
||||
return '<div class="col-6 col-md-3"><div class="card text-center"><div class="card-body py-3"><div class="text-body-secondary small text-uppercase mb-1">' + s[0] + '</div><div class="fs-4 fw-bold">' + esc(s[1]) + '</div></div></div></div>';
|
||||
}).join('');
|
||||
@@ -120,6 +131,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Every server with its process and what its last traffic report said (UGC: workers, storage and the database queue)
|
||||
function loadServers() {
|
||||
api.get('/api/servers').then(function (d) {
|
||||
var box = document.getElementById('servers');
|
||||
if (!d.success) { box.innerHTML = '<p class="text-danger mb-0">' + esc(d.error) + '</p>'; return; }
|
||||
var now = Date.now() / 1000;
|
||||
var details = function (s) {
|
||||
var g = (s.traffic && s.traffic.gauges) || {}, parts = [];
|
||||
if (s.players !== undefined) parts.push(nf.format(s.players) + ' player' + (s.players === 1 ? '' : 's'));
|
||||
if (s.traffic && s.traffic.connections) parts.push(nf.format(s.traffic.connections) + ' connection' + (s.traffic.connections === 1 ? '' : 's') + (s.traffic.ping_ms ? ', ' + s.traffic.ping_ms + ' ms' : ''));
|
||||
if (g.workers_threads !== undefined) parts.push('workers ' + g.workers_busy + '/' + g.workers_threads + (g.workers_queued ? ', ' + g.workers_queued + ' queued' : ''));
|
||||
if (s.key === 'ugc') {
|
||||
var c = d.ugc.counts || { model: {}, modular: {} };
|
||||
var sum = function (state) { return (c.model[state] || 0) + (c.modular[state] || 0); };
|
||||
parts.push(nf.format(sum('pending')) + ' waiting, ' + nf.format(sum('done')) + ' made, ' + nf.format(sum('failed')) + ' failed');
|
||||
if (g.ugc_stored_bytes !== undefined) parts.push(fmt.bytes(g.ugc_stored_bytes) + (g.ugc_max_storage_bytes ? ' of ' + fmt.bytes(g.ugc_max_storage_bytes) : '') + ' stored');
|
||||
}
|
||||
return esc(parts.join(' · '));
|
||||
};
|
||||
box.innerHTML = '<table class="table table-sm align-middle mb-0"><thead><tr><th>Server</th><th>Status</th><th>Up for</th><th class="text-end">PID</th><th class="text-end">Memory</th><th class="text-end">CPU</th><th>Details</th></tr></thead><tbody>' +
|
||||
d.servers.map(function (s) {
|
||||
var p = s.process;
|
||||
var started = (p && p.started_at) || s.since;
|
||||
var label = s.key === 'ugc' && DASH.can('properties_view') ? fmt.link('/ugc', s.label) : esc(s.label);
|
||||
return '<tr><td>' + label + '</td><td>' + (s.kind === 'OTHER' ? fmt.badge('Not listed', 'secondary') : fmt.badge(s.online ? 'Online' : 'Offline', s.online ? 'success' : 'danger')) + '</td>' +
|
||||
'<td>' + ((s.online || s.kind === 'OTHER') && started ? esc(fmt.duration(now - started)) : '-') + '</td>' +
|
||||
'<td class="text-end">' + (p ? esc(p.pid) : '-') + '</td><td class="text-end">' + (p ? esc(size(p.memory_kb)) : '-') + '</td>' +
|
||||
'<td class="text-end">' + (p ? esc(p.cpu_percent.toFixed(1) + '%') : '-') + '</td><td class="small">' + details(s) + '</td></tr>';
|
||||
}).join('') + '</tbody></table>';
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function loadDumps() {
|
||||
var box = document.getElementById('dumps');
|
||||
if (!box) return;
|
||||
@@ -153,7 +196,9 @@
|
||||
});
|
||||
window.addEventListener('resize', Live.throttle(load, 500));
|
||||
setInterval(load, 60000);
|
||||
setInterval(loadServers, 10000);
|
||||
load();
|
||||
loadServers();
|
||||
loadDumps();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
<div class="row g-3 mb-4">
|
||||
{% include "server_status.jinja2" %}
|
||||
{% include "statistics.jinja2" %}
|
||||
{% if can.health_view %}
|
||||
<div class="col-md-6 col-lg-4{% if not ugc.enabled %} d-none{% endif %}" id="ugcCard">
|
||||
<div class="card h-100">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">UGC Server</h5>
|
||||
{% if can.properties_view %}<a class="btn btn-sm btn-outline-secondary" href="/ugc">Models</a>{% endif %}
|
||||
</div>
|
||||
<div class="card-body" id="ugcCardBody"><p class="text-body-secondary mb-0">Loading…</p></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include "world_instances.jinja2" %}
|
||||
@@ -160,9 +171,43 @@ $(function () {
|
||||
if (el && value !== undefined) el.textContent = value;
|
||||
}
|
||||
|
||||
// The UGC server's card (staff with health_view): its state, the database queue and what its last report said
|
||||
var ugcLoadedAt = 0;
|
||||
function loadUgc(force) {
|
||||
var card = document.getElementById('ugcCard');
|
||||
if (!card || (!force && Date.now() - ugcLoadedAt < 15000)) return;
|
||||
ugcLoadedAt = Date.now();
|
||||
api.get('/api/servers/ugc').then(function (d) {
|
||||
if (!d.success) return;
|
||||
card.classList.toggle('d-none', !d.enabled && !d.online);
|
||||
var g = d.gauges || {}, c = d.counts || { model: {}, modular: {} };
|
||||
var sum = function (state) { return (c.model[state] || 0) + (c.modular[state] || 0); };
|
||||
var line = function (label, value, id) {
|
||||
return '<div class="d-flex justify-content-between align-items-center mb-2"><span>' + label + '</span><span' + (id ? ' id="' + id + '"' : '') + '>' + value + '</span></div>';
|
||||
};
|
||||
var status = fmt.badge(d.online ? 'Online' : 'Offline', d.online ? 'success' : 'danger') +
|
||||
(d.online && d.since ? ' <span class="small text-body-secondary">up ' + esc(fmt.duration(Date.now() / 1000 - d.since)) + '</span>' : '');
|
||||
var storage = g.ugc_stored_bytes !== undefined ? esc(fmt.bytes(g.ugc_stored_bytes)) + (g.ugc_max_storage_bytes ? ' <span class="text-body-secondary">of ' + esc(fmt.bytes(g.ugc_max_storage_bytes)) + '</span>' : '') : '<span class="text-body-secondary">-</span>';
|
||||
var workers = g.workers_threads !== undefined ? esc(g.workers_busy + ' / ' + g.workers_threads) + (g.workers_queued ? ' <span class="text-body-secondary">(' + esc(g.workers_queued) + ' queued)</span>' : '') : '<span class="text-body-secondary">-</span>';
|
||||
document.getElementById('ugcCardBody').innerHTML =
|
||||
line('Status', status) +
|
||||
line('Waiting', fmt.badge(sum('pending'), sum('pending') ? 'warning' : 'secondary')) +
|
||||
line('Made', fmt.badge(sum('done'), 'primary')) +
|
||||
line('Failed', fmt.badge(sum('failed'), sum('failed') ? 'danger' : 'secondary')) +
|
||||
line('Workers busy', workers) +
|
||||
line('Storage used', storage).replace('mb-2"', 'mb-0"');
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function update(data) {
|
||||
if (data.auth) setStatus('auth-status', data.auth.online);
|
||||
if (data.chat) setStatus('chat-status', data.chat.online);
|
||||
if (data.ugc) {
|
||||
setStatus('ugc-status', data.ugc.online);
|
||||
var ugcRow = document.getElementById('ugc-status-row');
|
||||
if (ugcRow) ugcRow.classList.toggle('d-none', !data.ugc.enabled);
|
||||
loadUgc();
|
||||
}
|
||||
if (data.worlds) {
|
||||
setText('world-count', data.stats && data.stats.worlds !== undefined ? data.stats.worlds : data.worlds.length);
|
||||
if (detailedWorlds) loadDetailedWorlds();
|
||||
@@ -181,6 +226,7 @@ $(function () {
|
||||
});
|
||||
|
||||
api.get('/api/status').then(update).catch(function () {});
|
||||
loadUgc(true);
|
||||
Live.onStatus(update);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
<span class="badge text-bg-danger" id="chat-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{# Only while master starts it (enable_ugc_server); the page shows it when that changes #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3{% if not ugc.enabled %} d-none{% endif %}" id="ugc-status-row">
|
||||
<span>UGC Server</span>
|
||||
{% if ugc.online %}
|
||||
<span class="badge text-bg-success" id="ugc-status">Online</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-danger" id="ugc-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Active Worlds</span>
|
||||
<span class="badge text-bg-primary" id="world-count">{{ stats.worlds }}</span>
|
||||
|
||||
Reference in New Issue
Block a user