Files
stats-gateway/stats-site/static/speedtest/index.html
T

187 lines
5.5 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Speedtest — PSP Speedtest Stats</title>
<link rel="stylesheet" href="/css/main.css">
<style>
.speedtest table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
font-variant-numeric: tabular-nums;
}
.speedtest th,
.speedtest td {
padding: 0.4rem 0.7rem;
border-bottom: 1px solid var(--border);
text-align: left;
}
.speedtest .muted {
color: var(--muted);
font-size: 0.9rem;
}
.speedtest .filters {
margin: 1rem 0;
color: var(--muted);
}
.speedtest .filters select,
.speedtest .filters input {
color: var(--fg);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.2rem 0.4rem;
font: inherit;
}
.speedtest h2 {
font-size: 1.1em;
margin-top: 2em;
}
</style>
</head>
<body>
<header>
<h1>Speedtest</h1>
</header>
<main>
<section class="speedtest">
<p class="muted">Updated <span id="ts"></span>. Retention 30 days.</p>
<h2>Last 24h / 7d / 30d (median)</h2>
<table class="agg">
<thead><tr><th>Node</th><th>Period</th><th>Count</th><th>↓ Mbps</th><th>↑ Mbps</th><th>Ping ms</th></tr></thead>
<tbody id="agg-tbody"></tbody>
</table>
<h2>Recent tests (last 200)</h2>
<div class="filters">
Node:
<select id="filter-node">
<option value="">all</option>
<option value="fl">fl</option>
<option value="pl1">pl1</option>
</select>
Country:
<input type="text" id="filter-country" maxlength="2" placeholder="RU" style="width: 4rem;">
</div>
<table class="recent">
<thead><tr>
<th>Time</th><th>Node</th><th>Country</th>
<th>↓ Mbps</th><th>↑ Mbps</th><th>Ping ms</th><th>Jitter ms</th>
</tr></thead>
<tbody id="recent-tbody"></tbody>
</table>
</section>
<nav class="footer-nav">
<a href="/">← Назад</a>
<a href="/about/">About</a>
</nav>
</main>
<script>
function clearChildren(el) {
while (el.firstChild) el.removeChild(el.firstChild);
}
function appendCell(row, text) {
const td = document.createElement('td');
td.textContent = text;
row.appendChild(td);
}
function fmtNumber(n, fixed) {
if (typeof n !== 'number' || !isFinite(n)) return '0';
return n.toFixed(fixed);
}
function fmtTimestamp(unixSeconds) {
if (typeof unixSeconds !== 'number') return '';
return new Date(unixSeconds * 1000).toISOString().replace('T', ' ').slice(0, 19);
}
function renderAggTable(snapshot) {
const aggBody = document.getElementById('agg-tbody');
clearChildren(aggBody);
const periods = [
{ key: 'agg_day', label: '24h' },
{ key: 'agg_week', label: '7d' },
{ key: 'agg_month', label: '30d' },
];
for (const { key, label } of periods) {
const m = snapshot[key] || {};
for (const node of ['fl', 'pl1']) {
const a = m[node];
if (!a) continue;
const tr = document.createElement('tr');
appendCell(tr, node);
appendCell(tr, label);
appendCell(tr, String(a.count | 0));
appendCell(tr, fmtNumber(a.median_dl, 1));
appendCell(tr, fmtNumber(a.median_ul, 1));
appendCell(tr, fmtNumber(a.median_ping, 1));
aggBody.appendChild(tr);
}
}
}
function renderRecentTable(rows) {
const body = document.getElementById('recent-tbody');
clearChildren(body);
const limited = rows.slice(0, 200);
for (const r of limited) {
const tr = document.createElement('tr');
appendCell(tr, fmtTimestamp(r.Timestamp));
appendCell(tr, String(r.Node || ''));
appendCell(tr, String(r.Country || ''));
appendCell(tr, fmtNumber(r.DlMbps, 1));
appendCell(tr, fmtNumber(r.UlMbps, 1));
appendCell(tr, fmtNumber(r.PingMs, 1));
appendCell(tr, fmtNumber(r.JitterMs, 1));
body.appendChild(tr);
}
}
async function loadSnapshot() {
let snapshot;
try {
const r = await fetch('/data/snapshot.json', { cache: 'no-store' });
snapshot = await r.json();
} catch (e) {
document.getElementById('ts').textContent = 'load error';
return;
}
const st = snapshot.speedtest;
if (!st) {
document.getElementById('ts').textContent = 'no data';
return;
}
document.getElementById('ts').textContent = fmtTimestamp(st.generated_at);
renderAggTable(st);
const recent = Array.isArray(st.recent) ? st.recent : [];
const filterNode = document.getElementById('filter-node');
const filterCountry = document.getElementById('filter-country');
function applyFilters() {
const fn = filterNode.value;
const fc = filterCountry.value.toUpperCase();
const filtered = recent.filter(row =>
(!fn || row.Node === fn) &&
(!fc || row.Country === fc));
renderRecentTable(filtered);
}
filterNode.addEventListener('change', applyFilters);
filterCountry.addEventListener('input', applyFilters);
applyFilters();
}
loadSnapshot();
</script>
</body>
</html>