sermon-js 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/api/index.d.cts +4 -4
- package/dist/api/index.d.ts +4 -4
- package/dist/{chunk-PARE6EUZ.cjs → chunk-NZTKXXW2.cjs} +7 -3
- package/dist/chunk-NZTKXXW2.cjs.map +1 -0
- package/dist/{chunk-2M4UBCDM.js → chunk-R7BDAQOM.js} +7 -3
- package/dist/chunk-R7BDAQOM.js.map +1 -0
- package/dist/{config-BEbP-uLf.d.cts → config-gro29syt.d.cts} +1 -1
- package/dist/{config-w1tm1bdu.d.ts → config-iY18j9IA.d.ts} +1 -1
- package/dist/{generic-lmrGxMDC.d.cts → generic-BUiaKpNp.d.cts} +2 -2
- package/dist/{generic-BJmlgtUM.d.ts → generic-CV2O4aNH.d.ts} +2 -2
- package/dist/index.cjs +6 -6
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +2 -2
- package/dist/presets/index.cjs +552 -47
- package/dist/presets/index.cjs.map +1 -1
- package/dist/presets/index.d.cts +261 -13
- package/dist/presets/index.d.ts +261 -13
- package/dist/presets/index.js +542 -48
- package/dist/presets/index.js.map +1 -1
- package/dist/react/heroui/index.cjs +27 -18
- package/dist/react/heroui/index.cjs.map +1 -1
- package/dist/react/heroui/index.d.cts +7 -2
- package/dist/react/heroui/index.d.ts +7 -2
- package/dist/react/heroui/index.js +13 -4
- package/dist/react/heroui/index.js.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/{status-DsWzgb9Q.d.cts → status-BExBb08T.d.cts} +3 -3
- package/dist/{status-zd04ZQ98.d.ts → status-DmRhwvVC.d.ts} +3 -3
- package/dist/{store-_GuzSP0z.d.ts → store-BfKP7CjW.d.ts} +1 -1
- package/dist/{store-DybhDphS.d.cts → store-CAIB7Esp.d.cts} +1 -1
- package/dist/stores/mongodb.d.cts +2 -2
- package/dist/stores/mongodb.d.ts +2 -2
- package/dist/stores/redis.d.cts +2 -2
- package/dist/stores/redis.d.ts +2 -2
- package/dist/stores/sql.d.cts +2 -2
- package/dist/stores/sql.d.ts +2 -2
- package/dist/stores/upstash.d.cts +2 -2
- package/dist/stores/upstash.d.ts +2 -2
- package/dist/testing/index.cjs +3 -3
- package/dist/testing/index.d.cts +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +1 -1
- package/dist/{tree-BXbXfjF1.d.cts → tree-J3riUyI9.d.cts} +9 -0
- package/dist/{tree-BXbXfjF1.d.ts → tree-J3riUyI9.d.ts} +9 -0
- package/dist/{types-CctvUAs2.d.cts → types-BVa6vIKk.d.cts} +9 -0
- package/dist/{types-CctvUAs2.d.ts → types-BVa6vIKk.d.ts} +9 -0
- package/package.json +1 -1
- package/dist/chunk-2M4UBCDM.js.map +0 -1
- package/dist/chunk-PARE6EUZ.cjs.map +0 -1
|
@@ -257,12 +257,13 @@ var FLY_REGIONS = {
|
|
|
257
257
|
function InstanceTable({
|
|
258
258
|
instances,
|
|
259
259
|
stats,
|
|
260
|
-
regionNames = FLY_REGIONS
|
|
260
|
+
regionNames = FLY_REGIONS,
|
|
261
|
+
labels
|
|
261
262
|
}) {
|
|
262
263
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "overflow-x-auto", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "table", { className: "w-full min-w-[34rem] text-xs", children: [
|
|
263
264
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "thead", { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { className: "text-left text-[10px] uppercase tracking-wide text-muted", children: [
|
|
264
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 font-normal", children: "Region" }),
|
|
265
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 font-normal", children: "Machine" }),
|
|
265
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 font-normal", children: _nullishCoalesce(_optionalChain([labels, 'optionalAccess', _2 => _2.key]), () => ( "Region")) }),
|
|
266
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 font-normal", children: labels ? _nullishCoalesce(labels.detail, () => ( "")) : "Machine" }),
|
|
266
267
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 font-normal", children: "Status" }),
|
|
267
268
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 text-right font-normal", children: "Response" }),
|
|
268
269
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "th", { className: "py-1 pr-3 text-right font-normal", children: "p50 24h" }),
|
|
@@ -270,14 +271,14 @@ function InstanceTable({
|
|
|
270
271
|
] }) }),
|
|
271
272
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "tbody", { className: "divide-y divide-border", children: instances.map((instance) => {
|
|
272
273
|
const tone = _chunkD7BBJTYUcjs.STATUS_TONE[instance.status];
|
|
273
|
-
const instanceStats = _optionalChain([stats, 'optionalAccess',
|
|
274
|
+
const instanceStats = _optionalChain([stats, 'optionalAccess', _3 => _3[instance.key]]);
|
|
274
275
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "tr", { className: "align-top", children: [
|
|
275
276
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex items-center gap-2", children: [
|
|
276
277
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: `size-2 shrink-0 rounded-full ${TONE_BG[tone]}`, "aria-hidden": true }),
|
|
277
278
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "font-mono", children: instance.key }),
|
|
278
279
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted", children: _nullishCoalesce((instance.region && regionNames[instance.region]), () => ( "")) })
|
|
279
280
|
] }) }),
|
|
280
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3 font-mono text-muted", title: instance.id, children: [_optionalChain([instance, 'access',
|
|
281
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3 font-mono text-muted", title: instance.id, children: [_optionalChain([instance, 'access', _4 => _4.id, 'optionalAccess', _5 => _5.slice, 'call', _6 => _6(0, 8)]), instance.state].filter(Boolean).join(" \xB7 ") }),
|
|
281
282
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "td", { className: "py-1.5 pr-3", children: [
|
|
282
283
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: TONE_TEXT[tone], children: _chunkD7BBJTYUcjs.STATUS_TEXT[instance.status] }),
|
|
283
284
|
instance.error && instance.status !== "up" && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "block text-muted", children: [
|
|
@@ -286,8 +287,8 @@ function InstanceTable({
|
|
|
286
287
|
] })
|
|
287
288
|
] }),
|
|
288
289
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3 text-right tabular-nums", children: instance.latencyMs !== void 0 ? _chunkD7BBJTYUcjs.formatMs.call(void 0, instance.latencyMs) : "\u2013" }),
|
|
289
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3 text-right tabular-nums", children: _chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([instanceStats, 'optionalAccess',
|
|
290
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 text-right tabular-nums", children: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([instanceStats, 'optionalAccess',
|
|
290
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 pr-3 text-right tabular-nums", children: _chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([instanceStats, 'optionalAccess', _7 => _7.p50])) }),
|
|
291
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "td", { className: "py-1.5 text-right tabular-nums", children: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([instanceStats, 'optionalAccess', _8 => _8.uptime24h])) })
|
|
291
292
|
] }, instance.key);
|
|
292
293
|
}) })
|
|
293
294
|
] }) });
|
|
@@ -320,7 +321,7 @@ function HistoryBars({ names = {} }) {
|
|
|
320
321
|
return () => observer.disconnect();
|
|
321
322
|
}, []);
|
|
322
323
|
const { bars, factor } = _react3.useMemo.call(void 0,
|
|
323
|
-
() => _chunkD7BBJTYUcjs.mergeBars.call(void 0, _nullishCoalesce(_optionalChain([history2, 'access',
|
|
324
|
+
() => _chunkD7BBJTYUcjs.mergeBars.call(void 0, _nullishCoalesce(_optionalChain([history2, 'access', _9 => _9.data, 'optionalAccess', _10 => _10.bars]), () => ( [])), width ? Math.floor(width / MIN_BAR_PX) : Infinity),
|
|
324
325
|
[history2.data, width]
|
|
325
326
|
);
|
|
326
327
|
const active = hovered !== null && bars[hovered] || bars.findLast((bar) => bar.runs > 0);
|
|
@@ -542,7 +543,7 @@ function ServiceRow({ service }) {
|
|
|
542
543
|
const { result, stats } = service;
|
|
543
544
|
const failing = status === "down" || status === "degraded";
|
|
544
545
|
const rateLimited = run.error instanceof _chunkD7BBJTYUcjs.RateLimitError ? run.error : void 0;
|
|
545
|
-
const lastFailure = !failing && result && _optionalChain([stats, 'optionalAccess',
|
|
546
|
+
const lastFailure = !failing && result && _optionalChain([stats, 'optionalAccess', _11 => _11.lastFailure]) && result.checkedAt - stats.lastFailure.at < DAY_MS ? stats.lastFailure : void 0;
|
|
546
547
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
547
548
|
"div",
|
|
548
549
|
{
|
|
@@ -587,21 +588,29 @@ function ServiceRow({ service }) {
|
|
|
587
588
|
] })
|
|
588
589
|
] }),
|
|
589
590
|
service.configured && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3 pl-5.5", children: [
|
|
590
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Response", value: _optionalChain([result, 'optionalAccess',
|
|
591
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "p50 / p95 24h", value: `${_chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([stats, 'optionalAccess',
|
|
592
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Uptime 24h", value: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([stats, 'optionalAccess',
|
|
593
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Uptime 7d", value: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([stats, 'optionalAccess',
|
|
591
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Response", value: _optionalChain([result, 'optionalAccess', _12 => _12.probed]) === false ? "not probed" : _chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([result, 'optionalAccess', _13 => _13.latencyMs])) }),
|
|
592
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "p50 / p95 24h", value: `${_chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([stats, 'optionalAccess', _14 => _14.p50]))} / ${_chunkD7BBJTYUcjs.formatMs.call(void 0, _optionalChain([stats, 'optionalAccess', _15 => _15.p95]))}` }),
|
|
593
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Uptime 24h", value: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([stats, 'optionalAccess', _16 => _16.uptime24h])) }),
|
|
594
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Uptime 7d", value: _chunkD7BBJTYUcjs.formatPct.call(void 0, _optionalChain([stats, 'optionalAccess', _17 => _17.uptime7d])) }),
|
|
594
595
|
stats && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Sparkline, { values: stats.latency, threshold: service.slowMs }),
|
|
595
596
|
result && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Stat, { label: "Checked", value: _chunkD7BBJTYUcjs.timeAgo.call(void 0, result.checkedAt) })
|
|
596
597
|
] }),
|
|
597
|
-
_optionalChain([result, 'optionalAccess',
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
_optionalChain([result, 'optionalAccess', _18 => _18.instances]) && result.instances.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "pl-5.5", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
599
|
+
InstanceTable,
|
|
600
|
+
{
|
|
601
|
+
instances: result.instances,
|
|
602
|
+
stats: _optionalChain([stats, 'optionalAccess', _19 => _19.instances]),
|
|
603
|
+
regionNames,
|
|
604
|
+
labels: result.instanceLabels
|
|
605
|
+
}
|
|
606
|
+
) }),
|
|
607
|
+
_optionalChain([result, 'optionalAccess', _20 => _20.usage]) && result.usage.length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex flex-col gap-3 pl-5.5", children: result.usage.map((usage) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, UsageBar, { usage }, usage.label)) }),
|
|
608
|
+
_optionalChain([result, 'optionalAccess', _21 => _21.metrics]) && Object.keys(result.metrics).length > 0 && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex flex-wrap gap-1.5 pl-5.5", children: Object.entries(result.metrics).filter(([, value]) => value !== "").map(([key, value]) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _react.Chip, { size: "sm", variant: "secondary", children: [
|
|
600
609
|
_chunkD7BBJTYUcjs.humanize.call(void 0, key),
|
|
601
610
|
": ",
|
|
602
611
|
value
|
|
603
612
|
] }, key)) }),
|
|
604
|
-
failing && _optionalChain([result, 'optionalAccess',
|
|
613
|
+
failing && _optionalChain([result, 'optionalAccess', _22 => _22.error]) && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
605
614
|
"div",
|
|
606
615
|
{
|
|
607
616
|
className: `ml-5.5 rounded-lg px-3 py-2 font-mono text-xs break-words ${status === "down" ? "bg-danger/10 text-danger" : "bg-warning/10 text-warning"}`,
|
|
@@ -737,7 +746,7 @@ function Page({
|
|
|
737
746
|
const snapshot = status.data;
|
|
738
747
|
const blocks = _react3.useMemo.call(void 0, () => snapshot ? _chunkD7BBJTYUcjs.buildLayout.call(void 0, snapshot.tree, { ungroupedTitle }) : [], [snapshot, ungroupedTitle]);
|
|
739
748
|
const names = _react3.useMemo.call(void 0,
|
|
740
|
-
() => Object.fromEntries(_chunkD7BBJTYUcjs.flattenServices.call(void 0, _nullishCoalesce(_optionalChain([snapshot, 'optionalAccess',
|
|
749
|
+
() => Object.fromEntries(_chunkD7BBJTYUcjs.flattenServices.call(void 0, _nullishCoalesce(_optionalChain([snapshot, 'optionalAccess', _23 => _23.tree]), () => ( []))).map((service) => [service.key, service.name])),
|
|
741
750
|
[snapshot]
|
|
742
751
|
);
|
|
743
752
|
const handledHash = _react3.useRef.call(void 0, false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/sermon-js/sermon-js/dist/react/heroui/index.cjs","../../../src/react/heroui/StatusPage.tsx","../../../src/react/heroui/controls.tsx","../../../src/react/heroui/icons.tsx","../../../src/react/heroui/details.tsx","../../../src/react/heroui/tones.ts","../../../src/react/heroui/HistoryBars.tsx","../../../src/react/heroui/ScoreHeader.tsx","../../../src/react/heroui/ServiceNav.tsx","../../../src/react/heroui/ServiceRow.tsx","../../../src/react/heroui/ServiceSection.tsx"],"names":["jsxs","jsx","Icon","history","Button"],"mappings":"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","file":"/home/runner/work/sermon-js/sermon-js/dist/react/heroui/index.cjs","sourcesContent":[null,"import { Card } from \"@heroui/react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { currentAnchor, focusAnchor } from \"../anchors\";\nimport type { StatusClient } from \"../client\";\nimport { formatDuration } from \"../format\";\nimport { StatusProvider, useStatus } from \"../hooks\";\nimport { buildLayout, flattenServices } from \"../tree\";\nimport { RunAllButton, StaleNotice, ThemeSwitcher } from \"./controls\";\nimport { FLY_REGIONS } from \"./details\";\nimport { HistoryBars } from \"./HistoryBars\";\nimport { ScoreHeader } from \"./ScoreHeader\";\nimport { ServiceNav } from \"./ServiceNav\";\nimport { KitOptions } from \"./ServiceRow\";\nimport { ServiceSection } from \"./ServiceSection\";\n\nexport type StatusPageProps = {\n /** Where `createStatusHandler` is mounted (default \"/api\") */\n baseUrl?: string;\n /** Or a ready client, e.g. with a custom `fetch` */\n client?: StatusClient;\n /** Your logo and title, shown at the top left (default: \"Status\") */\n brand?: ReactNode;\n /** Replaces the default footer text */\n footer?: ReactNode;\n /** Title of the section holding services declared outside any group (default \"Services\") */\n ungroupedTitle?: string;\n /** Start sections open for viewers who haven't chosen (default false) */\n defaultExpanded?: boolean;\n /** Offer on-demand runs: \"Run all checks\" and \"Check now\" (default true) */\n allowRuns?: boolean;\n /** Show the side index on wide screens (default true) */\n showNav?: boolean;\n /** Show the system / light / dark switch (default true) */\n showThemeSwitcher?: boolean;\n /** City names for instance regions (default: Fly.io's regions) */\n regionNames?: Record<string, string>;\n /** How often the page re-reads the API, in ms (default 30000) */\n refetchInterval?: number | false;\n /** Use your own React Query client instead of the built-in one */\n queryClient?: QueryClient;\n};\n\nconst defaultQueryClient = new QueryClient({ defaultOptions: { queries: { retry: 1 } } });\n\nfunction Page({\n brand,\n footer,\n ungroupedTitle,\n defaultExpanded,\n allowRuns = true,\n showNav = true,\n showThemeSwitcher = true,\n refetchInterval,\n}: StatusPageProps) {\n const status = useStatus({ refetchInterval });\n const snapshot = status.data;\n const blocks = useMemo(() => (snapshot ? buildLayout(snapshot.tree, { ungroupedTitle }) : []), [snapshot, ungroupedTitle]);\n const names = useMemo(\n () => Object.fromEntries(flattenServices(snapshot?.tree ?? []).map((service) => [service.key, service.name])),\n [snapshot]\n );\n\n // Deep links (#service-…, #group-…): open the right section once the data is there\n const handledHash = useRef(false);\n useEffect(() => {\n if (handledHash.current || !snapshot) return;\n handledHash.current = true;\n const anchor = currentAnchor();\n if (!anchor) return;\n // Otherwise the browser restores the previous scroll position after we jump\n history.scrollRestoration = \"manual\";\n focusAnchor(anchor, { smooth: false });\n }, [snapshot]);\n\n // The hash can also change on an open page: back/forward, or a link that only changes it\n useEffect(() => {\n const onHashChange = () => {\n const anchor = currentAnchor();\n if (anchor) focusAnchor(anchor);\n };\n window.addEventListener(\"hashchange\", onHashChange);\n return () => window.removeEventListener(\"hashchange\", onHashChange);\n }, []);\n\n return (\n // On wide screens the service index is attached to the right of the main column\n <div className=\"mx-auto flex min-h-screen max-w-5xl gap-6 px-4 py-8 sm:px-6 xl:max-w-[calc(64rem+12rem+1.5rem)]\">\n <main className=\"flex min-w-0 flex-1 flex-col gap-6\">\n <header className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-3\">\n {brand ?? <h1 className=\"text-lg font-semibold\">Status</h1>}\n {snapshot && allowRuns && <RunAllButton />}\n </header>\n\n {status.isPending && <div className=\"h-40 animate-pulse rounded-3xl bg-surface\" />}\n {status.isError && (\n <Card>\n <Card.Content className=\"text-danger\">Could not load status: {status.error.message}</Card.Content>\n </Card>\n )}\n\n {snapshot && (\n <>\n <StaleNotice snapshot={snapshot} />\n <Card>\n <Card.Content className=\"flex flex-col gap-6\">\n <ScoreHeader snapshot={snapshot} />\n <HistoryBars names={names} />\n </Card.Content>\n </Card>\n\n {blocks.map((block) =>\n block.type === \"section\" ? (\n <ServiceSection key={block.section.key} section={block.section} defaultExpanded={defaultExpanded} />\n ) : (\n <div key={block.key} className=\"contents\">\n <div className=\"mt-2 flex flex-col\">\n <h2 className=\"text-lg font-semibold leading-snug\">{block.title}</h2>\n {block.description && <p className=\"text-sm leading-snug text-muted\">{block.description}</p>}\n </div>\n {block.sections.map((section) => (\n <ServiceSection key={section.key} section={section} defaultExpanded={defaultExpanded} />\n ))}\n </div>\n )\n )}\n </>\n )}\n\n <footer className=\"flex flex-wrap items-center justify-between gap-3 pb-4 text-xs text-muted\">\n <span>\n {footer ??\n (snapshot\n ? `Checks run every ${formatDuration(snapshot.intervalMs)} · history kept for ${formatDuration(snapshot.retentionMs)}`\n : \"\")}\n </span>\n {showThemeSwitcher && <ThemeSwitcher />}\n </footer>\n </main>\n\n {snapshot && showNav && (\n <aside className=\"sticky top-4 hidden w-48 shrink-0 self-start xl:block\">\n <ServiceNav blocks={blocks} />\n </aside>\n )}\n </div>\n );\n}\n\n/**\n * A complete status page: overall score, history, collapsible sections with per-service\n * details, a side index and a theme switch. It reads everything from the API of\n * `createStatusHandler`, so the page mirrors the `services` tree you configured.\n *\n * ```tsx\n * <StatusPage baseUrl=\"/api\" brand={<Logo />} />\n * ```\n */\nexport function StatusPage(props: StatusPageProps) {\n const options = useMemo(\n () => ({ regionNames: props.regionNames ?? FLY_REGIONS, allowRuns: props.allowRuns ?? true }),\n [props.regionNames, props.allowRuns]\n );\n return (\n <QueryClientProvider client={props.queryClient ?? defaultQueryClient}>\n <StatusProvider baseUrl={props.baseUrl} client={props.client}>\n <KitOptions.Provider value={options}>\n <Page {...props} />\n </KitOptions.Provider>\n </StatusProvider>\n </QueryClientProvider>\n );\n}\n","import { Button, ToggleButton, ToggleButtonGroup } from \"@heroui/react\";\nimport type { StatusSnapshot } from \"../../types\";\nimport { RateLimitError } from \"../client\";\nimport { formatDuration, timeAgo } from \"../format\";\nimport { useRunChecks } from \"../hooks\";\nimport { useThemePreference, type ThemePreference } from \"../theme\";\nimport { isStale } from \"../tree\";\nimport { AlertIcon, DesktopIcon, MoonIcon, RefreshIcon, SunIcon } from \"./icons\";\n\n/** Runs every check on demand; the icon spins while they run */\nexport function RunAllButton({ label = \"Run all checks\" }: { label?: string }) {\n const run = useRunChecks();\n const rateLimited = run.error instanceof RateLimitError ? run.error : undefined;\n return (\n <div className=\"flex items-center gap-3\">\n {rateLimited && <span className=\"text-xs text-muted\">Try again in {rateLimited.retryAfter}s</span>}\n {run.error && !rateLimited && <span className=\"text-xs text-danger\">{run.error.message}</span>}\n <Button size=\"sm\" variant=\"tertiary\" isDisabled={run.isPending} onPress={() => run.mutate()}>\n {/* Not for viewers who prefer reduced motion */}\n <RefreshIcon size={16} className={run.isPending ? \"motion-safe:animate-spin\" : undefined} />\n {run.isPending ? \"Running checks…\" : label}\n </Button>\n </div>\n );\n}\n\nconst THEMES: Array<{ id: ThemePreference; label: string; Icon: typeof SunIcon }> = [\n { id: \"system\", label: \"System theme\", Icon: DesktopIcon },\n { id: \"light\", label: \"Light theme\", Icon: SunIcon },\n { id: \"dark\", label: \"Dark theme\", Icon: MoonIcon },\n];\n\n/** A discreet system / light / dark switch, remembered per viewer */\nexport function ThemeSwitcher() {\n const [preference, setPreference] = useThemePreference();\n return (\n <ToggleButtonGroup\n aria-label=\"Color theme\"\n size=\"sm\"\n disallowEmptySelection\n selectedKeys={[preference]}\n onSelectionChange={(keys) => {\n const [key] = [...keys];\n if (key) setPreference(key as ThemePreference);\n }}\n className=\"opacity-60 transition-opacity hover:opacity-100 focus-within:opacity-100\"\n >\n {THEMES.map(({ id, label, Icon }, index) => (\n <ToggleButton key={id} id={id} isIconOnly aria-label={label}>\n {index > 0 && <ToggleButtonGroup.Separator />}\n <Icon size={14} />\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n );\n}\n\n/**\n * Shown when the last full run is older than twice the interval: the scheduler has probably\n * stopped, and the page would otherwise keep showing old results as if they were current.\n */\nexport function StaleNotice({ snapshot }: { snapshot: StatusSnapshot }) {\n // Judged against the server's clock at the time of the snapshot, not the viewer's\n if (!isStale(snapshot, snapshot.generatedAt) || snapshot.lastRunAt === null) return null;\n return (\n <div role=\"status\" className=\"flex items-start gap-2 rounded-lg bg-warning/10 px-3 py-2 text-sm text-warning\">\n <AlertIcon size={16} className=\"mt-0.5 shrink-0\" />\n <span>\n These results are out of date: the last full check ran {timeAgo(snapshot.lastRunAt)}, and checks should run\n every {formatDuration(snapshot.intervalMs)}.\n </span>\n </div>\n );\n}\n","import type { SVGProps } from \"react\";\n\n// A handful of line icons, inlined so the kit needs no icon package\nfunction Icon({ size = 16, children, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n {...props}\n >\n {children}\n </svg>\n );\n}\n\ntype IconProps = SVGProps<SVGSVGElement> & { size?: number };\n\nexport const RefreshIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4\" />\n <path d=\"M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4\" />\n </Icon>\n);\n\nexport const PlayIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M7 4v16l13-8z\" />\n </Icon>\n);\n\nexport const DesktopIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM7 20h10M9 16v4M15 16v4\" />\n </Icon>\n);\n\nexport const SunIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0M3 12h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7\" />\n </Icon>\n);\n\nexport const MoonIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M12 3h.4A7.5 7.5 0 0 0 20 14.4A9 9 0 1 1 12 3\" />\n </Icon>\n);\n\nexport const AlertIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M12 9v4m0 4h.01M10.4 3.6L2.3 17.2a1.9 1.9 0 0 0 1.6 2.9h16.2a1.9 1.9 0 0 0 1.6-2.9L13.6 3.6a1.9 1.9 0 0 0-3.2 0\" />\n </Icon>\n);\n","import { Label, Meter } from \"@heroui/react\";\nimport type { Instance, ServiceStats, Usage } from \"../../types\";\nimport { formatMs, formatPct, STATUS_TEXT, STATUS_TONE, timeAgo } from \"../format\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\nconst WIDTH = 120;\nconst HEIGHT = 28;\n\n/** Latency over the last 24h; gaps are runs without a measurement (asleep, down) */\nexport function Sparkline({ values, threshold }: { values: Array<number | null>; threshold: number }) {\n const points = values.slice(-96);\n const measured = points.filter((value): value is number => value !== null);\n if (measured.length < 2) {\n return (\n <div style={{ width: WIDTH, height: HEIGHT }} className=\"text-[10px] text-muted\">\n no latency data\n </div>\n );\n }\n const max = Math.max(threshold, ...measured);\n const step = WIDTH / Math.max(1, points.length - 1);\n const y = (value: number) => HEIGHT - 2 - (value / max) * (HEIGHT - 4);\n\n let path = \"\";\n points.forEach((value, index) => {\n if (value === null) return;\n const previous = points[index - 1];\n path += `${previous === null || previous === undefined ? \"M\" : \"L\"}${(index * step).toFixed(1)},${y(value).toFixed(1)} `;\n });\n\n return (\n <svg width={WIDTH} height={HEIGHT} viewBox={`0 0 ${WIDTH} ${HEIGHT}`} aria-hidden className=\"shrink-0\">\n <line\n x1={0}\n x2={WIDTH}\n y1={y(threshold)}\n y2={y(threshold)}\n className=\"stroke-warning\"\n strokeDasharray=\"2 3\"\n strokeWidth={1}\n opacity={0.6}\n />\n <path d={path} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} className=\"text-accent\" />\n </svg>\n );\n}\n\nconst number = new Intl.NumberFormat(\"en\");\n\n/** A quota as a bar with its numbers: green below 75%, amber from 75%, red from 90% */\nexport function UsageBar({ usage }: { usage: Usage }) {\n const pct = usage.limit > 0 ? (usage.used / usage.limit) * 100 : 0;\n const unit = usage.unit ? ` ${usage.unit}` : \"\";\n return (\n <div className=\"flex w-full max-w-md flex-col gap-1\">\n <Meter\n value={Math.min(usage.used, usage.limit)}\n maxValue={usage.limit}\n color={pct >= 90 ? \"danger\" : pct >= 75 ? \"warning\" : \"success\"}\n size=\"sm\"\n valueLabel={\n <span className=\"tabular-nums\">\n {number.format(usage.used)} / {number.format(usage.limit)}\n {unit} · {pct.toFixed(pct < 10 ? 1 : 0)}%\n </span>\n }\n >\n <Label className=\"text-xs\">{usage.label}</Label>\n <Meter.Output className=\"text-xs\" />\n <Meter.Track>\n <Meter.Fill />\n </Meter.Track>\n </Meter>\n {usage.resetsAt && (\n <span className=\"text-[11px] text-muted\">\n {number.format(usage.limit - usage.used)}\n {unit} left · resets {timeAgo(usage.resetsAt)}\n </span>\n )}\n </div>\n );\n}\n\n/** Fly.io region codes → city. Extend or replace with the `regionNames` prop. */\nexport const FLY_REGIONS: Record<string, string> = {\n ams: \"Amsterdam\", arn: \"Stockholm\", atl: \"Atlanta\", bog: \"Bogotá\", bom: \"Mumbai\", bos: \"Boston\",\n cdg: \"Paris\", den: \"Denver\", dfw: \"Dallas\", ewr: \"Secaucus\", eze: \"Buenos Aires\", fra: \"Frankfurt\",\n gdl: \"Guadalajara\", gig: \"Rio de Janeiro\", gru: \"São Paulo\", hkg: \"Hong Kong\", iad: \"Ashburn\",\n jnb: \"Johannesburg\", lax: \"Los Angeles\", lhr: \"London\", mad: \"Madrid\", mia: \"Miami\", nrt: \"Tokyo\",\n ord: \"Chicago\", otp: \"Bucharest\", phx: \"Phoenix\", qro: \"Querétaro\", scl: \"Santiago\", sea: \"Seattle\",\n sin: \"Singapore\", sjc: \"San Jose\", syd: \"Sydney\", waw: \"Warsaw\", yul: \"Montreal\", yyz: \"Toronto\",\n};\n\n/** One line per machine/region of a multi-instance service */\nexport function InstanceTable({\n instances,\n stats,\n regionNames = FLY_REGIONS,\n}: {\n instances: Instance[];\n stats?: ServiceStats[\"instances\"];\n regionNames?: Record<string, string>;\n}) {\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[34rem] text-xs\">\n <thead>\n <tr className=\"text-left text-[10px] uppercase tracking-wide text-muted\">\n <th className=\"py-1 pr-3 font-normal\">Region</th>\n <th className=\"py-1 pr-3 font-normal\">Machine</th>\n <th className=\"py-1 pr-3 font-normal\">Status</th>\n <th className=\"py-1 pr-3 text-right font-normal\">Response</th>\n <th className=\"py-1 pr-3 text-right font-normal\">p50 24h</th>\n <th className=\"py-1 text-right font-normal\">Uptime 24h</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {instances.map((instance) => {\n const tone = STATUS_TONE[instance.status];\n const instanceStats = stats?.[instance.key];\n return (\n <tr key={instance.key} className=\"align-top\">\n <td className=\"py-1.5 pr-3\">\n <span className=\"flex items-center gap-2\">\n <span className={`size-2 shrink-0 rounded-full ${TONE_BG[tone]}`} aria-hidden />\n <span className=\"font-mono\">{instance.key}</span>\n <span className=\"text-muted\">{(instance.region && regionNames[instance.region]) ?? \"\"}</span>\n </span>\n </td>\n <td className=\"py-1.5 pr-3 font-mono text-muted\" title={instance.id}>\n {[instance.id?.slice(0, 8), instance.state].filter(Boolean).join(\" · \")}\n </td>\n <td className=\"py-1.5 pr-3\">\n <span className={TONE_TEXT[tone]}>{STATUS_TEXT[instance.status]}</span>\n {instance.error && instance.status !== \"up\" && (\n <span className=\"block text-muted\">\n {instance.httpStatus ? `[${instance.httpStatus}] ` : \"\"}\n {instance.error}\n </span>\n )}\n </td>\n <td className=\"py-1.5 pr-3 text-right tabular-nums\">\n {instance.latencyMs !== undefined ? formatMs(instance.latencyMs) : \"–\"}\n </td>\n <td className=\"py-1.5 pr-3 text-right tabular-nums\">{formatMs(instanceStats?.p50)}</td>\n <td className=\"py-1.5 text-right tabular-nums\">{formatPct(instanceStats?.uptime24h)}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\n","import type { Tone } from \"../format\";\n\n// Tailwind classes for each tone, using HeroUI's theme colors. Written out in full so\n// Tailwind finds them when it scans this package.\nexport const TONE_BG: Record<Tone, string> = {\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-danger\",\n accent: \"bg-accent\",\n default: \"bg-default\",\n};\n\nexport const TONE_TEXT: Record<Tone, string> = {\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-danger\",\n accent: \"text-accent\",\n default: \"text-muted\",\n};\n","import { Chip, Tabs } from \"@heroui/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { HistoryBar, HistoryRange } from \"../../types\";\nimport { formatDateTime, formatDuration, LABEL_TEXT, LABEL_TONE } from \"../format\";\nimport { useHistory } from \"../hooks\";\nimport { mergeBars } from \"../tree\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\n/** Minimum room for one bar and its gap */\nconst MIN_BAR_PX = 3;\n\nfunction average(bars: HistoryBar[]) {\n const scored = bars.filter((bar) => bar.score !== null);\n if (!scored.length) return null;\n return Math.round(scored.reduce((sum, bar) => sum + (bar.score as number), 0) / scored.length);\n}\n\n/**\n * Score history as bars over a full-height gray track: the gray above a bar is what was\n * missing to 100, and an all-gray column means no data. Hover a bar for its details.\n */\nexport function HistoryBars({ names = {} }: { /** Service names by key, to label failures */ names?: Record<string, string> }) {\n const [range, setRange] = useState<HistoryRange>(\"24h\");\n const [hovered, setHovered] = useState<number | null>(null);\n const history = useHistory(range);\n\n // On narrow screens there isn't a few pixels per bar: merge neighbours until there is\n const chart = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number | null>(null);\n useEffect(() => {\n const element = chart.current;\n if (!element) return;\n const measure = () => setWidth(element.clientWidth);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n const { bars, factor } = useMemo(\n () => mergeBars(history.data?.bars ?? [], width ? Math.floor(width / MIN_BAR_PX) : Infinity),\n [history.data, width]\n );\n const active = (hovered !== null && bars[hovered]) || bars.findLast((bar) => bar.runs > 0);\n const avg = average(bars);\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-sm font-medium\">System score history</h3>\n <p className=\"text-xs text-muted\">\n {history.data ? `Worst score per ${formatDuration(history.data.bucketMs * factor)}` : \"Loading…\"}\n {avg !== null && ` · average ${avg}`}\n </p>\n </div>\n <Tabs selectedKey={range} onSelectionChange={(key) => setRange(key as HistoryRange)}>\n <Tabs.ListContainer>\n <Tabs.List aria-label=\"History range\">\n <Tabs.Tab id=\"24h\">\n 24h\n <Tabs.Indicator />\n </Tabs.Tab>\n <Tabs.Tab id=\"7d\">\n 7d\n <Tabs.Indicator />\n </Tabs.Tab>\n </Tabs.List>\n </Tabs.ListContainer>\n </Tabs>\n </div>\n\n <div\n ref={chart}\n className=\"flex h-24 items-end gap-px\"\n onMouseLeave={() => setHovered(null)}\n role=\"img\"\n aria-label={`System score over the last ${range}`}\n >\n {history.isPending &&\n Array.from({ length: 48 }, (_, index) => (\n <div key={index} className=\"h-1/3 flex-1 animate-pulse rounded-sm bg-default\" />\n ))}\n {bars.map((bar, index) => (\n <div\n key={bar.start}\n className={`relative h-full flex-1 cursor-default overflow-hidden rounded-sm bg-default transition-opacity ${\n hovered !== null && hovered !== index ? \"opacity-50\" : \"\"\n }`}\n onMouseEnter={() => setHovered(index)}\n >\n {bar.score !== null && (\n <div\n className={`absolute inset-x-0 bottom-0 ${TONE_BG[LABEL_TONE[bar.label]]}`}\n // A sliver stays visible even at a score of 0\n style={{ height: `${Math.max(3, bar.score)}%` }}\n />\n )}\n </div>\n ))}\n </div>\n\n <div className=\"flex justify-between text-[11px] text-muted\">\n <span>{range === \"24h\" ? \"24 hours ago\" : \"7 days ago\"}</span>\n <span>Now</span>\n </div>\n\n <div className=\"min-h-10 rounded-lg bg-surface-secondary px-3 py-2 text-xs\">\n {active ? (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-x-3\">\n <span className=\"font-medium\">{formatDateTime(active.start)}</span>\n <span className={TONE_TEXT[LABEL_TONE[active.label]]}>\n {active.score === null ? \"No data\" : `Score ${active.score} · ${LABEL_TEXT[active.label]}`}\n </span>\n {active.runs > 0 && <span className=\"text-muted\">{active.runs} run(s)</span>}\n </div>\n {Object.keys(active.failing).length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {Object.entries(active.failing).map(([key, status]) => (\n <Chip\n key={key}\n size=\"sm\"\n variant=\"soft\"\n color={status === \"down\" ? \"danger\" : \"warning\"}\n aria-label={`${names[key] ?? key}: ${status}`}\n >\n {names[key] ?? key}\n </Chip>\n ))}\n </div>\n )}\n </div>\n ) : (\n <span className=\"text-muted\">Hover a bar to see details</span>\n )}\n </div>\n </div>\n );\n}\n","import { Chip } from \"@heroui/react\";\nimport type { Status, StatusSnapshot } from \"../../types\";\nimport { formatDuration, LABEL_TEXT, LABEL_TONE, STATUS_TEXT, STATUS_TONE, timeAgo, type Tone } from \"../format\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\nconst SIZE = 88;\nconst STROKE = 8;\nconst RADIUS = (SIZE - STROKE) / 2;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\n/** The overall score as a ring */\nexport function ScoreRing({ score, tone }: { score: number | null; tone: Tone }) {\n const progress = score === null ? 0 : score / 100;\n return (\n <div className=\"relative shrink-0\" style={{ width: SIZE, height: SIZE }}>\n <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} className=\"-rotate-90\">\n <circle cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} fill=\"none\" strokeWidth={STROKE} className=\"stroke-default\" />\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n strokeDasharray={CIRCUMFERENCE}\n strokeDashoffset={CIRCUMFERENCE * (1 - progress)}\n className={`${TONE_TEXT[tone]} transition-[stroke-dashoffset] duration-700`}\n stroke=\"currentColor\"\n />\n </svg>\n <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n <span className=\"text-2xl font-semibold tabular-nums\">{score ?? \"–\"}</span>\n <span className=\"text-[10px] uppercase tracking-wide text-muted\">score</span>\n </div>\n </div>\n );\n}\n\nconst COUNT_ORDER: Status[] = [\"up\", \"degraded\", \"down\", \"sleeping\", \"unconfigured\"];\n\n/** Score ring, overall label, counts per status and when checks last ran */\nexport function ScoreHeader({ snapshot }: { snapshot: StatusSnapshot }) {\n const tone = LABEL_TONE[snapshot.label];\n return (\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-center\">\n <ScoreRing score={snapshot.score} tone={tone} />\n <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-xl font-semibold\">{LABEL_TEXT[snapshot.label]}</h2>\n <Chip color={tone} variant=\"soft\" size=\"sm\">\n {snapshot.label}\n </Chip>\n </div>\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted\">\n {COUNT_ORDER.filter((status) => snapshot.counts[status] > 0).map((status) => (\n <span key={status} className=\"flex items-center gap-1.5\">\n <span className={`size-2 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} />\n {snapshot.counts[status]} {STATUS_TEXT[status].toLowerCase()}\n </span>\n ))}\n </div>\n <p className=\"text-xs text-muted\">\n {snapshot.lastCheckedAt\n ? `Last checked ${timeAgo(snapshot.lastCheckedAt)} · checks run every ${formatDuration(snapshot.intervalMs)}`\n : \"No checks have run yet\"}\n </p>\n </div>\n </div>\n );\n}\n","import { Card } from \"@heroui/react\";\nimport type { ReactNode } from \"react\";\nimport { focusAnchor, groupAnchor, serviceAnchor } from \"../anchors\";\nimport { STATUS_TEXT, STATUS_TONE } from \"../format\";\nimport { servicesOf, statusOf, type LayoutBlock, type Section } from \"../tree\";\nimport { TONE_BG } from \"./tones\";\n\nfunction NavLink({\n anchor,\n className,\n children,\n label,\n}: {\n anchor: string;\n className: string;\n children: ReactNode;\n label?: string;\n}) {\n return (\n <a\n href={`#${encodeURIComponent(anchor)}`}\n aria-label={label}\n className={className}\n onClick={(event) => {\n // Let the browser handle \"open in new tab/window\"\n if (event.metaKey || event.ctrlKey || event.shiftKey) return;\n event.preventDefault();\n focusAnchor(anchor);\n }}\n >\n {children}\n </a>\n );\n}\n\nfunction SectionLinks({ section }: { section: Section }) {\n return (\n <li>\n <NavLink\n anchor={groupAnchor(section.key)}\n className=\"block rounded px-1.5 pt-1 text-[11px] leading-4 font-medium text-muted hover:text-foreground\"\n >\n {section.title}\n </NavLink>\n <ul>\n {servicesOf(section).map((service) => {\n const status = statusOf(service);\n return (\n <li key={service.key}>\n <NavLink\n anchor={serviceAnchor(service.key)}\n label={`${service.name}: ${STATUS_TEXT[status]}`}\n className=\"flex items-center gap-1.5 rounded px-1.5 text-xs leading-[18px] hover:bg-default\"\n >\n <span className={`size-1.5 shrink-0 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} aria-hidden />\n <span className=\"truncate\">{service.name}</span>\n </NavLink>\n </li>\n );\n })}\n </ul>\n </li>\n );\n}\n\n/** A compact index of every service with its status, linking to its place on the page */\nexport function ServiceNav({ blocks, title = \"Services\" }: { blocks: LayoutBlock[]; title?: string }) {\n return (\n <Card className=\"max-h-[calc(100vh-2rem)] gap-1 overflow-y-auto p-3\">\n <Card.Header>\n <Card.Title className=\"px-1.5 text-xs font-semibold\">{title}</Card.Title>\n </Card.Header>\n <Card.Content>\n <nav aria-label=\"Tracked services\">\n <ul className=\"flex flex-col gap-0.5\">\n {blocks.map((block) =>\n block.type === \"section\" ? (\n <SectionLinks key={block.section.key} section={block.section} />\n ) : (\n <li key={block.key}>\n <p className=\"mt-2 px-1.5 text-[10px] leading-4 font-semibold uppercase tracking-wide text-muted\">\n {block.title}\n </p>\n <ul className=\"flex flex-col gap-0.5\">\n {block.sections.map((section) => (\n <SectionLinks key={section.key} section={section} />\n ))}\n </ul>\n </li>\n )\n )}\n </ul>\n </nav>\n </Card.Content>\n </Card>\n );\n}\n","import { Button, Chip } from \"@heroui/react\";\nimport { createContext, useContext } from \"react\";\nimport type { ServiceSnapshot } from \"../../types\";\nimport { serviceAnchor } from \"../anchors\";\nimport { RateLimitError } from \"../client\";\nimport { formatDateTime, formatMs, formatPct, humanize, STATUS_TEXT, STATUS_TONE, timeAgo } from \"../format\";\nimport { useRunChecks } from \"../hooks\";\nimport { statusOf } from \"../tree\";\nimport { FLY_REGIONS, InstanceTable, Sparkline, UsageBar } from \"./details\";\nimport { PlayIcon } from \"./icons\";\nimport { TONE_BG } from \"./tones\";\n\n/** Display options shared by the kit's components */\nexport const KitOptions = createContext<{ regionNames: Record<string, string>; allowRuns: boolean }>({\n regionNames: FLY_REGIONS,\n allowRuns: true,\n});\n\nfunction Stat({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted\">{label}</span>\n <span className=\"text-sm tabular-nums\">{value}</span>\n </div>\n );\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Everything known about one service: status, response time, percentiles, uptime, sparkline,\n * per-instance rows, usage bars, metrics, the current error, and a \"Check now\" button.\n */\nexport function ServiceRow({ service }: { service: ServiceSnapshot }) {\n const run = useRunChecks();\n const { regionNames, allowRuns } = useContext(KitOptions);\n const status = statusOf(service);\n const tone = STATUS_TONE[status];\n const { result, stats } = service;\n const failing = status === \"down\" || status === \"degraded\";\n const rateLimited = run.error instanceof RateLimitError ? run.error : undefined;\n // \"Recent\" is measured from the latest check, not the viewer's clock\n const lastFailure =\n !failing && result && stats?.lastFailure && result.checkedAt - stats.lastFailure.at < DAY_MS\n ? stats.lastFailure\n : undefined;\n\n return (\n <div\n id={serviceAnchor(service.key)}\n className={`flex scroll-mt-6 flex-col gap-3 py-4 ${service.configured ? \"\" : \"opacity-60\"}`}\n >\n <div className=\"flex flex-wrap items-start gap-3\">\n <span className={`mt-1.5 size-2.5 shrink-0 rounded-full ${TONE_BG[tone]}`} aria-hidden />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium\">{service.name}</span>\n {service.critical && (\n <Chip size=\"sm\" variant=\"tertiary\">\n critical\n </Chip>\n )}\n <Chip size=\"sm\" variant=\"soft\" color={tone}>\n {STATUS_TEXT[status]}\n </Chip>\n </div>\n {service.description && <p className=\"text-xs text-muted\">{service.description}</p>}\n {!service.configured && service.missing && service.missing.length > 0 && (\n <p className=\"text-xs text-muted\">Missing: {service.missing.join(\", \")}</p>\n )}\n </div>\n {allowRuns && (\n <div className=\"flex items-center gap-2\">\n {rateLimited && <span className=\"text-xs text-muted\">wait {rateLimited.retryAfter}s</span>}\n <Button\n size=\"sm\"\n variant=\"ghost\"\n isDisabled={!service.configured}\n isPending={run.isPending}\n onPress={() => run.mutate(service.key)}\n aria-label={`Check ${service.name} now`}\n >\n <PlayIcon size={14} />\n Check now\n </Button>\n </div>\n )}\n </div>\n\n {service.configured && (\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 pl-5.5\">\n <Stat label=\"Response\" value={result?.probed === false ? \"not probed\" : formatMs(result?.latencyMs)} />\n <Stat label=\"p50 / p95 24h\" value={`${formatMs(stats?.p50)} / ${formatMs(stats?.p95)}`} />\n <Stat label=\"Uptime 24h\" value={formatPct(stats?.uptime24h)} />\n <Stat label=\"Uptime 7d\" value={formatPct(stats?.uptime7d)} />\n {stats && <Sparkline values={stats.latency} threshold={service.slowMs} />}\n {result && <Stat label=\"Checked\" value={timeAgo(result.checkedAt)} />}\n </div>\n )}\n\n {result?.instances && result.instances.length > 0 && (\n <div className=\"pl-5.5\">\n <InstanceTable instances={result.instances} stats={stats?.instances} regionNames={regionNames} />\n </div>\n )}\n\n {result?.usage && result.usage.length > 0 && (\n <div className=\"flex flex-col gap-3 pl-5.5\">\n {result.usage.map((usage) => (\n <UsageBar key={usage.label} usage={usage} />\n ))}\n </div>\n )}\n\n {result?.metrics && Object.keys(result.metrics).length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 pl-5.5\">\n {Object.entries(result.metrics)\n .filter(([, value]) => value !== \"\")\n .map(([key, value]) => (\n <Chip key={key} size=\"sm\" variant=\"secondary\">\n {humanize(key)}: {value}\n </Chip>\n ))}\n </div>\n )}\n\n {failing && result?.error && (\n <div\n className={`ml-5.5 rounded-lg px-3 py-2 font-mono text-xs break-words ${\n status === \"down\" ? \"bg-danger/10 text-danger\" : \"bg-warning/10 text-warning\"\n }`}\n >\n {result.httpStatus ? `[${result.httpStatus}] ` : \"\"}\n {result.error}\n </div>\n )}\n\n {lastFailure && (\n <p className=\"pl-5.5 text-xs text-muted\">\n Last incident {formatDateTime(lastFailure.at)} ({lastFailure.status})\n {lastFailure.error ? `: ${lastFailure.error}` : \"\"}\n </p>\n )}\n\n {run.error && !rateLimited && <p className=\"pl-5.5 text-xs text-danger\">{run.error.message}</p>}\n </div>\n );\n}\n","import { Button, Card, Disclosure, Tooltip } from \"@heroui/react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { ServiceSnapshot } from \"../../types\";\nimport { FOCUS_EVENT, groupAnchor, serviceAnchor, type FocusDetail } from \"../anchors\";\nimport { STATUS_TEXT, STATUS_TONE } from \"../format\";\nimport { servicesOf, statusOf, type Section } from \"../tree\";\nimport { ServiceRow } from \"./ServiceRow\";\nimport { TONE_BG } from \"./tones\";\n\n/** Collapsed sections are a per-viewer preference, kept in localStorage when available */\nfunction useStoredExpanded(key: string, defaultExpanded: boolean) {\n const storageKey = `sermon:section:${key}`;\n const [expanded, setExpanded] = useState(() => {\n try {\n const stored = localStorage.getItem(storageKey);\n return stored === null ? defaultExpanded : stored === \"expanded\";\n } catch {\n return defaultExpanded;\n }\n });\n const update = useCallback(\n (value: boolean) => {\n setExpanded(value);\n try {\n localStorage.setItem(storageKey, value ? \"expanded\" : \"collapsed\");\n } catch {\n // Storage unavailable (private mode…): the state just isn't remembered\n }\n },\n [storageKey]\n );\n return [expanded, update] as const;\n}\n\n/** One circle per service, colored by status; the tooltip names the service */\nfunction ServiceDots({ services }: { services: ServiceSnapshot[] }) {\n return (\n <div className=\"flex min-w-0 flex-wrap items-center justify-end gap-1\">\n {services.map((service) => {\n const status = statusOf(service);\n return (\n <Tooltip key={service.key} delay={100} closeDelay={0}>\n <Tooltip.Trigger\n aria-label={`${service.name}: ${STATUS_TEXT[status]}`}\n className=\"flex size-4 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-focus\"\n >\n <span className={`size-2.5 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} />\n </Tooltip.Trigger>\n <Tooltip.Content>{service.name}</Tooltip.Content>\n </Tooltip>\n );\n })}\n </div>\n );\n}\n\n/** The rows of a group, then its nested groups under small headings */\nfunction SectionBody({ section, nested = false }: { section: Section; nested?: boolean }) {\n return (\n <>\n {nested && (\n <div id={groupAnchor(section.key)} className=\"mt-4 scroll-mt-6\">\n <h4 className=\"text-xs font-medium uppercase tracking-wide text-muted\">{section.title}</h4>\n {section.description && <p className=\"text-xs text-muted\">{section.description}</p>}\n </div>\n )}\n {section.services.length > 0 && (\n <div className=\"divide-y divide-border\">\n {section.services.map((service) => (\n <ServiceRow key={service.key} service={service} />\n ))}\n </div>\n )}\n {section.groups.map((group) => (\n <SectionBody key={group.key} section={group} nested />\n ))}\n </>\n );\n}\n\n/**\n * A group of services as a collapsible card. The header shows one status circle per service,\n * so a collapsed section still shows if something is wrong.\n */\nexport function ServiceSection({\n section,\n defaultExpanded = false,\n}: {\n section: Section;\n /** Whether the section starts open for viewers who haven't chosen (default false) */\n defaultExpanded?: boolean;\n}) {\n const [expanded, setExpanded] = useStoredExpanded(section.key, defaultExpanded);\n const services = useMemo(() => servicesOf(section), [section]);\n\n // Navigating to this section, one of its groups or one of its services opens it first\n useEffect(() => {\n const collectGroups = (s: Section): string[] => [groupAnchor(s.key), ...s.groups.flatMap(collectGroups)];\n const anchors = new Set([...collectGroups(section), ...services.map((service) => serviceAnchor(service.key))]);\n const onFocus = (event: Event) => {\n if (anchors.has((event as CustomEvent<FocusDetail>).detail.anchor)) setExpanded(true);\n };\n window.addEventListener(FOCUS_EVENT, onFocus);\n return () => window.removeEventListener(FOCUS_EVENT, onFocus);\n }, [section, services, setExpanded]);\n\n if (!services.length) return null;\n\n return (\n <Card id={groupAnchor(section.key)} className=\"scroll-mt-6\">\n <Disclosure isExpanded={expanded} onExpandedChange={setExpanded}>\n {/* The circles have their own tooltips, so they sit beside the trigger, not inside it */}\n <Disclosure.Heading className=\"flex flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <Disclosure.Trigger className=\"flex min-w-48 flex-1 cursor-pointer flex-col text-left\">\n <span className=\"font-medium leading-snug\">{section.title}</span>\n {section.description && <span className=\"text-sm leading-snug text-muted\">{section.description}</span>}\n </Disclosure.Trigger>\n {/* Circles and chevron share one row so they stay vertically centered together */}\n <div className=\"-mt-1 ml-auto flex max-w-full items-center gap-3 sm:max-w-[50%]\">\n <ServiceDots services={services} />\n <Button\n isIconOnly\n size=\"sm\"\n variant=\"ghost\"\n className=\"shrink-0\"\n aria-label={expanded ? `Collapse ${section.title}` : `Expand ${section.title}`}\n aria-expanded={expanded}\n onPress={() => setExpanded(!expanded)}\n >\n <Disclosure.Indicator className=\"text-muted\" />\n </Button>\n </div>\n </Disclosure.Heading>\n <Disclosure.Content>\n <Disclosure.Body>\n <div className=\"mt-2\">\n <SectionBody section={section} />\n </div>\n </Disclosure.Body>\n </Disclosure.Content>\n </Disclosure>\n </Card>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/sermon-js/sermon-js/dist/react/heroui/index.cjs","../../../src/react/heroui/StatusPage.tsx","../../../src/react/heroui/controls.tsx","../../../src/react/heroui/icons.tsx","../../../src/react/heroui/details.tsx","../../../src/react/heroui/tones.ts","../../../src/react/heroui/HistoryBars.tsx","../../../src/react/heroui/ScoreHeader.tsx","../../../src/react/heroui/ServiceNav.tsx","../../../src/react/heroui/ServiceRow.tsx","../../../src/react/heroui/ServiceSection.tsx"],"names":["jsxs","jsx","Icon","history","Button"],"mappings":"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","file":"/home/runner/work/sermon-js/sermon-js/dist/react/heroui/index.cjs","sourcesContent":[null,"import { Card } from \"@heroui/react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { useEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { currentAnchor, focusAnchor } from \"../anchors\";\nimport type { StatusClient } from \"../client\";\nimport { formatDuration } from \"../format\";\nimport { StatusProvider, useStatus } from \"../hooks\";\nimport { buildLayout, flattenServices } from \"../tree\";\nimport { RunAllButton, StaleNotice, ThemeSwitcher } from \"./controls\";\nimport { FLY_REGIONS } from \"./details\";\nimport { HistoryBars } from \"./HistoryBars\";\nimport { ScoreHeader } from \"./ScoreHeader\";\nimport { ServiceNav } from \"./ServiceNav\";\nimport { KitOptions } from \"./ServiceRow\";\nimport { ServiceSection } from \"./ServiceSection\";\n\nexport type StatusPageProps = {\n /** Where `createStatusHandler` is mounted (default \"/api\") */\n baseUrl?: string;\n /** Or a ready client, e.g. with a custom `fetch` */\n client?: StatusClient;\n /** Your logo and title, shown at the top left (default: \"Status\") */\n brand?: ReactNode;\n /** Replaces the default footer text */\n footer?: ReactNode;\n /** Title of the section holding services declared outside any group (default \"Services\") */\n ungroupedTitle?: string;\n /** Start sections open for viewers who haven't chosen (default false) */\n defaultExpanded?: boolean;\n /** Offer on-demand runs: \"Run all checks\" and \"Check now\" (default true) */\n allowRuns?: boolean;\n /** Show the side index on wide screens (default true) */\n showNav?: boolean;\n /** Show the system / light / dark switch (default true) */\n showThemeSwitcher?: boolean;\n /** City names for instance regions (default: Fly.io's regions) */\n regionNames?: Record<string, string>;\n /** How often the page re-reads the API, in ms (default 30000) */\n refetchInterval?: number | false;\n /** Use your own React Query client instead of the built-in one */\n queryClient?: QueryClient;\n};\n\nconst defaultQueryClient = new QueryClient({ defaultOptions: { queries: { retry: 1 } } });\n\nfunction Page({\n brand,\n footer,\n ungroupedTitle,\n defaultExpanded,\n allowRuns = true,\n showNav = true,\n showThemeSwitcher = true,\n refetchInterval,\n}: StatusPageProps) {\n const status = useStatus({ refetchInterval });\n const snapshot = status.data;\n const blocks = useMemo(() => (snapshot ? buildLayout(snapshot.tree, { ungroupedTitle }) : []), [snapshot, ungroupedTitle]);\n const names = useMemo(\n () => Object.fromEntries(flattenServices(snapshot?.tree ?? []).map((service) => [service.key, service.name])),\n [snapshot]\n );\n\n // Deep links (#service-…, #group-…): open the right section once the data is there\n const handledHash = useRef(false);\n useEffect(() => {\n if (handledHash.current || !snapshot) return;\n handledHash.current = true;\n const anchor = currentAnchor();\n if (!anchor) return;\n // Otherwise the browser restores the previous scroll position after we jump\n history.scrollRestoration = \"manual\";\n focusAnchor(anchor, { smooth: false });\n }, [snapshot]);\n\n // The hash can also change on an open page: back/forward, or a link that only changes it\n useEffect(() => {\n const onHashChange = () => {\n const anchor = currentAnchor();\n if (anchor) focusAnchor(anchor);\n };\n window.addEventListener(\"hashchange\", onHashChange);\n return () => window.removeEventListener(\"hashchange\", onHashChange);\n }, []);\n\n return (\n // On wide screens the service index is attached to the right of the main column\n <div className=\"mx-auto flex min-h-screen max-w-5xl gap-6 px-4 py-8 sm:px-6 xl:max-w-[calc(64rem+12rem+1.5rem)]\">\n <main className=\"flex min-w-0 flex-1 flex-col gap-6\">\n <header className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-3\">\n {brand ?? <h1 className=\"text-lg font-semibold\">Status</h1>}\n {snapshot && allowRuns && <RunAllButton />}\n </header>\n\n {status.isPending && <div className=\"h-40 animate-pulse rounded-3xl bg-surface\" />}\n {status.isError && (\n <Card>\n <Card.Content className=\"text-danger\">Could not load status: {status.error.message}</Card.Content>\n </Card>\n )}\n\n {snapshot && (\n <>\n <StaleNotice snapshot={snapshot} />\n <Card>\n <Card.Content className=\"flex flex-col gap-6\">\n <ScoreHeader snapshot={snapshot} />\n <HistoryBars names={names} />\n </Card.Content>\n </Card>\n\n {blocks.map((block) =>\n block.type === \"section\" ? (\n <ServiceSection key={block.section.key} section={block.section} defaultExpanded={defaultExpanded} />\n ) : (\n <div key={block.key} className=\"contents\">\n <div className=\"mt-2 flex flex-col\">\n <h2 className=\"text-lg font-semibold leading-snug\">{block.title}</h2>\n {block.description && <p className=\"text-sm leading-snug text-muted\">{block.description}</p>}\n </div>\n {block.sections.map((section) => (\n <ServiceSection key={section.key} section={section} defaultExpanded={defaultExpanded} />\n ))}\n </div>\n )\n )}\n </>\n )}\n\n <footer className=\"flex flex-wrap items-center justify-between gap-3 pb-4 text-xs text-muted\">\n <span>\n {footer ??\n (snapshot\n ? `Checks run every ${formatDuration(snapshot.intervalMs)} · history kept for ${formatDuration(snapshot.retentionMs)}`\n : \"\")}\n </span>\n {showThemeSwitcher && <ThemeSwitcher />}\n </footer>\n </main>\n\n {snapshot && showNav && (\n <aside className=\"sticky top-4 hidden w-48 shrink-0 self-start xl:block\">\n <ServiceNav blocks={blocks} />\n </aside>\n )}\n </div>\n );\n}\n\n/**\n * A complete status page: overall score, history, collapsible sections with per-service\n * details, a side index and a theme switch. It reads everything from the API of\n * `createStatusHandler`, so the page mirrors the `services` tree you configured.\n *\n * ```tsx\n * <StatusPage baseUrl=\"/api\" brand={<Logo />} />\n * ```\n */\nexport function StatusPage(props: StatusPageProps) {\n const options = useMemo(\n () => ({ regionNames: props.regionNames ?? FLY_REGIONS, allowRuns: props.allowRuns ?? true }),\n [props.regionNames, props.allowRuns]\n );\n return (\n <QueryClientProvider client={props.queryClient ?? defaultQueryClient}>\n <StatusProvider baseUrl={props.baseUrl} client={props.client}>\n <KitOptions.Provider value={options}>\n <Page {...props} />\n </KitOptions.Provider>\n </StatusProvider>\n </QueryClientProvider>\n );\n}\n","import { Button, ToggleButton, ToggleButtonGroup } from \"@heroui/react\";\nimport type { StatusSnapshot } from \"../../types\";\nimport { RateLimitError } from \"../client\";\nimport { formatDuration, timeAgo } from \"../format\";\nimport { useRunChecks } from \"../hooks\";\nimport { useThemePreference, type ThemePreference } from \"../theme\";\nimport { isStale } from \"../tree\";\nimport { AlertIcon, DesktopIcon, MoonIcon, RefreshIcon, SunIcon } from \"./icons\";\n\n/** Runs every check on demand; the icon spins while they run */\nexport function RunAllButton({ label = \"Run all checks\" }: { label?: string }) {\n const run = useRunChecks();\n const rateLimited = run.error instanceof RateLimitError ? run.error : undefined;\n return (\n <div className=\"flex items-center gap-3\">\n {rateLimited && <span className=\"text-xs text-muted\">Try again in {rateLimited.retryAfter}s</span>}\n {run.error && !rateLimited && <span className=\"text-xs text-danger\">{run.error.message}</span>}\n <Button size=\"sm\" variant=\"tertiary\" isDisabled={run.isPending} onPress={() => run.mutate()}>\n {/* Not for viewers who prefer reduced motion */}\n <RefreshIcon size={16} className={run.isPending ? \"motion-safe:animate-spin\" : undefined} />\n {run.isPending ? \"Running checks…\" : label}\n </Button>\n </div>\n );\n}\n\nconst THEMES: Array<{ id: ThemePreference; label: string; Icon: typeof SunIcon }> = [\n { id: \"system\", label: \"System theme\", Icon: DesktopIcon },\n { id: \"light\", label: \"Light theme\", Icon: SunIcon },\n { id: \"dark\", label: \"Dark theme\", Icon: MoonIcon },\n];\n\n/** A discreet system / light / dark switch, remembered per viewer */\nexport function ThemeSwitcher() {\n const [preference, setPreference] = useThemePreference();\n return (\n <ToggleButtonGroup\n aria-label=\"Color theme\"\n size=\"sm\"\n disallowEmptySelection\n selectedKeys={[preference]}\n onSelectionChange={(keys) => {\n const [key] = [...keys];\n if (key) setPreference(key as ThemePreference);\n }}\n className=\"opacity-60 transition-opacity hover:opacity-100 focus-within:opacity-100\"\n >\n {THEMES.map(({ id, label, Icon }, index) => (\n <ToggleButton key={id} id={id} isIconOnly aria-label={label}>\n {index > 0 && <ToggleButtonGroup.Separator />}\n <Icon size={14} />\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n );\n}\n\n/**\n * Shown when the last full run is older than twice the interval: the scheduler has probably\n * stopped, and the page would otherwise keep showing old results as if they were current.\n */\nexport function StaleNotice({ snapshot }: { snapshot: StatusSnapshot }) {\n // Judged against the server's clock at the time of the snapshot, not the viewer's\n if (!isStale(snapshot, snapshot.generatedAt) || snapshot.lastRunAt === null) return null;\n return (\n <div role=\"status\" className=\"flex items-start gap-2 rounded-lg bg-warning/10 px-3 py-2 text-sm text-warning\">\n <AlertIcon size={16} className=\"mt-0.5 shrink-0\" />\n <span>\n These results are out of date: the last full check ran {timeAgo(snapshot.lastRunAt)}, and checks should run\n every {formatDuration(snapshot.intervalMs)}.\n </span>\n </div>\n );\n}\n","import type { SVGProps } from \"react\";\n\n// A handful of line icons, inlined so the kit needs no icon package\nfunction Icon({ size = 16, children, ...props }: SVGProps<SVGSVGElement> & { size?: number }) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n {...props}\n >\n {children}\n </svg>\n );\n}\n\ntype IconProps = SVGProps<SVGSVGElement> & { size?: number };\n\nexport const RefreshIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4\" />\n <path d=\"M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4\" />\n </Icon>\n);\n\nexport const PlayIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M7 4v16l13-8z\" />\n </Icon>\n);\n\nexport const DesktopIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM7 20h10M9 16v4M15 16v4\" />\n </Icon>\n);\n\nexport const SunIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0M3 12h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7\" />\n </Icon>\n);\n\nexport const MoonIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M12 3h.4A7.5 7.5 0 0 0 20 14.4A9 9 0 1 1 12 3\" />\n </Icon>\n);\n\nexport const AlertIcon = (props: IconProps) => (\n <Icon {...props}>\n <path d=\"M12 9v4m0 4h.01M10.4 3.6L2.3 17.2a1.9 1.9 0 0 0 1.6 2.9h16.2a1.9 1.9 0 0 0 1.6-2.9L13.6 3.6a1.9 1.9 0 0 0-3.2 0\" />\n </Icon>\n);\n","import { Label, Meter } from \"@heroui/react\";\nimport type { Instance, ServiceStats, Usage } from \"../../types\";\nimport { formatMs, formatPct, STATUS_TEXT, STATUS_TONE, timeAgo } from \"../format\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\nconst WIDTH = 120;\nconst HEIGHT = 28;\n\n/** Latency over the last 24h; gaps are runs without a measurement (asleep, down) */\nexport function Sparkline({ values, threshold }: { values: Array<number | null>; threshold: number }) {\n const points = values.slice(-96);\n const measured = points.filter((value): value is number => value !== null);\n if (measured.length < 2) {\n return (\n <div style={{ width: WIDTH, height: HEIGHT }} className=\"text-[10px] text-muted\">\n no latency data\n </div>\n );\n }\n const max = Math.max(threshold, ...measured);\n const step = WIDTH / Math.max(1, points.length - 1);\n const y = (value: number) => HEIGHT - 2 - (value / max) * (HEIGHT - 4);\n\n let path = \"\";\n points.forEach((value, index) => {\n if (value === null) return;\n const previous = points[index - 1];\n path += `${previous === null || previous === undefined ? \"M\" : \"L\"}${(index * step).toFixed(1)},${y(value).toFixed(1)} `;\n });\n\n return (\n <svg width={WIDTH} height={HEIGHT} viewBox={`0 0 ${WIDTH} ${HEIGHT}`} aria-hidden className=\"shrink-0\">\n <line\n x1={0}\n x2={WIDTH}\n y1={y(threshold)}\n y2={y(threshold)}\n className=\"stroke-warning\"\n strokeDasharray=\"2 3\"\n strokeWidth={1}\n opacity={0.6}\n />\n <path d={path} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} className=\"text-accent\" />\n </svg>\n );\n}\n\nconst number = new Intl.NumberFormat(\"en\");\n\n/** A quota as a bar with its numbers: green below 75%, amber from 75%, red from 90% */\nexport function UsageBar({ usage }: { usage: Usage }) {\n const pct = usage.limit > 0 ? (usage.used / usage.limit) * 100 : 0;\n const unit = usage.unit ? ` ${usage.unit}` : \"\";\n return (\n <div className=\"flex w-full max-w-md flex-col gap-1\">\n <Meter\n value={Math.min(usage.used, usage.limit)}\n maxValue={usage.limit}\n color={pct >= 90 ? \"danger\" : pct >= 75 ? \"warning\" : \"success\"}\n size=\"sm\"\n valueLabel={\n <span className=\"tabular-nums\">\n {number.format(usage.used)} / {number.format(usage.limit)}\n {unit} · {pct.toFixed(pct < 10 ? 1 : 0)}%\n </span>\n }\n >\n <Label className=\"text-xs\">{usage.label}</Label>\n <Meter.Output className=\"text-xs\" />\n <Meter.Track>\n <Meter.Fill />\n </Meter.Track>\n </Meter>\n {usage.resetsAt && (\n <span className=\"text-[11px] text-muted\">\n {number.format(usage.limit - usage.used)}\n {unit} left · resets {timeAgo(usage.resetsAt)}\n </span>\n )}\n </div>\n );\n}\n\n/** Fly.io region codes → city. Extend or replace with the `regionNames` prop. */\nexport const FLY_REGIONS: Record<string, string> = {\n ams: \"Amsterdam\", arn: \"Stockholm\", atl: \"Atlanta\", bog: \"Bogotá\", bom: \"Mumbai\", bos: \"Boston\",\n cdg: \"Paris\", den: \"Denver\", dfw: \"Dallas\", ewr: \"Secaucus\", eze: \"Buenos Aires\", fra: \"Frankfurt\",\n gdl: \"Guadalajara\", gig: \"Rio de Janeiro\", gru: \"São Paulo\", hkg: \"Hong Kong\", iad: \"Ashburn\",\n jnb: \"Johannesburg\", lax: \"Los Angeles\", lhr: \"London\", mad: \"Madrid\", mia: \"Miami\", nrt: \"Tokyo\",\n ord: \"Chicago\", otp: \"Bucharest\", phx: \"Phoenix\", qro: \"Querétaro\", scl: \"Santiago\", sea: \"Seattle\",\n sin: \"Singapore\", sjc: \"San Jose\", syd: \"Sydney\", waw: \"Warsaw\", yul: \"Montreal\", yyz: \"Toronto\",\n};\n\n/** One line per machine/region of a multi-instance service */\nexport function InstanceTable({\n instances,\n stats,\n regionNames = FLY_REGIONS,\n labels,\n}: {\n instances: Instance[];\n /** Headings of the first two columns (default \"Region\" and \"Machine\") */\n labels?: { key: string; detail?: string };\n stats?: ServiceStats[\"instances\"];\n regionNames?: Record<string, string>;\n}) {\n return (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[34rem] text-xs\">\n <thead>\n <tr className=\"text-left text-[10px] uppercase tracking-wide text-muted\">\n <th className=\"py-1 pr-3 font-normal\">{labels?.key ?? \"Region\"}</th>\n <th className=\"py-1 pr-3 font-normal\">{labels ? (labels.detail ?? \"\") : \"Machine\"}</th>\n <th className=\"py-1 pr-3 font-normal\">Status</th>\n <th className=\"py-1 pr-3 text-right font-normal\">Response</th>\n <th className=\"py-1 pr-3 text-right font-normal\">p50 24h</th>\n <th className=\"py-1 text-right font-normal\">Uptime 24h</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {instances.map((instance) => {\n const tone = STATUS_TONE[instance.status];\n const instanceStats = stats?.[instance.key];\n return (\n <tr key={instance.key} className=\"align-top\">\n <td className=\"py-1.5 pr-3\">\n <span className=\"flex items-center gap-2\">\n <span className={`size-2 shrink-0 rounded-full ${TONE_BG[tone]}`} aria-hidden />\n <span className=\"font-mono\">{instance.key}</span>\n <span className=\"text-muted\">{(instance.region && regionNames[instance.region]) ?? \"\"}</span>\n </span>\n </td>\n <td className=\"py-1.5 pr-3 font-mono text-muted\" title={instance.id}>\n {[instance.id?.slice(0, 8), instance.state].filter(Boolean).join(\" · \")}\n </td>\n <td className=\"py-1.5 pr-3\">\n <span className={TONE_TEXT[tone]}>{STATUS_TEXT[instance.status]}</span>\n {instance.error && instance.status !== \"up\" && (\n <span className=\"block text-muted\">\n {instance.httpStatus ? `[${instance.httpStatus}] ` : \"\"}\n {instance.error}\n </span>\n )}\n </td>\n <td className=\"py-1.5 pr-3 text-right tabular-nums\">\n {instance.latencyMs !== undefined ? formatMs(instance.latencyMs) : \"–\"}\n </td>\n <td className=\"py-1.5 pr-3 text-right tabular-nums\">{formatMs(instanceStats?.p50)}</td>\n <td className=\"py-1.5 text-right tabular-nums\">{formatPct(instanceStats?.uptime24h)}</td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\n","import type { Tone } from \"../format\";\n\n// Tailwind classes for each tone, using HeroUI's theme colors. Written out in full so\n// Tailwind finds them when it scans this package.\nexport const TONE_BG: Record<Tone, string> = {\n success: \"bg-success\",\n warning: \"bg-warning\",\n danger: \"bg-danger\",\n accent: \"bg-accent\",\n default: \"bg-default\",\n};\n\nexport const TONE_TEXT: Record<Tone, string> = {\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-danger\",\n accent: \"text-accent\",\n default: \"text-muted\",\n};\n","import { Chip, Tabs } from \"@heroui/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { HistoryBar, HistoryRange } from \"../../types\";\nimport { formatDateTime, formatDuration, LABEL_TEXT, LABEL_TONE } from \"../format\";\nimport { useHistory } from \"../hooks\";\nimport { mergeBars } from \"../tree\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\n/** Minimum room for one bar and its gap */\nconst MIN_BAR_PX = 3;\n\nfunction average(bars: HistoryBar[]) {\n const scored = bars.filter((bar) => bar.score !== null);\n if (!scored.length) return null;\n return Math.round(scored.reduce((sum, bar) => sum + (bar.score as number), 0) / scored.length);\n}\n\n/**\n * Score history as bars over a full-height gray track: the gray above a bar is what was\n * missing to 100, and an all-gray column means no data. Hover a bar for its details.\n */\nexport function HistoryBars({ names = {} }: { /** Service names by key, to label failures */ names?: Record<string, string> }) {\n const [range, setRange] = useState<HistoryRange>(\"24h\");\n const [hovered, setHovered] = useState<number | null>(null);\n const history = useHistory(range);\n\n // On narrow screens there isn't a few pixels per bar: merge neighbours until there is\n const chart = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number | null>(null);\n useEffect(() => {\n const element = chart.current;\n if (!element) return;\n const measure = () => setWidth(element.clientWidth);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n const { bars, factor } = useMemo(\n () => mergeBars(history.data?.bars ?? [], width ? Math.floor(width / MIN_BAR_PX) : Infinity),\n [history.data, width]\n );\n const active = (hovered !== null && bars[hovered]) || bars.findLast((bar) => bar.runs > 0);\n const avg = average(bars);\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-sm font-medium\">System score history</h3>\n <p className=\"text-xs text-muted\">\n {history.data ? `Worst score per ${formatDuration(history.data.bucketMs * factor)}` : \"Loading…\"}\n {avg !== null && ` · average ${avg}`}\n </p>\n </div>\n <Tabs selectedKey={range} onSelectionChange={(key) => setRange(key as HistoryRange)}>\n <Tabs.ListContainer>\n <Tabs.List aria-label=\"History range\">\n <Tabs.Tab id=\"24h\">\n 24h\n <Tabs.Indicator />\n </Tabs.Tab>\n <Tabs.Tab id=\"7d\">\n 7d\n <Tabs.Indicator />\n </Tabs.Tab>\n </Tabs.List>\n </Tabs.ListContainer>\n </Tabs>\n </div>\n\n <div\n ref={chart}\n className=\"flex h-24 items-end gap-px\"\n onMouseLeave={() => setHovered(null)}\n role=\"img\"\n aria-label={`System score over the last ${range}`}\n >\n {history.isPending &&\n Array.from({ length: 48 }, (_, index) => (\n <div key={index} className=\"h-1/3 flex-1 animate-pulse rounded-sm bg-default\" />\n ))}\n {bars.map((bar, index) => (\n <div\n key={bar.start}\n className={`relative h-full flex-1 cursor-default overflow-hidden rounded-sm bg-default transition-opacity ${\n hovered !== null && hovered !== index ? \"opacity-50\" : \"\"\n }`}\n onMouseEnter={() => setHovered(index)}\n >\n {bar.score !== null && (\n <div\n className={`absolute inset-x-0 bottom-0 ${TONE_BG[LABEL_TONE[bar.label]]}`}\n // A sliver stays visible even at a score of 0\n style={{ height: `${Math.max(3, bar.score)}%` }}\n />\n )}\n </div>\n ))}\n </div>\n\n <div className=\"flex justify-between text-[11px] text-muted\">\n <span>{range === \"24h\" ? \"24 hours ago\" : \"7 days ago\"}</span>\n <span>Now</span>\n </div>\n\n <div className=\"min-h-10 rounded-lg bg-surface-secondary px-3 py-2 text-xs\">\n {active ? (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-wrap items-center gap-x-3\">\n <span className=\"font-medium\">{formatDateTime(active.start)}</span>\n <span className={TONE_TEXT[LABEL_TONE[active.label]]}>\n {active.score === null ? \"No data\" : `Score ${active.score} · ${LABEL_TEXT[active.label]}`}\n </span>\n {active.runs > 0 && <span className=\"text-muted\">{active.runs} run(s)</span>}\n </div>\n {Object.keys(active.failing).length > 0 && (\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {Object.entries(active.failing).map(([key, status]) => (\n <Chip\n key={key}\n size=\"sm\"\n variant=\"soft\"\n color={status === \"down\" ? \"danger\" : \"warning\"}\n aria-label={`${names[key] ?? key}: ${status}`}\n >\n {names[key] ?? key}\n </Chip>\n ))}\n </div>\n )}\n </div>\n ) : (\n <span className=\"text-muted\">Hover a bar to see details</span>\n )}\n </div>\n </div>\n );\n}\n","import { Chip } from \"@heroui/react\";\nimport type { Status, StatusSnapshot } from \"../../types\";\nimport { formatDuration, LABEL_TEXT, LABEL_TONE, STATUS_TEXT, STATUS_TONE, timeAgo, type Tone } from \"../format\";\nimport { TONE_BG, TONE_TEXT } from \"./tones\";\n\nconst SIZE = 88;\nconst STROKE = 8;\nconst RADIUS = (SIZE - STROKE) / 2;\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS;\n\n/** The overall score as a ring */\nexport function ScoreRing({ score, tone }: { score: number | null; tone: Tone }) {\n const progress = score === null ? 0 : score / 100;\n return (\n <div className=\"relative shrink-0\" style={{ width: SIZE, height: SIZE }}>\n <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`} className=\"-rotate-90\">\n <circle cx={SIZE / 2} cy={SIZE / 2} r={RADIUS} fill=\"none\" strokeWidth={STROKE} className=\"stroke-default\" />\n <circle\n cx={SIZE / 2}\n cy={SIZE / 2}\n r={RADIUS}\n fill=\"none\"\n strokeWidth={STROKE}\n strokeLinecap=\"round\"\n strokeDasharray={CIRCUMFERENCE}\n strokeDashoffset={CIRCUMFERENCE * (1 - progress)}\n className={`${TONE_TEXT[tone]} transition-[stroke-dashoffset] duration-700`}\n stroke=\"currentColor\"\n />\n </svg>\n <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n <span className=\"text-2xl font-semibold tabular-nums\">{score ?? \"–\"}</span>\n <span className=\"text-[10px] uppercase tracking-wide text-muted\">score</span>\n </div>\n </div>\n );\n}\n\nconst COUNT_ORDER: Status[] = [\"up\", \"degraded\", \"down\", \"sleeping\", \"unconfigured\"];\n\n/** Score ring, overall label, counts per status and when checks last ran */\nexport function ScoreHeader({ snapshot }: { snapshot: StatusSnapshot }) {\n const tone = LABEL_TONE[snapshot.label];\n return (\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-center\">\n <ScoreRing score={snapshot.score} tone={tone} />\n <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"text-xl font-semibold\">{LABEL_TEXT[snapshot.label]}</h2>\n <Chip color={tone} variant=\"soft\" size=\"sm\">\n {snapshot.label}\n </Chip>\n </div>\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted\">\n {COUNT_ORDER.filter((status) => snapshot.counts[status] > 0).map((status) => (\n <span key={status} className=\"flex items-center gap-1.5\">\n <span className={`size-2 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} />\n {snapshot.counts[status]} {STATUS_TEXT[status].toLowerCase()}\n </span>\n ))}\n </div>\n <p className=\"text-xs text-muted\">\n {snapshot.lastCheckedAt\n ? `Last checked ${timeAgo(snapshot.lastCheckedAt)} · checks run every ${formatDuration(snapshot.intervalMs)}`\n : \"No checks have run yet\"}\n </p>\n </div>\n </div>\n );\n}\n","import { Card } from \"@heroui/react\";\nimport type { ReactNode } from \"react\";\nimport { focusAnchor, groupAnchor, serviceAnchor } from \"../anchors\";\nimport { STATUS_TEXT, STATUS_TONE } from \"../format\";\nimport { servicesOf, statusOf, type LayoutBlock, type Section } from \"../tree\";\nimport { TONE_BG } from \"./tones\";\n\nfunction NavLink({\n anchor,\n className,\n children,\n label,\n}: {\n anchor: string;\n className: string;\n children: ReactNode;\n label?: string;\n}) {\n return (\n <a\n href={`#${encodeURIComponent(anchor)}`}\n aria-label={label}\n className={className}\n onClick={(event) => {\n // Let the browser handle \"open in new tab/window\"\n if (event.metaKey || event.ctrlKey || event.shiftKey) return;\n event.preventDefault();\n focusAnchor(anchor);\n }}\n >\n {children}\n </a>\n );\n}\n\nfunction SectionLinks({ section }: { section: Section }) {\n return (\n <li>\n <NavLink\n anchor={groupAnchor(section.key)}\n className=\"block rounded px-1.5 pt-1 text-[11px] leading-4 font-medium text-muted hover:text-foreground\"\n >\n {section.title}\n </NavLink>\n <ul>\n {servicesOf(section).map((service) => {\n const status = statusOf(service);\n return (\n <li key={service.key}>\n <NavLink\n anchor={serviceAnchor(service.key)}\n label={`${service.name}: ${STATUS_TEXT[status]}`}\n className=\"flex items-center gap-1.5 rounded px-1.5 text-xs leading-[18px] hover:bg-default\"\n >\n <span className={`size-1.5 shrink-0 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} aria-hidden />\n <span className=\"truncate\">{service.name}</span>\n </NavLink>\n </li>\n );\n })}\n </ul>\n </li>\n );\n}\n\n/** A compact index of every service with its status, linking to its place on the page */\nexport function ServiceNav({ blocks, title = \"Services\" }: { blocks: LayoutBlock[]; title?: string }) {\n return (\n <Card className=\"max-h-[calc(100vh-2rem)] gap-1 overflow-y-auto p-3\">\n <Card.Header>\n <Card.Title className=\"px-1.5 text-xs font-semibold\">{title}</Card.Title>\n </Card.Header>\n <Card.Content>\n <nav aria-label=\"Tracked services\">\n <ul className=\"flex flex-col gap-0.5\">\n {blocks.map((block) =>\n block.type === \"section\" ? (\n <SectionLinks key={block.section.key} section={block.section} />\n ) : (\n <li key={block.key}>\n <p className=\"mt-2 px-1.5 text-[10px] leading-4 font-semibold uppercase tracking-wide text-muted\">\n {block.title}\n </p>\n <ul className=\"flex flex-col gap-0.5\">\n {block.sections.map((section) => (\n <SectionLinks key={section.key} section={section} />\n ))}\n </ul>\n </li>\n )\n )}\n </ul>\n </nav>\n </Card.Content>\n </Card>\n );\n}\n","import { Button, Chip } from \"@heroui/react\";\nimport { createContext, useContext } from \"react\";\nimport type { ServiceSnapshot } from \"../../types\";\nimport { serviceAnchor } from \"../anchors\";\nimport { RateLimitError } from \"../client\";\nimport { formatDateTime, formatMs, formatPct, humanize, STATUS_TEXT, STATUS_TONE, timeAgo } from \"../format\";\nimport { useRunChecks } from \"../hooks\";\nimport { statusOf } from \"../tree\";\nimport { FLY_REGIONS, InstanceTable, Sparkline, UsageBar } from \"./details\";\nimport { PlayIcon } from \"./icons\";\nimport { TONE_BG } from \"./tones\";\n\n/** Display options shared by the kit's components */\nexport const KitOptions = createContext<{ regionNames: Record<string, string>; allowRuns: boolean }>({\n regionNames: FLY_REGIONS,\n allowRuns: true,\n});\n\nfunction Stat({ label, value }: { label: string; value: string }) {\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted\">{label}</span>\n <span className=\"text-sm tabular-nums\">{value}</span>\n </div>\n );\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Everything known about one service: status, response time, percentiles, uptime, sparkline,\n * per-instance rows, usage bars, metrics, the current error, and a \"Check now\" button.\n */\nexport function ServiceRow({ service }: { service: ServiceSnapshot }) {\n const run = useRunChecks();\n const { regionNames, allowRuns } = useContext(KitOptions);\n const status = statusOf(service);\n const tone = STATUS_TONE[status];\n const { result, stats } = service;\n const failing = status === \"down\" || status === \"degraded\";\n const rateLimited = run.error instanceof RateLimitError ? run.error : undefined;\n // \"Recent\" is measured from the latest check, not the viewer's clock\n const lastFailure =\n !failing && result && stats?.lastFailure && result.checkedAt - stats.lastFailure.at < DAY_MS\n ? stats.lastFailure\n : undefined;\n\n return (\n <div\n id={serviceAnchor(service.key)}\n className={`flex scroll-mt-6 flex-col gap-3 py-4 ${service.configured ? \"\" : \"opacity-60\"}`}\n >\n <div className=\"flex flex-wrap items-start gap-3\">\n <span className={`mt-1.5 size-2.5 shrink-0 rounded-full ${TONE_BG[tone]}`} aria-hidden />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium\">{service.name}</span>\n {service.critical && (\n <Chip size=\"sm\" variant=\"tertiary\">\n critical\n </Chip>\n )}\n <Chip size=\"sm\" variant=\"soft\" color={tone}>\n {STATUS_TEXT[status]}\n </Chip>\n </div>\n {service.description && <p className=\"text-xs text-muted\">{service.description}</p>}\n {!service.configured && service.missing && service.missing.length > 0 && (\n <p className=\"text-xs text-muted\">Missing: {service.missing.join(\", \")}</p>\n )}\n </div>\n {allowRuns && (\n <div className=\"flex items-center gap-2\">\n {rateLimited && <span className=\"text-xs text-muted\">wait {rateLimited.retryAfter}s</span>}\n <Button\n size=\"sm\"\n variant=\"ghost\"\n isDisabled={!service.configured}\n isPending={run.isPending}\n onPress={() => run.mutate(service.key)}\n aria-label={`Check ${service.name} now`}\n >\n <PlayIcon size={14} />\n Check now\n </Button>\n </div>\n )}\n </div>\n\n {service.configured && (\n <div className=\"flex flex-wrap items-center gap-x-6 gap-y-3 pl-5.5\">\n <Stat label=\"Response\" value={result?.probed === false ? \"not probed\" : formatMs(result?.latencyMs)} />\n <Stat label=\"p50 / p95 24h\" value={`${formatMs(stats?.p50)} / ${formatMs(stats?.p95)}`} />\n <Stat label=\"Uptime 24h\" value={formatPct(stats?.uptime24h)} />\n <Stat label=\"Uptime 7d\" value={formatPct(stats?.uptime7d)} />\n {stats && <Sparkline values={stats.latency} threshold={service.slowMs} />}\n {result && <Stat label=\"Checked\" value={timeAgo(result.checkedAt)} />}\n </div>\n )}\n\n {result?.instances && result.instances.length > 0 && (\n <div className=\"pl-5.5\">\n <InstanceTable\n instances={result.instances}\n stats={stats?.instances}\n regionNames={regionNames}\n labels={result.instanceLabels}\n />\n </div>\n )}\n\n {result?.usage && result.usage.length > 0 && (\n <div className=\"flex flex-col gap-3 pl-5.5\">\n {result.usage.map((usage) => (\n <UsageBar key={usage.label} usage={usage} />\n ))}\n </div>\n )}\n\n {result?.metrics && Object.keys(result.metrics).length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 pl-5.5\">\n {Object.entries(result.metrics)\n .filter(([, value]) => value !== \"\")\n .map(([key, value]) => (\n <Chip key={key} size=\"sm\" variant=\"secondary\">\n {humanize(key)}: {value}\n </Chip>\n ))}\n </div>\n )}\n\n {failing && result?.error && (\n <div\n className={`ml-5.5 rounded-lg px-3 py-2 font-mono text-xs break-words ${\n status === \"down\" ? \"bg-danger/10 text-danger\" : \"bg-warning/10 text-warning\"\n }`}\n >\n {result.httpStatus ? `[${result.httpStatus}] ` : \"\"}\n {result.error}\n </div>\n )}\n\n {lastFailure && (\n <p className=\"pl-5.5 text-xs text-muted\">\n Last incident {formatDateTime(lastFailure.at)} ({lastFailure.status})\n {lastFailure.error ? `: ${lastFailure.error}` : \"\"}\n </p>\n )}\n\n {run.error && !rateLimited && <p className=\"pl-5.5 text-xs text-danger\">{run.error.message}</p>}\n </div>\n );\n}\n","import { Button, Card, Disclosure, Tooltip } from \"@heroui/react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport type { ServiceSnapshot } from \"../../types\";\nimport { FOCUS_EVENT, groupAnchor, serviceAnchor, type FocusDetail } from \"../anchors\";\nimport { STATUS_TEXT, STATUS_TONE } from \"../format\";\nimport { servicesOf, statusOf, type Section } from \"../tree\";\nimport { ServiceRow } from \"./ServiceRow\";\nimport { TONE_BG } from \"./tones\";\n\n/** Collapsed sections are a per-viewer preference, kept in localStorage when available */\nfunction useStoredExpanded(key: string, defaultExpanded: boolean) {\n const storageKey = `sermon:section:${key}`;\n const [expanded, setExpanded] = useState(() => {\n try {\n const stored = localStorage.getItem(storageKey);\n return stored === null ? defaultExpanded : stored === \"expanded\";\n } catch {\n return defaultExpanded;\n }\n });\n const update = useCallback(\n (value: boolean) => {\n setExpanded(value);\n try {\n localStorage.setItem(storageKey, value ? \"expanded\" : \"collapsed\");\n } catch {\n // Storage unavailable (private mode…): the state just isn't remembered\n }\n },\n [storageKey]\n );\n return [expanded, update] as const;\n}\n\n/** One circle per service, colored by status; the tooltip names the service */\nfunction ServiceDots({ services }: { services: ServiceSnapshot[] }) {\n return (\n <div className=\"flex min-w-0 flex-wrap items-center justify-end gap-1\">\n {services.map((service) => {\n const status = statusOf(service);\n return (\n <Tooltip key={service.key} delay={100} closeDelay={0}>\n <Tooltip.Trigger\n aria-label={`${service.name}: ${STATUS_TEXT[status]}`}\n className=\"flex size-4 items-center justify-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-focus\"\n >\n <span className={`size-2.5 rounded-full ${TONE_BG[STATUS_TONE[status]]}`} />\n </Tooltip.Trigger>\n <Tooltip.Content>{service.name}</Tooltip.Content>\n </Tooltip>\n );\n })}\n </div>\n );\n}\n\n/** The rows of a group, then its nested groups under small headings */\nfunction SectionBody({ section, nested = false }: { section: Section; nested?: boolean }) {\n return (\n <>\n {nested && (\n <div id={groupAnchor(section.key)} className=\"mt-4 scroll-mt-6\">\n <h4 className=\"text-xs font-medium uppercase tracking-wide text-muted\">{section.title}</h4>\n {section.description && <p className=\"text-xs text-muted\">{section.description}</p>}\n </div>\n )}\n {section.services.length > 0 && (\n <div className=\"divide-y divide-border\">\n {section.services.map((service) => (\n <ServiceRow key={service.key} service={service} />\n ))}\n </div>\n )}\n {section.groups.map((group) => (\n <SectionBody key={group.key} section={group} nested />\n ))}\n </>\n );\n}\n\n/**\n * A group of services as a collapsible card. The header shows one status circle per service,\n * so a collapsed section still shows if something is wrong.\n */\nexport function ServiceSection({\n section,\n defaultExpanded = false,\n}: {\n section: Section;\n /** Whether the section starts open for viewers who haven't chosen (default false) */\n defaultExpanded?: boolean;\n}) {\n const [expanded, setExpanded] = useStoredExpanded(section.key, defaultExpanded);\n const services = useMemo(() => servicesOf(section), [section]);\n\n // Navigating to this section, one of its groups or one of its services opens it first\n useEffect(() => {\n const collectGroups = (s: Section): string[] => [groupAnchor(s.key), ...s.groups.flatMap(collectGroups)];\n const anchors = new Set([...collectGroups(section), ...services.map((service) => serviceAnchor(service.key))]);\n const onFocus = (event: Event) => {\n if (anchors.has((event as CustomEvent<FocusDetail>).detail.anchor)) setExpanded(true);\n };\n window.addEventListener(FOCUS_EVENT, onFocus);\n return () => window.removeEventListener(FOCUS_EVENT, onFocus);\n }, [section, services, setExpanded]);\n\n if (!services.length) return null;\n\n return (\n <Card id={groupAnchor(section.key)} className=\"scroll-mt-6\">\n <Disclosure isExpanded={expanded} onExpandedChange={setExpanded}>\n {/* The circles have their own tooltips, so they sit beside the trigger, not inside it */}\n <Disclosure.Heading className=\"flex flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <Disclosure.Trigger className=\"flex min-w-48 flex-1 cursor-pointer flex-col text-left\">\n <span className=\"font-medium leading-snug\">{section.title}</span>\n {section.description && <span className=\"text-sm leading-snug text-muted\">{section.description}</span>}\n </Disclosure.Trigger>\n {/* Circles and chevron share one row so they stay vertically centered together */}\n <div className=\"-mt-1 ml-auto flex max-w-full items-center gap-3 sm:max-w-[50%]\">\n <ServiceDots services={services} />\n <Button\n isIconOnly\n size=\"sm\"\n variant=\"ghost\"\n className=\"shrink-0\"\n aria-label={expanded ? `Collapse ${section.title}` : `Expand ${section.title}`}\n aria-expanded={expanded}\n onPress={() => setExpanded(!expanded)}\n >\n <Disclosure.Indicator className=\"text-muted\" />\n </Button>\n </div>\n </Disclosure.Heading>\n <Disclosure.Content>\n <Disclosure.Body>\n <div className=\"mt-2\">\n <SectionBody section={section} />\n </div>\n </Disclosure.Body>\n </Disclosure.Content>\n </Disclosure>\n </Card>\n );\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { QueryClient } from '@tanstack/react-query';
|
|
4
|
-
import { S as StatusClient, a as StatusSnapshot, T as Tone, b as Section, c as ServiceSnapshot, L as LayoutBlock, I as Instance, d as ServiceStats, U as Usage } from '../../tree-
|
|
4
|
+
import { S as StatusClient, a as StatusSnapshot, T as Tone, b as Section, c as ServiceSnapshot, L as LayoutBlock, I as Instance, d as ServiceStats, U as Usage } from '../../tree-J3riUyI9.cjs';
|
|
5
5
|
|
|
6
6
|
type StatusPageProps = {
|
|
7
7
|
/** Where `createStatusHandler` is mounted (default "/api") */
|
|
@@ -99,8 +99,13 @@ declare function UsageBar({ usage }: {
|
|
|
99
99
|
/** Fly.io region codes → city. Extend or replace with the `regionNames` prop. */
|
|
100
100
|
declare const FLY_REGIONS: Record<string, string>;
|
|
101
101
|
/** One line per machine/region of a multi-instance service */
|
|
102
|
-
declare function InstanceTable({ instances, stats, regionNames, }: {
|
|
102
|
+
declare function InstanceTable({ instances, stats, regionNames, labels, }: {
|
|
103
103
|
instances: Instance[];
|
|
104
|
+
/** Headings of the first two columns (default "Region" and "Machine") */
|
|
105
|
+
labels?: {
|
|
106
|
+
key: string;
|
|
107
|
+
detail?: string;
|
|
108
|
+
};
|
|
104
109
|
stats?: ServiceStats["instances"];
|
|
105
110
|
regionNames?: Record<string, string>;
|
|
106
111
|
}): react.JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { QueryClient } from '@tanstack/react-query';
|
|
4
|
-
import { S as StatusClient, a as StatusSnapshot, T as Tone, b as Section, c as ServiceSnapshot, L as LayoutBlock, I as Instance, d as ServiceStats, U as Usage } from '../../tree-
|
|
4
|
+
import { S as StatusClient, a as StatusSnapshot, T as Tone, b as Section, c as ServiceSnapshot, L as LayoutBlock, I as Instance, d as ServiceStats, U as Usage } from '../../tree-J3riUyI9.js';
|
|
5
5
|
|
|
6
6
|
type StatusPageProps = {
|
|
7
7
|
/** Where `createStatusHandler` is mounted (default "/api") */
|
|
@@ -99,8 +99,13 @@ declare function UsageBar({ usage }: {
|
|
|
99
99
|
/** Fly.io region codes → city. Extend or replace with the `regionNames` prop. */
|
|
100
100
|
declare const FLY_REGIONS: Record<string, string>;
|
|
101
101
|
/** One line per machine/region of a multi-instance service */
|
|
102
|
-
declare function InstanceTable({ instances, stats, regionNames, }: {
|
|
102
|
+
declare function InstanceTable({ instances, stats, regionNames, labels, }: {
|
|
103
103
|
instances: Instance[];
|
|
104
|
+
/** Headings of the first two columns (default "Region" and "Machine") */
|
|
105
|
+
labels?: {
|
|
106
|
+
key: string;
|
|
107
|
+
detail?: string;
|
|
108
|
+
};
|
|
104
109
|
stats?: ServiceStats["instances"];
|
|
105
110
|
regionNames?: Record<string, string>;
|
|
106
111
|
}): react.JSX.Element;
|
|
@@ -256,12 +256,13 @@ var FLY_REGIONS = {
|
|
|
256
256
|
function InstanceTable({
|
|
257
257
|
instances,
|
|
258
258
|
stats,
|
|
259
|
-
regionNames = FLY_REGIONS
|
|
259
|
+
regionNames = FLY_REGIONS,
|
|
260
|
+
labels
|
|
260
261
|
}) {
|
|
261
262
|
return /* @__PURE__ */ jsx3("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs3("table", { className: "w-full min-w-[34rem] text-xs", children: [
|
|
262
263
|
/* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs3("tr", { className: "text-left text-[10px] uppercase tracking-wide text-muted", children: [
|
|
263
|
-
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 font-normal", children: "Region" }),
|
|
264
|
-
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 font-normal", children: "Machine" }),
|
|
264
|
+
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 font-normal", children: labels?.key ?? "Region" }),
|
|
265
|
+
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 font-normal", children: labels ? labels.detail ?? "" : "Machine" }),
|
|
265
266
|
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 font-normal", children: "Status" }),
|
|
266
267
|
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 text-right font-normal", children: "Response" }),
|
|
267
268
|
/* @__PURE__ */ jsx3("th", { className: "py-1 pr-3 text-right font-normal", children: "p50 24h" }),
|
|
@@ -593,7 +594,15 @@ function ServiceRow({ service }) {
|
|
|
593
594
|
stats && /* @__PURE__ */ jsx7(Sparkline, { values: stats.latency, threshold: service.slowMs }),
|
|
594
595
|
result && /* @__PURE__ */ jsx7(Stat, { label: "Checked", value: timeAgo(result.checkedAt) })
|
|
595
596
|
] }),
|
|
596
|
-
result?.instances && result.instances.length > 0 && /* @__PURE__ */ jsx7("div", { className: "pl-5.5", children: /* @__PURE__ */ jsx7(
|
|
597
|
+
result?.instances && result.instances.length > 0 && /* @__PURE__ */ jsx7("div", { className: "pl-5.5", children: /* @__PURE__ */ jsx7(
|
|
598
|
+
InstanceTable,
|
|
599
|
+
{
|
|
600
|
+
instances: result.instances,
|
|
601
|
+
stats: stats?.instances,
|
|
602
|
+
regionNames,
|
|
603
|
+
labels: result.instanceLabels
|
|
604
|
+
}
|
|
605
|
+
) }),
|
|
597
606
|
result?.usage && result.usage.length > 0 && /* @__PURE__ */ jsx7("div", { className: "flex flex-col gap-3 pl-5.5", children: result.usage.map((usage) => /* @__PURE__ */ jsx7(UsageBar, { usage }, usage.label)) }),
|
|
598
607
|
result?.metrics && Object.keys(result.metrics).length > 0 && /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap gap-1.5 pl-5.5", children: Object.entries(result.metrics).filter(([, value]) => value !== "").map(([key, value]) => /* @__PURE__ */ jsxs7(Chip3, { size: "sm", variant: "secondary", children: [
|
|
599
608
|
humanize(key),
|