@firebreak/vitals 1.2.3 → 1.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 +25 -1
- package/dist/checks/databricks.d.cts +1 -1
- package/dist/checks/databricks.d.ts +1 -1
- package/dist/checks/http.d.cts +1 -1
- package/dist/checks/http.d.ts +1 -1
- package/dist/checks/postgres.d.cts +1 -1
- package/dist/checks/postgres.d.ts +1 -1
- package/dist/checks/redis.d.cts +1 -1
- package/dist/checks/redis.d.ts +1 -1
- package/dist/{core-BJ2Z0rRi.d.cts → core-Bee03bJm.d.cts} +7 -1
- package/dist/{core-BJ2Z0rRi.d.ts → core-Bee03bJm.d.ts} +7 -1
- package/dist/{handler-R2U3ygLo.d.ts → handler-COH7lot9.d.cts} +2 -1
- package/dist/{handler-Bvf66wzh.d.cts → handler-CVYUad84.d.ts} +2 -1
- package/dist/index.cjs +281 -4
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.mjs +280 -4
- package/dist/integrations/express.cjs +261 -5
- package/dist/integrations/express.d.cts +2 -2
- package/dist/integrations/express.d.ts +2 -2
- package/dist/integrations/express.mjs +261 -5
- package/dist/integrations/next.cjs +287 -4
- package/dist/integrations/next.d.cts +2 -2
- package/dist/integrations/next.d.ts +2 -2
- package/dist/integrations/next.mjs +287 -4
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ function statusToLabel(status) {
|
|
|
35
35
|
return labels[status];
|
|
36
36
|
}
|
|
37
37
|
function toJson(response) {
|
|
38
|
-
|
|
38
|
+
const json = {
|
|
39
39
|
status: statusToLabel(response.status),
|
|
40
40
|
timestamp: response.timestamp,
|
|
41
41
|
checks: Object.fromEntries(
|
|
@@ -45,16 +45,26 @@ function toJson(response) {
|
|
|
45
45
|
])
|
|
46
46
|
)
|
|
47
47
|
};
|
|
48
|
+
if (response.cachedAt !== void 0) {
|
|
49
|
+
json.cachedAt = response.cachedAt;
|
|
50
|
+
}
|
|
51
|
+
return json;
|
|
48
52
|
}
|
|
49
53
|
function httpStatusCode(status) {
|
|
50
54
|
return status === Status.HEALTHY ? 200 : 503;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
// src/handler.ts
|
|
54
|
-
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks"];
|
|
58
|
+
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks", "cachedAt"];
|
|
55
59
|
function createHealthcheckHandler(options) {
|
|
56
|
-
const { registry, token: rawToken, queryParamName = "token", metadata = {} } = options;
|
|
57
|
-
const
|
|
60
|
+
const { registry, token: rawToken, deep = false, queryParamName = "token", metadata = {} } = options;
|
|
61
|
+
const isEmptyToken = typeof rawToken === "string" && rawToken.trim() === "";
|
|
62
|
+
if (deep && isEmptyToken) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Cannot use `deep: true` with an empty string token. This would expose deep healthcheck data without authentication. Either set a non-empty token or explicitly set `token: null`."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const token = rawToken == null || isEmptyToken ? null : rawToken;
|
|
58
68
|
for (const key of Object.keys(metadata)) {
|
|
59
69
|
if (RESERVED_METADATA_KEYS.includes(key)) {
|
|
60
70
|
throw new Error(`Metadata key '${key}' is reserved. Use a different key name.`);
|
|
@@ -62,6 +72,13 @@ function createHealthcheckHandler(options) {
|
|
|
62
72
|
}
|
|
63
73
|
return async (req) => {
|
|
64
74
|
if (token === null) {
|
|
75
|
+
if (deep) {
|
|
76
|
+
const response2 = await registry.run();
|
|
77
|
+
return {
|
|
78
|
+
status: httpStatusCode(response2.status),
|
|
79
|
+
body: { ...metadata, ...toJson(response2) }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
65
82
|
const body = {
|
|
66
83
|
status: "ok",
|
|
67
84
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -93,6 +110,240 @@ function createHealthcheckHandler(options) {
|
|
|
93
110
|
};
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
// src/html.ts
|
|
114
|
+
function escapeHtml(str) {
|
|
115
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
116
|
+
}
|
|
117
|
+
function statusColor(status) {
|
|
118
|
+
switch (status) {
|
|
119
|
+
case "healthy":
|
|
120
|
+
case "ok":
|
|
121
|
+
return "#22C55E";
|
|
122
|
+
case "degraded":
|
|
123
|
+
return "#F59E0B";
|
|
124
|
+
case "outage":
|
|
125
|
+
return "#ED1C24";
|
|
126
|
+
default:
|
|
127
|
+
return "#ED1C24";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var DEEP_KNOWN_KEYS = /* @__PURE__ */ new Set(["status", "timestamp", "checks", "cachedAt"]);
|
|
131
|
+
function isDeepResponse(body) {
|
|
132
|
+
return "checks" in body && "status" in body && body.status !== "ok";
|
|
133
|
+
}
|
|
134
|
+
function isShallowResponse(body) {
|
|
135
|
+
return "status" in body && body.status === "ok";
|
|
136
|
+
}
|
|
137
|
+
function isErrorResponse(body) {
|
|
138
|
+
return "error" in body && !("status" in body);
|
|
139
|
+
}
|
|
140
|
+
function renderStatusBadge(label, color) {
|
|
141
|
+
return `<span style="
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
padding: 4px 12px;
|
|
146
|
+
border-radius: 9999px;
|
|
147
|
+
background: ${color}1A;
|
|
148
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
letter-spacing: 0.05em;
|
|
152
|
+
text-transform: uppercase;
|
|
153
|
+
color: ${color};
|
|
154
|
+
"><span style="
|
|
155
|
+
width: 8px;
|
|
156
|
+
height: 8px;
|
|
157
|
+
border-radius: 50%;
|
|
158
|
+
background: ${color};
|
|
159
|
+
box-shadow: 0 0 6px ${color}80;
|
|
160
|
+
"></span>${escapeHtml(label)}</span>`;
|
|
161
|
+
}
|
|
162
|
+
function renderMetadataItems(items) {
|
|
163
|
+
if (items.length === 0) return "";
|
|
164
|
+
return `<div style="
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
gap: 24px;
|
|
168
|
+
margin-top: 16px;
|
|
169
|
+
">${items.map(
|
|
170
|
+
([label, value]) => `<div>
|
|
171
|
+
<div style="
|
|
172
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
173
|
+
font-size: 10px;
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
letter-spacing: 0.05em;
|
|
176
|
+
text-transform: uppercase;
|
|
177
|
+
color: #A1A1AA;
|
|
178
|
+
margin-bottom: 4px;
|
|
179
|
+
">${escapeHtml(label)}</div>
|
|
180
|
+
<div style="
|
|
181
|
+
font-family: 'JetBrains Mono', monospace;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
color: #E4E4E7;
|
|
184
|
+
">${escapeHtml(value)}</div>
|
|
185
|
+
</div>`
|
|
186
|
+
).join("")}</div>`;
|
|
187
|
+
}
|
|
188
|
+
function renderCheckRow(name, check) {
|
|
189
|
+
const color = statusColor(check.status);
|
|
190
|
+
return `<div style="
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 12px;
|
|
194
|
+
padding: 12px 0;
|
|
195
|
+
border-top: 1px solid #27272A;
|
|
196
|
+
">
|
|
197
|
+
<span style="
|
|
198
|
+
width: 8px;
|
|
199
|
+
height: 8px;
|
|
200
|
+
border-radius: 50%;
|
|
201
|
+
background: ${color};
|
|
202
|
+
box-shadow: 0 0 6px ${color}80;
|
|
203
|
+
flex-shrink: 0;
|
|
204
|
+
"></span>
|
|
205
|
+
<span style="
|
|
206
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 0.05em;
|
|
210
|
+
color: ${color};
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
min-width: 72px;
|
|
213
|
+
">${escapeHtml(check.status)}</span>
|
|
214
|
+
<span style="
|
|
215
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
216
|
+
font-size: 14px;
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
color: #E4E4E7;
|
|
219
|
+
flex: 1;
|
|
220
|
+
">${escapeHtml(name)}</span>
|
|
221
|
+
<span style="
|
|
222
|
+
font-family: 'JetBrains Mono', monospace;
|
|
223
|
+
font-size: 12px;
|
|
224
|
+
color: #A1A1AA;
|
|
225
|
+
min-width: 60px;
|
|
226
|
+
text-align: right;
|
|
227
|
+
">${escapeHtml(String(check.latencyMs))}ms</span>
|
|
228
|
+
<span style="
|
|
229
|
+
font-family: 'JetBrains Mono', monospace;
|
|
230
|
+
font-size: 12px;
|
|
231
|
+
color: #71717A;
|
|
232
|
+
max-width: 200px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
text-overflow: ellipsis;
|
|
235
|
+
white-space: nowrap;
|
|
236
|
+
">${escapeHtml(check.message)}</span>
|
|
237
|
+
</div>`;
|
|
238
|
+
}
|
|
239
|
+
function renderPage(borderColor, content) {
|
|
240
|
+
return `<!DOCTYPE html>
|
|
241
|
+
<html lang="en">
|
|
242
|
+
<head>
|
|
243
|
+
<meta charset="utf-8">
|
|
244
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
245
|
+
<title>Healthcheck</title>
|
|
246
|
+
<style></style>
|
|
247
|
+
</head>
|
|
248
|
+
<body style="
|
|
249
|
+
margin: 0;
|
|
250
|
+
padding: 40px 20px;
|
|
251
|
+
background: #08080A;
|
|
252
|
+
min-height: 100vh;
|
|
253
|
+
display: flex;
|
|
254
|
+
justify-content: center;
|
|
255
|
+
align-items: flex-start;
|
|
256
|
+
box-sizing: border-box;
|
|
257
|
+
">
|
|
258
|
+
<div style="
|
|
259
|
+
width: 100%;
|
|
260
|
+
max-width: 560px;
|
|
261
|
+
background: #18181B;
|
|
262
|
+
border: 1px solid #27272A;
|
|
263
|
+
border-left: 3px solid ${borderColor};
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
padding: 24px;
|
|
266
|
+
">
|
|
267
|
+
${content}
|
|
268
|
+
</div>
|
|
269
|
+
</body>
|
|
270
|
+
</html>`;
|
|
271
|
+
}
|
|
272
|
+
function renderHealthcheckHtml(body) {
|
|
273
|
+
if (isErrorResponse(body)) {
|
|
274
|
+
const color = "#ED1C24";
|
|
275
|
+
return renderPage(
|
|
276
|
+
color,
|
|
277
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
278
|
+
<span style="
|
|
279
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
280
|
+
font-size: 18px;
|
|
281
|
+
font-weight: 700;
|
|
282
|
+
color: #E4E4E7;
|
|
283
|
+
">Healthcheck</span>
|
|
284
|
+
${renderStatusBadge("ERROR", color)}
|
|
285
|
+
</div>
|
|
286
|
+
<div style="
|
|
287
|
+
margin-top: 16px;
|
|
288
|
+
font-family: 'JetBrains Mono', monospace;
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
color: #EF4444;
|
|
291
|
+
">${escapeHtml(body.error)}</div>`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (isShallowResponse(body)) {
|
|
295
|
+
const color = statusColor("ok");
|
|
296
|
+
const { status: _, timestamp, ...rest } = body;
|
|
297
|
+
const metadataItems = Object.entries(rest).map(
|
|
298
|
+
([k, v]) => [k, String(v)]
|
|
299
|
+
);
|
|
300
|
+
const allItems = [
|
|
301
|
+
["timestamp", timestamp],
|
|
302
|
+
...metadataItems
|
|
303
|
+
];
|
|
304
|
+
return renderPage(
|
|
305
|
+
color,
|
|
306
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
307
|
+
<span style="
|
|
308
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
309
|
+
font-size: 18px;
|
|
310
|
+
font-weight: 700;
|
|
311
|
+
color: #E4E4E7;
|
|
312
|
+
">Healthcheck</span>
|
|
313
|
+
${renderStatusBadge("OK", color)}
|
|
314
|
+
</div>
|
|
315
|
+
${renderMetadataItems(allItems)}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (isDeepResponse(body)) {
|
|
319
|
+
const color = statusColor(body.status);
|
|
320
|
+
const { timestamp, checks } = body;
|
|
321
|
+
const metadataItems = Object.entries(body).filter(([k]) => !DEEP_KNOWN_KEYS.has(k)).map(([k, v]) => [k, String(v)]);
|
|
322
|
+
const allItems = [
|
|
323
|
+
["timestamp", timestamp],
|
|
324
|
+
...metadataItems
|
|
325
|
+
];
|
|
326
|
+
const checkRows = Object.entries(checks).sort(([a], [b]) => a.localeCompare(b)).map(([name, check]) => renderCheckRow(name, check)).join("");
|
|
327
|
+
return renderPage(
|
|
328
|
+
color,
|
|
329
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
330
|
+
<span style="
|
|
331
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
332
|
+
font-size: 18px;
|
|
333
|
+
font-weight: 700;
|
|
334
|
+
color: #E4E4E7;
|
|
335
|
+
">Healthcheck</span>
|
|
336
|
+
${renderStatusBadge(body.status, color)}
|
|
337
|
+
</div>
|
|
338
|
+
${renderMetadataItems(allItems)}
|
|
339
|
+
<div style="margin-top: 20px;">
|
|
340
|
+
${checkRows}
|
|
341
|
+
</div>`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return renderPage("#ED1C24", `<pre style="color: #E4E4E7; font-family: 'JetBrains Mono', monospace;">${escapeHtml(JSON.stringify(body, null, 2))}</pre>`);
|
|
345
|
+
}
|
|
346
|
+
|
|
96
347
|
// src/integrations/express.ts
|
|
97
348
|
function createHealthcheckMiddleware(options) {
|
|
98
349
|
const handle = createHealthcheckHandler(options);
|
|
@@ -102,7 +353,12 @@ function createHealthcheckMiddleware(options) {
|
|
|
102
353
|
queryParams: req.query,
|
|
103
354
|
authorizationHeader: req.headers.authorization ?? null
|
|
104
355
|
});
|
|
105
|
-
|
|
356
|
+
const preferred = req.accepts(["json", "html"]);
|
|
357
|
+
if (preferred === "html") {
|
|
358
|
+
res.status(result.status).type("html").send(renderHealthcheckHtml(result.body));
|
|
359
|
+
} else {
|
|
360
|
+
res.status(result.status).json(result.body);
|
|
361
|
+
}
|
|
106
362
|
} catch {
|
|
107
363
|
res.status(500).json({ error: "Internal Server Error" });
|
|
108
364
|
}
|
|
@@ -61,7 +61,7 @@ function statusToLabel(status) {
|
|
|
61
61
|
return labels[status];
|
|
62
62
|
}
|
|
63
63
|
function toJson(response) {
|
|
64
|
-
|
|
64
|
+
const json = {
|
|
65
65
|
status: statusToLabel(response.status),
|
|
66
66
|
timestamp: response.timestamp,
|
|
67
67
|
checks: Object.fromEntries(
|
|
@@ -71,16 +71,26 @@ function toJson(response) {
|
|
|
71
71
|
])
|
|
72
72
|
)
|
|
73
73
|
};
|
|
74
|
+
if (response.cachedAt !== void 0) {
|
|
75
|
+
json.cachedAt = response.cachedAt;
|
|
76
|
+
}
|
|
77
|
+
return json;
|
|
74
78
|
}
|
|
75
79
|
function httpStatusCode(status) {
|
|
76
80
|
return status === Status.HEALTHY ? 200 : 503;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// src/handler.ts
|
|
80
|
-
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks"];
|
|
84
|
+
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks", "cachedAt"];
|
|
81
85
|
function createHealthcheckHandler(options) {
|
|
82
|
-
const { registry, token: rawToken, queryParamName = "token", metadata = {} } = options;
|
|
83
|
-
const
|
|
86
|
+
const { registry, token: rawToken, deep = false, queryParamName = "token", metadata = {} } = options;
|
|
87
|
+
const isEmptyToken = typeof rawToken === "string" && rawToken.trim() === "";
|
|
88
|
+
if (deep && isEmptyToken) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Cannot use `deep: true` with an empty string token. This would expose deep healthcheck data without authentication. Either set a non-empty token or explicitly set `token: null`."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const token = rawToken == null || isEmptyToken ? null : rawToken;
|
|
84
94
|
for (const key of Object.keys(metadata)) {
|
|
85
95
|
if (RESERVED_METADATA_KEYS.includes(key)) {
|
|
86
96
|
throw new Error(`Metadata key '${key}' is reserved. Use a different key name.`);
|
|
@@ -88,6 +98,13 @@ function createHealthcheckHandler(options) {
|
|
|
88
98
|
}
|
|
89
99
|
return async (req) => {
|
|
90
100
|
if (token === null) {
|
|
101
|
+
if (deep) {
|
|
102
|
+
const response2 = await registry.run();
|
|
103
|
+
return {
|
|
104
|
+
status: httpStatusCode(response2.status),
|
|
105
|
+
body: { ...metadata, ...toJson(response2) }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
91
108
|
const body = {
|
|
92
109
|
status: "ok",
|
|
93
110
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -119,7 +136,267 @@ function createHealthcheckHandler(options) {
|
|
|
119
136
|
};
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
// src/html.ts
|
|
140
|
+
function escapeHtml(str) {
|
|
141
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
142
|
+
}
|
|
143
|
+
function statusColor(status) {
|
|
144
|
+
switch (status) {
|
|
145
|
+
case "healthy":
|
|
146
|
+
case "ok":
|
|
147
|
+
return "#22C55E";
|
|
148
|
+
case "degraded":
|
|
149
|
+
return "#F59E0B";
|
|
150
|
+
case "outage":
|
|
151
|
+
return "#ED1C24";
|
|
152
|
+
default:
|
|
153
|
+
return "#ED1C24";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
var DEEP_KNOWN_KEYS = /* @__PURE__ */ new Set(["status", "timestamp", "checks", "cachedAt"]);
|
|
157
|
+
function isDeepResponse(body) {
|
|
158
|
+
return "checks" in body && "status" in body && body.status !== "ok";
|
|
159
|
+
}
|
|
160
|
+
function isShallowResponse(body) {
|
|
161
|
+
return "status" in body && body.status === "ok";
|
|
162
|
+
}
|
|
163
|
+
function isErrorResponse(body) {
|
|
164
|
+
return "error" in body && !("status" in body);
|
|
165
|
+
}
|
|
166
|
+
function renderStatusBadge(label, color) {
|
|
167
|
+
return `<span style="
|
|
168
|
+
display: inline-flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
padding: 4px 12px;
|
|
172
|
+
border-radius: 9999px;
|
|
173
|
+
background: ${color}1A;
|
|
174
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
letter-spacing: 0.05em;
|
|
178
|
+
text-transform: uppercase;
|
|
179
|
+
color: ${color};
|
|
180
|
+
"><span style="
|
|
181
|
+
width: 8px;
|
|
182
|
+
height: 8px;
|
|
183
|
+
border-radius: 50%;
|
|
184
|
+
background: ${color};
|
|
185
|
+
box-shadow: 0 0 6px ${color}80;
|
|
186
|
+
"></span>${escapeHtml(label)}</span>`;
|
|
187
|
+
}
|
|
188
|
+
function renderMetadataItems(items) {
|
|
189
|
+
if (items.length === 0) return "";
|
|
190
|
+
return `<div style="
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
gap: 24px;
|
|
194
|
+
margin-top: 16px;
|
|
195
|
+
">${items.map(
|
|
196
|
+
([label, value]) => `<div>
|
|
197
|
+
<div style="
|
|
198
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
199
|
+
font-size: 10px;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
letter-spacing: 0.05em;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
color: #A1A1AA;
|
|
204
|
+
margin-bottom: 4px;
|
|
205
|
+
">${escapeHtml(label)}</div>
|
|
206
|
+
<div style="
|
|
207
|
+
font-family: 'JetBrains Mono', monospace;
|
|
208
|
+
font-size: 13px;
|
|
209
|
+
color: #E4E4E7;
|
|
210
|
+
">${escapeHtml(value)}</div>
|
|
211
|
+
</div>`
|
|
212
|
+
).join("")}</div>`;
|
|
213
|
+
}
|
|
214
|
+
function renderCheckRow(name, check) {
|
|
215
|
+
const color = statusColor(check.status);
|
|
216
|
+
return `<div style="
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: 12px;
|
|
220
|
+
padding: 12px 0;
|
|
221
|
+
border-top: 1px solid #27272A;
|
|
222
|
+
">
|
|
223
|
+
<span style="
|
|
224
|
+
width: 8px;
|
|
225
|
+
height: 8px;
|
|
226
|
+
border-radius: 50%;
|
|
227
|
+
background: ${color};
|
|
228
|
+
box-shadow: 0 0 6px ${color}80;
|
|
229
|
+
flex-shrink: 0;
|
|
230
|
+
"></span>
|
|
231
|
+
<span style="
|
|
232
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
text-transform: uppercase;
|
|
235
|
+
letter-spacing: 0.05em;
|
|
236
|
+
color: ${color};
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
min-width: 72px;
|
|
239
|
+
">${escapeHtml(check.status)}</span>
|
|
240
|
+
<span style="
|
|
241
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
242
|
+
font-size: 14px;
|
|
243
|
+
font-weight: 500;
|
|
244
|
+
color: #E4E4E7;
|
|
245
|
+
flex: 1;
|
|
246
|
+
">${escapeHtml(name)}</span>
|
|
247
|
+
<span style="
|
|
248
|
+
font-family: 'JetBrains Mono', monospace;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
color: #A1A1AA;
|
|
251
|
+
min-width: 60px;
|
|
252
|
+
text-align: right;
|
|
253
|
+
">${escapeHtml(String(check.latencyMs))}ms</span>
|
|
254
|
+
<span style="
|
|
255
|
+
font-family: 'JetBrains Mono', monospace;
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
color: #71717A;
|
|
258
|
+
max-width: 200px;
|
|
259
|
+
overflow: hidden;
|
|
260
|
+
text-overflow: ellipsis;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
">${escapeHtml(check.message)}</span>
|
|
263
|
+
</div>`;
|
|
264
|
+
}
|
|
265
|
+
function renderPage(borderColor, content) {
|
|
266
|
+
return `<!DOCTYPE html>
|
|
267
|
+
<html lang="en">
|
|
268
|
+
<head>
|
|
269
|
+
<meta charset="utf-8">
|
|
270
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
271
|
+
<title>Healthcheck</title>
|
|
272
|
+
<style></style>
|
|
273
|
+
</head>
|
|
274
|
+
<body style="
|
|
275
|
+
margin: 0;
|
|
276
|
+
padding: 40px 20px;
|
|
277
|
+
background: #08080A;
|
|
278
|
+
min-height: 100vh;
|
|
279
|
+
display: flex;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
align-items: flex-start;
|
|
282
|
+
box-sizing: border-box;
|
|
283
|
+
">
|
|
284
|
+
<div style="
|
|
285
|
+
width: 100%;
|
|
286
|
+
max-width: 560px;
|
|
287
|
+
background: #18181B;
|
|
288
|
+
border: 1px solid #27272A;
|
|
289
|
+
border-left: 3px solid ${borderColor};
|
|
290
|
+
border-radius: 8px;
|
|
291
|
+
padding: 24px;
|
|
292
|
+
">
|
|
293
|
+
${content}
|
|
294
|
+
</div>
|
|
295
|
+
</body>
|
|
296
|
+
</html>`;
|
|
297
|
+
}
|
|
298
|
+
function renderHealthcheckHtml(body) {
|
|
299
|
+
if (isErrorResponse(body)) {
|
|
300
|
+
const color = "#ED1C24";
|
|
301
|
+
return renderPage(
|
|
302
|
+
color,
|
|
303
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
304
|
+
<span style="
|
|
305
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
306
|
+
font-size: 18px;
|
|
307
|
+
font-weight: 700;
|
|
308
|
+
color: #E4E4E7;
|
|
309
|
+
">Healthcheck</span>
|
|
310
|
+
${renderStatusBadge("ERROR", color)}
|
|
311
|
+
</div>
|
|
312
|
+
<div style="
|
|
313
|
+
margin-top: 16px;
|
|
314
|
+
font-family: 'JetBrains Mono', monospace;
|
|
315
|
+
font-size: 13px;
|
|
316
|
+
color: #EF4444;
|
|
317
|
+
">${escapeHtml(body.error)}</div>`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (isShallowResponse(body)) {
|
|
321
|
+
const color = statusColor("ok");
|
|
322
|
+
const { status: _, timestamp, ...rest } = body;
|
|
323
|
+
const metadataItems = Object.entries(rest).map(
|
|
324
|
+
([k, v]) => [k, String(v)]
|
|
325
|
+
);
|
|
326
|
+
const allItems = [
|
|
327
|
+
["timestamp", timestamp],
|
|
328
|
+
...metadataItems
|
|
329
|
+
];
|
|
330
|
+
return renderPage(
|
|
331
|
+
color,
|
|
332
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
333
|
+
<span style="
|
|
334
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
335
|
+
font-size: 18px;
|
|
336
|
+
font-weight: 700;
|
|
337
|
+
color: #E4E4E7;
|
|
338
|
+
">Healthcheck</span>
|
|
339
|
+
${renderStatusBadge("OK", color)}
|
|
340
|
+
</div>
|
|
341
|
+
${renderMetadataItems(allItems)}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (isDeepResponse(body)) {
|
|
345
|
+
const color = statusColor(body.status);
|
|
346
|
+
const { timestamp, checks } = body;
|
|
347
|
+
const metadataItems = Object.entries(body).filter(([k]) => !DEEP_KNOWN_KEYS.has(k)).map(([k, v]) => [k, String(v)]);
|
|
348
|
+
const allItems = [
|
|
349
|
+
["timestamp", timestamp],
|
|
350
|
+
...metadataItems
|
|
351
|
+
];
|
|
352
|
+
const checkRows = Object.entries(checks).sort(([a], [b]) => a.localeCompare(b)).map(([name, check]) => renderCheckRow(name, check)).join("");
|
|
353
|
+
return renderPage(
|
|
354
|
+
color,
|
|
355
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
356
|
+
<span style="
|
|
357
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
358
|
+
font-size: 18px;
|
|
359
|
+
font-weight: 700;
|
|
360
|
+
color: #E4E4E7;
|
|
361
|
+
">Healthcheck</span>
|
|
362
|
+
${renderStatusBadge(body.status, color)}
|
|
363
|
+
</div>
|
|
364
|
+
${renderMetadataItems(allItems)}
|
|
365
|
+
<div style="margin-top: 20px;">
|
|
366
|
+
${checkRows}
|
|
367
|
+
</div>`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return renderPage("#ED1C24", `<pre style="color: #E4E4E7; font-family: 'JetBrains Mono', monospace;">${escapeHtml(JSON.stringify(body, null, 2))}</pre>`);
|
|
371
|
+
}
|
|
372
|
+
|
|
122
373
|
// src/integrations/next.ts
|
|
374
|
+
function parseMediaType(header, mediaType) {
|
|
375
|
+
const parts = header.split(",");
|
|
376
|
+
let bestMatch = null;
|
|
377
|
+
for (let i = 0; i < parts.length; i++) {
|
|
378
|
+
const trimmed = parts[i].trim();
|
|
379
|
+
const [type, ...params] = trimmed.split(";").map((s) => s.trim());
|
|
380
|
+
const exact = type === mediaType;
|
|
381
|
+
if (!exact && type !== "*/*") continue;
|
|
382
|
+
const qParam = params.find((p) => p.startsWith("q="));
|
|
383
|
+
const quality = qParam ? parseFloat(qParam.slice(2)) || 0 : 1;
|
|
384
|
+
if (!bestMatch || exact && !bestMatch.exact || exact === bestMatch.exact && quality > bestMatch.quality) {
|
|
385
|
+
bestMatch = { quality, index: i, exact };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return bestMatch ? { quality: bestMatch.quality, index: bestMatch.index } : null;
|
|
389
|
+
}
|
|
390
|
+
function prefersHtml(acceptHeader) {
|
|
391
|
+
if (!acceptHeader) return false;
|
|
392
|
+
const lower = acceptHeader.toLowerCase();
|
|
393
|
+
const html = parseMediaType(lower, "text/html");
|
|
394
|
+
const json = parseMediaType(lower, "application/json");
|
|
395
|
+
if (!html || html.quality <= 0) return false;
|
|
396
|
+
if (!json) return true;
|
|
397
|
+
if (html.quality !== json.quality) return html.quality > json.quality;
|
|
398
|
+
return html.index < json.index;
|
|
399
|
+
}
|
|
123
400
|
function createNextHandler(options) {
|
|
124
401
|
const handle = createHealthcheckHandler(options);
|
|
125
402
|
return async (request) => {
|
|
@@ -130,6 +407,12 @@ function createNextHandler(options) {
|
|
|
130
407
|
queryParams,
|
|
131
408
|
authorizationHeader: request.headers.get("authorization")
|
|
132
409
|
});
|
|
410
|
+
if (prefersHtml(request.headers.get("accept"))) {
|
|
411
|
+
return new Response(renderHealthcheckHtml(result.body), {
|
|
412
|
+
status: result.status,
|
|
413
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
414
|
+
});
|
|
415
|
+
}
|
|
133
416
|
return Response.json(result.body, { status: result.status });
|
|
134
417
|
} catch {
|
|
135
418
|
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HealthcheckHandlerOptions } from '../handler-
|
|
2
|
-
import '../core-
|
|
1
|
+
import { H as HealthcheckHandlerOptions } from '../handler-COH7lot9.cjs';
|
|
2
|
+
import '../core-Bee03bJm.cjs';
|
|
3
3
|
|
|
4
4
|
type NextHealthcheckOptions = HealthcheckHandlerOptions;
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HealthcheckHandlerOptions } from '../handler-
|
|
2
|
-
import '../core-
|
|
1
|
+
import { H as HealthcheckHandlerOptions } from '../handler-CVYUad84.js';
|
|
2
|
+
import '../core-Bee03bJm.js';
|
|
3
3
|
|
|
4
4
|
type NextHealthcheckOptions = HealthcheckHandlerOptions;
|
|
5
5
|
/**
|