diff --git a/pb_hooks/status.pb.js b/pb_hooks/status.pb.js index c2eb7a6..420b879 100644 --- a/pb_hooks/status.pb.js +++ b/pb_hooks/status.pb.js @@ -191,7 +191,11 @@ routerAdd("GET", "/api/status", (e) => { const stats = fetchJson(base.replace(/\/+$/, "") + "/v1/admin/stats", { "x-amber-admin": secret }, 8) return { configured: true, probe: stats.probe, queue: stats.queue, - metadata: stats.metadata, health: stats.health } + metadata: stats.metadata, health: stats.health, + // 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 } }) return e.json(200, { diff --git a/pb_public/status.html b/pb_public/status.html index a370c4c..bd0deb6 100644 --- a/pb_public/status.html +++ b/pb_public/status.html @@ -366,6 +366,57 @@ function panelApi(a){ ""; } +// Which of the services we depend on are alive, and how much of the time they +// have been. +// +// This section exists because AniList was down for days and nothing said so: +// the app fell back, the fallback was quietly wrong, and the first anyone knew +// was a family member reporting a rail of stale episodes. What makes it useful +// rather than decorative is the two columns beside the light — what breaks when +// this one is out, and since when — because "anilist: down" only says to go and +// look, while "anilist: down since pátek, anime katalog" is the whole answer. +function panelUpstreams(a){ + var u = a && a.upstreams; + if (!u || !u.length) return '

Cizí služby

' + + '

zatím žádné měření

'; + var down = 0; + var rows = u.map(function(x){ + if (!x.ok) down++; + var pill = x.ok ? 'běží' + : 'nedostupné'; + // A percentage is only meaningful once there is something to average. + var day = x.uptime24h === null ? "—" : x.uptime24h + " %"; + var week = x.uptime7d === null ? "—" : x.uptime7d + " %"; + // The reason, when the service gave one. AniList says why it is off in the + // body of a 403, and that sentence saves the next person an investigation. + var why = x.detail ? '
' + esc(String(x.detail).slice(0,110)) + "
" : ""; + return "" + pill + "" + esc(x.name) + "" + why + + '' + esc(x.matters || "") + "" + + '' + (x.since ? esc(sinceLabel(x.since)) : "—") + "" + + '' + day + '' + week + + '' + (x.ms || 0) + " ms"; + }).join(""); + return '

Cizí služby' + + (down ? ' ' + down + " mimo provoz" : "") + "

" + + "" + + '' + + rows + "
službana čem záležíbeze změny od24 h7 dníodezva
" + + '

Měří se každých pět minut. Sloupce 24 h a 7 dní jsou podíl měření, ' + + "kdy služba odpovídala.

"; +} + +// "od pátku 14:05" beats an ISO timestamp on a wall-mounted dashboard, and a +// relative age beats both once it is under a day. +function sinceLabel(unix){ + var d = new Date(unix * 1000), now = Date.now(); + var mins = Math.round((now - d.getTime()) / 60000); + if (mins < 60) return "před " + mins + " min"; + if (mins < 60 * 24) return "před " + Math.round(mins / 60) + " h"; + var days = ["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"]; + return days[d.getDay()] + " " + ("0" + d.getHours()).slice(-2) + ":" + + ("0" + d.getMinutes()).slice(-2); +} + function panelAccounts(a){ if (!a || a.error) return ""; return '

Účty

' + @@ -1140,6 +1191,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) + panelErrors(errs); wire(); } catch (e) {