@ninhache/probe 0.9.0 → 0.10.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
CHANGED
|
@@ -113,13 +113,14 @@ of macOS.
|
|
|
113
113
|
| --------------- | ---------------------------------------------- |
|
|
114
114
|
| `↑` `↓` `j` `k` | Move |
|
|
115
115
|
| `r` | Check now |
|
|
116
|
-
| `Enter` | Open the
|
|
116
|
+
| `Enter` | Open the detail of the selected service |
|
|
117
117
|
| `u` | Open its URL in the browser |
|
|
118
118
|
| `c` | Copy its curl command |
|
|
119
119
|
| `/` | Filter by name; `Enter` keeps it, `Esc` clears |
|
|
120
120
|
| `a` | Add a service |
|
|
121
121
|
| `e` | Edit the selected service |
|
|
122
122
|
| `d` | Remove the selected service, after a question |
|
|
123
|
+
| `:` | Find a command by what it does; `Enter` runs |
|
|
123
124
|
| `o` | Options: interval and timeout |
|
|
124
125
|
| `?` | Every key; `?` or `Esc` closes |
|
|
125
126
|
| `q` | Quit |
|
|
@@ -132,7 +133,12 @@ to copy by hand.
|
|
|
132
133
|
`a` and `e` open a form: name, port, host, path, expected status. `↑` `↓` or `Tab` change field, `Enter` saves,
|
|
133
134
|
`Esc` cancels. A field left empty takes its default. `d` asks first: only `y` removes. All three write
|
|
134
135
|
`.probe/config.yml` the way `probe add` and `probe rm` do: comments kept, [Makefile](#makefile) block kept up to
|
|
135
|
-
date. While you type, in the filter or
|
|
136
|
+
date. While you type, in the filter, the form or the command palette, `q` and `?` are letters.
|
|
137
|
+
|
|
138
|
+
`Enter` shows the last answer of the selected service above its two settings: status, latencies of this
|
|
139
|
+
session, headers and the first lines of the body; for a service that is down, what failed and what to try. The
|
|
140
|
+
value of a header that can carry a secret (`Authorization`, `Set-Cookie`, anything named after a token or a key)
|
|
141
|
+
is replaced by `(hidden)`. None of it is written anywhere, and `probe check --json` prints what it printed before.
|
|
136
142
|
|
|
137
143
|
The panels save what you change in `.probe/local.yml`, for your machine only:
|
|
138
144
|
|
|
@@ -14,6 +14,9 @@ var DEFAULT_MAKEFILE = "Makefile";
|
|
|
14
14
|
var DEFAULT_CHECKER_TYPE = "http";
|
|
15
15
|
var DEFAULT_TIMEOUT_SECONDS = 3;
|
|
16
16
|
var DASHBOARD_INTERVAL_SECONDS = 2;
|
|
17
|
+
var DASHBOARD_HISTORY_LENGTH = 30;
|
|
18
|
+
var RESPONSE_BODY_BYTES = 2048;
|
|
19
|
+
var RESPONSE_BODY_WAIT_MS = 500;
|
|
17
20
|
var DASHBOARD_INTERVAL_CHOICES_SECONDS = [0.5, 1, 2, 5, 10, 30, 60];
|
|
18
21
|
var DASHBOARD_TIMEOUT_CHOICES_SECONDS = [1, 2, 3, 5, 10, 30];
|
|
19
22
|
var MAX_TIMEOUT_SECONDS = 2147483;
|
|
@@ -162,6 +165,79 @@ function diagnoseStatus(service, status) {
|
|
|
162
165
|
return { error, hint: `the service answers: check \`path\` and \`expect\` in ${CONFIG}` };
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// src/checkers/response.ts
|
|
169
|
+
var HIDDEN_VALUE = "(hidden)";
|
|
170
|
+
var SENSITIVE_HEADER = /auth|cookie|token|secret|key|session|password|credential/i;
|
|
171
|
+
var TEXT_TYPE = /^text\/|json|xml|javascript|yaml|x-www-form-urlencoded/i;
|
|
172
|
+
var NUL = 0;
|
|
173
|
+
var FIRST_PRINTABLE = 32;
|
|
174
|
+
var DELETE = 127;
|
|
175
|
+
var LAST_C1_CONTROL = 159;
|
|
176
|
+
var REPLACEMENT = "\uFFFD";
|
|
177
|
+
function isControl(character) {
|
|
178
|
+
const code = character.codePointAt(0) ?? NUL;
|
|
179
|
+
return code < FIRST_PRINTABLE || code >= DELETE && code <= LAST_C1_CONTROL;
|
|
180
|
+
}
|
|
181
|
+
function printable(text, kept = "") {
|
|
182
|
+
return Array.from(text).filter((character) => kept.includes(character) || !isControl(character)).join("");
|
|
183
|
+
}
|
|
184
|
+
async function readStart(body) {
|
|
185
|
+
const reader = body.getReader();
|
|
186
|
+
const chunks = [];
|
|
187
|
+
let size = 0;
|
|
188
|
+
let ended = false;
|
|
189
|
+
let timer;
|
|
190
|
+
const late = new Promise((resolve) => {
|
|
191
|
+
timer = setTimeout(() => resolve("late"), RESPONSE_BODY_WAIT_MS);
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
while (size < RESPONSE_BODY_BYTES) {
|
|
195
|
+
const read = reader.read();
|
|
196
|
+
read.catch(() => {
|
|
197
|
+
});
|
|
198
|
+
const next = await Promise.race([read, late]);
|
|
199
|
+
if (next === "late") break;
|
|
200
|
+
if (next.done) {
|
|
201
|
+
ended = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
chunks.push(next.value);
|
|
205
|
+
size += next.value.length;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
void reader.cancel().catch(() => {
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
bytes: Buffer.concat(chunks).subarray(0, RESPONSE_BODY_BYTES),
|
|
215
|
+
cut: !ended || size > RESPONSE_BODY_BYTES
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function describeResponse(response) {
|
|
219
|
+
const headers = [...response.headers].map(([name, value]) => [
|
|
220
|
+
printable(name),
|
|
221
|
+
SENSITIVE_HEADER.test(name) ? HIDDEN_VALUE : printable(value)
|
|
222
|
+
]);
|
|
223
|
+
const detail = { statusText: printable(response.statusText), headers };
|
|
224
|
+
if (!response.body) return detail;
|
|
225
|
+
const type = printable(response.headers.get("content-type")?.split(";")[0] ?? "").trim();
|
|
226
|
+
if (type && !TEXT_TYPE.test(type)) {
|
|
227
|
+
void response.body.cancel().catch(() => {
|
|
228
|
+
});
|
|
229
|
+
return { ...detail, bodyNote: `${type} body, not shown` };
|
|
230
|
+
}
|
|
231
|
+
const { bytes, cut } = await readStart(response.body);
|
|
232
|
+
if (bytes.length === 0) return detail;
|
|
233
|
+
if (bytes.includes(NUL)) return { ...detail, bodyNote: "binary body, not shown" };
|
|
234
|
+
const decoded = new TextDecoder().decode(bytes).replaceAll(" ", " ");
|
|
235
|
+
const text = printable(cut ? decoded.replace(new RegExp(`${REPLACEMENT}+$`), "") : decoded, "\n");
|
|
236
|
+
const body = text.trimEnd();
|
|
237
|
+
if (!body) return detail;
|
|
238
|
+
return cut ? { ...detail, body, bodyCut: true } : { ...detail, body };
|
|
239
|
+
}
|
|
240
|
+
|
|
165
241
|
// src/checkers/http.ts
|
|
166
242
|
function targetUrl(service) {
|
|
167
243
|
const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
|
|
@@ -183,7 +259,7 @@ function expectsRedirect(expect) {
|
|
|
183
259
|
}
|
|
184
260
|
var httpChecker = {
|
|
185
261
|
type: "http",
|
|
186
|
-
async check(name, service, { timeoutMs, signal }) {
|
|
262
|
+
async check(name, service, { timeoutMs, signal, detail }) {
|
|
187
263
|
const target = targetUrl(service);
|
|
188
264
|
const base = { name, type: "http", target };
|
|
189
265
|
const started = performance.now();
|
|
@@ -196,10 +272,20 @@ var httpChecker = {
|
|
|
196
272
|
redirect: expectsRedirect(service.expect) ? "manual" : "follow"
|
|
197
273
|
});
|
|
198
274
|
const latencyMs = elapsed();
|
|
199
|
-
await response.body?.cancel();
|
|
200
275
|
const { status } = response;
|
|
201
|
-
|
|
202
|
-
|
|
276
|
+
const described = detail ? { response: await describeResponse(response) } : {};
|
|
277
|
+
if (!detail) await response.body?.cancel();
|
|
278
|
+
if (isExpected(service.expect, status)) {
|
|
279
|
+
return { ...base, ok: true, status, latencyMs, ...described };
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
ok: false,
|
|
284
|
+
status,
|
|
285
|
+
latencyMs,
|
|
286
|
+
...diagnoseStatus(service, status),
|
|
287
|
+
...described
|
|
288
|
+
};
|
|
203
289
|
} catch (error) {
|
|
204
290
|
const diagnosis = diagnoseFailure(error, service, timeoutMs);
|
|
205
291
|
return { ...base, ok: false, latencyMs: elapsed(), ...diagnosis };
|
|
@@ -251,7 +337,8 @@ async function runChecks(config, names, options, skipped = /* @__PURE__ */ new S
|
|
|
251
337
|
}
|
|
252
338
|
return getChecker(type).check(name, service, {
|
|
253
339
|
timeoutMs: resolveTimeoutMs(service, options),
|
|
254
|
-
signal: options.signal
|
|
340
|
+
signal: options.signal,
|
|
341
|
+
detail: options.detail
|
|
255
342
|
});
|
|
256
343
|
})
|
|
257
344
|
);
|
|
@@ -417,6 +504,7 @@ export {
|
|
|
417
504
|
DEFAULT_MAKEFILE,
|
|
418
505
|
DEFAULT_TIMEOUT_SECONDS,
|
|
419
506
|
DASHBOARD_INTERVAL_SECONDS,
|
|
507
|
+
DASHBOARD_HISTORY_LENGTH,
|
|
420
508
|
DASHBOARD_INTERVAL_CHOICES_SECONDS,
|
|
421
509
|
DASHBOARD_TIMEOUT_CHOICES_SECONDS,
|
|
422
510
|
MAX_TIMEOUT_SECONDS,
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
serviceSchemaV1,
|
|
51
51
|
stripStyles,
|
|
52
52
|
targetUrl
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-VSQVZK7F.js";
|
|
54
54
|
import "./chunk-M3ASEG5F.js";
|
|
55
55
|
|
|
56
56
|
// src/cli.ts
|
|
@@ -624,7 +624,7 @@ async function writeLock(paths, lock) {
|
|
|
624
624
|
}
|
|
625
625
|
|
|
626
626
|
// src/version.ts
|
|
627
|
-
var CLI_VERSION = true ? "0.
|
|
627
|
+
var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
628
628
|
|
|
629
629
|
// src/makefile/generator.ts
|
|
630
630
|
import { createHash } from "crypto";
|
|
@@ -1796,7 +1796,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1796
1796
|
if (!asked) return false;
|
|
1797
1797
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1798
1798
|
}
|
|
1799
|
-
const { openDashboard } = await import("./open-
|
|
1799
|
+
const { openDashboard } = await import("./open-W2TWG636.js");
|
|
1800
1800
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1801
1801
|
const saves = createSaveQueue();
|
|
1802
1802
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "node:module";
|
|
2
2
|
const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
|
+
DASHBOARD_HISTORY_LENGTH,
|
|
4
5
|
DASHBOARD_INTERVAL_CHOICES_SECONDS,
|
|
5
6
|
DASHBOARD_TIMEOUT_CHOICES_SECONDS,
|
|
6
7
|
createColors,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
runChecks,
|
|
18
19
|
targetUrl,
|
|
19
20
|
truncate
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-VSQVZK7F.js";
|
|
21
22
|
import {
|
|
22
23
|
__commonJS,
|
|
23
24
|
__export,
|
|
@@ -18596,7 +18597,7 @@ var Box_default = Box;
|
|
|
18596
18597
|
|
|
18597
18598
|
// node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.1/node_modules/ink/build/components/Text.js
|
|
18598
18599
|
var import_react12 = __toESM(require_react(), 1);
|
|
18599
|
-
function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
|
|
18600
|
+
function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap: wrap2 = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false }) {
|
|
18600
18601
|
const { isScreenReaderEnabled } = (0, import_react12.use)(AccessibilityContext);
|
|
18601
18602
|
const inheritedBackgroundColor = (0, import_react12.use)(BackgroundContext);
|
|
18602
18603
|
const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
|
|
@@ -18634,7 +18635,7 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
|
|
|
18634
18635
|
flexGrow: 0,
|
|
18635
18636
|
flexShrink: 1,
|
|
18636
18637
|
flexDirection: "row",
|
|
18637
|
-
textWrap:
|
|
18638
|
+
textWrap: wrap2
|
|
18638
18639
|
}, internal_transform: isScreenReaderEnabled ? void 0 : transform }, childrenOrAriaLabel)
|
|
18639
18640
|
);
|
|
18640
18641
|
}
|
|
@@ -20312,6 +20313,111 @@ ${command}`;
|
|
|
20312
20313
|
// src/dashboard/Dashboard.tsx
|
|
20313
20314
|
var import_react36 = __toESM(require_react(), 1);
|
|
20314
20315
|
|
|
20316
|
+
// src/dashboard/detail.ts
|
|
20317
|
+
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
20318
|
+
var HEADER_LINES = 6;
|
|
20319
|
+
var BODY_LINES = 5;
|
|
20320
|
+
var HINT_LINES = 3;
|
|
20321
|
+
var TRANSPORT_HEADERS = /* @__PURE__ */ new Set([
|
|
20322
|
+
"connection",
|
|
20323
|
+
"content-length",
|
|
20324
|
+
"date",
|
|
20325
|
+
"keep-alive",
|
|
20326
|
+
"transfer-encoding"
|
|
20327
|
+
]);
|
|
20328
|
+
function recordLatencies(history, results) {
|
|
20329
|
+
return Object.fromEntries(
|
|
20330
|
+
results.map(({ name, status, skipped, latencyMs }) => {
|
|
20331
|
+
const before = history[name] ?? [];
|
|
20332
|
+
const answered = status !== void 0 && !skipped;
|
|
20333
|
+
return [name, answered ? [...before, latencyMs].slice(-DASHBOARD_HISTORY_LENGTH) : before];
|
|
20334
|
+
})
|
|
20335
|
+
);
|
|
20336
|
+
}
|
|
20337
|
+
function sparkline(values, unicode, width) {
|
|
20338
|
+
if (values.length === 0) return "";
|
|
20339
|
+
const low = Math.min(...values);
|
|
20340
|
+
const high = Math.max(...values);
|
|
20341
|
+
const range = low === high ? `${low}ms` : `${low}${unicode ? "\u2013" : "-"}${high}ms`;
|
|
20342
|
+
if (!unicode) return `${range} over ${values.length} ${values.length === 1 ? "check" : "checks"}`;
|
|
20343
|
+
const room = width - range.length - 1;
|
|
20344
|
+
if (room < 1) return range;
|
|
20345
|
+
const top = BLOCKS.length - 1;
|
|
20346
|
+
const bars = values.slice(-room).map((value) => BLOCKS[high === low ? 0 : Math.round((value - low) / (high - low) * top)]).join("");
|
|
20347
|
+
return `${bars} ${range}`;
|
|
20348
|
+
}
|
|
20349
|
+
function wrap(text, width, maxLines) {
|
|
20350
|
+
const lines = [];
|
|
20351
|
+
let line = "";
|
|
20352
|
+
for (const word of text.split(" ")) {
|
|
20353
|
+
if (line && line.length + 1 + word.length > width) {
|
|
20354
|
+
lines.push(line);
|
|
20355
|
+
line = word;
|
|
20356
|
+
} else line = line ? `${line} ${word}` : word;
|
|
20357
|
+
}
|
|
20358
|
+
lines.push(line);
|
|
20359
|
+
const kept = lines.slice(0, maxLines).map((kept2) => truncate(kept2, width));
|
|
20360
|
+
if (lines.length > maxLines) kept[maxLines - 1] = truncate(`${kept[maxLines - 1]} \u2026`, width);
|
|
20361
|
+
return kept;
|
|
20362
|
+
}
|
|
20363
|
+
function detailLines(result, history, { unicode, width, room }) {
|
|
20364
|
+
const dot = unicode ? "\xB7" : "-";
|
|
20365
|
+
const more = unicode ? "\u2026" : "...";
|
|
20366
|
+
const cut = (text) => truncate(text, width);
|
|
20367
|
+
if (result.skipped) return [{ id: "status", text: cut("skipped: not checked"), tone: "dim" }];
|
|
20368
|
+
const { response, status } = result;
|
|
20369
|
+
const tone = result.ok ? "good" : "bad";
|
|
20370
|
+
const lines = [];
|
|
20371
|
+
if (status === void 0) lines.push({ id: "status", text: cut(result.error ?? "down"), tone });
|
|
20372
|
+
else {
|
|
20373
|
+
const answer = `${status} ${response?.statusText ?? ""}`.trim();
|
|
20374
|
+
lines.push({ id: "status", text: cut(`${answer} ${dot} ${result.latencyMs}ms`), tone });
|
|
20375
|
+
if (result.error) lines.push({ id: "error", text: cut(result.error), tone });
|
|
20376
|
+
}
|
|
20377
|
+
if (result.hint) {
|
|
20378
|
+
for (const [index, text] of wrap(result.hint, width, HINT_LINES).entries())
|
|
20379
|
+
lines.push({ id: `hint ${index}`, text, tone: "dim" });
|
|
20380
|
+
}
|
|
20381
|
+
const latencies = sparkline(history, unicode, width);
|
|
20382
|
+
if (latencies) lines.push({ id: "latencies", text: cut(latencies), tone: "dim" });
|
|
20383
|
+
if (!response) return lines;
|
|
20384
|
+
const transport = ([name]) => TRANSPORT_HEADERS.has(name.toLowerCase());
|
|
20385
|
+
const headers = [
|
|
20386
|
+
...response.headers.filter((header) => !transport(header)),
|
|
20387
|
+
...response.headers.filter(transport)
|
|
20388
|
+
].map(([name, value]) => `${name}: ${value}`);
|
|
20389
|
+
const body = response.bodyNote ? [response.bodyNote] : response.body?.split("\n") ?? [];
|
|
20390
|
+
let headerRoom = Math.min(headers.length, HEADER_LINES);
|
|
20391
|
+
let bodyRoom = Math.min(body.length, BODY_LINES);
|
|
20392
|
+
const left = Math.max(0, room - lines.length);
|
|
20393
|
+
while (headerRoom + bodyRoom > left) {
|
|
20394
|
+
if (bodyRoom > 1 && bodyRoom >= headerRoom) bodyRoom -= 1;
|
|
20395
|
+
else if (headerRoom > 0) headerRoom -= 1;
|
|
20396
|
+
else bodyRoom -= 1;
|
|
20397
|
+
}
|
|
20398
|
+
const hiddenHeaders = headers.length - headerRoom;
|
|
20399
|
+
for (const [index, header] of headers.slice(0, headerRoom).entries()) {
|
|
20400
|
+
const last = index === headerRoom - 1 && hiddenHeaders > 0;
|
|
20401
|
+
lines.push(
|
|
20402
|
+
last ? {
|
|
20403
|
+
id: `header ${index}`,
|
|
20404
|
+
text: cut(`${more} ${hiddenHeaders + 1} more headers`),
|
|
20405
|
+
tone: "dim"
|
|
20406
|
+
} : { id: `header ${index}`, text: cut(header), tone: "dim" }
|
|
20407
|
+
);
|
|
20408
|
+
}
|
|
20409
|
+
const cutBody = body.length > bodyRoom || response.bodyCut;
|
|
20410
|
+
for (const [index, text] of body.slice(0, bodyRoom).entries()) {
|
|
20411
|
+
const last = index === bodyRoom - 1 && cutBody;
|
|
20412
|
+
lines.push({
|
|
20413
|
+
id: `body ${index}`,
|
|
20414
|
+
text: last ? `${truncate(text, Math.max(1, width - more.length - 1))} ${more}`.trim() : cut(text),
|
|
20415
|
+
tone: response.bodyNote ? "dim" : void 0
|
|
20416
|
+
});
|
|
20417
|
+
}
|
|
20418
|
+
return lines;
|
|
20419
|
+
}
|
|
20420
|
+
|
|
20315
20421
|
// src/dashboard/form.ts
|
|
20316
20422
|
var ROWS = [
|
|
20317
20423
|
{ key: "name", label: "Name", placeholder: "api" },
|
|
@@ -20357,16 +20463,24 @@ var list = (binding) => ({
|
|
|
20357
20463
|
...binding
|
|
20358
20464
|
});
|
|
20359
20465
|
var BINDINGS = [
|
|
20360
|
-
list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop:
|
|
20361
|
-
list({ keys: "r", action: "check", help: "Check now", barless: true }),
|
|
20362
|
-
list({
|
|
20363
|
-
|
|
20364
|
-
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
|
|
20466
|
+
list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop: 2 }),
|
|
20467
|
+
list({ keys: "r", action: "check", help: "Check now", barless: true, command: "check" }),
|
|
20468
|
+
list({
|
|
20469
|
+
keys: "\u21B5",
|
|
20470
|
+
ascii: "enter",
|
|
20471
|
+
action: "service",
|
|
20472
|
+
help: "Service detail",
|
|
20473
|
+
drop: 5,
|
|
20474
|
+
command: "detail"
|
|
20475
|
+
}),
|
|
20476
|
+
list({ keys: "u", action: "open", help: "Open its URL", barless: true, command: "open" }),
|
|
20477
|
+
list({ keys: "c", action: "copy", help: "Copy curl command", barless: true, command: "copy" }),
|
|
20478
|
+
list({ keys: "e", action: "edit", help: "Edit the service", barless: true, command: "edit" }),
|
|
20479
|
+
list({ keys: "d", action: "remove", help: "Remove it", barless: true, command: "remove" }),
|
|
20480
|
+
list({ keys: "a", action: "add", help: "Add a service", drop: 3, command: "add" }),
|
|
20481
|
+
list({ keys: "/", action: "filter", help: "Filter by name", drop: 4, command: "filter" }),
|
|
20482
|
+
list({ keys: ":", action: "commands", help: "Find a command", drop: 1 }),
|
|
20483
|
+
list({ keys: "o", action: "options", help: "Options", barless: true, command: "options" }),
|
|
20370
20484
|
{
|
|
20371
20485
|
screens: ["panel"],
|
|
20372
20486
|
section: "Panels",
|
|
@@ -20404,6 +20518,9 @@ var BINDINGS = [
|
|
|
20404
20518
|
help: "Keep the filter"
|
|
20405
20519
|
},
|
|
20406
20520
|
{ screens: ["filter"], section: "Filter", keys: "esc", action: "clear", help: "Clear it" },
|
|
20521
|
+
{ screens: ["palette"], keys: "\u2191\u2193", ascii: "arrows", action: "move", drop: 1 },
|
|
20522
|
+
{ screens: ["palette"], keys: "\u21B5", ascii: "enter", action: "run" },
|
|
20523
|
+
{ screens: ["palette"], keys: "esc", action: "close" },
|
|
20407
20524
|
{
|
|
20408
20525
|
screens: ["form"],
|
|
20409
20526
|
section: "Add and edit",
|
|
@@ -20430,14 +20547,16 @@ var BINDINGS = [
|
|
|
20430
20547
|
section: "Outside a text field",
|
|
20431
20548
|
keys: "?",
|
|
20432
20549
|
action: "help",
|
|
20433
|
-
help: "This help"
|
|
20550
|
+
help: "This help",
|
|
20551
|
+
command: "help"
|
|
20434
20552
|
},
|
|
20435
20553
|
{
|
|
20436
20554
|
screens: ["list", "panel", "help"],
|
|
20437
20555
|
section: "Outside a text field",
|
|
20438
20556
|
keys: "q",
|
|
20439
20557
|
action: "quit",
|
|
20440
|
-
help: "Quit"
|
|
20558
|
+
help: "Quit",
|
|
20559
|
+
command: "quit"
|
|
20441
20560
|
}
|
|
20442
20561
|
];
|
|
20443
20562
|
var SECTIONS = [
|
|
@@ -20459,6 +20578,17 @@ function helpBar(screen, unicode, columns) {
|
|
|
20459
20578
|
}
|
|
20460
20579
|
return line();
|
|
20461
20580
|
}
|
|
20581
|
+
function keyCommand(input, enter) {
|
|
20582
|
+
return BINDINGS.find(({ keys, command }) => command && (enter ? keys === "\u21B5" : keys === input))?.command;
|
|
20583
|
+
}
|
|
20584
|
+
function paletteEntries(unicode, query = "") {
|
|
20585
|
+
const wanted = query.trim().toLowerCase();
|
|
20586
|
+
return BINDINGS.flatMap((binding) => {
|
|
20587
|
+
const { command, help, action } = binding;
|
|
20588
|
+
if (!command || !help || !`${help} ${action}`.toLowerCase().includes(wanted)) return [];
|
|
20589
|
+
return [{ command, label: help, keys: written(binding, unicode) }];
|
|
20590
|
+
});
|
|
20591
|
+
}
|
|
20462
20592
|
var HELP_GAP = 3;
|
|
20463
20593
|
function sectionLines(sections, unicode) {
|
|
20464
20594
|
const described = BINDINGS.filter(({ help }) => help !== void 0);
|
|
@@ -20539,6 +20669,8 @@ var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
|
|
|
20539
20669
|
var NO_TIMEOUTS = {};
|
|
20540
20670
|
var MARKER_WIDTH = 2;
|
|
20541
20671
|
var PANEL_WIDTH = 30;
|
|
20672
|
+
var DETAIL_WIDTH = 60;
|
|
20673
|
+
var DEFAULT_ROWS = 24;
|
|
20542
20674
|
var FORM_WIDTH = 48;
|
|
20543
20675
|
var CURSOR = "_";
|
|
20544
20676
|
var PANEL_FRAME_WIDTH = 4;
|
|
@@ -20563,8 +20695,9 @@ function Dashboard({
|
|
|
20563
20695
|
onServiceWrite
|
|
20564
20696
|
}) {
|
|
20565
20697
|
const { exit } = useApp();
|
|
20566
|
-
const { columns } = use_window_size_default();
|
|
20698
|
+
const { columns, rows: windowRows } = use_window_size_default();
|
|
20567
20699
|
const [latest, setLatest] = (0, import_react36.useState)(null);
|
|
20700
|
+
const [history, setHistory] = (0, import_react36.useState)({});
|
|
20568
20701
|
const [selected, setSelected] = (0, import_react36.useState)(0);
|
|
20569
20702
|
const [settings, setSettings] = (0, import_react36.useState)(initial);
|
|
20570
20703
|
const [skipped, setSkipped] = (0, import_react36.useState)(() => new Set(initiallySkipped));
|
|
@@ -20573,6 +20706,7 @@ function Dashboard({
|
|
|
20573
20706
|
const [dialog, setDialog] = (0, import_react36.useState)(null);
|
|
20574
20707
|
const [filter, setFilter] = (0, import_react36.useState)("");
|
|
20575
20708
|
const [filtering, setFiltering] = (0, import_react36.useState)(false);
|
|
20709
|
+
const [palette, setPalette] = (0, import_react36.useState)(null);
|
|
20576
20710
|
const [writes, setWrites] = (0, import_react36.useState)(0);
|
|
20577
20711
|
const [help, setHelp] = (0, import_react36.useState)(false);
|
|
20578
20712
|
const [unsaved, setUnsaved] = (0, import_react36.useState)();
|
|
@@ -20591,6 +20725,7 @@ function Dashboard({
|
|
|
20591
20725
|
const next = await check2(signal, timeoutMs, skipped, timeouts);
|
|
20592
20726
|
if (signal.aborted) return;
|
|
20593
20727
|
setLatest(next);
|
|
20728
|
+
setHistory((before) => recordLatencies(before, next));
|
|
20594
20729
|
await new Promise((resolve) => {
|
|
20595
20730
|
const timer = setTimeout(resolve, intervalMs);
|
|
20596
20731
|
skipPause.current = () => {
|
|
@@ -20634,8 +20769,8 @@ ${outcome.warning}` : done);
|
|
|
20634
20769
|
else if (key === "backspace" && field) edit(form.values[field].slice(0, -1));
|
|
20635
20770
|
else if (typed && field) edit(form.values[field] + typed);
|
|
20636
20771
|
else if (key === "return") {
|
|
20637
|
-
const
|
|
20638
|
-
const problem = formProblem(rows2, form.values,
|
|
20772
|
+
const taken2 = (latest ?? []).map(({ name }) => name);
|
|
20773
|
+
const problem = formProblem(rows2, form.values, taken2);
|
|
20639
20774
|
if (problem) return setDialog({ ...form, row: problem.row, problem: problem.message });
|
|
20640
20775
|
setDialog(null);
|
|
20641
20776
|
const fields = formFields(form.values);
|
|
@@ -20678,6 +20813,54 @@ ${outcome.warning}` : done);
|
|
|
20678
20813
|
setTimeouts({ ...timeouts, [name]: value });
|
|
20679
20814
|
track(onTimeoutChange?.(name, value));
|
|
20680
20815
|
};
|
|
20816
|
+
const run = (command) => {
|
|
20817
|
+
const name = shown[current]?.name;
|
|
20818
|
+
switch (command) {
|
|
20819
|
+
case "check":
|
|
20820
|
+
return skipPause.current();
|
|
20821
|
+
case "options":
|
|
20822
|
+
return setPanel({ kind: "options", row: 0 });
|
|
20823
|
+
case "detail":
|
|
20824
|
+
return name ? setPanel({ kind: "service", name, row: 0 }) : void 0;
|
|
20825
|
+
case "open":
|
|
20826
|
+
return name ? void onOpen?.(name).then(setNotice) : void 0;
|
|
20827
|
+
case "copy":
|
|
20828
|
+
return name ? void onCopy?.(name, timeouts[name]).then(setNotice) : void 0;
|
|
20829
|
+
case "filter":
|
|
20830
|
+
return setFiltering(true);
|
|
20831
|
+
case "add":
|
|
20832
|
+
return onServiceWrite ? setDialog({ kind: "form", row: 0, values: EMPTY_FORM }) : void 0;
|
|
20833
|
+
case "edit": {
|
|
20834
|
+
const fields = onServiceWrite && name ? serviceFields?.(name) : void 0;
|
|
20835
|
+
if (fields) setDialog({ kind: "form", editing: name, row: 0, values: formValues(fields) });
|
|
20836
|
+
return;
|
|
20837
|
+
}
|
|
20838
|
+
case "remove":
|
|
20839
|
+
return onServiceWrite && name ? setDialog({ kind: "confirm", name }) : void 0;
|
|
20840
|
+
case "help":
|
|
20841
|
+
return setHelp(true);
|
|
20842
|
+
case "quit":
|
|
20843
|
+
return exit();
|
|
20844
|
+
default:
|
|
20845
|
+
return command;
|
|
20846
|
+
}
|
|
20847
|
+
};
|
|
20848
|
+
const typePalette = ({ query, row }, keystroke) => {
|
|
20849
|
+
const { key } = keystroke;
|
|
20850
|
+
const typed = typedCharacter(keystroke);
|
|
20851
|
+
const entries = paletteEntries(terminal.unicode, query);
|
|
20852
|
+
if (key === "escape") setPalette(null);
|
|
20853
|
+
else if (key === "return") {
|
|
20854
|
+
const entry = entries[row];
|
|
20855
|
+
if (!entry) return;
|
|
20856
|
+
setPalette(null);
|
|
20857
|
+
run(entry.command);
|
|
20858
|
+
} else if (key === "up") setPalette({ query, row: Math.max(0, row - 1) });
|
|
20859
|
+
else if (key === "down")
|
|
20860
|
+
setPalette({ query, row: Math.max(0, Math.min(entries.length - 1, row + 1)) });
|
|
20861
|
+
else if (key === "backspace") setPalette({ query: query.slice(0, -1), row: 0 });
|
|
20862
|
+
else if (typed) setPalette({ query: query + typed, row: 0 });
|
|
20863
|
+
};
|
|
20681
20864
|
const press = (keystroke) => {
|
|
20682
20865
|
const { input, key } = keystroke;
|
|
20683
20866
|
const up2 = key === "up" || input === "k";
|
|
@@ -20685,35 +20868,23 @@ ${outcome.warning}` : done);
|
|
|
20685
20868
|
const left = key === "left" || input === "h";
|
|
20686
20869
|
const right = key === "right" || input === "l";
|
|
20687
20870
|
const enter = key === "return";
|
|
20688
|
-
const selectedName = shown[current]?.name;
|
|
20689
20871
|
setNotice(void 0);
|
|
20690
20872
|
if (dialog?.kind === "form") typeInForm(dialog, keystroke);
|
|
20691
20873
|
else if (dialog) {
|
|
20692
20874
|
setDialog(null);
|
|
20693
20875
|
if (input === "y")
|
|
20694
20876
|
void write({ kind: "remove", name: dialog.name }, `Removed ${dialog.name}`);
|
|
20695
|
-
} else if (
|
|
20877
|
+
} else if (palette) typePalette(palette, keystroke);
|
|
20878
|
+
else if (filtering) typeFilter(keystroke);
|
|
20696
20879
|
else if (input === "q") exit();
|
|
20697
20880
|
else if (help) {
|
|
20698
20881
|
if (input === "?" || key === "escape") setHelp(false);
|
|
20699
20882
|
} else if (input === "?") setHelp(true);
|
|
20700
20883
|
else if (panel === null) {
|
|
20701
|
-
|
|
20702
|
-
|
|
20703
|
-
else if (
|
|
20704
|
-
else if (input === "u" && selectedName) void onOpen?.(selectedName).then(setNotice);
|
|
20705
|
-
else if (input === "c" && selectedName)
|
|
20706
|
-
void onCopy?.(selectedName, timeouts[selectedName]).then(setNotice);
|
|
20707
|
-
else if (input === "/") setFiltering(true);
|
|
20884
|
+
const command = keyCommand(input, enter);
|
|
20885
|
+
if (command) run(command);
|
|
20886
|
+
else if (input === ":") setPalette({ query: "", row: 0 });
|
|
20708
20887
|
else if (key === "escape") setFilter("");
|
|
20709
|
-
else if (input === "a" && onServiceWrite)
|
|
20710
|
-
setDialog({ kind: "form", row: 0, values: EMPTY_FORM });
|
|
20711
|
-
else if (input === "e" && onServiceWrite && selectedName) {
|
|
20712
|
-
const fields = serviceFields?.(selectedName);
|
|
20713
|
-
if (fields)
|
|
20714
|
-
setDialog({ kind: "form", editing: selectedName, row: 0, values: formValues(fields) });
|
|
20715
|
-
} else if (input === "d" && onServiceWrite && selectedName)
|
|
20716
|
-
setDialog({ kind: "confirm", name: selectedName });
|
|
20717
20888
|
else if (up2) setSelected(Math.max(0, current - 1));
|
|
20718
20889
|
else if (down) setSelected(Math.min(last, current + 1));
|
|
20719
20890
|
} else if (key === "escape" || (panel.kind === "options" ? input === "o" : enter))
|
|
@@ -20775,11 +20946,12 @@ ${outcome.warning}` : done);
|
|
|
20775
20946
|
] });
|
|
20776
20947
|
}
|
|
20777
20948
|
if (!latest) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut("probe: checking\u2026") });
|
|
20778
|
-
const
|
|
20949
|
+
const asShown = (result) => {
|
|
20779
20950
|
if (result.skipped || !skipped.has(result.name)) return result;
|
|
20780
20951
|
const { name, type, target } = result;
|
|
20781
20952
|
return { name, type, target, ok: true, skipped: true, latencyMs: 0 };
|
|
20782
|
-
}
|
|
20953
|
+
};
|
|
20954
|
+
const results = shown.map(asShown);
|
|
20783
20955
|
const all = latest.filter((result) => !result.skipped && !skipped.has(result.name));
|
|
20784
20956
|
const up = all.filter((result) => result.ok).length;
|
|
20785
20957
|
const aside = latest.length - all.length;
|
|
@@ -20790,13 +20962,25 @@ ${outcome.warning}` : done);
|
|
|
20790
20962
|
`every ${seconds(intervalMs)}`,
|
|
20791
20963
|
...filter && !filtering ? [`filter: ${filter}`] : []
|
|
20792
20964
|
].join(` ${dot} `);
|
|
20793
|
-
const rows = results.length === 0 ? [] :
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
20965
|
+
const rows = results.length === 0 ? [] : (
|
|
20966
|
+
// One line per service: the hint of the selected one goes under the list.
|
|
20967
|
+
formatCheckResults(
|
|
20968
|
+
results.map(({ hint: _, ...result }) => result),
|
|
20969
|
+
{ ...terminal, columns: Math.max(1, columns - MARKER_WIDTH) }
|
|
20970
|
+
).split("\n")
|
|
20971
|
+
);
|
|
20797
20972
|
const empty = filter ? `no service matches "${filter}"` : onServiceWrite ? "no service: press a to add one" : "no service";
|
|
20798
20973
|
const focused = results[current];
|
|
20799
|
-
const panelWidth = Math.min(PANEL_WIDTH, columns);
|
|
20974
|
+
const panelWidth = Math.min(panel?.kind === "service" ? DETAIL_WIDTH : PANEL_WIDTH, columns);
|
|
20975
|
+
const panelRoom = Math.max(1, panelWidth - PANEL_FRAME_WIDTH);
|
|
20976
|
+
const detailed = panel?.kind === "service" ? latest.find(({ name }) => name === panel.name) : void 0;
|
|
20977
|
+
const taken = 1 + Math.max(1, rows.length) + 1 + 2 + 1 + 1 + SERVICE_ROWS.length + 1 + 1;
|
|
20978
|
+
const detail = detailed ? detailLines(asShown(detailed), history[detailed.name] ?? [], {
|
|
20979
|
+
unicode,
|
|
20980
|
+
width: panelRoom,
|
|
20981
|
+
room: (windowRows || DEFAULT_ROWS) - taken - (unsaved === void 0 ? 0 : 1)
|
|
20982
|
+
}) : [];
|
|
20983
|
+
const toned = ({ text, tone }) => tone === "good" ? colors.green(text) : tone === "bad" ? colors.red(text) : tone === "dim" ? colors.dim(text) : text;
|
|
20800
20984
|
const panelRows = panel?.kind === "options" ? SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]) : panel ? [
|
|
20801
20985
|
["Skip", skipped.has(panel.name) ? "yes" : "no"],
|
|
20802
20986
|
[
|
|
@@ -20805,8 +20989,15 @@ ${outcome.warning}` : done);
|
|
|
20805
20989
|
]
|
|
20806
20990
|
] : [];
|
|
20807
20991
|
const labelWidth = Math.max(0, ...panelRows.map(([label]) => label.length));
|
|
20808
|
-
const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : filtering ? "filter" : panel ? "panel" : "list";
|
|
20992
|
+
const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : palette ? "palette" : filtering ? "filter" : panel ? "panel" : "list";
|
|
20809
20993
|
const hint = helpBar(screen, unicode, columns);
|
|
20994
|
+
const commands = palette ? paletteEntries(unicode, palette.query) : [];
|
|
20995
|
+
const commandWidth = Math.max(0, ...commands.map(({ label }) => label.length));
|
|
20996
|
+
const paletteRoom = Math.max(
|
|
20997
|
+
1,
|
|
20998
|
+
(windowRows || DEFAULT_ROWS) - (1 + Math.max(1, rows.length) + (focused ? 1 : 0) + (focused?.hint ? 1 : 0) + 1 + 1 + 1) - (unsaved === void 0 ? 0 : 1)
|
|
20999
|
+
);
|
|
21000
|
+
const firstCommand = Math.max(0, (palette?.row ?? 0) - paletteRoom + 1);
|
|
20810
21001
|
const formWidth = Math.min(FORM_WIDTH, columns);
|
|
20811
21002
|
const formLabels = dialog?.kind === "form" ? formRows(dialog.editing === void 0) : [];
|
|
20812
21003
|
const formLabelWidth = Math.max(0, ...formLabels.map(({ label }) => label.length));
|
|
@@ -20820,6 +21011,7 @@ ${outcome.warning}` : done);
|
|
|
20820
21011
|
focused && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(
|
|
20821
21012
|
cut(focused.error ? `${focused.target} ${dot} ${focused.error}` : focused.target)
|
|
20822
21013
|
) }),
|
|
21014
|
+
focused?.hint && panel?.kind !== "service" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(focused.hint)) }),
|
|
20823
21015
|
panel !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
20824
21016
|
Box_default,
|
|
20825
21017
|
{
|
|
@@ -20828,20 +21020,22 @@ ${outcome.warning}` : done);
|
|
|
20828
21020
|
borderStyle: unicode ? "single" : "classic",
|
|
20829
21021
|
paddingX: 1,
|
|
20830
21022
|
children: [
|
|
20831
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
Math.max(1, panelWidth - PANEL_FRAME_WIDTH)
|
|
20835
|
-
)
|
|
20836
|
-
) }),
|
|
21023
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(truncate(panel.kind === "options" ? "Options" : panel.name, panelRoom)) }),
|
|
21024
|
+
detail.map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: toned(line) }, line.id)),
|
|
21025
|
+
detail.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim((unicode ? "\u2500" : "-").repeat(panelRoom)) }),
|
|
20837
21026
|
panelRows.map(([label, value], index) => {
|
|
20838
21027
|
const line = `${index === panel.row ? marker : " "}${label.padEnd(labelWidth)} ${unicode ? `\u2039 ${value} \u203A` : `< ${value} >`}`;
|
|
20839
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line,
|
|
21028
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, panelRoom) }, label);
|
|
20840
21029
|
})
|
|
20841
21030
|
]
|
|
20842
21031
|
}
|
|
20843
21032
|
),
|
|
20844
21033
|
filtering && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`Filter: ${filter}${CURSOR}`) }),
|
|
21034
|
+
palette && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`: ${palette.query}${CURSOR}`) }),
|
|
21035
|
+
palette && commands.slice(firstCommand, firstCommand + paletteRoom).map(({ command, label, keys }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(
|
|
21036
|
+
`${commands[palette.row]?.command === command ? marker : " "}${label.padEnd(commandWidth)} ${keys}`
|
|
21037
|
+
) }, command)),
|
|
21038
|
+
palette && commands.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(`no command matches "${palette.query}"`)) }),
|
|
20845
21039
|
dialog?.kind === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(`Remove ${dialog.name} from the config?`)) }),
|
|
20846
21040
|
dialog?.kind === "form" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
20847
21041
|
Box_default,
|
|
@@ -20896,7 +21090,7 @@ async function openDashboard(initial, terminal, { onServiceWrite, ...state }) {
|
|
|
20896
21090
|
{ ...service, timeout: timeouts2[name] }
|
|
20897
21091
|
])
|
|
20898
21092
|
);
|
|
20899
|
-
return runChecks({ ...config, services }, [], { timeoutMs, signal }, skipped);
|
|
21093
|
+
return runChecks({ ...config, services }, [], { timeoutMs, signal, detail: true }, skipped);
|
|
20900
21094
|
};
|
|
20901
21095
|
const timeouts = Object.fromEntries(
|
|
20902
21096
|
Object.entries(config.services).flatMap(
|