bunch of shit
This commit is contained in:
132
streaming/disk.html
Normal file
132
streaming/disk.html
Normal file
@@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Disk — Vanguard</title>
|
||||
<style>
|
||||
:root{--bg:#141414;--fg:#e6e6e6;--mut:#9a9a9a;--acc:#3d8bfd;--ok:#2ecc71;--warn:#e3a008;--bad:#e05260;--box:#1f1f1f;--line:#2e2e2e}
|
||||
html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 system-ui,sans-serif}
|
||||
main{max-width:1100px;margin:0 auto;padding:24px 16px 48px}
|
||||
header{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:10px}
|
||||
h1{font-size:24px;margin:0}.who{color:var(--mut)}.who b{color:var(--fg)}
|
||||
nav{display:flex;gap:6px;margin:6px 0 18px;border-bottom:1px solid var(--line)}
|
||||
nav a{padding:8px 14px;color:var(--mut);text-decoration:none;border-bottom:2px solid transparent}
|
||||
nav a.on{color:var(--fg);border-color:var(--acc)}
|
||||
section{background:var(--box);border:1px solid var(--line);border-radius:14px;padding:14px 18px;margin-bottom:12px}
|
||||
section h2{margin:0 0 8px;font-size:15px;color:var(--mut);font-weight:600;text-transform:uppercase;letter-spacing:.3px}
|
||||
table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;padding:6px 8px;border-top:1px solid var(--line);vertical-align:top}
|
||||
th{color:var(--mut);font-weight:600;border-top:0}td.n{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
.mono{font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
|
||||
.pill{display:inline-block;font-size:12px;border-radius:999px;padding:1px 9px;white-space:nowrap}
|
||||
.ok{background:#1f3b2a;color:var(--ok)}.warn{background:#3a3220;color:var(--warn)}.bad{background:#43242a;color:var(--bad)}.info{background:#1e3147;color:var(--acc)}
|
||||
.state{font-size:12px;border-radius:999px;padding:2px 10px;text-align:center;white-space:nowrap}
|
||||
.parked{background:#43242a;color:var(--bad)}.published{background:#3a3220;color:var(--warn)}.waiting{background:#1e3147;color:var(--acc)}.writing{background:#1f3b2a;color:var(--ok)}
|
||||
.row{display:grid;grid-template-columns:96px 1fr auto;gap:12px;align-items:start}
|
||||
.name{font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
|
||||
.meta{color:var(--mut);font-size:13px;margin-top:2px}
|
||||
.why{color:var(--mut);font-size:13px;margin-top:6px;border-left:2px solid var(--line);padding-left:10px;white-space:pre-wrap}
|
||||
.acts{display:flex;gap:8px;align-items:center}
|
||||
button{font:inherit;font-size:13px;border-radius:8px;border:1px solid #3a3a3a;background:#262626;color:var(--fg);padding:6px 12px;cursor:pointer}
|
||||
select.stopmins{font:inherit;font-size:12px;border-radius:8px;border:1px solid #3a3a3a;background:#262626;color:var(--fg);padding:5px 6px}
|
||||
button:hover:not(:disabled){border-color:var(--acc)}button:disabled{opacity:.45;cursor:default}
|
||||
button.danger:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
|
||||
input[type=number]{font:inherit;background:#121212;color:var(--fg);border:1px solid #3a3a3a;border-radius:6px;padding:4px 6px}
|
||||
.note{color:var(--mut);font-size:13px}.err{color:var(--bad)}.hidden{display:none}
|
||||
pre{background:#121212;border-radius:8px;padding:10px;overflow:auto;font-size:12px;margin:8px 0 0;max-height:280px}
|
||||
.verdict{margin-top:8px;font-size:14px}
|
||||
footer{color:var(--mut);font-size:13px;margin-top:20px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header><h1>Disk</h1><div class="who" id="who"></div></header>
|
||||
<nav><a href="/disk" class="on">Disk</a><a href="/" style="margin-left:auto">Dashboards ↗</a></nav>
|
||||
<div id="pane-disk">
|
||||
<section>
|
||||
<div class="row" style="grid-template-columns:1fr auto;align-items:center">
|
||||
<div id="fs" class="note">Loading…</div>
|
||||
<div class="acts"><span class="note" id="diskgen"></span><button id="diskrefresh">Rewalk now</button></div>
|
||||
</div>
|
||||
</section>
|
||||
<section><h2>Streamers — recordings by month</h2><div id="dstreamers" class="note">Loading…</div></section>
|
||||
<section><h2>Clippers — clips by month</h2><div id="dclippers" class="note">Loading…</div></section>
|
||||
<section><h2>Logs</h2><div id="dlogs" class="note">Loading…</div></section>
|
||||
<section><h2>System</h2><div id="dsystem" class="note">Loading…</div></section>
|
||||
<footer>Sizes are allocated blocks, what the disk actually loses. A recording's month is the date in its own name; clips and
|
||||
everything else go by modification time. Cached for ten minutes; Rewalk now re-reads every file.</footer>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const API = '/streaming/api';
|
||||
let admin = false;
|
||||
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
const hms = s => { s = Math.max(0, s|0); const h = s/3600|0, m = (s%3600)/60|0; return h ? `${h}h ${m}m` : `${m}m`; };
|
||||
const dur = s => { s = Math.max(0, s|0); return `${s/3600|0}:${String((s%3600)/60|0).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; };
|
||||
const short = p => p.replace(/^.*\/streams\//, '').replace('/.live/', ' · ');
|
||||
const mb = b => (b/1e6).toFixed(b < 10e6 ? 1 : 0) + ' MB';
|
||||
|
||||
async function getJSON(path) {
|
||||
const r = await fetch(path, { credentials: 'same-origin' });
|
||||
if (r.status === 401) throw new Error('Not signed in. Reload the page.');
|
||||
const text = await r.text();
|
||||
let data; try { data = JSON.parse(text); }
|
||||
catch { throw new Error(`Server answered HTTP ${r.status} with no usable body${text ? ': ' + text.slice(0, 160) : ' (an empty 502 means dumpd did not reply)'}`); }
|
||||
if (data.error) throw new Error(`HTTP ${r.status}: ${data.error}`);
|
||||
return data;
|
||||
}
|
||||
function whoami(d) { admin = !!d.admin; document.getElementById('who').innerHTML = `signed in as <b>${esc(d.user || '?')}</b>${admin ? '' : ' · read only'}`; }
|
||||
|
||||
const gb = b => b >= 1e12 ? (b / 1e12).toFixed(2) + ' TB' : b >= 1e9 ? (b / 1e9).toFixed(1) + ' GB' : b >= 1e6 ? (b / 1e6).toFixed(0) + ' MB' : b ? (b / 1e3).toFixed(0) + ' kB' : '';
|
||||
const monthName = m => { const [y, mo] = m.split('-'); return new Date(+y, +mo - 1, 1).toLocaleString(undefined, { month: 'short', year: '2-digit' }); };
|
||||
function monthTable(rows, months, extra) {
|
||||
// rows: [{name, total, months:{}, note}] ; months: newest first, older ones folded into "older"
|
||||
const shown = months.slice(0, 6), older = months.slice(6);
|
||||
const cell = (r, m) => { const v = r.months[m] || 0; return `<td class="n">${v ? gb(v) : '<span class="note">·</span>'}</td>`; };
|
||||
const oldSum = r => older.reduce((a, m) => a + (r.months[m] || 0), 0);
|
||||
const tot = key => rows.reduce((a, r) => a + (key === 'total' ? r.total : key === 'older' ? oldSum(r) : (r.months[key] || 0)), 0);
|
||||
return `<table>
|
||||
<tr><th></th>${shown.map(m => `<th class="n">${monthName(m)}</th>`).join('')}${older.length ? '<th class="n">older</th>' : ''}<th class="n">total</th>${extra ? '<th></th>' : ''}</tr>
|
||||
${rows.map(r => `<tr><td><b>${esc(r.name)}</b></td>${shown.map(m => cell(r, m)).join('')}${older.length ? `<td class="n">${gb(oldSum(r)) || '<span class="note">·</span>'}</td>` : ''}<td class="n"><b>${gb(r.total)}</b></td>${extra ? `<td class="note">${r.note || ''}</td>` : ''}</tr>`).join('')}
|
||||
${rows.length > 1 ? `<tr><td class="note">all</td>${shown.map(m => `<td class="n">${gb(tot(m))}</td>`).join('')}${older.length ? `<td class="n">${gb(tot('older'))}</td>` : ''}<td class="n"><b>${gb(tot('total'))}</b></td>${extra ? '<td></td>' : ''}</tr>` : ''}
|
||||
</table>`;
|
||||
}
|
||||
async function loadDisk(force) {
|
||||
let d;
|
||||
document.getElementById('diskgen').textContent = force ? 'walking every file…' : '';
|
||||
try { d = await getJSON(API + '/disk' + (force ? '?refresh=1' : '')); }
|
||||
catch (e) { document.getElementById('fs').innerHTML = `<span class="err">${esc(e.message)}</span>`; return; }
|
||||
whoami(d);
|
||||
document.getElementById('diskgen').textContent = `walked ${new Date(d.generated).toLocaleTimeString()} in ${d.seconds}s`;
|
||||
const bar = (label, f) => { const pct = f.total ? 100 * f.used / f.total : 0; const cls = pct > 90 ? 'bad' : pct > 75 ? 'warn' : 'ok';
|
||||
return `<div style="margin:4px 0"><b>${esc(label)}</b> <span class="mono note">${esc(f.path)}</span> — ${gb(f.used)} of ${gb(f.total)} used, <span class="pill ${cls}">${gb(f.free)} free</span>
|
||||
<div style="height:8px;background:#262626;border-radius:4px;margin-top:4px"><div style="width:${pct.toFixed(1)}%;height:8px;border-radius:4px;background:var(--${cls === 'ok' ? 'ok' : cls === 'warn' ? 'warn' : 'bad'})"></div></div></div>`; };
|
||||
const fs = d.fs || {};
|
||||
document.getElementById('fs').innerHTML = (fs.media ? bar('media', fs.media) : '') + (fs.root && !fs.same_filesystem ? bar('system disk', fs.root) : '') + (fs.same_filesystem ? '<div class="note">media and the system share one filesystem</div>' : '');
|
||||
const monthsOf = objs => [...new Set(objs.flatMap(o => Object.keys(o.months || {})))].sort().reverse();
|
||||
const toRows = obj => Object.entries(obj).map(([name, v]) => ({ name, total: v.total, months: v.months || {},
|
||||
note: [v.raw ? `${gb(v.raw)} raw in .src` : '', v.unpublished ? `${gb(v.unpublished)} unpublished in .live` : ''].filter(Boolean).join(' · ') })).sort((a, b) => b.total - a.total);
|
||||
const sRows = toRows(d.streamers || {}); const trash = d.trash || { total: 0, months: {} };
|
||||
if (trash.total) sRows.push({ name: 'trash (all users, 3-day undo)', total: trash.total, months: trash.months, note: 'dumped nights and binned recordings, purged after 3 days' });
|
||||
document.getElementById('dstreamers').innerHTML = sRows.length ? monthTable(sRows, monthsOf(sRows), true) : '<span class="note">no recordings</span>';
|
||||
const cRows = toRows(d.clippers || {});
|
||||
document.getElementById('dclippers').innerHTML = cRows.length ? monthTable(cRows, monthsOf(cRows), false) : '<span class="note">no clips</span>';
|
||||
const L = d.logs || {};
|
||||
const lRows = [];
|
||||
if (L.months) lRows.push({ name: 'Elasticsearch indices (logs + metrics)', total: L.es_total || 0, months: L.months, note: L.es_data_dir ? `${gb(L.es_data_dir)} on disk in appdata/elasticsearch` : '' });
|
||||
document.getElementById('dlogs').innerHTML = (lRows.length ? monthTable(lRows, monthsOf(lRows), true) : '') + `<table style="margin-top:8px">
|
||||
${L.es_data_dir != null ? `<tr><td>Elasticsearch data dir</td><td class="n">${gb(L.es_data_dir)}</td><td class="note">the on-disk truth for the indices above</td></tr>` : ''}
|
||||
<tr><td>journald</td><td class="n">${gb(L.journald || 0)}</td><td class="note">systemd journal (dumpd, sweep, timers)</td></tr>
|
||||
<tr><td>Docker json logs</td><td class="n">${gb(L.docker_json_logs || 0)}</td><td class="note">per-container log files Filebeat reads</td></tr>
|
||||
</table>` + (L.indices ? `<details style="margin-top:8px"><summary class="note">${L.indices.length} indices</summary><table>${L.indices.sort((a, b) => b.bytes - a.bytes).map(i => `<tr><td class="mono">${esc(i.index)}</td><td class="n">${gb(i.bytes)}</td><td class="note">${esc(i.month)}</td></tr>`).join('')}</table></details>` : '');
|
||||
const S = d.system || {};
|
||||
document.getElementById('dsystem').innerHTML = `<table>${Object.entries(S).sort((a, b) => (typeof b[1] === 'number' ? b[1] : 0) - (typeof a[1] === 'number' ? a[1] : 0))
|
||||
.map(([k, v]) => `<tr><td>${esc(k)}</td><td class="n">${typeof v === 'number' ? gb(v) : esc(v)}</td><td class="note">${k.startsWith('docker') ? "Docker's own figure" : k === 'media_other' ? 'markers, Syncthing state, versions under /srv/media' : ''}</td></tr>`).join('')}</table>`
|
||||
+ (d.errors && d.errors.length ? `<div class="err" style="margin-top:8px">${d.errors.map(esc).join('<br>')}</div>` : '');
|
||||
}
|
||||
document.getElementById('diskrefresh').onclick = () => loadDisk(true);
|
||||
|
||||
loadDisk(false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user