Show on the dashboard which outside services are alive
Added after a day spent establishing by hand that AniList had been refusing every query for days. Nothing said so: the app fell back, the fallback was quietly wrong about episodes, and the first anyone knew was a family member reporting a rail of week-old episodes. What makes the section useful rather than decorative is the two columns beside the light. "anilist: down" only says to go and look; "anilist: nedostupné, anime katalog a kalendář, beze změny od pátku" is the whole answer, including the sentence AniList itself puts in the body of its 403. Uptime over a day and a week, from raw samples taken every five minutes, so the question "was it flaky or has it been out since Friday" has an answer on the screen rather than in a log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bc6ab04673
commit
7a40dd908d
2 changed files with 57 additions and 1 deletions
|
|
@ -191,7 +191,11 @@ routerAdd("GET", "/api/status", (e) => {
|
||||||
const stats = fetchJson(base.replace(/\/+$/, "") + "/v1/admin/stats",
|
const stats = fetchJson(base.replace(/\/+$/, "") + "/v1/admin/stats",
|
||||||
{ "x-amber-admin": secret }, 8)
|
{ "x-amber-admin": secret }, 8)
|
||||||
return { configured: true, probe: stats.probe, queue: stats.queue,
|
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, {
|
return e.json(200, {
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,57 @@ function panelApi(a){
|
||||||
"</table></section>";
|
"</table></section>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 '<section class="wide"><h2>Cizí služby</h2>' +
|
||||||
|
'<p class="note">zatím žádné měření</p></section>';
|
||||||
|
var down = 0;
|
||||||
|
var rows = u.map(function(x){
|
||||||
|
if (!x.ok) down++;
|
||||||
|
var pill = x.ok ? '<span class="pill ok">běží</span>'
|
||||||
|
: '<span class="pill no">nedostupné</span>';
|
||||||
|
// 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 ? '<div class="note">' + esc(String(x.detail).slice(0,110)) + "</div>" : "";
|
||||||
|
return "<tr><td>" + pill + "</td><td><b>" + esc(x.name) + "</b>" + why +
|
||||||
|
'</td><td class="note">' + esc(x.matters || "") + "</td>" +
|
||||||
|
'<td class="note">' + (x.since ? esc(sinceLabel(x.since)) : "—") + "</td>" +
|
||||||
|
'<td class="num">' + day + '</td><td class="num">' + week +
|
||||||
|
'</td><td class="num">' + (x.ms || 0) + " ms</td></tr>";
|
||||||
|
}).join("");
|
||||||
|
return '<section class="wide"><h2>Cizí služby' +
|
||||||
|
(down ? ' <span class="pill no">' + down + " mimo provoz</span>" : "") + "</h2>" +
|
||||||
|
"<table><tr><th></th><th>služba</th><th>na čem záleží</th><th>beze změny od</th>" +
|
||||||
|
'<th class="num">24 h</th><th class="num">7 dní</th><th class="num">odezva</th></tr>' +
|
||||||
|
rows + "</table>" +
|
||||||
|
'<p class="note">Měří se každých pět minut. Sloupce 24 h a 7 dní jsou podíl měření, ' +
|
||||||
|
"kdy služba odpovídala.</p></section>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// "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){
|
function panelAccounts(a){
|
||||||
if (!a || a.error) return "";
|
if (!a || a.error) return "";
|
||||||
return '<section><h2>Účty</h2><div class="kv">' +
|
return '<section><h2>Účty</h2><div class="kv">' +
|
||||||
|
|
@ -1140,6 +1191,7 @@ async function load(){
|
||||||
el("main").innerHTML =
|
el("main").innerHTML =
|
||||||
panelServices(d.coolify) + panelReleases(d.releases) +
|
panelServices(d.coolify) + panelReleases(d.releases) +
|
||||||
panelClients(d.clients) + panelApi(d.amberApi) + panelAccounts(d.accounts) +
|
panelClients(d.clients) + panelApi(d.amberApi) + panelAccounts(d.accounts) +
|
||||||
|
panelUpstreams(d.amberApi) +
|
||||||
panelErrors(errs);
|
panelErrors(errs);
|
||||||
wire();
|
wire();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue