A single figure with its verdict: read against limits, with the history before it, the deviation from its target and how fresh it is (also called a KPI tile or metric card). Reach for it for the few numbers a screen is watched for.
import { Stat, useFreshness } from "@umriss-ui/core";
The tile gets a rule, not a colour: limits are values with a side and a severity, and the colour and the verdict word follow from them. The same limit model drives the charts' limit lines (ADR-0006).
Four verdicts, in order: in order, unknown, warning, alarm. A missing or non-finite value is unknown, never in order. A target is missed by an amount and shown as a signed deviation; it is never a limit.
Freshness is a separate axis: with asOf and ages the tile says when its value is stale or lost and keeps its verdict, since the last value is the best one there is (ADR-0010). useFreshness gives the same reading for your own parts.
A label, a value and its unit; without limits the figure is neutral, as it should be for a number nobody judged.
import { Stat } from "@umriss-ui/core";
export default function AFigure() {
return (
<div style={{ maxWidth: 240 }}>
<Stat label="Requests per minute · Checkout" value={812} decimals={0} />
</div>
);
}
Pass limits – a value, a side and a severity each – and the tile takes its colour and its verdict word from them.
import { Grid, Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/** Checkout's latency objective: warn from 240 ms, alarm from 300 ms. */
const CHECKOUT_P95: LimitSet = {
limits: [
{ value: 240, side: "upper", severity: "warning" },
{ value: 300, side: "upper", severity: "alarm" },
],
};
export default function AgainstLimits() {
return (
<Grid minItemWidth="200px" gap={4}>
<Stat label="p95 latency at 08:00" value={187} unit="ms" decimals={0} limits={CHECKOUT_P95} />
<Stat label="p95 latency at 09:35" value={262} unit="ms" decimals={0} limits={CHECKOUT_P95} />
<Stat label="p95 latency at 09:50" value={498} unit="ms" decimals={0} limits={CHECKOUT_P95} />
</Grid>
);
}
A null, missing or non-finite value is judged unknown and says so – never a zero, never “in order”.
import { Grid, Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
const ERROR_RATE: LimitSet = {
limits: [
{ value: 1, side: "upper", severity: "warning" },
{ value: 2, side: "upper", severity: "alarm" },
],
};
export default function Unknown() {
return (
<Grid minItemWidth="200px" gap={4}>
<Stat label="Error rate · Sign-in · no data" value={null} unit="%" limits={ERROR_RATE} />
<Stat label="Error rate · Search · not reported" value={undefined} unit="%" limits={ERROR_RATE} />
<Stat label="Error rate · Reporting · no requests" value={0 / 0} unit="%" limits={ERROR_RATE} />
</Grid>
);
}
history draws the values before it as a line across the tile - under the figure, or beside it where the tile is wide: the shape of the last hours, not a trend arrow.
import { Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: number;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
/** A service's latency and error rate every five minutes of today, from
midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(1000 + index * 37);
const points: MetricPoint[] = [];
let drift = 0;
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / (60 * MINUTE);
const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
drift = 0.8 * drift + (r() - 0.5) * 0.08;
const base = service.latencySlo * (0.45 + 0.2 * load + drift);
const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
const p95 = Math.round(base * surge * (1 + r() * 0.08));
points.push({
t,
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
});
}
return points;
}
const CHECKOUT_P95: LimitSet = {
limits: [
{ value: 240, side: "upper", severity: "warning" },
{ value: 300, side: "upper", severity: "alarm" },
],
};
/** The last two hours, every five minutes. */
const LAST = metrics("checkout").slice(-24);
export default function History() {
return (
<div style={{ maxWidth: 260 }}>
<Stat
label="p95 latency · Checkout"
value={LAST.at(-1)?.p95}
unit="ms"
decimals={0}
limits={CHECKOUT_P95}
history={LAST.map((point) => point.p95)}
/>
</div>
);
}
A tile of 380 px and more puts its history beside the figures, as high as they are; a narrower one keeps it beneath. The tile decides by its own width.
import { Grid, Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: number;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
/** A service's latency and error rate every five minutes of today, from
midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(1000 + index * 37);
const points: MetricPoint[] = [];
let drift = 0;
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / (60 * MINUTE);
const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
drift = 0.8 * drift + (r() - 0.5) * 0.08;
const base = service.latencySlo * (0.45 + 0.2 * load + drift);
const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
const p95 = Math.round(base * surge * (1 + r() * 0.08));
points.push({
t,
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
});
}
return points;
}
const P95: LimitSet = {
limits: [
{ value: 240, side: "upper", severity: "warning" },
{ value: 300, side: "upper", severity: "alarm" },
],
};
const ERRORS: LimitSet = {
limits: [
{ value: 1, side: "upper", severity: "warning" },
{ value: 2, side: "upper", severity: "alarm" },
],
};
/** Checkout since six this morning, every five minutes. */
const MORNING = metrics("checkout").slice(-54);
export default function GiveTheHistoryRoom() {
return (
<Grid minItemWidth="380px" gap={4}>
<Stat
label="p95 latency · Checkout · since 06:00"
value={MORNING.at(-1)?.p95}
unit="ms"
decimals={0}
limits={P95}
history={MORNING.map((point) => point.p95)}
/>
<Stat
label="Error rate · Checkout · since 06:00"
value={MORNING.at(-1)?.errorRate}
unit="%"
decimals={2}
limits={ERRORS}
history={MORNING.map((point) => point.errorRate)}
/>
</Grid>
);
}
A target is missed by an amount, never violated: the tile shows the signed deviation beside its verdict from the limits.
import { Grid, Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/** February's spend in k€, against each centre's monthly budget as target; over by 5 % warns, by 15 % alarms. */
function budget(monthly: number): LimitSet {
return {
target: monthly,
limits: [
{ value: monthly * 1.05, side: "upper", severity: "warning" },
{ value: monthly * 1.15, side: "upper", severity: "alarm" },
],
};
}
export default function ATarget() {
return (
<Grid minItemWidth="200px" gap={4}>
<Stat label="Sales · February" value={138.9} unit="k€" decimals={1} limits={budget(142)} />
<Stat label="Marketing · February" value={74.3} unit="k€" decimals={1} limits={budget(68)} />
<Stat label="IT · February" value={96.1} unit="k€" decimals={1} limits={budget(83)} />
</Grid>
);
}
With asOf and ages the tile says when its value turns stale or lost – and keeps its verdict, since the last value is still the best one.
import { Grid, Stack, Stat, Text, useFreshness } from "@umriss-ui/core";
import type { FreshnessAges, LimitSet } from "@umriss-ui/core";
/* Distances from the moment the page loaded, so the three always read the same. */
const LOADED = Date.now();
const MIN = 60_000;
/** Stale after two minutes without a new value, lost after ten. */
const AGES: FreshnessAges = { stale: 2 * MIN, lost: 10 * MIN };
const CHECKOUT_P95: LimitSet = {
limits: [
{ value: 240, side: "upper", severity: "warning" },
{ value: 300, side: "upper", severity: "alarm" },
],
};
function Caption({ asOf }: { asOf: number }) {
const { freshness } = useFreshness(asOf, AGES);
return (
<Text size="xs" tone="muted">
Feed: {freshness}
</Text>
);
}
export default function Freshness() {
return (
<Grid minItemWidth="200px" gap={4}>
{[
{ name: "live", asOf: LOADED - 20_000 },
{ name: "delayed", asOf: LOADED - 4 * MIN },
{ name: "cut off", asOf: LOADED - 25 * MIN },
].map((feed) => (
<Stack key={feed.name} gap={2}>
<Stat label={`p95 latency · ${feed.name}`} value={318} unit="ms" decimals={0} limits={CHECKOUT_P95} asOf={feed.asOf} ages={AGES} />
<Caption asOf={feed.asOf} />
</Stack>
))}
</Grid>
);
}
One limit set with a target and limits on both sides: the tolerance band between the warnings is derived, never passed.
import { Grid, Stat } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/* Data from the plant world, written out here so the example runs on its own. */
/** A small LCG - reproducible across runs and platforms. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
/** The early shift, 06:00 to 14:00. */
const SHIFT_MINUTES = 480;
/** The kiln's zone 3, in °C: the numbers every part reads it against, once.
Between the two warnings lies the tolerance - a tile fired outside it is
scrap; `returnTo` is the alarm's dead band. */
const KILN = { target: 1200, tolerance: [1185, 1215], alarm: 1230, returnTo: 1222 } as const;
/** The same numbers as the limit set the tile and the verdict read. */
const KILN_LIMITS: LimitSet = {
target: KILN.target,
limits: [
{ value: KILN.tolerance[1], side: "upper", severity: "warning" },
{ value: KILN.alarm, side: "upper", severity: "alarm" },
{ value: KILN.tolerance[0], side: "lower", severity: "warning" },
],
};
/** The kiln fires one tile every quarter of a minute when nothing holds it up -
the ideal cycle time of the OEE's performance. */
const IDEAL_CYCLE_MINUTES = 0.25;
/** One minute of the line. */
interface Reading {
/** Minutes since the start of the shift. */
minute: number;
/** Zone 3 of the kiln, in °C. */
kiln: number;
/** The exit pyrometer, in °C - `null` while it sends nothing. */
exit: number | null;
/** Whether the belt runs. */
running: boolean;
/** Tiles out of the kiln in this minute. */
fired: number;
/** Of them, the ones fired inside the tolerance. */
good: number;
}
/** A tile taken off the belt and measured - one every ten minutes while it runs. */
interface Sample {
minute: number;
/** The tile's length after firing, in mm (nominal 600). */
length: number;
}
/** A batch of tiles through the three stations. */
interface Batch {
id: string;
name: string;
/** How many tiles the batch is planned for. */
tiles: number;
/** Minutes since the start of the shift, per station. */
press: readonly [number, number];
dryer: readonly [number, number];
kiln: readonly [number, number];
}
interface Plant {
readings: readonly Reading[];
samples: readonly Sample[];
batches: readonly Batch[];
}
/* When the exit pyrometer goes silent, and for how long. Fixed rather than
seeded, so that a running control room shows its reading turn stale, then
lost, and come back. */
const SILENT_FROM = 288;
const SILENT_FOR = 50;
/** The shift, from a seed. */
function plant(seed: number): Plant {
const r = random(seed);
/* The one excursion: a burner overshoots somewhere between 02:30 and 03:30
into the shift, for half an hour, peaking near 1242 °C. The operator stops
the feed twenty-two minutes in, for eighteen minutes. The noise around it
is too small to reach a warning on its own, so this is the only crossing -
which is what the tests hold. */
const surgeFrom = 150 + Math.floor(r() * 60);
const surgeFor = 30;
const stopFrom = surgeFrom + 22;
const stopFor = 18;
/* The plan: batches through the kiln one after another, each pressed an
hour and dried half an hour before - the first ones during the night
shift. Tiles come out of the kiln only while a batch is in it, so the
OEE's count and the batches' counts are the same tiles. The plan is a
plan: a stop of the belt does not move it. */
const batches: Batch[] = [];
for (let at = 0, i = 0; at < SHIFT_MINUTES; i++) {
const length = 70 + Math.floor(r() * 25);
batches.push({
id: `b-${4121 + i}`,
name: `B-${4121 + i}`,
tiles: Math.round(length / IDEAL_CYCLE_MINUTES / 10) * 10,
press: [at - 60, at - 60 + length],
dryer: [at - 30, at - 30 + length],
kiln: [at, at + length],
});
at += length + 5;
}
const readings: Reading[] = [];
const samples: Sample[] = [];
let drift = 0;
for (let minute = 0; minute < SHIFT_MINUTES; minute++) {
drift = 0.9 * drift + (r() - 0.5) * 3;
const into = minute - surgeFrom;
const surge = into >= 0 && into <= surgeFor ? 42 * Math.sin((Math.PI * into) / surgeFor) : 0;
const kiln = Math.round((KILN.target + drift + surge) * 10) / 10;
const running = !(minute >= stopFrom && minute < stopFrom + stopFor);
const loaded = batches.some((batch) => batch.kiln[0] <= minute && minute < batch.kiln[1]);
const jam = r() < 0.12;
const fired = running && loaded ? (jam ? 3 : 4) : 0;
const inTolerance = kiln >= KILN.tolerance[0] && kiln <= KILN.tolerance[1];
const silent = minute >= SILENT_FROM && minute < SILENT_FROM + SILENT_FOR;
const exit = silent ? null : Math.round((kiln - 380 + (r() - 0.5) * 4) * 10) / 10;
readings.push({ minute, kiln, exit, running, fired, good: inTolerance ? fired : 0 });
if (running && minute % 10 === 0) {
/* Hotter shrinks more: the length follows the kiln, so the control
chart sees the excursion the trend sees. */
const length = 600 + (KILN.target - kiln) * 0.05 + (r() - 0.5) * 0.6;
samples.push({ minute, length: Math.round(length * 100) / 100 });
}
}
return { readings, samples, batches };
}
const { readings } = plant(17);
/** The last hour of the shift so far, minute by minute. */
function lastHour(until: number) {
return readings.filter((one) => one.minute > until - 60 && one.minute <= until).map((one) => one.kiln);
}
export default function AKilnZone() {
return (
<Grid minItemWidth="220px" gap={4}>
{[120, 170, 180].map((minute) => {
const hour = lastHour(minute);
return (
<Stat
key={minute}
label={`Kiln K1 · zone 3 · ${String(6 + Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`}
value={hour.at(-1)}
unit="°C"
decimals={0}
limits={KILN_LIMITS}
history={hour}
/>
);
})}
</Grid>
);
}
A row of tiles per service, each read against its own objective, with the last two hours beneath and a freshness for the feed.
import { Card, CardBody, CardHeader, Grid, Stat } from "@umriss-ui/core";
import type { FreshnessAges, LimitSet } from "@umriss-ui/core";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: number;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
/** A service's latency and error rate every five minutes of today, from
midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(1000 + index * 37);
const points: MetricPoint[] = [];
let drift = 0;
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / (60 * MINUTE);
const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
drift = 0.8 * drift + (r() - 0.5) * 0.08;
const base = service.latencySlo * (0.45 + 0.2 * load + drift);
const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
const p95 = Math.round(base * surge * (1 + r() * 0.08));
points.push({
t,
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
});
}
return points;
}
const LOADED = Date.now();
const AGES: FreshnessAges = { stale: 2 * 60_000, lost: 10 * 60_000 };
/** Warn at 80 % of the latency objective, alarm at the objective. */
function latency(service: Service): LimitSet {
return {
limits: [
{ value: service.latencySlo * 0.8, side: "upper", severity: "warning" },
{ value: service.latencySlo, side: "upper", severity: "alarm" },
],
};
}
const ERRORS: LimitSet = {
limits: [
{ value: 1, side: "upper", severity: "warning" },
{ value: 2, side: "upper", severity: "alarm" },
],
};
export default function AServiceOverview() {
return (
<Grid minItemWidth="320px" gap={4}>
{SERVICES.filter((service) => service.tier === 1).map((service) => {
const last = metrics(service.id).slice(-24);
const now = last.at(-1);
return (
<Card key={service.id}>
<CardHeader eyebrow={service.team} title={service.name} />
<CardBody>
<Grid minItemWidth="130px" gap={3}>
<Stat
label="p95 latency"
value={now?.p95}
unit="ms"
decimals={0}
limits={latency(service)}
history={last.map((point) => point.p95)}
asOf={LOADED - 40_000}
ages={AGES}
/>
<Stat
label="Error rate"
value={now?.errorRate}
unit="%"
decimals={2}
limits={ERRORS}
history={last.map((point) => point.errorRate)}
asOf={LOADED - 40_000}
ages={AGES}
/>
</Grid>
</CardBody>
</Card>
);
})}
</Grid>
);
}
Line and LimitLine from @umriss-ui/chartsA group whose name says what the tile shows: the label, the value with its unit, and where the verdict is not OK its words, as in "p95 latency at 09:50: 498 ms, Alarm limit exceeded". A missing value is named "No value". The verdict also stands in the tile as a word, so the colour is never alone; the history line is hidden from screen readers.
It announces nothing: a value that changes or a verdict that turns is read when a reader reaches the tile. Where a change must be heard at once, say it in an Alert.
StatProps| Name | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | What is being measured. Shown in: Configurator, A figure, Read a value against its limits and 15 more |
valuerequired | | number | — | The measured value. Absent or non-finite gives the verdict unknown –
and the tile says so, instead of showing a zero or an empty bar.Shown in: Configurator, A figure, Read a value against its limits and 15 more |
unit | string | — | The unit as it stands behind the value – „ms", „%", „GB". Shown in: Configurator, Read a value against its limits, Show a value nobody has and 14 more |
decimals | number | — | Fixed number of decimal places. Without it the notation decides; a metric
whose number of digits jumps reads as a jump in the value. Shown in: Configurator, A figure, Read a value against its limits and 12 more |
limits | LimitSet | — | The rule the value is read against. Without it the tile is neutral – the
honest display of a number for which nobody has named a rule. Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more |
history | readonly number[] | — | Values in chronological order; yields the history line, across the
tile beneath the figures, or beside them in a tile of 380 px and more. Shown in: Add a history, Give the history room, Watch a kiln zone and 4 more |
asOf | | Date | — | When the value was true – not when it was fetched. Shown in: Show how fresh a value is, Watch the services that page at night, Watch a kiln line over a shift (Scenarios) |
ages | FreshnessAges | — | Without ages there is no freshness display and no cadence. Shown in: Show how fresh a value is, Watch the services that page at night, Watch a kiln line over a shift (Scenarios) |
ref | Ref<HTMLDivElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
Also takes every attribute of <div> – without children.
LimitSetThe set a value is read against. The target stands beside the limits and is not one: it is missed, never violated, and it is never the edge of a region.
| Name | Type | Default | Description |
|---|---|---|---|
limits | readonly Limit[] | — | In any order; none, one or several. Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more |
target | number | — | Shown in: Measure against a target, Watch a kiln zone, English and German side by side (Language) and 3 more |
FreshnessAgesThe two ages at which a value changes its state, in milliseconds. Both
bounds belong to the older state: exactly five minutes old is already
stale.
alt <= lost is the sensible order. Stated the other way round, the
harder statement still wins - the middle state simply falls away.
| Name | Type | Default | Description |
|---|---|---|---|
stalerequired | number | — | From this age on the value counts as stale, but keeps its verdict. Shown in: Show how fresh a value is, Watch the services that page at night, Watch a kiln line over a shift (Scenarios) |
lostrequired | number | — | From this age on the connection counts as disconnected. Shown in: Show how fresh a value is, Watch the services that page at night, Watch a kiln line over a shift (Scenarios) |
LimitA bound: a value, a side, a severity.
No tolerance band as input. A band is derived - two limits of equal
severity on opposite sides enclose one - and as an input type it would
force a null on one edge in the one-sided case.
| Name | Type | Default | Description |
|---|---|---|---|
valuerequired | number | — | Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more |
siderequired | Side"upper" | "lower" | — | Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more |
severityrequired | Severity"warning" | "alarm" | — | Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more |
SideWhether a limit is an upper or a lower bound.
type Side = "upper" | "lower";SeverityHow seriously a violation weighs: one severity that advises and one that calls for action. Exactly two; for a third distinction one sets a second limit.
type Severity = "warning" | "alarm";asOf you give it.What umriss deliberately does not build, and why: ADR-0032.