145 lines
4.1 KiB
JavaScript
145 lines
4.1 KiB
JavaScript
import { decode } from "/vendor/msgpack/index.mjs";
|
||
|
||
const target = document.querySelector("#target");
|
||
const day = document.querySelector("#day");
|
||
const detail = document.querySelector("#detail");
|
||
const observed = document.querySelector("#observed");
|
||
|
||
const percent = (value) =>
|
||
value === null ? "Unknown" : `${(value * 100).toFixed(2)}% healthy`;
|
||
|
||
function state(value) {
|
||
switch (value) {
|
||
case null:
|
||
return "unknown";
|
||
case 1:
|
||
return "good";
|
||
case 0:
|
||
return "down";
|
||
default:
|
||
return "mixed";
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
const response = await fetch(
|
||
document.querySelector("script[data-api]").dataset.api,
|
||
);
|
||
|
||
return response.ok ? decode(await response.arrayBuffer()) : null;
|
||
}
|
||
|
||
function renderRow(record, recordIndex, dates, select) {
|
||
const row = document.createElement("article");
|
||
row.className = "health-row";
|
||
|
||
const title = document.createElement("h3");
|
||
title.textContent = record.name;
|
||
|
||
const bars = document.createElement("div");
|
||
bars.className = "bars";
|
||
bars.setAttribute("role", "group");
|
||
bars.setAttribute("aria-label", `${record.name}, daily history`);
|
||
|
||
for (const [dayIndex, value] of record.values.entries()) {
|
||
const button = document.createElement("button");
|
||
button.type = "button";
|
||
button.className = `bar ${state(value)}`;
|
||
button.title = `${dates[dayIndex]}: ${percent(value)}`;
|
||
button.setAttribute("aria-label", `${record.name} · ${button.title}`);
|
||
button.setAttribute("aria-pressed", "false");
|
||
button.dataset.target = String(recordIndex);
|
||
button.dataset.day = String(dayIndex);
|
||
|
||
button.addEventListener("click", () => {
|
||
target.value = String(recordIndex);
|
||
day.value = String(dayIndex);
|
||
select();
|
||
});
|
||
|
||
bars.append(button);
|
||
}
|
||
|
||
row.append(title, bars);
|
||
return row;
|
||
}
|
||
|
||
function renderGroup(group, groupIndex, records, dates, select) {
|
||
const [header, names, values] = group;
|
||
const section = document.createElement("section");
|
||
section.className = "health-group";
|
||
|
||
const heading = document.createElement("h2");
|
||
heading.id = `group-${groupIndex}`;
|
||
heading.textContent = `${String(groupIndex + 1).padStart(2, "0")} ${header}`;
|
||
section.setAttribute("aria-labelledby", heading.id);
|
||
section.append(heading);
|
||
|
||
for (const [seriesIndex, name] of names.entries()) {
|
||
const record = { name, values: values[seriesIndex] };
|
||
const recordIndex = records.length;
|
||
|
||
records.push(record);
|
||
target.add(new Option(`${header} · ${name}`, String(recordIndex)));
|
||
section.append(renderRow(record, recordIndex, dates, select));
|
||
}
|
||
|
||
return section;
|
||
}
|
||
|
||
function render([start, groups]) {
|
||
const dates = Array.from({ length: 30 }, (_, index) =>
|
||
new Date((start + index * 86400) * 1000).toISOString().slice(0, 10),
|
||
);
|
||
observed.textContent = `${dates[0]} – ${dates.at(-1)} · UTC`;
|
||
|
||
const records = [];
|
||
const select = () => {
|
||
const record = records[Number(target.value)];
|
||
const dayIndex = Number(day.value);
|
||
detail.textContent = `${record.name} · ${dates[dayIndex]}: ${percent(record.values[dayIndex])}.`;
|
||
|
||
for (const bar of document.querySelectorAll(".bar")) {
|
||
const selected =
|
||
bar.dataset.target === target.value && bar.dataset.day === day.value;
|
||
bar.setAttribute("aria-pressed", String(selected));
|
||
}
|
||
};
|
||
|
||
for (const [index, date] of dates.entries()) {
|
||
day.add(new Option(date, String(index)));
|
||
}
|
||
day.value = "29";
|
||
|
||
groups.sort((left, right) => left[1].length - right[1].length);
|
||
const container = document.querySelector("#groups");
|
||
|
||
for (const [groupIndex, group] of groups.entries()) {
|
||
container.append(renderGroup(group, groupIndex, records, dates, select));
|
||
}
|
||
|
||
if (records.length) {
|
||
document.querySelector(".controls").hidden = false;
|
||
target.addEventListener("change", select);
|
||
day.addEventListener("change", select);
|
||
select();
|
||
} else {
|
||
observed.textContent = "No data";
|
||
}
|
||
}
|
||
|
||
let snapshot;
|
||
|
||
try {
|
||
snapshot = await load();
|
||
} catch {
|
||
snapshot = null;
|
||
}
|
||
|
||
if (snapshot === null) {
|
||
observed.textContent = "No data";
|
||
} else {
|
||
render(snapshot);
|
||
}
|
||
|
||
document.documentElement.dataset.ready = "true";
|