Files
2026-08-05 14:17:41 -05:00

344 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% load static %}
{% block title %}Server Stats - Packs Site{% endblock %}
{% block content %}
<h1><i class="fas fa-chart-line"></i> Server Stats</h1>
<p class="empty-hint">Auto-refreshes every 20s — data served from a single cached JSON endpoint.</p>
<div class="stats-toolbar">
<span class="stats-updated"><i class="fas fa-sync-alt"></i> Updated <span id="stats-updated"></span></span>
<button type="button" class="btn btn-secondary btn-sm" id="stats-refresh"><i class="fas fa-redo"></i> Refresh</button>
<span class="stats-error" id="stats-error" hidden><i class="fas fa-exclamation-triangle"></i> Could not load stats.</span>
</div>
<div class="stats-grid">
<section class="card">
<h2><i class="fas fa-cube"></i> Application</h2>
<table class="stat-table">
<tr><td>Environment</td><td id="app-env"></td></tr>
<tr><td>Git commit</td><td><span class="stat-mono" id="app-git"></span></td></tr>
<tr><td>Django</td><td id="app-django"></td></tr>
<tr><td>Python</td><td id="app-python"></td></tr>
<tr><td>Hostname</td><td id="app-hostname"></td></tr>
<tr><td>Database</td><td id="app-db"></td></tr>
<tr><td>Time zone</td><td id="app-tz"></td></tr>
<tr><td>Media root</td><td class="stat-mono" id="app-media"></td></tr>
<tr><td>Gunicorn</td><td id="app-gunicorn"></td></tr>
<tr><td>Data generated in</td><td id="app-pagegen"></td></tr>
</table>
</section>
<section class="card">
<h2><i class="fas fa-server"></i> System</h2>
<table class="stat-table">
<tr><td>Booted</td><td id="sys-boot"></td></tr>
<tr><td>Uptime</td><td id="sys-uptime"></td></tr>
<tr><td>Load average</td><td><span class="stat-mono" id="sys-load"></span></td></tr>
<tr><td>CPU</td><td id="sys-cpu"></td></tr>
<tr><td>Processes</td><td id="sys-procs"></td></tr>
<tr><td>Network (since boot)</td><td id="sys-net"></td></tr>
</table>
</section>
<section class="card">
<h2><i class="fas fa-memory"></i> Memory &amp; Swap</h2>
<p class="stat-bar-label">RAM — <span id="mem-pct"></span>% used</p>
<div class="bar-track"><div class="bar-fill" id="mem-bar"></div></div>
<p class="stat-bar-caption"><span id="mem-used"></span> of <span id="mem-total"></span></p>
<p class="stat-bar-label">Swap — <span id="swap-pct"></span>% used</p>
<div class="bar-track"><div class="bar-fill bar-fill-swap" id="swap-bar"></div></div>
<p class="stat-bar-caption"><span id="swap-used"></span> of <span id="swap-total"></span></p>
</section>
<section class="card">
<h2><i class="fas fa-hdd"></i> Disk</h2>
{% for label in disk_labels %}
<div data-disk="{{ label }}">
<p class="stat-bar-label">{{ label }} <span class="stat-muted" data-f="path"></span><span data-f="percent"></span>% used <span class="stat-muted" data-f="scope"></span></p>
<div class="bar-track"><div class="bar-fill" data-f="bar"></div></div>
<p class="stat-bar-caption"><span data-f="used"></span> of <span data-f="total"></span> · <span data-f="free"></span> free</p>
</div>
{% endfor %}
</section>
<section class="card">
<h2><i class="fas fa-fire"></i> Top processes by CPU</h2>
<table class="stat-table">
<tbody id="top-cpu">
<tr><td colspan="3" class="empty-hint">Loading…</td></tr>
</tbody>
</table>
</section>
<section class="card">
<h2><i class="fas fa-layer-group"></i> Top processes by memory</h2>
<table class="stat-table">
<tbody id="top-mem">
<tr><td colspan="3" class="empty-hint">Loading…</td></tr>
</tbody>
</table>
</section>
<section class="card" id="traffic-card">
<h2><i class="fas fa-traffic-light"></i> Traffic</h2>
<div id="traffic-body">
<p class="empty-hint">Loading…</p>
</div>
</section>
<section class="card">
<h2><i class="fas fa-database"></i> Site content</h2>
<div class="stat-tiles">
<div class="stat-tile"><span class="stat-tile-value" data-tile="users"></span><span class="stat-tile-label">Users</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="projects"></span><span class="stat-tile-label">Packs</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="versions"></span><span class="stat-tile-label">Versions</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="files"></span><span class="stat-tile-label">Files</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="downloads"></span><span class="stat-tile-label">Downloads</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="comments"></span><span class="stat-tile-label">Comments</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="ratings"></span><span class="stat-tile-label">Votes</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="assets"></span><span class="stat-tile-label">Gallery</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="sessions"></span><span class="stat-tile-label">Active sessions</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="pending_uploads"></span><span class="stat-tile-label">Pending uploads</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="drafts"></span><span class="stat-tile-label">Drafts</span></div>
<div class="stat-tile"><span class="stat-tile-value" data-tile="storage"></span><span class="stat-tile-label">Stored media</span></div>
</div>
<h3 class="stat-subhead">Accounts</h3>
<p class="stat-line">Staff <span data-tile="staff"></span> · Superusers <span data-tile="superusers"></span> · Active 24h <span data-tile="active_24h"></span> · Active 7d <span data-tile="active_7d"></span></p>
<h3 class="stat-subhead">Packs by category</h3>
<p class="stat-line" id="categories"></p>
<h3 class="stat-subhead">Ratings</h3>
<p class="stat-line" id="ratings-line"></p>
</section>
<section class="card stats-recent">
<h2><i class="fas fa-history"></i> Latest versions</h2>
<ul class="stat-list" data-list="versions"><li class="empty-hint">Loading…</li></ul>
</section>
<section class="card stats-recent">
<h2><i class="fas fa-comments"></i> Latest comments</h2>
<ul class="stat-list" data-list="comments"><li class="empty-hint">Loading…</li></ul>
</section>
<section class="card stats-recent">
<h2><i class="fas fa-box-open"></i> Latest packs</h2>
<ul class="stat-list" data-list="projects"><li class="empty-hint">Loading…</li></ul>
</section>
<section class="card stats-recent">
<h2><i class="fas fa-thumbs-up"></i> Latest votes</h2>
<ul class="stat-list" data-list="ratings"><li class="empty-hint">Loading…</li></ul>
</section>
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const URL = "{% url 'landing:server_stats_api' %}";
const INTERVAL_MS = 20000;
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function set(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value == null ? '—' : value;
}
function setHtml(id, value) {
const el = document.getElementById(id);
if (el) el.innerHTML = value == null ? '—' : value;
}
function fmtBytes(n) {
if (n == null) return '—';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = Number(n);
for (const u of units) {
if (size < 1024 || u === 'TB') return size.toFixed(1) + ' ' + u;
size /= 1024;
}
}
function timeAgo(iso) {
const then = new Date(iso);
const s = Math.max(1, Math.floor((Date.now() - then.getTime()) / 1000));
if (s < 60) return s + 's ago';
const m = Math.floor(s / 60);
if (m < 60) return m + 'm ago';
const h = Math.floor(m / 60);
if (h < 24) return h + 'h ago';
return Math.floor(h / 24) + 'd ago';
}
function fmtDuration(sec) {
sec = Math.floor(sec || 0);
const d = Math.floor(sec / 86400);
const h = Math.floor((sec % 86400) / 3600);
const m = Math.floor((sec % 3600) / 60);
return (d ? d + 'd ' : '') + (h || d ? h + 'h ' : '') + m + 'm';
}
function setTiles(d) {
const content = d.content || {};
document.querySelectorAll('[data-tile]').forEach(el => {
el.textContent = content[el.dataset.tile] == null ? '—' : content[el.dataset.tile];
});
const cats = d.categories_list || [];
document.getElementById('categories').innerHTML = cats.length
? cats.map(([name, n]) => `<span class="stat-chip">${esc(name)}: ${n}</span>`).join('')
: '—';
const line = document.getElementById('ratings-line');
if (line) line.textContent = `${content.ratings_positive} positive · ${content.ratings_negative} negative · ${content.tags} tags`;
}
function buildProcesses(items, tbodyId) {
const tbody = document.getElementById(tbodyId);
if (!items || !items.length) {
tbody.innerHTML = '<tr><td colspan="3" class="empty-hint">No process data.</td></tr>';
return;
}
tbody.innerHTML = items.map(p =>
`<tr><td>${esc(p.name)}</td><td><span class="stat-mono">#${esc(p.pid)}</span></td><td><strong>${p.value}%</strong></td></tr>`
).join('');
}
function buildDisks(system) {
document.querySelectorAll('[data-disk]').forEach(block => {
const label = block.dataset.disk;
const disk = system && system.disks ? system.disks[label] : null;
const setF = (k, v) => { const el = block.querySelector('[data-f="' + k + '"]'); if (el) el.textContent = v; };
const bar = block.querySelector('[data-f="bar"]');
if (!disk) {
setF('path', 'unavailable');
if (bar) bar.style.width = '0%';
return;
}
setF('path', disk.path);
setF('percent', disk.percent);
setF('scope', disk.scope === 'folder' ? '· folder size' : '');
if (bar) bar.style.width = disk.percent + '%';
setF('used', fmtBytes(disk.used));
setF('total', fmtBytes(disk.total));
setF('free', fmtBytes(disk.free));
});
}
function buildRecent(d) {
const lists = {
versions: items => items.map(v =>
`<li><a href="/packs/${esc(v.slug)}/">${esc(v.project)}</a> ` +
`<span class="stat-muted">v${esc(v.version)} · ${timeAgo(v.created_at)}</span></li>`
).join(''),
comments: items => items.map(c =>
`<li><strong>${esc(c.user)}</strong> on <a href="/packs/${esc(c.slug)}/">${esc(c.project)}</a> ` +
`<span class="stat-muted">· ${timeAgo(c.created_at)}</span>` +
`<p class="stat-excerpt">${esc(c.body.length > 80 ? c.body.slice(0, 80) + '…' : c.body)}</p></li>`
).join(''),
projects: items => items.map(p =>
`<li><a href="/packs/${esc(p.slug)}/">${esc(p.title)}</a> ` +
`<span class="stat-muted">${esc(p.category)} · ${esc(p.owner)} · ${timeAgo(p.created_at)}</span></li>`
).join(''),
ratings: items => items.map(r =>
`<li><strong>${esc(r.user)}</strong> ` +
`<span class="${r.value ? 'stat-up' : 'stat-down'}">${r.value ? '+1' : '1'}</span> ` +
`<a href="/packs/${esc(r.slug)}/">${esc(r.project)}</a> ` +
`<span class="stat-muted">· ${timeAgo(r.created_at)}</span></li>`
).join(''),
};
document.querySelectorAll('[data-list]').forEach(ul => {
const key = ul.dataset.list;
const items = (d.recent && d.recent[key]) || [];
ul.innerHTML = items.length ? lists[key](items) : '<li class="empty-hint">None yet.</li>';
});
}
function buildTraffic(d) {
const body = document.getElementById('traffic-body');
if (!body) return;
const t = d.traffic;
if (!t) {
body.innerHTML = '<p class="empty-hint">Gunicorn access log is not enabled — start via run_prod.sh to see live traffic.</p>';
return;
}
const st = t.status || {};
const dur = (v) => v != null ? v + ' ms' : '—';
body.innerHTML =
'<div class="stat-tiles">' +
`<div class="stat-tile"><span class="stat-tile-value">${t.requests_min}</span><span class="stat-tile-label">Req / min</span></div>` +
`<div class="stat-tile"><span class="stat-tile-value">${t.requests_total}</span><span class="stat-tile-label">Requests (tail)</span></div>` +
`<div class="stat-tile"><span class="stat-tile-value">${dur(t.avg_ms)}</span><span class="stat-tile-label">Avg resp</span></div>` +
`<div class="stat-tile"><span class="stat-tile-value">${dur(t.p50_ms)}</span><span class="stat-tile-label">Median resp</span></div>` +
`<div class="stat-tile"><span class="stat-tile-value">${dur(t.max_ms)}</span><span class="stat-tile-label">Max resp</span></div>` +
'</div>' +
'<p class="stat-line">' +
`<span class="stat-chip">2xx: ${st['2xx'] || 0}</span>` +
`<span class="stat-chip">3xx: ${st['3xx'] || 0}</span>` +
`<span class="stat-chip">4xx: ${st['4xx'] || 0}</span>` +
`<span class="stat-chip">5xx: ${st['5xx'] || 0}</span>` +
'</p>' +
'<h3 class="stat-subhead">Recent requests</h3>' +
'<ul class="stat-list">' +
(t.recent || []).map(r => {
const cls = r.status >= 500 ? 'stat-down' : (r.status >= 400 ? 'stat-muted' : 'stat-up');
return `<li><span class="stat-muted">${timeAgo(r.time)}</span> <code class="stat-mono">${esc(r.method)}</code> ` +
`<span class="stat-mono">${esc(r.path)}</span> ` +
`<span class="${cls}">${r.status}</span>` +
(r.dur_ms != null ? ` <span class="stat-muted">${r.dur_ms} ms</span>` : '') +
'</li>';
}).join('') +
'</ul>';
}
function apply(d) {
const a = d.app || {};
setHtml('app-env', a.app_env + (a.debug ? ' <span class="badge badge-staff">DEBUG</span>' : ''));
set('app-git', a.git);
set('app-django', a.django);
set('app-python', a.python);
set('app-hostname', a.hostname);
set('app-db', a.db);
set('app-tz', a.tz);
set('app-media', a.media_root);
set('app-gunicorn', a.gunicorn);
set('app-pagegen', a.page_gen_ms != null ? a.page_gen_ms + ' ms' : '—');
const s = d.system;
if (s) {
set('sys-boot', new Date(s.boot_time).toLocaleString());
set('sys-uptime', fmtDuration(s.uptime));
set('sys-load', s.load_avg ? s.load_avg.join(' / ') : '—');
setHtml('sys-cpu', `${s.cpus.physical} core / ${s.cpus.logical} thread · <strong>${s.cpus.percent}%</strong>`);
set('sys-procs', s.processes + ' running');
set('sys-net', s.net ? `${fmtBytes(s.net.sent)} up · ${fmtBytes(s.net.recv)} down` : '—');
setHtml('app-gunicorn', a.gunicorn + ` <span class="stat-muted">(${s.gunicorn} proc)</span>`);
set('mem-pct', s.memory.percent);
document.getElementById('mem-bar').style.width = s.memory.percent + '%';
set('mem-used', fmtBytes(s.memory.used));
set('mem-total', fmtBytes(s.memory.total));
set('swap-pct', s.swap.percent);
document.getElementById('swap-bar').style.width = s.swap.percent + '%';
set('swap-used', fmtBytes(s.swap.used));
set('swap-total', fmtBytes(s.swap.total));
buildDisks(s);
buildProcesses(s.top_cpu, 'top-cpu');
buildProcesses(s.top_mem, 'top-mem');
}
setTiles(d);
buildTraffic(d);
buildRecent(d);
set('stats-updated', new Date(d.generated_at).toLocaleTimeString());
}
async function load() {
const err = document.getElementById('stats-error');
err.hidden = true;
try {
const res = await fetch(URL, { headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error('HTTP ' + res.status);
apply(await res.json());
} catch (e) {
err.hidden = false;
}
}
document.getElementById('stats-refresh').addEventListener('click', load);
load();
setInterval(load, INTERVAL_MS);
})();
</script>
{% endblock %}