344 lines
17 KiB
HTML
344 lines
17 KiB
HTML
{% 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 & 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
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 %}
|