{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slo-1",
  "title": "SLO 1: service level objectives",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Service level objectives as cards: the goal, how it is going, error budget left in percent and minutes as a labelled meter, burn rate in words, a chart with the target drawn on it, risky ones listed first, and a filter by service.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/slo-1/slo-1.tsx",
      "content": "// Ballmac UI: SLO 1. https://ui.ballmac.com/blocks/slo-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, Flame, ShieldCheck, TriangleAlert, type LucideIcon } from \"lucide-react\"\n\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Slo1Item = {\n  id: string\n  /** For example \"Checkout availability\". */\n  name: string\n  service: string\n  /** The goal, as a percentage, such as 99.9. */\n  target: number\n  /** How it is doing over the window, as a percentage, such as 99.94. */\n  current: number\n  /** Length of the window in days. */\n  windowDays: number\n  /** How fast the error budget is being used compared with a steady pace: 1 is exactly on pace. */\n  burn: number\n  /** Daily results over the window, oldest first, as percentages. */\n  series: number[]\n}\n\ntype Slo1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    slos?: Slo1Item[]\n    title?: string\n  }\n\nconst sample: Slo1Item[] = [\n  { id: \"s1\", name: \"Checkout availability\", service: \"Payments\", target: 99.9, current: 99.94, windowDays: 30, burn: 0.6, series: [100, 100, 99.98, 100, 99.9, 100, 100, 99.95, 100, 100, 99.99, 100, 100, 99.97] },\n  { id: \"s2\", name: \"Search latency under 300 ms\", service: \"Search\", target: 99.5, current: 99.62, windowDays: 30, burn: 1.4, series: [99.8, 99.6, 99.5, 99.2, 99.4, 99.1, 99.3, 99.4, 99.0, 99.2, 99.5, 99.3, 99.1, 99.3] },\n  { id: \"s3\", name: \"API success rate\", service: \"Platform\", target: 99.95, current: 99.71, windowDays: 30, burn: 4.8, series: [99.99, 99.98, 99.9, 99.8, 99.7, 99.6, 99.7, 99.5, 99.6, 99.7, 99.8, 99.7, 99.6, 99.7] },\n  { id: \"s4\", name: \"Image processing within 10 s\", service: \"Media\", target: 99, current: 99.62, windowDays: 30, burn: 0.3, series: [99.4, 99.7, 99.6, 99.8, 99.5, 99.6, 99.7, 99.6, 99.5, 99.7, 99.6, 99.8, 99.6, 99.7] },\n]\n\n/** How much of the error budget is left, as a percentage that can go below zero when the target is missed. */\nfunction budgetLeft(s: Slo1Item) {\n  const allowed = 100 - s.target\n  return allowed > 0 ? (1 - (100 - s.current) / allowed) * 100 : 0\n}\ntype Level = \"healthy\" | \"watch\" | \"burning\" | \"breached\"\nfunction levelOf(s: Slo1Item): Level {\n  const left = budgetLeft(s)\n  if (left <= 0) return \"breached\"\n  if (s.burn >= 2 || left < 25) return \"burning\"\n  if (s.burn > 1 || left < 50) return \"watch\"\n  return \"healthy\"\n}\nconst LEVELS: Record<Level, { icon: LucideIcon; label: string; tone: string }> = {\n  healthy: { icon: ShieldCheck, label: \"Healthy\", tone: \"text-foreground\" },\n  watch: { icon: TriangleAlert, label: \"Keep an eye on it\", tone: \"text-foreground\" },\n  burning: { icon: Flame, label: \"Burning budget fast\", tone: \"text-foreground font-medium\" },\n  breached: { icon: CircleAlert, label: \"Target missed\", tone: \"text-destructive font-medium\" },\n}\n\nfunction Spark({ values, target }: { values: number[]; target: number }) {\n  const lo = Math.min(...values, target)\n  const hi = Math.max(...values, 100)\n  const y = (v: number) => 28 - ((v - lo) / Math.max(hi - lo, 0.001)) * 26\n  const pts = values.map((v, i) => `${(i / Math.max(values.length - 1, 1)) * 100},${y(v)}`).join(\" \")\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 100 30\" preserveAspectRatio=\"none\" className=\"h-8 w-full overflow-visible\" style={{ direction: \"ltr\" }}>\n      <line x1=\"0\" x2=\"100\" y1={y(target)} y2={y(target)} stroke=\"currentColor\" strokeWidth=\"1\" strokeDasharray=\"3 3\" className=\"text-muted-foreground/60\" vectorEffect=\"non-scaling-stroke\" />\n      <polyline points={pts} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" vectorEffect=\"non-scaling-stroke\" strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n    </svg>\n  )\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-4 sm:grid-cols-2\">\n      {[0, 1, 2, 3].map((i) => (\n        <Skeleton key={i} className=\"h-52 rounded-xl\" />\n      ))}\n    </div>\n  )\n}\n\nconst pct = (n: number) => `${+n.toFixed(2)}%`\n\n/**\n * Service level objectives as cards: the goal, how it is going, how much of the error budget is left in percent and in minutes, how fast it is being used,\n * and a small chart with the target drawn on it. Anything burning budget fast or past its target is put first and written out. Filter by service.\n */\nfunction Slo1({ slos = sample, title = \"Service level objectives\", state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Slo1Props) {\n  const id = React.useId()\n  const [service, setService] = React.useState(\"all\")\n  const view = state === \"ready\" && slos.length === 0 ? \"empty\" : state\n  const services = [...new Set(slos.map((s) => s.service))]\n  const order: Level[] = [\"breached\", \"burning\", \"watch\", \"healthy\"]\n  const shown = slos.filter((s) => service === \"all\" || s.service === service).sort((a, b) => order.indexOf(levelOf(a)) - order.indexOf(levelOf(b)))\n  const atRisk = slos.filter((s) => levelOf(s) === \"breached\" || levelOf(s) === \"burning\").length\n\n  return (\n    <section data-slot=\"slo-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-5xl px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-8 flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h1 id={`${id}-title`} className=\"text-3xl font-semibold tracking-[-0.035em] text-balance\">\n            {title}\n          </h1>\n          <p className=\"text-muted-foreground mt-2 max-w-xl text-pretty\">How reliable each service has been against the promise made for it, and how much room for failure is left.</p>\n        </div>\n        {view === \"ready\" && services.length > 1 && (\n          <label className=\"flex items-center gap-2 text-sm\">\n            <span className=\"text-muted-foreground\">Service</span>\n            <select value={service} onChange={(e) => setService(e.target.value)} className=\"bg-background focus-visible:ring-ring/50 h-9 rounded-md border px-2 text-sm outline-none focus-visible:ring-[3px]\">\n              <option value=\"all\">All services</option>\n              {services.map((s) => (\n                <option key={s}>{s}</option>\n              ))}\n            </select>\n          </label>\n        )}\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading objectives\" loading={<Skel />} empty={{ title: \"No objectives yet\", description: \"Set a target for a service, and its error budget will be tracked here.\", ...emptyState }} error={{ title: \"The objectives could not be loaded\", description: \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        <p aria-live=\"polite\" className=\"text-muted-foreground mb-4 text-sm\">\n          {atRisk ? <><strong className=\"text-foreground\">{atRisk} {atRisk === 1 ? \"objective needs\" : \"objectives need\"} attention.</strong> They are listed first.</> : \"Every objective is on track.\"}\n        </p>\n        <ul className=\"grid gap-4 sm:grid-cols-2\">\n          {shown.map((s) => {\n            const left = budgetLeft(s)\n            const lv = levelOf(s)\n            const L = LEVELS[lv]\n            const Icon = L.icon\n            const minutes = s.windowDays * 24 * 60 * ((100 - s.target) / 100)\n            const minutesLeft = Math.max(0, (minutes * Math.max(left, 0)) / 100)\n            return (\n              <li key={s.id} data-level={lv} className={cn(\"bg-card flex flex-col gap-4 rounded-xl border p-5\", lv === \"breached\" && \"border-destructive/50\")}>\n                <div className=\"flex items-start justify-between gap-3\">\n                  <div className=\"min-w-0\">\n                    <p className=\"font-semibold tracking-tight\">{s.name}</p>\n                    <p className=\"text-muted-foreground text-xs\">{s.service} · target {pct(s.target)} over {s.windowDays} days</p>\n                  </div>\n                  <span className={cn(\"inline-flex shrink-0 items-center gap-1.5 text-xs\", L.tone)}>\n                    <Icon aria-hidden=\"true\" className=\"size-3.5\" />\n                    {L.label}\n                  </span>\n                </div>\n                <p className=\"flex items-baseline gap-2\">\n                  <span className=\"text-3xl font-semibold tracking-tight tabular-nums\">{pct(s.current)}</span>\n                  <span className=\"text-muted-foreground text-xs\">now</span>\n                </p>\n                <div>\n                  <div role=\"meter\" aria-label={`Error budget left for ${s.name}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.max(0, Math.round(left))} aria-valuetext={left <= 0 ? \"No error budget left\" : `${Math.round(left)} percent of the error budget left, about ${Math.round(minutesLeft)} of ${Math.round(minutes)} minutes`} className=\"bg-muted h-2 overflow-hidden rounded-full\">\n                    <div className={cn(\"h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none\", lv === \"breached\" ? \"bg-destructive\" : lv === \"burning\" ? \"bg-chart-3\" : \"bg-foreground\")} style={{ width: `${Math.min(100, Math.max(0, left))}%` }} />\n                  </div>\n                  <p className=\"text-muted-foreground mt-1.5 text-xs tabular-nums\">{left <= 0 ? \"No error budget left.\" : `${Math.round(left)}% of the error budget left, about ${Math.round(minutesLeft)} of ${Math.round(minutes)} minutes.`}</p>\n                </div>\n                <div className=\"text-muted-foreground\"><Spark values={s.series} target={s.target} /></div>\n                <p className=\"text-xs tabular-nums\">\n                  {s.burn > 1 ? <span className=\"text-foreground font-medium\">Using budget {+s.burn.toFixed(1)}× faster than a steady pace.</span> : <span className=\"text-muted-foreground\">Using budget at {+s.burn.toFixed(1)}× a steady pace.</span>}\n                </p>\n              </li>\n            )\n          })}\n        </ul>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Slo1, budgetLeft, type Slo1Item, type Slo1Props }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/slo-1/slo-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "slo",
      "sli",
      "reliability",
      "error budget",
      "sre",
      "uptime",
      "monitoring",
      "devops"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Service level objectives. <Slo1 slos={[{ id, name, service, target: 99.9, current: 99.94, windowDays: 30, burn: 0.6, series: [99.9, 100, ...] }]} />. Error budget left = 1 minus the shortfall divided by the allowed shortfall; it is shown in percent and minutes. Order is target missed, burning fast (burn 2 or more, or under 25% left), watch, healthy. Exports budgetLeft.",
      "whenToUse": [
        "A reliability overview for a team that promises uptime or speed",
        "Deciding whether it is safe to ship or time to slow down"
      ],
      "whenNotToUse": [
        "A public status page; use status-1",
        "Live charts of a metric; use charts-1"
      ],
      "composesWith": [
        "incident-1",
        "queues-1",
        "status-1",
        "dashboard-1"
      ],
      "a11y": [
        {
          "keys": "Budget",
          "action": "A meter per objective with a spoken value such as '40 percent of the error budget left, about 17 of 43 minutes'"
        },
        {
          "keys": "Level",
          "action": "Written as words with an icon (Healthy, Burning budget fast, Target missed); colour only reinforces it"
        },
        {
          "keys": "Order",
          "action": "Objectives that need attention are first, and the sentence above the list says how many"
        },
        {
          "keys": "Service filter",
          "action": "A labelled native select"
        }
      ],
      "customization": [
        "slos with target, current, burn and series",
        "windowDays per objective",
        "budgetLeft is exported for your own use"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "slo-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton cards under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No objectives yet",
        "example": "slo-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says no objectives yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "slo-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry)."
      }
    ],
    "examples": [
      "slo-1-demo",
      "slo-1-healthy",
      "slo-1-loading",
      "slo-1-empty",
      "slo-1-error"
    ],
    "url": "https://ui.ballmac.com/components/slo-1"
  },
  "categories": [
    "blocks",
    "slo"
  ],
  "type": "registry:block"
}