feat: Updated monitor chart
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 1m9s

This commit is contained in:
2026-08-25 15:02:04 +00:00
parent c440b59b93
commit 049e005873
4 changed files with 86 additions and 20 deletions
+46 -5
View File
@@ -1,6 +1,6 @@
"use client";
import { Monitor, MonitorSample, MonitorStatus, Rollup } from "@/lib/api";
import { Incident, Monitor, MonitorSample, MonitorStatus, Rollup } from "@/lib/api";
/*
* Shared vocabulary for the monitors screens.
@@ -117,6 +117,14 @@ export interface Slot {
pct: number | null;
checks: number;
latency: number | null;
/**
* An incident overlapped this slot — the monitor was actually down for
* some of it. A failed check on its own is not this: `retries` exists so a
* transient failure never opens an incident, and painting one red taught
* operators the check had gone offline when nothing had. `pct` still
* reports every failed check honestly; `down` is what colour follows.
*/
down: boolean;
}
/**
@@ -147,6 +155,7 @@ export function buildSlots(rollups: Rollup[], hours = 48): Slot[] {
checks: r?.checks ?? 0,
pct: r && r.checks > 0 ? (r.up_count / r.checks) * 100 : null,
latency: r && r.checks > 0 ? r.sum_latency / r.checks : null,
down: false,
});
}
return slots;
@@ -184,19 +193,50 @@ export function buildSampleSlots(samples: MonitorSample[], windowMs: number, buc
checks: bucket.checks,
pct: bucket.checks > 0 ? (bucket.up / bucket.checks) * 100 : null,
latency: bucket.checks > 0 ? bucket.latency / bucket.checks : null,
down: false,
}));
}
/**
* Mark the slots an incident ran through. Incidents are the record of what the
* monitor decided after `retries`, so this is the only thing that may paint a
* slot as down — the raw check results say only that a check failed, which is
* a different and much more common event.
*
* An unresolved incident runs to now. Returns new slots; the input is left
* alone so the same tape can be built once and marked per view.
*/
export function markIncidents(slots: Slot[], incidents: Incident[]): Slot[] {
if (incidents.length === 0) return slots;
const spans = incidents.map((i) => ({
from: new Date(i.started_at).getTime(),
to: i.resolved_at ? new Date(i.resolved_at).getTime() : Date.now(),
}));
return slots.map((s) => {
const from = s.at.getTime();
const to = from + s.spanMs;
return spans.some((sp) => sp.from < to && sp.to > from) ? { ...s, down: true } : s;
});
}
function slotColor(s: Slot): string {
if (s.down) return "bg-danger";
if (s.pct === null) return "bg-border-soft";
if (s.pct >= 99.5) return "bg-success";
if (s.pct >= 80) return "bg-warning";
return "bg-danger";
return "bg-warning";
}
/** The same three states at the chart's lower contrast. */
export function slotChartColor(s: Slot): string {
if (s.down) return "bg-danger/80";
if (s.pct === null) return "bg-border-soft";
if (s.pct >= 99.5) return "bg-success/60";
return "bg-warning/70";
}
function slotHeight(s: Slot): number {
if (s.pct === null) return 26;
if (s.pct < 80) return 100;
if (s.down || s.pct < 80) return 100;
return 55 + (s.pct - 80) * 2.2;
}
@@ -205,7 +245,8 @@ function slotHeight(s: Slot): number {
export function slotLabel(s: Slot): string {
const when = s.at.toLocaleString(undefined, { weekday: "short", hour: "2-digit", minute: "2-digit" });
if (s.pct === null) return `${when} · no checks ran`;
return `${when} · ${s.pct.toFixed(1)}% up · ${s.checks} checks`;
const base = `${when} · ${s.pct.toFixed(1)}% up · ${s.checks} checks`;
return s.down ? `${base} · incident` : base;
}
/**