sermon-js 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +239 -2
- package/dist/api/index.cjs +174 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +77 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.js +171 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-2M4UBCDM.js +178 -0
- package/dist/chunk-2M4UBCDM.js.map +1 -0
- package/dist/chunk-AH372HLN.js +98 -0
- package/dist/chunk-AH372HLN.js.map +1 -0
- package/dist/chunk-APC2HVSI.js +154 -0
- package/dist/chunk-APC2HVSI.js.map +1 -0
- package/dist/chunk-D7BBJTYU.cjs +335 -0
- package/dist/chunk-D7BBJTYU.cjs.map +1 -0
- package/dist/chunk-ELV4ZU6G.cjs +51 -0
- package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
- package/dist/chunk-EMP7XVKC.js +334 -0
- package/dist/chunk-EMP7XVKC.js.map +1 -0
- package/dist/chunk-I3346MUL.js +49 -0
- package/dist/chunk-I3346MUL.js.map +1 -0
- package/dist/chunk-NP3YYVRZ.cjs +163 -0
- package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
- package/dist/chunk-PARE6EUZ.cjs +184 -0
- package/dist/chunk-PARE6EUZ.cjs.map +1 -0
- package/dist/chunk-XHXEKB3Q.cjs +241 -0
- package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
- package/dist/chunk-XPXIDJUU.js +229 -0
- package/dist/chunk-XPXIDJUU.js.map +1 -0
- package/dist/chunk-ZI442ZA5.cjs +100 -0
- package/dist/chunk-ZI442ZA5.cjs.map +1 -0
- package/dist/config-BEbP-uLf.d.cts +125 -0
- package/dist/config-w1tm1bdu.d.ts +125 -0
- package/dist/generic-BJmlgtUM.d.ts +31 -0
- package/dist/generic-lmrGxMDC.d.cts +31 -0
- package/dist/index.cjs +407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/index.cjs +1012 -0
- package/dist/presets/index.cjs.map +1 -0
- package/dist/presets/index.d.cts +516 -0
- package/dist/presets/index.d.ts +516 -0
- package/dist/presets/index.js +971 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/react/heroui/index.cjs +822 -0
- package/dist/react/heroui/index.cjs.map +1 -0
- package/dist/react/heroui/index.d.cts +122 -0
- package/dist/react/heroui/index.d.ts +122 -0
- package/dist/react/heroui/index.js +821 -0
- package/dist/react/heroui/index.js.map +1 -0
- package/dist/react/heroui/styles.css +11 -0
- package/dist/react/index.cjs +71 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +53 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/status-DsWzgb9Q.d.cts +146 -0
- package/dist/status-zd04ZQ98.d.ts +146 -0
- package/dist/store-DybhDphS.d.cts +77 -0
- package/dist/store-_GuzSP0z.d.ts +77 -0
- package/dist/stores/mongodb.cjs +99 -0
- package/dist/stores/mongodb.cjs.map +1 -0
- package/dist/stores/mongodb.d.cts +40 -0
- package/dist/stores/mongodb.d.ts +40 -0
- package/dist/stores/mongodb.js +97 -0
- package/dist/stores/mongodb.js.map +1 -0
- package/dist/stores/redis.cjs +23 -0
- package/dist/stores/redis.cjs.map +1 -0
- package/dist/stores/redis.d.cts +31 -0
- package/dist/stores/redis.d.ts +31 -0
- package/dist/stores/redis.js +21 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/sql.cjs +164 -0
- package/dist/stores/sql.cjs.map +1 -0
- package/dist/stores/sql.d.cts +39 -0
- package/dist/stores/sql.d.ts +39 -0
- package/dist/stores/sql.js +162 -0
- package/dist/stores/sql.js.map +1 -0
- package/dist/stores/upstash.cjs +27 -0
- package/dist/stores/upstash.cjs.map +1 -0
- package/dist/stores/upstash.d.cts +19 -0
- package/dist/stores/upstash.d.ts +19 -0
- package/dist/stores/upstash.js +25 -0
- package/dist/stores/upstash.js.map +1 -0
- package/dist/testing/index.cjs +209 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +205 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tree-BXbXfjF1.d.cts +236 -0
- package/dist/tree-BXbXfjF1.d.ts +236 -0
- package/dist/types-CctvUAs2.d.cts +192 -0
- package/dist/types-CctvUAs2.d.ts +192 -0
- package/package.json +233 -3
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/duration.ts
|
|
2
|
+
var UNITS = {
|
|
3
|
+
ms: 1,
|
|
4
|
+
s: 1e3,
|
|
5
|
+
m: 6e4,
|
|
6
|
+
h: 36e5,
|
|
7
|
+
d: 864e5
|
|
8
|
+
};
|
|
9
|
+
function toMs(duration) {
|
|
10
|
+
if (typeof duration === "number") {
|
|
11
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid duration: ${duration}`);
|
|
12
|
+
return duration;
|
|
13
|
+
}
|
|
14
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(duration.trim());
|
|
15
|
+
if (!match) throw new Error(`Invalid duration: "${duration}" (use e.g. "30s", "5m", "1h", "7d")`);
|
|
16
|
+
return Number(match[1]) * UNITS[match[2]];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/score.ts
|
|
20
|
+
var DEFAULT_SCORING = {
|
|
21
|
+
criticalWeight: 3,
|
|
22
|
+
degradedValue: 0.6,
|
|
23
|
+
thresholds: { operational: 98, degraded: 85 }
|
|
24
|
+
};
|
|
25
|
+
function resolveScoring(input = {}) {
|
|
26
|
+
return {
|
|
27
|
+
criticalWeight: input.criticalWeight ?? DEFAULT_SCORING.criticalWeight,
|
|
28
|
+
degradedValue: input.degradedValue ?? DEFAULT_SCORING.degradedValue,
|
|
29
|
+
thresholds: { ...DEFAULT_SCORING.thresholds, ...input.thresholds }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function labelForScore(score, scoring = DEFAULT_SCORING) {
|
|
33
|
+
if (score === null) return "unknown";
|
|
34
|
+
if (score >= scoring.thresholds.operational) return "operational";
|
|
35
|
+
if (score >= scoring.thresholds.degraded) return "degraded";
|
|
36
|
+
return "outage";
|
|
37
|
+
}
|
|
38
|
+
function computeScore({
|
|
39
|
+
results,
|
|
40
|
+
scoring = DEFAULT_SCORING
|
|
41
|
+
}) {
|
|
42
|
+
const counts = { up: 0, degraded: 0, down: 0, sleeping: 0, unconfigured: 0 };
|
|
43
|
+
const values = {
|
|
44
|
+
up: 1,
|
|
45
|
+
sleeping: 1,
|
|
46
|
+
degraded: scoring.degradedValue,
|
|
47
|
+
down: 0
|
|
48
|
+
};
|
|
49
|
+
let weighted = 0;
|
|
50
|
+
let totalWeight = 0;
|
|
51
|
+
let criticalDown = false;
|
|
52
|
+
for (const { status, critical } of results) {
|
|
53
|
+
counts[status] += 1;
|
|
54
|
+
if (status === "unconfigured") continue;
|
|
55
|
+
const weight = critical ? scoring.criticalWeight : 1;
|
|
56
|
+
weighted += weight * values[status];
|
|
57
|
+
totalWeight += weight;
|
|
58
|
+
if (critical && status === "down") criticalDown = true;
|
|
59
|
+
}
|
|
60
|
+
if (totalWeight === 0) return { score: null, label: "unknown", counts };
|
|
61
|
+
const score = Math.round(100 * weighted / totalWeight);
|
|
62
|
+
return { score, label: criticalDown ? "outage" : labelForScore(score, scoring), counts };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/history.ts
|
|
66
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
67
|
+
var RANGES = {
|
|
68
|
+
"24h": { durationMs: DAY_MS, bucketMs: 5 * 60 * 1e3 },
|
|
69
|
+
"7d": { durationMs: 7 * DAY_MS, bucketMs: 60 * 60 * 1e3 }
|
|
70
|
+
};
|
|
71
|
+
function percentile(sorted, p) {
|
|
72
|
+
if (!sorted.length) return null;
|
|
73
|
+
const index = Math.min(sorted.length - 1, Math.ceil(p / 100 * sorted.length) - 1);
|
|
74
|
+
return sorted[Math.max(0, index)] ?? null;
|
|
75
|
+
}
|
|
76
|
+
function uptime(samples) {
|
|
77
|
+
if (!samples.length) return null;
|
|
78
|
+
const available = samples.filter((sample) => sample.status !== "down").length;
|
|
79
|
+
return Math.round(available / samples.length * 1e4) / 100;
|
|
80
|
+
}
|
|
81
|
+
var measured = (sample) => sample.latencyMs !== void 0 && (sample.status === "up" || sample.status === "degraded");
|
|
82
|
+
function computeServiceStats({
|
|
83
|
+
samples,
|
|
84
|
+
now = Date.now()
|
|
85
|
+
}) {
|
|
86
|
+
const day = samples.filter((sample) => sample.at >= now - DAY_MS);
|
|
87
|
+
const week = samples.filter((sample) => sample.at >= now - 7 * DAY_MS);
|
|
88
|
+
const latencies = day.filter(measured).map((sample) => sample.latencyMs).sort((a, b) => a - b);
|
|
89
|
+
const lastFailure = [...samples].reverse().find((sample) => sample.status === "down" || sample.status === "degraded");
|
|
90
|
+
const byInstance = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const sample of day) {
|
|
92
|
+
for (const [key, [status, latencyMs]] of Object.entries(sample.instances ?? {})) {
|
|
93
|
+
byInstance.set(key, [...byInstance.get(key) ?? [], { status, latencyMs }]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const instances = byInstance.size ? Object.fromEntries(
|
|
97
|
+
[...byInstance].map(([key, list]) => [
|
|
98
|
+
key,
|
|
99
|
+
{
|
|
100
|
+
uptime24h: uptime(list),
|
|
101
|
+
p50: percentile(
|
|
102
|
+
list.filter(measured).map((item) => item.latencyMs).sort((a, b) => a - b),
|
|
103
|
+
50
|
|
104
|
+
),
|
|
105
|
+
samples24h: list.length
|
|
106
|
+
}
|
|
107
|
+
])
|
|
108
|
+
) : void 0;
|
|
109
|
+
return {
|
|
110
|
+
uptime24h: uptime(day),
|
|
111
|
+
uptime7d: uptime(week),
|
|
112
|
+
p50: percentile(latencies, 50),
|
|
113
|
+
p95: percentile(latencies, 95),
|
|
114
|
+
samples24h: day.length,
|
|
115
|
+
latency: day.map((sample) => sample.latencyMs ?? null),
|
|
116
|
+
lastFailure: lastFailure ? { at: lastFailure.at, status: lastFailure.status, error: lastFailure.error } : void 0,
|
|
117
|
+
instances
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var LABEL_SEVERITY = { unknown: 0, operational: 1, degraded: 2, outage: 3 };
|
|
121
|
+
function bucketRuns({
|
|
122
|
+
runs,
|
|
123
|
+
range,
|
|
124
|
+
now = Date.now(),
|
|
125
|
+
scoring
|
|
126
|
+
}) {
|
|
127
|
+
const { durationMs, bucketMs } = RANGES[range];
|
|
128
|
+
const end = Math.floor(now / bucketMs) * bucketMs + bucketMs;
|
|
129
|
+
const count = Math.round(durationMs / bucketMs);
|
|
130
|
+
const start = end - count * bucketMs;
|
|
131
|
+
const bars = Array.from({ length: count }, (_, i) => ({
|
|
132
|
+
start: start + i * bucketMs,
|
|
133
|
+
score: null,
|
|
134
|
+
label: "unknown",
|
|
135
|
+
runs: 0,
|
|
136
|
+
failing: {}
|
|
137
|
+
}));
|
|
138
|
+
for (const run of runs) {
|
|
139
|
+
const bar = bars[Math.floor((run.at - start) / bucketMs)];
|
|
140
|
+
if (!bar) continue;
|
|
141
|
+
bar.runs += 1;
|
|
142
|
+
if (run.score !== null && (bar.score === null || run.score < bar.score)) bar.score = run.score;
|
|
143
|
+
const label = run.label ?? labelForScore(run.score, scoring);
|
|
144
|
+
if (LABEL_SEVERITY[label] > LABEL_SEVERITY[bar.label]) bar.label = label;
|
|
145
|
+
for (const [key, status] of Object.entries(run.failing)) {
|
|
146
|
+
if (bar.failing[key] !== "down") bar.failing[key] = status;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return bars;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { DEFAULT_SCORING, RANGES, bucketRuns, computeScore, computeServiceStats, labelForScore, resolveScoring, toMs };
|
|
153
|
+
//# sourceMappingURL=chunk-APC2HVSI.js.map
|
|
154
|
+
//# sourceMappingURL=chunk-APC2HVSI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/duration.ts","../src/score.ts","../src/history.ts"],"names":[],"mappings":"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","file":"chunk-APC2HVSI.js","sourcesContent":["const UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\n/** A number of milliseconds, or a string like \"30s\", \"5m\", \"1h\", \"7d\" */\nexport type Duration = number | `${number}${\"ms\" | \"s\" | \"m\" | \"h\" | \"d\"}`;\n\nexport function toMs(duration: Duration): number {\n if (typeof duration === \"number\") {\n if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid duration: ${duration}`);\n return duration;\n }\n const match = /^(\\d+(?:\\.\\d+)?)(ms|s|m|h|d)$/.exec(duration.trim());\n if (!match) throw new Error(`Invalid duration: \"${duration}\" (use e.g. \"30s\", \"5m\", \"1h\", \"7d\")`);\n return Number(match[1]) * UNITS[match[2] as string]!;\n}\n","import type { ScoreLabel, Status } from \"./types\";\n\nexport type Scoring = {\n /** How much more a critical service weighs than a normal one (default 3) */\n criticalWeight: number;\n /** Value of a degraded service between 0 and 1 (default 0.6) */\n degradedValue: number;\n thresholds: {\n /** Score from which the system is \"operational\" (default 98) */\n operational: number;\n /** Score from which the system is \"degraded\" rather than \"outage\" (default 85) */\n degraded: number;\n };\n};\n\nexport const DEFAULT_SCORING: Scoring = {\n criticalWeight: 3,\n degradedValue: 0.6,\n thresholds: { operational: 98, degraded: 85 },\n};\n\nexport type ScoringInput = {\n criticalWeight?: number;\n degradedValue?: number;\n thresholds?: Partial<Scoring[\"thresholds\"]>;\n};\n\nexport function resolveScoring(input: ScoringInput = {}): Scoring {\n return {\n criticalWeight: input.criticalWeight ?? DEFAULT_SCORING.criticalWeight,\n degradedValue: input.degradedValue ?? DEFAULT_SCORING.degradedValue,\n thresholds: { ...DEFAULT_SCORING.thresholds, ...input.thresholds },\n };\n}\n\nexport function labelForScore(score: number | null, scoring: Scoring = DEFAULT_SCORING): ScoreLabel {\n if (score === null) return \"unknown\";\n if (score >= scoring.thresholds.operational) return \"operational\";\n if (score >= scoring.thresholds.degraded) return \"degraded\";\n return \"outage\";\n}\n\n/**\n * Weighted share of healthy services, 0–100. Sleeping services are expected (scale to zero)\n * and count as healthy; unconfigured ones are left out. Any critical service being down\n * forces the \"outage\" label, whatever the number.\n */\nexport function computeScore({\n results,\n scoring = DEFAULT_SCORING,\n}: {\n results: Array<{ status: Status; critical: boolean }>;\n scoring?: Scoring;\n}): { score: number | null; label: ScoreLabel; counts: Record<Status, number> } {\n const counts: Record<Status, number> = { up: 0, degraded: 0, down: 0, sleeping: 0, unconfigured: 0 };\n const values: Record<Exclude<Status, \"unconfigured\">, number> = {\n up: 1,\n sleeping: 1,\n degraded: scoring.degradedValue,\n down: 0,\n };\n let weighted = 0;\n let totalWeight = 0;\n let criticalDown = false;\n\n for (const { status, critical } of results) {\n counts[status] += 1;\n if (status === \"unconfigured\") continue;\n const weight = critical ? scoring.criticalWeight : 1;\n weighted += weight * values[status];\n totalWeight += weight;\n if (critical && status === \"down\") criticalDown = true;\n }\n\n if (totalWeight === 0) return { score: null, label: \"unknown\", counts };\n const score = Math.round((100 * weighted) / totalWeight);\n return { score, label: criticalDown ? \"outage\" : labelForScore(score, scoring), counts };\n}\n","import { labelForScore, type Scoring } from \"./score\";\nimport type { CheckSample, RunSummary } from \"./store\";\nimport type { HistoryBar, HistoryRange, InstanceStats, ScoreLabel, ServiceStats, Status } from \"./types\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport const RANGES: Record<HistoryRange, { durationMs: number; bucketMs: number }> = {\n \"24h\": { durationMs: DAY_MS, bucketMs: 5 * 60 * 1000 },\n \"7d\": { durationMs: 7 * DAY_MS, bucketMs: 60 * 60 * 1000 },\n};\n\nfunction percentile(sorted: number[], p: number): number | null {\n if (!sorted.length) return null;\n const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);\n return sorted[Math.max(0, index)] ?? null;\n}\n\n/** Share of samples that were not \"down\", in percent with two decimals */\nfunction uptime(samples: Array<{ status: Status }>): number | null {\n if (!samples.length) return null;\n const available = samples.filter((sample) => sample.status !== \"down\").length;\n return Math.round((available / samples.length) * 10000) / 100;\n}\n\nconst measured = (sample: { status: Status; latencyMs?: number }) =>\n sample.latencyMs !== undefined && (sample.status === \"up\" || sample.status === \"degraded\");\n\n/** Uptime, latency percentiles, sparkline series and last failure of one service */\nexport function computeServiceStats({\n samples,\n now = Date.now(),\n}: {\n /** Samples of the whole retention window, oldest first */\n samples: CheckSample[];\n now?: number;\n}): ServiceStats {\n const day = samples.filter((sample) => sample.at >= now - DAY_MS);\n const week = samples.filter((sample) => sample.at >= now - 7 * DAY_MS);\n const latencies = day\n .filter(measured)\n .map((sample) => sample.latencyMs as number)\n .sort((a, b) => a - b);\n const lastFailure = [...samples]\n .reverse()\n .find((sample) => sample.status === \"down\" || sample.status === \"degraded\");\n\n // Same figures per instance, over the last 24h\n const byInstance = new Map<string, Array<{ status: Status; latencyMs?: number }>>();\n for (const sample of day) {\n for (const [key, [status, latencyMs]] of Object.entries(sample.instances ?? {})) {\n byInstance.set(key, [...(byInstance.get(key) ?? []), { status, latencyMs }]);\n }\n }\n const instances: Record<string, InstanceStats> | undefined = byInstance.size\n ? Object.fromEntries(\n [...byInstance].map(([key, list]) => [\n key,\n {\n uptime24h: uptime(list),\n p50: percentile(\n list\n .filter(measured)\n .map((item) => item.latencyMs as number)\n .sort((a, b) => a - b),\n 50\n ),\n samples24h: list.length,\n },\n ])\n )\n : undefined;\n\n return {\n uptime24h: uptime(day),\n uptime7d: uptime(week),\n p50: percentile(latencies, 50),\n p95: percentile(latencies, 95),\n samples24h: day.length,\n latency: day.map((sample) => sample.latencyMs ?? null),\n lastFailure: lastFailure\n ? { at: lastFailure.at, status: lastFailure.status, error: lastFailure.error }\n : undefined,\n instances,\n };\n}\n\nconst LABEL_SEVERITY: Record<ScoreLabel, number> = { unknown: 0, operational: 1, degraded: 2, outage: 3 };\n\n/** Group runs into fixed buckets; each bucket keeps its worst score, label and failures */\nexport function bucketRuns({\n runs,\n range,\n now = Date.now(),\n scoring,\n}: {\n runs: RunSummary[];\n range: HistoryRange;\n now?: number;\n scoring?: Scoring;\n}): HistoryBar[] {\n const { durationMs, bucketMs } = RANGES[range];\n const end = Math.floor(now / bucketMs) * bucketMs + bucketMs;\n const count = Math.round(durationMs / bucketMs);\n const start = end - count * bucketMs;\n\n const bars: HistoryBar[] = Array.from({ length: count }, (_, i) => ({\n start: start + i * bucketMs,\n score: null,\n label: \"unknown\",\n runs: 0,\n failing: {},\n }));\n\n for (const run of runs) {\n const bar = bars[Math.floor((run.at - start) / bucketMs)];\n if (!bar) continue;\n bar.runs += 1;\n if (run.score !== null && (bar.score === null || run.score < bar.score)) bar.score = run.score;\n const label = run.label ?? labelForScore(run.score, scoring);\n if (LABEL_SEVERITY[label] > LABEL_SEVERITY[bar.label]) bar.label = label;\n for (const [key, status] of Object.entries(run.failing)) {\n // \"down\" wins over \"degraded\" within a bucket\n if (bar.failing[key] !== \"down\") bar.failing[key] = status;\n }\n }\n return bars;\n}\n"]}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
3
|
+
|
|
4
|
+
// src/react/client.ts
|
|
5
|
+
var RateLimitError = class extends Error {
|
|
6
|
+
constructor(retryAfter) {
|
|
7
|
+
super(`Please wait ${retryAfter}s before running this again`);
|
|
8
|
+
this.retryAfter = retryAfter;
|
|
9
|
+
this.name = "RateLimitError";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
};
|
|
13
|
+
function createStatusClient({
|
|
14
|
+
baseUrl = "/api",
|
|
15
|
+
fetch: fetchImpl
|
|
16
|
+
} = {}) {
|
|
17
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
18
|
+
async function request(path, init) {
|
|
19
|
+
const response = await (_nullishCoalesce(fetchImpl, () => ( fetch)))(`${base}${path}`, init);
|
|
20
|
+
if (response.status === 429) {
|
|
21
|
+
const body = await response.json().catch(() => ({}));
|
|
22
|
+
throw new RateLimitError(_nullishCoalesce(body.retryAfter, () => ( Number(_nullishCoalesce(response.headers.get("retry-after"), () => ( 30))))));
|
|
23
|
+
}
|
|
24
|
+
if (!response.ok) throw new Error(`Request failed (${response.status})`);
|
|
25
|
+
return await response.json();
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
status: () => request("/status"),
|
|
29
|
+
history: (range) => request(`/history?range=${range}`),
|
|
30
|
+
serviceHistory: (key, range) => request(`/services/${encodeURIComponent(key)}/history?range=${range}`),
|
|
31
|
+
run: (key) => request(key ? `/run/${encodeURIComponent(key)}` : "/run", { method: "POST" })
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/react/hooks.tsx
|
|
36
|
+
var _reactquery = require('@tanstack/react-query');
|
|
37
|
+
var _react = require('react');
|
|
38
|
+
var _jsxruntime = require('react/jsx-runtime');
|
|
39
|
+
var ClientContext = _react.createContext.call(void 0, void 0);
|
|
40
|
+
function StatusProvider({
|
|
41
|
+
baseUrl,
|
|
42
|
+
client,
|
|
43
|
+
children
|
|
44
|
+
}) {
|
|
45
|
+
const value = _react.useMemo.call(void 0, () => _nullishCoalesce(client, () => ( createStatusClient({ baseUrl }))), [client, baseUrl]);
|
|
46
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ClientContext.Provider, { value, children });
|
|
47
|
+
}
|
|
48
|
+
var fallbackClient = createStatusClient();
|
|
49
|
+
function useStatusClient() {
|
|
50
|
+
return _nullishCoalesce(_react.useContext.call(void 0, ClientContext), () => ( fallbackClient));
|
|
51
|
+
}
|
|
52
|
+
function useStatus({ refetchInterval = 3e4 } = {}) {
|
|
53
|
+
const client = useStatusClient();
|
|
54
|
+
return _reactquery.useQuery.call(void 0, { queryKey: ["sermon", "status"], queryFn: () => client.status(), refetchInterval });
|
|
55
|
+
}
|
|
56
|
+
function useHistory(range, { refetchInterval = 6e4 } = {}) {
|
|
57
|
+
const client = useStatusClient();
|
|
58
|
+
return _reactquery.useQuery.call(void 0, { queryKey: ["sermon", "history", range], queryFn: () => client.history(range), refetchInterval });
|
|
59
|
+
}
|
|
60
|
+
function useRunChecks() {
|
|
61
|
+
const client = useStatusClient();
|
|
62
|
+
const queryClient = _reactquery.useQueryClient.call(void 0, );
|
|
63
|
+
return _reactquery.useMutation.call(void 0, {
|
|
64
|
+
mutationFn: (key) => client.run(key || void 0),
|
|
65
|
+
onSuccess: (snapshot) => {
|
|
66
|
+
queryClient.setQueryData(["sermon", "status"], snapshot);
|
|
67
|
+
void queryClient.invalidateQueries({ queryKey: ["sermon", "history"] });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/react/tree.ts
|
|
73
|
+
var STATUS_ORDER = { down: 0, degraded: 1, up: 2, sleeping: 3, unconfigured: 4 };
|
|
74
|
+
var statusOf = (service) => service.configured ? _nullishCoalesce(_optionalChain([service, 'access', _ => _.result, 'optionalAccess', _2 => _2.status]), () => ( "unconfigured")) : "unconfigured";
|
|
75
|
+
function sortServices(services) {
|
|
76
|
+
return [...services].sort(
|
|
77
|
+
(a, b) => STATUS_ORDER[statusOf(a)] - STATUS_ORDER[statusOf(b)] || Number(b.critical) - Number(a.critical) || a.name.localeCompare(b.name)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function flattenServices(nodes) {
|
|
81
|
+
return nodes.flatMap((node) => node.type === "service" ? [node] : flattenServices(node.children));
|
|
82
|
+
}
|
|
83
|
+
function toSection(node) {
|
|
84
|
+
return {
|
|
85
|
+
key: node.key,
|
|
86
|
+
title: node.title,
|
|
87
|
+
description: node.description,
|
|
88
|
+
services: sortServices(node.children.filter((child) => child.type === "service")),
|
|
89
|
+
groups: node.children.filter((child) => child.type === "group").map(toSection)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function buildLayout(tree, { ungroupedTitle = "Services" } = {}) {
|
|
93
|
+
const blocks = [];
|
|
94
|
+
let loose = [];
|
|
95
|
+
let looseCount = 0;
|
|
96
|
+
const flushLoose = () => {
|
|
97
|
+
if (!loose.length) return;
|
|
98
|
+
looseCount += 1;
|
|
99
|
+
blocks.push({
|
|
100
|
+
type: "section",
|
|
101
|
+
section: {
|
|
102
|
+
key: looseCount === 1 ? "ungrouped" : `ungrouped-${looseCount}`,
|
|
103
|
+
title: ungroupedTitle,
|
|
104
|
+
services: sortServices(loose),
|
|
105
|
+
groups: []
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
loose = [];
|
|
109
|
+
};
|
|
110
|
+
for (const node of tree) {
|
|
111
|
+
if (node.type === "service") {
|
|
112
|
+
loose.push(node);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
flushLoose();
|
|
116
|
+
const section = toSection(node);
|
|
117
|
+
if (section.services.length === 0 && section.groups.length > 0) {
|
|
118
|
+
blocks.push({
|
|
119
|
+
type: "heading",
|
|
120
|
+
key: section.key,
|
|
121
|
+
title: section.title,
|
|
122
|
+
description: section.description,
|
|
123
|
+
sections: section.groups
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
blocks.push({ type: "section", section });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
flushLoose();
|
|
130
|
+
return blocks;
|
|
131
|
+
}
|
|
132
|
+
function servicesOf(section) {
|
|
133
|
+
return [...section.services, ...section.groups.flatMap(servicesOf)];
|
|
134
|
+
}
|
|
135
|
+
function sectionsOf(blocks) {
|
|
136
|
+
return blocks.flatMap((block) => block.type === "section" ? [block.section] : block.sections);
|
|
137
|
+
}
|
|
138
|
+
function isStale(snapshot, now = Date.now()) {
|
|
139
|
+
return snapshot.lastRunAt !== null && now - snapshot.lastRunAt > 2 * snapshot.intervalMs;
|
|
140
|
+
}
|
|
141
|
+
var LABEL_SEVERITY = { unknown: 0, operational: 1, degraded: 2, outage: 3 };
|
|
142
|
+
function mergeBars(bars, maxBars) {
|
|
143
|
+
if (maxBars < 1 || bars.length <= maxBars) return { bars, factor: 1 };
|
|
144
|
+
const factor = Math.ceil(bars.length / maxBars);
|
|
145
|
+
const merged = [];
|
|
146
|
+
for (let end = bars.length; end > 0; end -= factor) {
|
|
147
|
+
const group = bars.slice(Math.max(0, end - factor), end);
|
|
148
|
+
const combined = { start: group[0].start, score: null, label: "unknown", runs: 0, failing: {} };
|
|
149
|
+
for (const bar of group) {
|
|
150
|
+
combined.runs += bar.runs;
|
|
151
|
+
if (bar.score !== null && (combined.score === null || bar.score < combined.score)) combined.score = bar.score;
|
|
152
|
+
if (LABEL_SEVERITY[bar.label] > LABEL_SEVERITY[combined.label]) combined.label = bar.label;
|
|
153
|
+
for (const [key, status] of Object.entries(bar.failing)) {
|
|
154
|
+
if (combined.failing[key] !== "down") combined.failing[key] = status;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
merged.unshift(combined);
|
|
158
|
+
}
|
|
159
|
+
return { bars: merged, factor };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/react/anchors.ts
|
|
163
|
+
var serviceAnchor = (key) => `service-${key}`;
|
|
164
|
+
var groupAnchor = (key) => `group-${key}`;
|
|
165
|
+
var FOCUS_EVENT = "sermon:focus";
|
|
166
|
+
function focusAnchor(anchor, { smooth = true } = {}) {
|
|
167
|
+
history.replaceState(null, "", `#${encodeURIComponent(anchor)}`);
|
|
168
|
+
window.dispatchEvent(new CustomEvent(FOCUS_EVENT, { detail: { anchor } }));
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
const target = document.getElementById(anchor);
|
|
171
|
+
if (!target) return;
|
|
172
|
+
const reduceMotion = _nullishCoalesce(_optionalChain([window, 'access', _3 => _3.matchMedia, 'optionalCall', _4 => _4("(prefers-reduced-motion: reduce)"), 'access', _5 => _5.matches]), () => ( false));
|
|
173
|
+
_optionalChain([target, 'access', _6 => _6.scrollIntoView, 'optionalCall', _7 => _7({ behavior: smooth && !reduceMotion ? "smooth" : "auto", block: "start" })]);
|
|
174
|
+
if (anchor.startsWith("service-") && !reduceMotion) {
|
|
175
|
+
_optionalChain([target, 'access', _8 => _8.animate, 'optionalCall', _9 => _9(
|
|
176
|
+
[
|
|
177
|
+
{ backgroundColor: "color-mix(in oklch, var(--accent) 14%, transparent)" },
|
|
178
|
+
{ backgroundColor: "transparent" }
|
|
179
|
+
],
|
|
180
|
+
{ duration: 1600, easing: "ease-out" }
|
|
181
|
+
)]);
|
|
182
|
+
}
|
|
183
|
+
}, 50);
|
|
184
|
+
}
|
|
185
|
+
function currentAnchor() {
|
|
186
|
+
const hash = window.location.hash.slice(1);
|
|
187
|
+
if (!hash) return void 0;
|
|
188
|
+
try {
|
|
189
|
+
return decodeURIComponent(hash);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return hash;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/react/theme.ts
|
|
196
|
+
|
|
197
|
+
var STORAGE_KEY = "sermon:theme";
|
|
198
|
+
function readPreference() {
|
|
199
|
+
try {
|
|
200
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
201
|
+
return stored === "light" || stored === "dark" ? stored : "system";
|
|
202
|
+
} catch (e2) {
|
|
203
|
+
return "system";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
var prefersDark = () => typeof window !== "undefined" && (_nullishCoalesce(_optionalChain([window, 'access', _10 => _10.matchMedia, 'optionalCall', _11 => _11("(prefers-color-scheme: dark)"), 'access', _12 => _12.matches]), () => ( false)));
|
|
207
|
+
function applyTheme(preference = readPreference()) {
|
|
208
|
+
if (typeof document === "undefined") return;
|
|
209
|
+
const theme = preference === "system" ? prefersDark() ? "dark" : "light" : preference;
|
|
210
|
+
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
211
|
+
document.documentElement.dataset.theme = theme;
|
|
212
|
+
document.documentElement.style.colorScheme = theme;
|
|
213
|
+
}
|
|
214
|
+
function useThemePreference() {
|
|
215
|
+
const [preference, setPreference] = _react.useState.call(void 0, readPreference);
|
|
216
|
+
_react.useEffect.call(void 0, () => {
|
|
217
|
+
applyTheme(preference);
|
|
218
|
+
try {
|
|
219
|
+
if (preference === "system") localStorage.removeItem(STORAGE_KEY);
|
|
220
|
+
else localStorage.setItem(STORAGE_KEY, preference);
|
|
221
|
+
} catch (e3) {
|
|
222
|
+
}
|
|
223
|
+
if (preference !== "system") return;
|
|
224
|
+
const media = _optionalChain([window, 'access', _13 => _13.matchMedia, 'optionalCall', _14 => _14("(prefers-color-scheme: dark)")]);
|
|
225
|
+
const onChange = () => applyTheme("system");
|
|
226
|
+
_optionalChain([media, 'optionalAccess', _15 => _15.addEventListener, 'call', _16 => _16("change", onChange)]);
|
|
227
|
+
return () => _optionalChain([media, 'optionalAccess', _17 => _17.removeEventListener, 'call', _18 => _18("change", onChange)]);
|
|
228
|
+
}, [preference]);
|
|
229
|
+
return [preference, setPreference];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/react/format.ts
|
|
233
|
+
var STATUS_TONE = {
|
|
234
|
+
up: "success",
|
|
235
|
+
degraded: "warning",
|
|
236
|
+
down: "danger",
|
|
237
|
+
sleeping: "accent",
|
|
238
|
+
unconfigured: "default"
|
|
239
|
+
};
|
|
240
|
+
var STATUS_TEXT = {
|
|
241
|
+
up: "Operational",
|
|
242
|
+
degraded: "Degraded",
|
|
243
|
+
down: "Down",
|
|
244
|
+
sleeping: "Asleep (expected)",
|
|
245
|
+
unconfigured: "Not configured"
|
|
246
|
+
};
|
|
247
|
+
var LABEL_TONE = {
|
|
248
|
+
operational: "success",
|
|
249
|
+
degraded: "warning",
|
|
250
|
+
outage: "danger",
|
|
251
|
+
unknown: "default"
|
|
252
|
+
};
|
|
253
|
+
var LABEL_TEXT = {
|
|
254
|
+
operational: "All systems operational",
|
|
255
|
+
degraded: "Degraded performance",
|
|
256
|
+
outage: "Major outage",
|
|
257
|
+
unknown: "No data yet"
|
|
258
|
+
};
|
|
259
|
+
var relative = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
260
|
+
function timeAgo(timestamp, now = Date.now()) {
|
|
261
|
+
const seconds = Math.round((timestamp - now) / 1e3);
|
|
262
|
+
if (Math.abs(seconds) < 60) return relative.format(seconds, "second");
|
|
263
|
+
const minutes = Math.round(seconds / 60);
|
|
264
|
+
if (Math.abs(minutes) < 60) return relative.format(minutes, "minute");
|
|
265
|
+
const hours = Math.round(minutes / 60);
|
|
266
|
+
if (Math.abs(hours) < 24) return relative.format(hours, "hour");
|
|
267
|
+
return relative.format(Math.round(hours / 24), "day");
|
|
268
|
+
}
|
|
269
|
+
function formatMs(ms) {
|
|
270
|
+
if (ms === null || ms === void 0) return "\u2013";
|
|
271
|
+
return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)} s` : `${ms} ms`;
|
|
272
|
+
}
|
|
273
|
+
function formatPct(pct) {
|
|
274
|
+
if (pct === null || pct === void 0) return "\u2013";
|
|
275
|
+
return `${pct.toFixed(pct === 100 ? 0 : 2)}%`;
|
|
276
|
+
}
|
|
277
|
+
var dateTime = new Intl.DateTimeFormat("en", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
278
|
+
function formatDateTime(timestamp) {
|
|
279
|
+
return dateTime.format(timestamp);
|
|
280
|
+
}
|
|
281
|
+
function formatDuration(ms) {
|
|
282
|
+
const units = [
|
|
283
|
+
[864e5, "day"],
|
|
284
|
+
[36e5, "hour"],
|
|
285
|
+
[6e4, "minute"],
|
|
286
|
+
[1e3, "second"]
|
|
287
|
+
];
|
|
288
|
+
for (const [size, name] of units) {
|
|
289
|
+
if (ms >= size) {
|
|
290
|
+
const count = Math.round(ms / size);
|
|
291
|
+
return `${count} ${name}${count === 1 ? "" : "s"}`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return `${ms} ms`;
|
|
295
|
+
}
|
|
296
|
+
function humanize(key) {
|
|
297
|
+
const spaced = key.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
298
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
exports.RateLimitError = RateLimitError; exports.createStatusClient = createStatusClient; exports.StatusProvider = StatusProvider; exports.useStatusClient = useStatusClient; exports.useStatus = useStatus; exports.useHistory = useHistory; exports.useRunChecks = useRunChecks; exports.statusOf = statusOf; exports.sortServices = sortServices; exports.flattenServices = flattenServices; exports.buildLayout = buildLayout; exports.servicesOf = servicesOf; exports.sectionsOf = sectionsOf; exports.isStale = isStale; exports.mergeBars = mergeBars; exports.serviceAnchor = serviceAnchor; exports.groupAnchor = groupAnchor; exports.FOCUS_EVENT = FOCUS_EVENT; exports.focusAnchor = focusAnchor; exports.currentAnchor = currentAnchor; exports.applyTheme = applyTheme; exports.useThemePreference = useThemePreference; exports.STATUS_TONE = STATUS_TONE; exports.STATUS_TEXT = STATUS_TEXT; exports.LABEL_TONE = LABEL_TONE; exports.LABEL_TEXT = LABEL_TEXT; exports.timeAgo = timeAgo; exports.formatMs = formatMs; exports.formatPct = formatPct; exports.formatDateTime = formatDateTime; exports.formatDuration = formatDuration; exports.humanize = humanize;
|
|
335
|
+
//# sourceMappingURL=chunk-D7BBJTYU.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/sermon-js/sermon-js/dist/chunk-D7BBJTYU.cjs","../src/react/client.ts","../src/react/hooks.tsx","../src/react/tree.ts","../src/react/anchors.ts","../src/react/theme.ts","../src/react/format.ts"],"names":[],"mappings":"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6B,IAAA;AAEK,IAAA;AACQ,IAAA;AACA,oBAAA;AACF,IAAA;AAC3B,EAAA;AAEkB,EAAA;AACnC;ALqLiD;AACA;AMlOA;AAC3C,EAAA;AACM,EAAA;AACJ,EAAA;AACI,EAAA;AACI,EAAA;AAChB;AAEmD;AAC7C,EAAA;AACM,EAAA;AACJ,EAAA;AACI,EAAA;AACI,EAAA;AAChB;AAEoD;AACrC,EAAA;AACH,EAAA;AACF,EAAA;AACC,EAAA;AACX;AAEsD;AACvC,EAAA;AACH,EAAA;AACF,EAAA;AACC,EAAA;AACX;AAEmD;AAEU;AACZ,EAAA;AACH,EAAA;AACL,EAAA;AACK,EAAA;AACP,EAAA;AACK,EAAA;AACK,EAAA;AACjD;AAEwD;AACV,EAAA;AACC,EAAA;AAC/C;AAE0D;AACV,EAAA;AACJ,EAAA;AAC5C;AAEiD;AAEC;AAChB,EAAA;AAClC;AAG2C;AACF,EAAA;AACnB,IAAA;AACA,IAAA;AACD,IAAA;AACF,IAAA;AACjB,EAAA;AACkC,EAAA;AAChB,IAAA;AACoB,MAAA;AACM,MAAA;AAC1C,IAAA;AACF,EAAA;AACY,EAAA;AACd;AAGsC;AACU,EAAA;AACC,EAAA;AACjD;ANuNiD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/sermon-js/sermon-js/dist/chunk-D7BBJTYU.cjs","sourcesContent":[null,"import type { ServiceHistory } from \"../status\";\nimport type { HistoryPayload, HistoryRange, StatusSnapshot } from \"../types\";\n\n/** Thrown when an on-demand run is refused because one happened too recently */\nexport class RateLimitError extends Error {\n constructor(readonly retryAfter: number) {\n super(`Please wait ${retryAfter}s before running this again`);\n this.name = \"RateLimitError\";\n }\n}\n\nexport type StatusClient = {\n status(): Promise<StatusSnapshot>;\n history(range: HistoryRange): Promise<HistoryPayload>;\n serviceHistory(key: string, range: HistoryRange): Promise<ServiceHistory>;\n /** Run everything, or one service; resolves with the fresh snapshot */\n run(key?: string): Promise<StatusSnapshot>;\n};\n\n/** Talks to the routes of `createStatusHandler` */\nexport function createStatusClient({\n baseUrl = \"/api\",\n fetch: fetchImpl,\n}: {\n /** Where the API is mounted (default \"/api\"); may be an absolute URL */\n baseUrl?: string;\n fetch?: typeof fetch;\n} = {}): StatusClient {\n const base = baseUrl.replace(/\\/+$/, \"\");\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await (fetchImpl ?? fetch)(`${base}${path}`, init);\n if (response.status === 429) {\n const body = (await response.json().catch(() => ({}))) as { retryAfter?: number };\n throw new RateLimitError(body.retryAfter ?? Number(response.headers.get(\"retry-after\") ?? 30));\n }\n if (!response.ok) throw new Error(`Request failed (${response.status})`);\n return (await response.json()) as T;\n }\n return {\n status: () => request(\"/status\"),\n history: (range) => request(`/history?range=${range}`),\n serviceHistory: (key, range) => request(`/services/${encodeURIComponent(key)}/history?range=${range}`),\n run: (key) => request(key ? `/run/${encodeURIComponent(key)}` : \"/run\", { method: \"POST\" }),\n };\n}\n","import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createContext, useContext, useMemo, type ReactNode } from \"react\";\nimport type { HistoryRange } from \"../types\";\nimport { createStatusClient, type StatusClient } from \"./client\";\n\nconst ClientContext = createContext<StatusClient | undefined>(undefined);\n\n/** Tells the hooks where the status API lives. Needs a React Query `QueryClientProvider` above it. */\nexport function StatusProvider({\n baseUrl,\n client,\n children,\n}: {\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 children: ReactNode;\n}) {\n const value = useMemo(() => client ?? createStatusClient({ baseUrl }), [client, baseUrl]);\n return <ClientContext.Provider value={value}>{children}</ClientContext.Provider>;\n}\n\nconst fallbackClient = createStatusClient();\n\nexport function useStatusClient(): StatusClient {\n return useContext(ClientContext) ?? fallbackClient;\n}\n\n/** The snapshot, refreshed periodically (default every 30 s) */\nexport function useStatus({ refetchInterval = 30_000 }: { refetchInterval?: number | false } = {}) {\n const client = useStatusClient();\n return useQuery({ queryKey: [\"sermon\", \"status\"], queryFn: () => client.status(), refetchInterval });\n}\n\n/** The score history for a range */\nexport function useHistory(range: HistoryRange, { refetchInterval = 60_000 }: { refetchInterval?: number | false } = {}) {\n const client = useStatusClient();\n return useQuery({ queryKey: [\"sermon\", \"history\", range], queryFn: () => client.history(range), refetchInterval });\n}\n\n/**\n * Run everything (`mutate()`) or one service (`mutate(key)`) on demand. The snapshot updates\n * in place; a refused run surfaces as a `RateLimitError` in `error`.\n */\nexport function useRunChecks() {\n const client = useStatusClient();\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: (key?: string | void) => client.run(key || undefined),\n onSuccess: (snapshot) => {\n queryClient.setQueryData([\"sermon\", \"status\"], snapshot);\n void queryClient.invalidateQueries({ queryKey: [\"sermon\", \"history\"] });\n },\n });\n}\n","import type { HistoryBar, ScoreLabel, ServiceSnapshot, SnapshotNode, Status, StatusSnapshot } from \"../types\";\n\nconst STATUS_ORDER: Record<Status, number> = { down: 0, degraded: 1, up: 2, sleeping: 3, unconfigured: 4 };\n\n/** The status to show for a service: its latest result, or \"unconfigured\" */\nexport const statusOf = (service: ServiceSnapshot): Status =>\n service.configured ? (service.result?.status ?? \"unconfigured\") : \"unconfigured\";\n\n/** Failing first, then critical, then by name */\nexport function sortServices(services: ServiceSnapshot[]): ServiceSnapshot[] {\n return [...services].sort(\n (a, b) =>\n STATUS_ORDER[statusOf(a)] - STATUS_ORDER[statusOf(b)] ||\n Number(b.critical) - Number(a.critical) ||\n a.name.localeCompare(b.name)\n );\n}\n\n/** Every service of a tree, in declaration order */\nexport function flattenServices(nodes: SnapshotNode[]): ServiceSnapshot[] {\n return nodes.flatMap((node) => (node.type === \"service\" ? [node] : flattenServices(node.children)));\n}\n\n/** A group ready to render: its own services, sorted, and its nested groups */\nexport type Section = {\n key: string;\n title: string;\n description?: string;\n services: ServiceSnapshot[];\n groups: Section[];\n};\n\n/** A page is a list of blocks: sections (cards), or a heading introducing several sections */\nexport type LayoutBlock =\n | { type: \"section\"; section: Section }\n | { type: \"heading\"; key: string; title: string; description?: string; sections: Section[] };\n\nfunction toSection(node: Extract<SnapshotNode, { type: \"group\" }>): Section {\n return {\n key: node.key,\n title: node.title,\n description: node.description,\n services: sortServices(node.children.filter((child): child is ServiceSnapshot => child.type === \"service\")),\n groups: node.children\n .filter((child): child is Extract<SnapshotNode, { type: \"group\" }> => child.type === \"group\")\n .map(toSection),\n };\n}\n\n/**\n * Turn the snapshot tree into page blocks:\n * - a top-level group with services of its own is one section\n * - a top-level group that only contains groups is a heading, followed by one section per group\n * - services outside any group are gathered into a section titled `ungroupedTitle`\n */\nexport function buildLayout(\n tree: StatusSnapshot[\"tree\"],\n { ungroupedTitle = \"Services\" }: { ungroupedTitle?: string } = {}\n): LayoutBlock[] {\n const blocks: LayoutBlock[] = [];\n let loose: ServiceSnapshot[] = [];\n let looseCount = 0;\n const flushLoose = () => {\n if (!loose.length) return;\n looseCount += 1;\n blocks.push({\n type: \"section\",\n section: {\n key: looseCount === 1 ? \"ungrouped\" : `ungrouped-${looseCount}`,\n title: ungroupedTitle,\n services: sortServices(loose),\n groups: [],\n },\n });\n loose = [];\n };\n\n for (const node of tree) {\n if (node.type === \"service\") {\n loose.push(node);\n continue;\n }\n flushLoose();\n const section = toSection(node);\n if (section.services.length === 0 && section.groups.length > 0) {\n blocks.push({\n type: \"heading\",\n key: section.key,\n title: section.title,\n description: section.description,\n sections: section.groups,\n });\n } else {\n blocks.push({ type: \"section\", section });\n }\n }\n flushLoose();\n return blocks;\n}\n\n/** Every service of a section, including its nested groups */\nexport function servicesOf(section: Section): ServiceSnapshot[] {\n return [...section.services, ...section.groups.flatMap(servicesOf)];\n}\n\n/** Every section of a layout, flattened (headings contribute their sections) */\nexport function sectionsOf(blocks: LayoutBlock[]): Section[] {\n return blocks.flatMap((block) => (block.type === \"section\" ? [block.section] : block.sections));\n}\n\n/** True when the last full run is older than twice the interval: the scheduler probably stopped */\nexport function isStale(snapshot: Pick<StatusSnapshot, \"lastRunAt\" | \"intervalMs\">, now = Date.now()): boolean {\n return snapshot.lastRunAt !== null && now - snapshot.lastRunAt > 2 * snapshot.intervalMs;\n}\n\nconst LABEL_SEVERITY: Record<ScoreLabel, number> = { unknown: 0, operational: 1, degraded: 2, outage: 3 };\n\n/**\n * Fit history bars into a narrow space by merging neighbours: each merged bar keeps the\n * worst score and label of its group, the sum of runs and every failure (\"down\" wins).\n * Returns the bars and how many original bars each one now covers.\n */\nexport function mergeBars(bars: HistoryBar[], maxBars: number): { bars: HistoryBar[]; factor: number } {\n if (maxBars < 1 || bars.length <= maxBars) return { bars, factor: 1 };\n const factor = Math.ceil(bars.length / maxBars);\n const merged: HistoryBar[] = [];\n // Group from the end, so the newest bar is always a full group ending \"now\"\n for (let end = bars.length; end > 0; end -= factor) {\n const group = bars.slice(Math.max(0, end - factor), end);\n const combined: HistoryBar = { start: group[0]!.start, score: null, label: \"unknown\", runs: 0, failing: {} };\n for (const bar of group) {\n combined.runs += bar.runs;\n if (bar.score !== null && (combined.score === null || bar.score < combined.score)) combined.score = bar.score;\n if (LABEL_SEVERITY[bar.label] > LABEL_SEVERITY[combined.label]) combined.label = bar.label;\n for (const [key, status] of Object.entries(bar.failing)) {\n if (combined.failing[key] !== \"down\") combined.failing[key] = status;\n }\n }\n merged.unshift(combined);\n }\n return { bars: merged, factor };\n}\n","export const serviceAnchor = (key: string) => `service-${key}`;\nexport const groupAnchor = (key: string) => `group-${key}`;\n\n/** Fired when navigating to a service or a group: its section expands before we scroll */\nexport const FOCUS_EVENT = \"sermon:focus\";\nexport type FocusDetail = { anchor: string };\n\n/**\n * Navigate to a service or group on the page: update the URL hash, let collapsed sections\n * open, then scroll to it and briefly highlight it.\n */\nexport function focusAnchor(anchor: string, { smooth = true }: { smooth?: boolean } = {}) {\n history.replaceState(null, \"\", `#${encodeURIComponent(anchor)}`);\n window.dispatchEvent(new CustomEvent<FocusDetail>(FOCUS_EVENT, { detail: { anchor } }));\n // Give React a tick to render the section expanded before measuring positions.\n // (setTimeout, not requestAnimationFrame: rAF doesn't run while the tab is hidden)\n setTimeout(() => {\n const target = document.getElementById(anchor);\n if (!target) return;\n const reduceMotion = window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n target.scrollIntoView?.({ behavior: smooth && !reduceMotion ? \"smooth\" : \"auto\", block: \"start\" });\n if (anchor.startsWith(\"service-\") && !reduceMotion) {\n target.animate?.(\n [\n { backgroundColor: \"color-mix(in oklch, var(--accent) 14%, transparent)\" },\n { backgroundColor: \"transparent\" },\n ],\n { duration: 1600, easing: \"ease-out\" }\n );\n }\n }, 50);\n}\n\n/** The anchor in the current URL, if any */\nexport function currentAnchor(): string | undefined {\n const hash = window.location.hash.slice(1);\n if (!hash) return undefined;\n try {\n return decodeURIComponent(hash);\n } catch {\n return hash;\n }\n}\n","import { useEffect, useState } from \"react\";\n\nexport type ThemePreference = \"system\" | \"light\" | \"dark\";\n\nconst STORAGE_KEY = \"sermon:theme\";\n\nfunction readPreference(): ThemePreference {\n try {\n const stored = localStorage.getItem(STORAGE_KEY);\n return stored === \"light\" || stored === \"dark\" ? stored : \"system\";\n } catch {\n return \"system\";\n }\n}\n\nconst prefersDark = () =>\n typeof window !== \"undefined\" && (window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches ?? false);\n\n/** Apply a preference the way HeroUI and Tailwind expect: `dark` class and `data-theme` on <html> */\nexport function applyTheme(preference: ThemePreference = readPreference()) {\n if (typeof document === \"undefined\") return;\n const theme = preference === \"system\" ? (prefersDark() ? \"dark\" : \"light\") : preference;\n document.documentElement.classList.toggle(\"dark\", theme === \"dark\");\n document.documentElement.dataset.theme = theme;\n document.documentElement.style.colorScheme = theme;\n}\n\n/** The viewer's theme choice (system, light or dark), remembered in their browser */\nexport function useThemePreference() {\n const [preference, setPreference] = useState<ThemePreference>(readPreference);\n\n useEffect(() => {\n applyTheme(preference);\n try {\n if (preference === \"system\") localStorage.removeItem(STORAGE_KEY);\n else localStorage.setItem(STORAGE_KEY, preference);\n } catch {\n // Storage unavailable: the choice lasts for this page view only\n }\n if (preference !== \"system\") return;\n // Follow the system setting live while it is the choice\n const media = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n const onChange = () => applyTheme(\"system\");\n media?.addEventListener(\"change\", onChange);\n return () => media?.removeEventListener(\"change\", onChange);\n }, [preference]);\n\n return [preference, setPreference] as const;\n}\n","import type { ScoreLabel, Status } from \"../types\";\n\nexport type Tone = \"success\" | \"warning\" | \"danger\" | \"default\" | \"accent\";\n\nexport const STATUS_TONE: Record<Status, Tone> = {\n up: \"success\",\n degraded: \"warning\",\n down: \"danger\",\n sleeping: \"accent\",\n unconfigured: \"default\",\n};\n\nexport const STATUS_TEXT: Record<Status, string> = {\n up: \"Operational\",\n degraded: \"Degraded\",\n down: \"Down\",\n sleeping: \"Asleep (expected)\",\n unconfigured: \"Not configured\",\n};\n\nexport const LABEL_TONE: Record<ScoreLabel, Tone> = {\n operational: \"success\",\n degraded: \"warning\",\n outage: \"danger\",\n unknown: \"default\",\n};\n\nexport const LABEL_TEXT: Record<ScoreLabel, string> = {\n operational: \"All systems operational\",\n degraded: \"Degraded performance\",\n outage: \"Major outage\",\n unknown: \"No data yet\",\n};\n\nconst relative = new Intl.RelativeTimeFormat(\"en\", { numeric: \"auto\" });\n\nexport function timeAgo(timestamp: number, now = Date.now()) {\n const seconds = Math.round((timestamp - now) / 1000);\n if (Math.abs(seconds) < 60) return relative.format(seconds, \"second\");\n const minutes = Math.round(seconds / 60);\n if (Math.abs(minutes) < 60) return relative.format(minutes, \"minute\");\n const hours = Math.round(minutes / 60);\n if (Math.abs(hours) < 24) return relative.format(hours, \"hour\");\n return relative.format(Math.round(hours / 24), \"day\");\n}\n\nexport function formatMs(ms: number | null | undefined) {\n if (ms === null || ms === undefined) return \"–\";\n return ms >= 1000 ? `${(ms / 1000).toFixed(1)} s` : `${ms} ms`;\n}\n\nexport function formatPct(pct: number | null | undefined) {\n if (pct === null || pct === undefined) return \"–\";\n return `${pct.toFixed(pct === 100 ? 0 : 2)}%`;\n}\n\nconst dateTime = new Intl.DateTimeFormat(\"en\", { month: \"short\", day: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" });\n\nexport function formatDateTime(timestamp: number) {\n return dateTime.format(timestamp);\n}\n\n/** A duration in words: 300000 → \"5 minutes\" */\nexport function formatDuration(ms: number) {\n const units: Array<[number, string]> = [\n [86_400_000, \"day\"],\n [3_600_000, \"hour\"],\n [60_000, \"minute\"],\n [1000, \"second\"],\n ];\n for (const [size, name] of units) {\n if (ms >= size) {\n const count = Math.round(ms / size);\n return `${count} ${name}${count === 1 ? \"\" : \"s\"}`;\n }\n }\n return `${ms} ms`;\n}\n\n/** \"quotaUsed\" → \"Quota used\" */\nexport function humanize(key: string) {\n const spaced = key.replace(/([a-z])([A-Z])/g, \"$1 $2\").toLowerCase();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkXHXEKB3Q_cjs = require('./chunk-XHXEKB3Q.cjs');
|
|
4
|
+
|
|
5
|
+
// src/presets/generic.ts
|
|
6
|
+
var SENSITIVE_HEADER = /auth|key|token|secret|cookie|password|signature|credential/i;
|
|
7
|
+
function sensitiveHeaderValues(headers) {
|
|
8
|
+
if (!headers || typeof headers === "function") return [];
|
|
9
|
+
return Object.entries(headers).filter(([name]) => SENSITIVE_HEADER.test(name)).flatMap(([, value]) => [value, value.replace(/^\S+\s+/, "")]);
|
|
10
|
+
}
|
|
11
|
+
var preset = chunkXHXEKB3Q_cjs.definePreset()({
|
|
12
|
+
kind: "generic.http",
|
|
13
|
+
requiredFields: ["url"],
|
|
14
|
+
requiredSecrets: "all",
|
|
15
|
+
derive: (options) => ({
|
|
16
|
+
name: options.name,
|
|
17
|
+
description: `HTTP ${options.method ?? "GET"} request`
|
|
18
|
+
}),
|
|
19
|
+
sensitive: (options) => sensitiveHeaderValues(options.headers),
|
|
20
|
+
check: async ({ options, secrets, http }) => {
|
|
21
|
+
const headers = typeof options.headers === "function" ? options.headers(secrets) : options.headers;
|
|
22
|
+
const expected = options.track?.status;
|
|
23
|
+
const response = await http(options.url, {
|
|
24
|
+
method: options.method,
|
|
25
|
+
headers,
|
|
26
|
+
body: options.body,
|
|
27
|
+
// With an explicit list, only those statuses are healthy
|
|
28
|
+
okStatuses: expected
|
|
29
|
+
});
|
|
30
|
+
if (expected && response.status !== void 0 && !expected.includes(response.status)) {
|
|
31
|
+
return {
|
|
32
|
+
status: "down",
|
|
33
|
+
latencyMs: response.latencyMs,
|
|
34
|
+
httpStatus: response.status,
|
|
35
|
+
error: `HTTP ${response.status} (expected ${expected.join(", ")})`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) return response.outcome;
|
|
39
|
+
if (!options.track?.json) return response.outcome;
|
|
40
|
+
const refined = chunkXHXEKB3Q_cjs.normalizeReturn(options.track.json(response.json, response));
|
|
41
|
+
return { latencyMs: response.latencyMs, httpStatus: response.status, ...refined };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
var generic = {
|
|
45
|
+
/** Any HTTP endpoint: up on 2xx/3xx (or `track.status`), degraded on 429 or when slow, down otherwise */
|
|
46
|
+
http: preset
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
exports.generic = generic;
|
|
50
|
+
//# sourceMappingURL=chunk-ELV4ZU6G.cjs.map
|
|
51
|
+
//# sourceMappingURL=chunk-ELV4ZU6G.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/presets/generic.ts"],"names":["definePreset","normalizeReturn"],"mappings":";;;;;AAuBA,IAAM,gBAAA,GAAmB,6DAAA;AAGzB,SAAS,sBAAsB,OAAA,EAAwC;AACrE,EAAA,IAAI,CAAC,OAAA,IAAW,OAAO,OAAA,KAAY,UAAA,SAAmB,EAAC;AACvD,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,CAC1B,MAAA,CAAO,CAAC,CAAC,IAAI,CAAA,KAAM,gBAAA,CAAiB,IAAA,CAAK,IAAI,CAAC,CAAA,CAE9C,OAAA,CAAQ,CAAC,GAAG,KAAK,CAAA,KAAM,CAAC,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAC,CAAC,CAAA;AACjE;AAEA,IAAM,MAAA,GAASA,gCAAiC,CAAE;AAAA,EAChD,IAAA,EAAM,cAAA;AAAA,EACN,cAAA,EAAgB,CAAC,KAAK,CAAA;AAAA,EACtB,eAAA,EAAiB,KAAA;AAAA,EACjB,MAAA,EAAQ,CAAC,OAAA,MAAa;AAAA,IACpB,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,WAAA,EAAa,CAAA,KAAA,EAAQ,OAAA,CAAQ,MAAA,IAAU,KAAK,CAAA,QAAA;AAAA,GAC9C,CAAA;AAAA,EACA,SAAA,EAAW,CAAC,OAAA,KAAY,qBAAA,CAAsB,QAAQ,OAAO,CAAA;AAAA,EAC7D,OAAO,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,MAAK,KAAM;AAC3C,IAAA,MAAM,OAAA,GACJ,OAAO,OAAA,CAAQ,OAAA,KAAY,aACvB,OAAA,CAAQ,OAAA,CAAQ,OAAiC,CAAA,GACjD,OAAA,CAAQ,OAAA;AACd,IAAA,MAAM,QAAA,GAAW,QAAQ,KAAA,EAAO,MAAA;AAChC,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAe;AAAA,MACjD,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,OAAA;AAAA,MACA,MAAM,OAAA,CAAQ,IAAA;AAAA;AAAA,MAEd,UAAA,EAAY;AAAA,KACb,CAAA;AAED,IAAA,IAAI,QAAA,IAAY,SAAS,MAAA,KAAW,MAAA,IAAa,CAAC,QAAA,CAAS,QAAA,CAAS,QAAA,CAAS,MAAM,CAAA,EAAG;AACpF,MAAA,OAAO;AAAA,QACL,MAAA,EAAQ,MAAA;AAAA,QACR,WAAW,QAAA,CAAS,SAAA;AAAA,QACpB,YAAY,QAAA,CAAS,MAAA;AAAA,QACrB,KAAA,EAAO,QAAQ,QAAA,CAAS,MAAM,cAAc,QAAA,CAAS,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,OACjE;AAAA,IACF;AACA,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,OAAO,QAAA,CAAS,OAAA;AAClC,IAAA,IAAI,CAAC,OAAA,CAAQ,KAAA,EAAO,IAAA,SAAa,QAAA,CAAS,OAAA;AAE1C,IAAA,MAAM,OAAA,GAAUC,kCAAgB,OAAA,CAAQ,KAAA,CAAM,KAAK,QAAA,CAAS,IAAA,EAAM,QAAQ,CAAC,CAAA;AAC3E,IAAA,OAAO,EAAE,WAAW,QAAA,CAAS,SAAA,EAAW,YAAY,QAAA,CAAS,MAAA,EAAQ,GAAG,OAAA,EAAQ;AAAA,EAClF;AACF,CAAC,CAAA;AAGM,IAAM,OAAA,GAAU;AAAA;AAAA,EAErB,IAAA,EAAM;AAGR","file":"chunk-ELV4ZU6G.cjs","sourcesContent":["import { definePreset, type PresetOptions } from \"../config\";\nimport { normalizeReturn } from \"../probe\";\nimport type { CheckReturn, HttpResult, SecretsInput } from \"../types\";\n\nexport type GenericHttpOptions<Secrets extends SecretsInput = SecretsInput> = PresetOptions & {\n /** How to call it */\n name: string;\n /** The URL to call. When empty (e.g. an unset environment variable) the service is \"not configured\". */\n url: string | undefined;\n method?: string;\n /** Static headers, or a function of the secrets (e.g. to build an Authorization header) */\n headers?: Record<string, string> | ((secrets: { [K in keyof Secrets]-?: string }) => Record<string, string>);\n body?: string;\n /** Every secret given is required and redacted from public errors */\n secrets?: Secrets;\n track?: {\n /** Statuses that mean \"up\" (default: any 2xx/3xx) */\n status?: number[];\n /** Inspect the JSON body of a healthy answer to refine the outcome (metrics, usage, degraded…) */\n json?: (body: unknown, response: HttpResult) => CheckReturn;\n };\n};\n\nconst SENSITIVE_HEADER = /auth|key|token|secret|cookie|password|signature|credential/i;\n\n/** Credentials are often written straight into a header: redact them like declared secrets */\nfunction sensitiveHeaderValues(headers: GenericHttpOptions[\"headers\"]) {\n if (!headers || typeof headers === \"function\") return [];\n return Object.entries(headers)\n .filter(([name]) => SENSITIVE_HEADER.test(name))\n // \"Bearer abc\" → also \"abc\" alone, which is how an API would echo it\n .flatMap(([, value]) => [value, value.replace(/^\\S+\\s+/, \"\")]);\n}\n\nconst preset = definePreset<GenericHttpOptions>()({\n kind: \"generic.http\",\n requiredFields: [\"url\"],\n requiredSecrets: \"all\",\n derive: (options) => ({\n name: options.name,\n description: `HTTP ${options.method ?? \"GET\"} request`,\n }),\n sensitive: (options) => sensitiveHeaderValues(options.headers),\n check: async ({ options, secrets, http }) => {\n const headers =\n typeof options.headers === \"function\"\n ? options.headers(secrets as Record<string, string>)\n : options.headers;\n const expected = options.track?.status;\n const response = await http(options.url as string, {\n method: options.method,\n headers,\n body: options.body,\n // With an explicit list, only those statuses are healthy\n okStatuses: expected,\n });\n\n if (expected && response.status !== undefined && !expected.includes(response.status)) {\n return {\n status: \"down\",\n latencyMs: response.latencyMs,\n httpStatus: response.status,\n error: `HTTP ${response.status} (expected ${expected.join(\", \")})`,\n };\n }\n if (!response.ok) return response.outcome;\n if (!options.track?.json) return response.outcome;\n\n const refined = normalizeReturn(options.track.json(response.json, response));\n return { latencyMs: response.latencyMs, httpStatus: response.status, ...refined };\n },\n});\n\n/** Presets that work with any service */\nexport const generic = {\n /** Any HTTP endpoint: up on 2xx/3xx (or `track.status`), degraded on 429 or when slow, down otherwise */\n http: preset as <Secrets extends SecretsInput = Record<never, never>>(\n options: GenericHttpOptions<Secrets>\n ) => ReturnType<typeof preset>,\n};\n"]}
|