Files
NoN-Site/nonpacks/templates/landing/server_stats.html
T

298 lines
15 KiB
HTML
Raw 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</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">
<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);
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 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);
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 %}