uptime bar
[github]60-day uptime bars for three services, a new day slides in
data 64×15 8 fps 4.0 kB
all systems operational api operational ▌▌▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▖▌▌▌▌▌▌▌▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 60 days ago ············· 99.82% uptime ·············· today website operational ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 60 days ago ············· 99.82% uptime ·············· today database operational ▌▌▌▌▌▌▌▌▌▌▖▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▌▌▌▌▌▌▌▌ 60 days ago ············· 99.89% uptime ·············· today all systems operational api operational ▌▌▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▖▌▌▌▌▌▌▌▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 60 days ago ············· 99.82% uptime ·············· today website operational ▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌ 60 days ago ············· 99.82% uptime ·············· today database operational ▌▌▌▌▌▌▌▌▌▌▖▖▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▁▌▌▌▌▌▌▌▌ 60 days ago ············· 99.89% uptime ·············· today
use it
html
<script type="module" src="https://ascii.rest/ascii.js"></script>
<ascii-art piece="uptime-bar" options='{"services":["api","website","database"]}'></ascii-art> No install and no build step. ascii.js defines the <ascii-art> tag, loads the piece from ascii.rest, plays it while it is on screen, and holds the first frame for anyone who prefers reduced motion.
react
// npm install github:bas3line/ascii
import { Ascii } from "ascii.rest/react";
import { uptimeBar } from "ascii.rest/pieces";
<Ascii piece={uptimeBar} options={{"services":["api","website","database"]}} /> A client component, so it works in the Next.js app router as it is. Pass the name instead, piece="uptime-bar", to fetch the piece only when it mounts.
astro
---
// npm install github:bas3line/ascii
import Ascii from "ascii.rest/astro";
---
<Ascii piece="uptime-bar" options={{"services":["api","website","database"]}} /> Renders the first frame on the server, so the page is whole before any script runs, then plays the piece once it loads.
typescript
// npm install github:bas3line/ascii
import { mount } from "ascii.rest";
import { uptimeBar } from "ascii.rest/pieces";
const el = document.querySelector<HTMLPreElement>("pre")!;
const stop = mount(el, uptimeBar, {"services":["api","website","database"]}); mount plays a piece in a <pre>, or on a <canvas> for the coloured ones, and returns a function that stops it. Every piece, option and frame is typed.
/*
* uptime-bar: a status page's 60-day uptime bars, one thin bar a day, for up
* to three services (more are cut). A degraded day is a short bar, a down day flat.
*/
import type { Frame, Meta } from "../types.ts";
export interface UptimeBarOptions {
[key: string]: unknown;
services: string[];
}
export const meta = {
name: "uptime bar",
category: "data",
note: "60-day uptime bars for three services, a new day slides in",
cols: 64,
rows: 15,
fps: 8,
options: { services: ["api", "website", "database"] },
} satisfies Meta<UptimeBarOptions>;
const DAYS = 60;
const MAX = 3; // services shown; the frame has room for three
const STEP = 1.25; // seconds per day
const SLIDE = 0.25; // the end of each step, when the bars sit half a cell along
const START = 2646; // today at t = 0, picked for a good opening 60 days
// Each state as a left-half bar, and as the same bar half a cell to the left.
// A down day is flat on the floor.
const BAR = ["▁", "▖", "▌"];
const MID = ["▁", "▗", "▐"];
const RATES: [number, number][] = [
[0.03, 0.01],
[0.045, 0.014],
[0.025, 0.008],
];
function mulberry32(a: number) {
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const hash = (s: number, d: number, k: number) => mulberry32(s * 7919 + d * 104729 + k * 15485863)();
export default function uptimeBar({ services = meta.options.services }: Partial<UptimeBarOptions> = {}): Frame {
const { cols, rows } = meta;
const names = services.slice(0, MAX).map((n) => String(n).slice(0, 24));
const left = 2;
const W = DAYS;
// 2 up, 1 degraded, 0 down. An outage is often followed by a degraded day.
const base = (s: number, d: number) => {
const [deg, down] = RATES[s % RATES.length];
const r = hash(s, d, 1);
return r < down ? 0 : r < down + deg ? 1 : 2;
};
const state = (s: number, d: number) => {
const b = base(s, d);
if (b < 2) return b;
return base(s, d - 1) < 2 && hash(s, d, 2) < 0.5 ? 1 : 2;
};
// Percent of the day the service was up.
const uptime = (s: number, d: number) => {
const st = state(s, d);
if (st === 2) return 100;
return st === 1 ? 99.9 - hash(s, d, 3) * 0.6 : 96 - hash(s, d, 4) * 9;
};
const pad = (l: string, r = "") => (" ".repeat(left) + l + " ".repeat(Math.max(1, W - l.length - r.length)) + r).padEnd(cols);
// "60 days ago ···· 99.92% uptime ···· today", dotted to the bar's width.
const footer = (pct: number) => {
const a = `${DAYS} days ago `, b = " today", mid = " " + pct.toFixed(2) + "% uptime ";
const gap = W - a.length - b.length - mid.length;
const l = gap >> 1;
return pad(a + "·".repeat(l) + mid + "·".repeat(gap - l) + b);
};
// Fewer services sit in the middle of the frame.
const top = 1 + (MAX - names.length) * 2;
return (t) => {
const p = (t + 0.5) / STEP;
const today = START + Math.floor(p);
const half = p - Math.floor(p) > 1 - SLIDE / STEP;
const now = names.map((_, s) => state(s, today));
const worst = Math.min(2, ...now);
const lines: string[] = [];
while (lines.length < top) lines.push("");
lines.push(pad(worst === 2 ? "all systems operational" : worst === 1 ? "degraded performance" : "partial outage"));
lines.push("");
names.forEach((name, s) => {
let sum = 0, bar = "";
for (let d = today - DAYS + 1; d <= today; d++) sum += uptime(s, d);
// Mid-slide, each cell holds the next day, drawn in its left neighbour's right half.
for (let j = 0; j < W; j++) {
const d = today - DAYS + 1 + j + (half ? 1 : 0);
bar += (half ? MID : BAR)[state(s, d)];
}
const word = now[s] === 2 ? "operational" : now[s] === 1 ? "degraded" : "outage";
lines.push(pad(name, word));
lines.push(pad(bar));
lines.push(footer(sum / DAYS));
lines.push("");
});
while (lines.length < rows) lines.push("");
return lines.slice(0, rows).map((l) => l.padEnd(cols)).join("\n");
};
}