Stat

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.

Examples

A figure

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>
  );
}

Read a value against its limits

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>
  );
}

Show a value nobody has

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>
  );
}

Add a history

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>
  );
}

Give the history room

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>
  );
}

Measure against a target

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>
  );
}

Show how fresh a value is

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>
  );
}

Watch a kiln zone

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>
  );
}

Watch the services that page at night

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>
  );
}

When to use something else

Accessibility

A 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.

API

StatProps

NameTypeDefaultDescription
labelrequiredstring—What is being measured.

Shown in: Configurator, A figure, Read a value against its limits and 15 more

valuerequired| number
| null
| undefined
—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

unitstring—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

decimalsnumber—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

limitsLimitSet—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

historyreadonly 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
| number
| null
—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)

agesFreshnessAges—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)

refRef<HTMLDivElement>—Receives the component's root element. from RefProp

Shown 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.

Types on this page

LimitSet

The 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.

NameTypeDefaultDescription
limitsreadonly 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

targetnumber—

Shown in: Measure against a target, Watch a kiln zone, English and German side by side (Language) and 3 more

FreshnessAges

The 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.

NameTypeDefaultDescription
stalerequirednumber—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)

lostrequirednumber—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)

Limit

A 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.

NameTypeDefaultDescription
valuerequirednumber—

Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more

siderequiredSide
"upper" | "lower"
—

Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more

severityrequiredSeverity
"warning" | "alarm"
—

Shown in: Read a value against its limits, Show a value nobody has, Add a history and 12 more

Side

Whether a limit is an upper or a lower bound.

type Side = "upper" | "lower";

Severity

How 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";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index