Show which provider's bytes were slow

Added beside the upstream panel and for the same reason: the owner reported
anime stuttering and it took an evening of measurement to establish that the
television was fine and TorBox's CDN had dipped to 5.5 Mbps. That is a question
a dashboard should answer in a glance.

Median rather than mean, because one fast session should not hide ten slow ones,
and the share of watched time spent waiting alongside it, because that is the
number that matches what the viewer actually felt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-09-07 22:26:28 +02:00
parent 7a40dd908d
commit 73a86d3e23
2 changed files with 30 additions and 2 deletions

View file

@ -195,7 +195,8 @@ routerAdd("GET", "/api/status", (e) => {
// Upstream liveness rides along on the same call rather than a
// second round trip: it is produced by the same service and read
// by the same screen.
upstreams: stats.upstreams }
upstreams: stats.upstreams,
throughput: stats.throughput }
})
return e.json(200, {

View file

@ -417,6 +417,33 @@ function sinceLabel(unix){
("0" + d.getMinutes()).slice(-2);
}
// Whose bytes were slow. Not a ranking input and deliberately not per file: a
// stall is a property of neither the upload nor the hostname, because TorBox
// hands out CDN nodes per request. This exists to answer "was it them or us"
// for a person looking at a screen, which is the question that took an evening
// of measurement the first time it was asked.
function panelThroughput(a){
var t = a && a.throughput;
if (!t || !t.length) return "";
var rows = t.map(function(x){
// Mbit/s reads the way a person thinks about a connection; kbps does not.
var med = (x.medianKbps / 1000).toFixed(1);
var worst = x.worstMinKbps === null ? "—" : (x.worstMinKbps / 1000).toFixed(1);
// Time spent waiting is the number that matches what the viewer felt.
var stall = x.stalledPct === null ? "—" : x.stalledPct + " %";
var bad = x.stalledPct !== null && x.stalledPct >= 2;
return "<tr><td><b>" + esc(x.provider) + '</b></td><td class="num">' + x.sessions +
'</td><td class="num">' + med + '</td><td class="num">' + worst +
'</td><td class="num"' + (bad ? ' style="color:var(--warn)"' : "") + ">" + stall +
"</td></tr>";
}).join("");
return '<section class="wide"><h2>Rychlost zdrojů za 24 h</h2><table>' +
'<tr><th>poskytovatel</th><th class="num">relací</th><th class="num">medián Mb/s</th>' +
'<th class="num">nejhorší propad</th><th class="num">čekání</th></tr>' + rows +
'</table><p class="note">Medián, ne průměr: jedna rychlá relace nemá schovat deset pomalých. ' +
"Sloupec čekání je podíl sledovaného času stráveného načítáním.</p></section>";
}
function panelAccounts(a){
if (!a || a.error) return "";
return '<section><h2>Účty</h2><div class="kv">' +
@ -1191,7 +1218,7 @@ async function load(){
el("main").innerHTML =
panelServices(d.coolify) + panelReleases(d.releases) +
panelClients(d.clients) + panelApi(d.amberApi) + panelAccounts(d.accounts) +
panelUpstreams(d.amberApi) +
panelUpstreams(d.amberApi) + panelThroughput(d.amberApi) +
panelErrors(errs);
wire();
} catch (e) {