@ninhache/probe 0.9.0 → 0.11.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 +14 -4
- package/dist/{chunk-6UGVXAZY.js → chunk-VHDD7JN7.js} +95 -5
- package/dist/cli.js +13 -4
- package/dist/{open-YISUDM6V.js → open-QWB24XAM.js} +332 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,14 +113,15 @@ 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
|
-
|
|
|
123
|
+
| `:` | Find a command by what it does; `Enter` runs |
|
|
124
|
+
| `o` | Options: interval, timeout and notifications |
|
|
124
125
|
| `?` | Every key; `?` or `Esc` closes |
|
|
125
126
|
| `q` | Quit |
|
|
126
127
|
|
|
@@ -132,16 +133,25 @@ 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
|
|
|
139
145
|
```yaml
|
|
140
|
-
dashboard: { interval: 5, timeout: 3 }
|
|
146
|
+
dashboard: { interval: 5, timeout: 3, notify: true }
|
|
141
147
|
services:
|
|
142
148
|
db: { skip: true }
|
|
143
149
|
```
|
|
144
150
|
|
|
151
|
+
With `notify` on, a desktop notification says when a service goes down or comes back: one per round of checks
|
|
152
|
+
that saw a change, none when the dashboard starts. It names the services and nothing else, and needs
|
|
153
|
+
`notify-send` on Linux (`osascript`, already there, on macOS); without it the dashboard says so once and goes on.
|
|
154
|
+
|
|
145
155
|
A skipped service is left out of `probe check`, `probe wait` and the dashboard, and shown as `skipped`. Name it
|
|
146
156
|
(`probe check db`) and it is checked anyway.
|
|
147
157
|
|
|
@@ -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;
|
|
@@ -33,6 +36,7 @@ var EXIT_ERROR = 2;
|
|
|
33
36
|
var DETECT_PORTS = [3e3, 3001, 4200, 4321, 5173, 8e3, 8080];
|
|
34
37
|
var DETECT_PORTS_ENV = "PROBE_DETECT_PORTS";
|
|
35
38
|
var DETECT_TIMEOUT_MS = 300;
|
|
39
|
+
var NOTIFY_WAIT_MS = 5e3;
|
|
36
40
|
|
|
37
41
|
// src/errors.ts
|
|
38
42
|
var ProbeError = class extends Error {
|
|
@@ -162,6 +166,79 @@ function diagnoseStatus(service, status) {
|
|
|
162
166
|
return { error, hint: `the service answers: check \`path\` and \`expect\` in ${CONFIG}` };
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
// src/checkers/response.ts
|
|
170
|
+
var HIDDEN_VALUE = "(hidden)";
|
|
171
|
+
var SENSITIVE_HEADER = /auth|cookie|token|secret|key|session|password|credential/i;
|
|
172
|
+
var TEXT_TYPE = /^text\/|json|xml|javascript|yaml|x-www-form-urlencoded/i;
|
|
173
|
+
var NUL = 0;
|
|
174
|
+
var FIRST_PRINTABLE = 32;
|
|
175
|
+
var DELETE = 127;
|
|
176
|
+
var LAST_C1_CONTROL = 159;
|
|
177
|
+
var REPLACEMENT = "\uFFFD";
|
|
178
|
+
function isControl(character) {
|
|
179
|
+
const code = character.codePointAt(0) ?? NUL;
|
|
180
|
+
return code < FIRST_PRINTABLE || code >= DELETE && code <= LAST_C1_CONTROL;
|
|
181
|
+
}
|
|
182
|
+
function printable(text, kept = "") {
|
|
183
|
+
return Array.from(text).filter((character) => kept.includes(character) || !isControl(character)).join("");
|
|
184
|
+
}
|
|
185
|
+
async function readStart(body) {
|
|
186
|
+
const reader = body.getReader();
|
|
187
|
+
const chunks = [];
|
|
188
|
+
let size = 0;
|
|
189
|
+
let ended = false;
|
|
190
|
+
let timer;
|
|
191
|
+
const late = new Promise((resolve) => {
|
|
192
|
+
timer = setTimeout(() => resolve("late"), RESPONSE_BODY_WAIT_MS);
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
while (size < RESPONSE_BODY_BYTES) {
|
|
196
|
+
const read = reader.read();
|
|
197
|
+
read.catch(() => {
|
|
198
|
+
});
|
|
199
|
+
const next = await Promise.race([read, late]);
|
|
200
|
+
if (next === "late") break;
|
|
201
|
+
if (next.done) {
|
|
202
|
+
ended = true;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
chunks.push(next.value);
|
|
206
|
+
size += next.value.length;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
void reader.cancel().catch(() => {
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
bytes: Buffer.concat(chunks).subarray(0, RESPONSE_BODY_BYTES),
|
|
216
|
+
cut: !ended || size > RESPONSE_BODY_BYTES
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function describeResponse(response) {
|
|
220
|
+
const headers = [...response.headers].map(([name, value]) => [
|
|
221
|
+
printable(name),
|
|
222
|
+
SENSITIVE_HEADER.test(name) ? HIDDEN_VALUE : printable(value)
|
|
223
|
+
]);
|
|
224
|
+
const detail = { statusText: printable(response.statusText), headers };
|
|
225
|
+
if (!response.body) return detail;
|
|
226
|
+
const type = printable(response.headers.get("content-type")?.split(";")[0] ?? "").trim();
|
|
227
|
+
if (type && !TEXT_TYPE.test(type)) {
|
|
228
|
+
void response.body.cancel().catch(() => {
|
|
229
|
+
});
|
|
230
|
+
return { ...detail, bodyNote: `${type} body, not shown` };
|
|
231
|
+
}
|
|
232
|
+
const { bytes, cut } = await readStart(response.body);
|
|
233
|
+
if (bytes.length === 0) return detail;
|
|
234
|
+
if (bytes.includes(NUL)) return { ...detail, bodyNote: "binary body, not shown" };
|
|
235
|
+
const decoded = new TextDecoder().decode(bytes).replaceAll(" ", " ");
|
|
236
|
+
const text = printable(cut ? decoded.replace(new RegExp(`${REPLACEMENT}+$`), "") : decoded, "\n");
|
|
237
|
+
const body = text.trimEnd();
|
|
238
|
+
if (!body) return detail;
|
|
239
|
+
return cut ? { ...detail, body, bodyCut: true } : { ...detail, body };
|
|
240
|
+
}
|
|
241
|
+
|
|
165
242
|
// src/checkers/http.ts
|
|
166
243
|
function targetUrl(service) {
|
|
167
244
|
const bareIpv6 = service.host.includes(":") && !service.host.startsWith("[");
|
|
@@ -183,7 +260,7 @@ function expectsRedirect(expect) {
|
|
|
183
260
|
}
|
|
184
261
|
var httpChecker = {
|
|
185
262
|
type: "http",
|
|
186
|
-
async check(name, service, { timeoutMs, signal }) {
|
|
263
|
+
async check(name, service, { timeoutMs, signal, detail }) {
|
|
187
264
|
const target = targetUrl(service);
|
|
188
265
|
const base = { name, type: "http", target };
|
|
189
266
|
const started = performance.now();
|
|
@@ -196,10 +273,20 @@ var httpChecker = {
|
|
|
196
273
|
redirect: expectsRedirect(service.expect) ? "manual" : "follow"
|
|
197
274
|
});
|
|
198
275
|
const latencyMs = elapsed();
|
|
199
|
-
await response.body?.cancel();
|
|
200
276
|
const { status } = response;
|
|
201
|
-
|
|
202
|
-
|
|
277
|
+
const described = detail ? { response: await describeResponse(response) } : {};
|
|
278
|
+
if (!detail) await response.body?.cancel();
|
|
279
|
+
if (isExpected(service.expect, status)) {
|
|
280
|
+
return { ...base, ok: true, status, latencyMs, ...described };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
...base,
|
|
284
|
+
ok: false,
|
|
285
|
+
status,
|
|
286
|
+
latencyMs,
|
|
287
|
+
...diagnoseStatus(service, status),
|
|
288
|
+
...described
|
|
289
|
+
};
|
|
203
290
|
} catch (error) {
|
|
204
291
|
const diagnosis = diagnoseFailure(error, service, timeoutMs);
|
|
205
292
|
return { ...base, ok: false, latencyMs: elapsed(), ...diagnosis };
|
|
@@ -251,7 +338,8 @@ async function runChecks(config, names, options, skipped = /* @__PURE__ */ new S
|
|
|
251
338
|
}
|
|
252
339
|
return getChecker(type).check(name, service, {
|
|
253
340
|
timeoutMs: resolveTimeoutMs(service, options),
|
|
254
|
-
signal: options.signal
|
|
341
|
+
signal: options.signal,
|
|
342
|
+
detail: options.detail
|
|
255
343
|
});
|
|
256
344
|
})
|
|
257
345
|
);
|
|
@@ -417,6 +505,7 @@ export {
|
|
|
417
505
|
DEFAULT_MAKEFILE,
|
|
418
506
|
DEFAULT_TIMEOUT_SECONDS,
|
|
419
507
|
DASHBOARD_INTERVAL_SECONDS,
|
|
508
|
+
DASHBOARD_HISTORY_LENGTH,
|
|
420
509
|
DASHBOARD_INTERVAL_CHOICES_SECONDS,
|
|
421
510
|
DASHBOARD_TIMEOUT_CHOICES_SECONDS,
|
|
422
511
|
MAX_TIMEOUT_SECONDS,
|
|
@@ -436,6 +525,7 @@ export {
|
|
|
436
525
|
DETECT_PORTS,
|
|
437
526
|
DETECT_PORTS_ENV,
|
|
438
527
|
DETECT_TIMEOUT_MS,
|
|
528
|
+
NOTIFY_WAIT_MS,
|
|
439
529
|
CURRENT_SCHEMA_VERSION,
|
|
440
530
|
SERVICE_NAME_PATTERN,
|
|
441
531
|
serviceSchemaV1,
|
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-VHDD7JN7.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.11.0" : "0.0.0-dev";
|
|
628
628
|
|
|
629
629
|
// src/makefile/generator.ts
|
|
630
630
|
import { createHash } from "crypto";
|
|
@@ -885,6 +885,7 @@ import { dirname as dirname2, join as join5 } from "path";
|
|
|
885
885
|
import { isMap as isMap2, isScalar as isScalar2, parseDocument as parseDocument2 } from "yaml";
|
|
886
886
|
var SECTION = "dashboard";
|
|
887
887
|
var KEYS = ["interval", "timeout"];
|
|
888
|
+
var NOTIFY = "notify";
|
|
888
889
|
var SERVICES = "services";
|
|
889
890
|
var SKIP = "skip";
|
|
890
891
|
function isDelay(value) {
|
|
@@ -907,18 +908,21 @@ async function readDashboardPreferences(paths) {
|
|
|
907
908
|
const value = doc.getIn([SECTION, key]);
|
|
908
909
|
if (isDelay(value)) preferences[key] = value;
|
|
909
910
|
}
|
|
911
|
+
if (doc.getIn([SECTION, NOTIFY]) === true) preferences.notify = true;
|
|
910
912
|
return preferences;
|
|
911
913
|
}
|
|
912
914
|
function initialDashboardSettings(askedIntervalMs, saved) {
|
|
913
915
|
return {
|
|
914
916
|
intervalMs: askedIntervalMs ?? (saved.interval ?? DASHBOARD_INTERVAL_SECONDS) * 1e3,
|
|
915
|
-
timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3
|
|
917
|
+
timeoutMs: (saved.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3,
|
|
918
|
+
...saved.notify ? { notify: true } : {}
|
|
916
919
|
};
|
|
917
920
|
}
|
|
918
921
|
function changedPreferences(previous, next) {
|
|
919
922
|
const changed = {};
|
|
920
923
|
if (next.intervalMs !== previous.intervalMs) changed.interval = next.intervalMs / 1e3;
|
|
921
924
|
if (next.timeoutMs !== previous.timeoutMs) changed.timeout = next.timeoutMs / 1e3;
|
|
925
|
+
if (Boolean(next.notify) !== Boolean(previous.notify)) changed.notify = Boolean(next.notify);
|
|
922
926
|
return changed;
|
|
923
927
|
}
|
|
924
928
|
async function editLocal(paths, edit) {
|
|
@@ -951,6 +955,11 @@ function writeDashboardPreferences(paths, preferences) {
|
|
|
951
955
|
for (const key of KEYS) {
|
|
952
956
|
if (preferences[key] !== void 0) doc.setIn([SECTION, key], preferences[key]);
|
|
953
957
|
}
|
|
958
|
+
if (preferences.notify) doc.setIn([SECTION, NOTIFY], true);
|
|
959
|
+
else if (preferences.notify === false && isMap2(section)) {
|
|
960
|
+
section.delete(NOTIFY);
|
|
961
|
+
if (section.items.length === 0) doc.delete(SECTION);
|
|
962
|
+
}
|
|
954
963
|
return true;
|
|
955
964
|
});
|
|
956
965
|
}
|
|
@@ -1796,7 +1805,7 @@ async function openedDashboard(rawArgs) {
|
|
|
1796
1805
|
if (!asked) return false;
|
|
1797
1806
|
throw new ProbeError("USAGE", "No services yet. Add one: probe add api -p 3000");
|
|
1798
1807
|
}
|
|
1799
|
-
const { openDashboard } = await import("./open-
|
|
1808
|
+
const { openDashboard } = await import("./open-QWB24XAM.js");
|
|
1800
1809
|
let settings = initialDashboardSettings(askedMs, await readDashboardPreferences(paths));
|
|
1801
1810
|
const saves = createSaveQueue();
|
|
1802
1811
|
const local = `${PROBE_DIR}/${LOCAL_FILE}`;
|
|
@@ -1,8 +1,10 @@
|
|
|
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,
|
|
7
|
+
NOTIFY_WAIT_MS,
|
|
6
8
|
createColors,
|
|
7
9
|
curlCommand,
|
|
8
10
|
expectProblem,
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
runChecks,
|
|
18
20
|
targetUrl,
|
|
19
21
|
truncate
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-VHDD7JN7.js";
|
|
21
23
|
import {
|
|
22
24
|
__commonJS,
|
|
23
25
|
__export,
|
|
@@ -18596,7 +18598,7 @@ var Box_default = Box;
|
|
|
18596
18598
|
|
|
18597
18599
|
// node_modules/.pnpm/ink@8.0.0_@types+react@19.3.0_react@19.3.1/node_modules/ink/build/components/Text.js
|
|
18598
18600
|
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 }) {
|
|
18601
|
+
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
18602
|
const { isScreenReaderEnabled } = (0, import_react12.use)(AccessibilityContext);
|
|
18601
18603
|
const inheritedBackgroundColor = (0, import_react12.use)(BackgroundContext);
|
|
18602
18604
|
const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
|
|
@@ -18634,7 +18636,7 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
|
|
|
18634
18636
|
flexGrow: 0,
|
|
18635
18637
|
flexShrink: 1,
|
|
18636
18638
|
flexDirection: "row",
|
|
18637
|
-
textWrap:
|
|
18639
|
+
textWrap: wrap2
|
|
18638
18640
|
}, internal_transform: isScreenReaderEnabled ? void 0 : transform }, childrenOrAriaLabel)
|
|
18639
18641
|
);
|
|
18640
18642
|
}
|
|
@@ -20278,12 +20280,50 @@ async function copyText(text, platform2 = process.platform, env3 = process.env,
|
|
|
20278
20280
|
return false;
|
|
20279
20281
|
}
|
|
20280
20282
|
|
|
20283
|
+
// src/notify.ts
|
|
20284
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
20285
|
+
function notifierName(platform2) {
|
|
20286
|
+
return platform2 === "darwin" ? "osascript" : "notify-send";
|
|
20287
|
+
}
|
|
20288
|
+
function notifierCommand(platform2, title, text) {
|
|
20289
|
+
if (platform2 !== "darwin") return ["notify-send", "--app-name=probe", "--", title, text];
|
|
20290
|
+
return [
|
|
20291
|
+
"osascript",
|
|
20292
|
+
"-e",
|
|
20293
|
+
"on run argv",
|
|
20294
|
+
"-e",
|
|
20295
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
20296
|
+
"-e",
|
|
20297
|
+
"end run",
|
|
20298
|
+
title,
|
|
20299
|
+
text
|
|
20300
|
+
];
|
|
20301
|
+
}
|
|
20302
|
+
function sendNotification(title, text, platform2 = process.platform, spawn = nodeSpawn2) {
|
|
20303
|
+
const [command = "", ...args] = notifierCommand(platform2, title, text);
|
|
20304
|
+
return new Promise((resolve) => {
|
|
20305
|
+
const child = spawn(command, args, { stdio: "ignore" });
|
|
20306
|
+
const late = setTimeout(() => {
|
|
20307
|
+
child.kill();
|
|
20308
|
+
resolve(false);
|
|
20309
|
+
}, NOTIFY_WAIT_MS);
|
|
20310
|
+
const settle = (shown) => {
|
|
20311
|
+
clearTimeout(late);
|
|
20312
|
+
resolve(shown);
|
|
20313
|
+
};
|
|
20314
|
+
child.once("error", () => settle(false));
|
|
20315
|
+
child.once("exit", (code) => settle(code === 0));
|
|
20316
|
+
});
|
|
20317
|
+
}
|
|
20318
|
+
|
|
20281
20319
|
// src/dashboard/actions.ts
|
|
20320
|
+
var NOTIFICATION_TITLE = "probe";
|
|
20282
20321
|
var system = () => ({
|
|
20283
20322
|
platform: process.platform,
|
|
20284
20323
|
env: process.env,
|
|
20285
20324
|
openUrl,
|
|
20286
|
-
copyText
|
|
20325
|
+
copyText,
|
|
20326
|
+
notify: sendNotification
|
|
20287
20327
|
});
|
|
20288
20328
|
function serviceActions(config, deps = system()) {
|
|
20289
20329
|
const service = (name) => config().services[name];
|
|
@@ -20305,6 +20345,10 @@ ${url}`;
|
|
|
20305
20345
|
const tools = clipboardTools(deps.platform, deps.env).map(([tool]) => tool);
|
|
20306
20346
|
return `No ${tools.join(" or ")} to copy with. The command:
|
|
20307
20347
|
${command}`;
|
|
20348
|
+
},
|
|
20349
|
+
async onNotify(message) {
|
|
20350
|
+
if (await deps.notify(NOTIFICATION_TITLE, message)) return void 0;
|
|
20351
|
+
return `${notifierName(deps.platform)} could not show a desktop notification`;
|
|
20308
20352
|
}
|
|
20309
20353
|
};
|
|
20310
20354
|
}
|
|
@@ -20312,6 +20356,129 @@ ${command}`;
|
|
|
20312
20356
|
// src/dashboard/Dashboard.tsx
|
|
20313
20357
|
var import_react36 = __toESM(require_react(), 1);
|
|
20314
20358
|
|
|
20359
|
+
// src/dashboard/changes.ts
|
|
20360
|
+
function stateChanges(known, results) {
|
|
20361
|
+
const checked = results.filter(({ skipped }) => !skipped);
|
|
20362
|
+
return {
|
|
20363
|
+
known: new Map(checked.map(({ name, ok }) => [name, ok])),
|
|
20364
|
+
changes: checked.filter(({ name, ok }) => known.has(name) && known.get(name) !== ok).map(({ name, ok }) => ({ name, up: ok }))
|
|
20365
|
+
};
|
|
20366
|
+
}
|
|
20367
|
+
function changeMessage(changes) {
|
|
20368
|
+
const [only] = changes;
|
|
20369
|
+
if (only && changes.length === 1) return `${only.name} is ${only.up ? "back up" : "down"}`;
|
|
20370
|
+
const names = (up) => changes.filter((change) => change.up === up).map(({ name }) => name).join(", ");
|
|
20371
|
+
return [
|
|
20372
|
+
...names(false) ? [`down: ${names(false)}`] : [],
|
|
20373
|
+
...names(true) ? [`back up: ${names(true)}`] : []
|
|
20374
|
+
].join("; ");
|
|
20375
|
+
}
|
|
20376
|
+
|
|
20377
|
+
// src/dashboard/detail.ts
|
|
20378
|
+
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
20379
|
+
var HEADER_LINES = 6;
|
|
20380
|
+
var BODY_LINES = 5;
|
|
20381
|
+
var HINT_LINES = 3;
|
|
20382
|
+
var TRANSPORT_HEADERS = /* @__PURE__ */ new Set([
|
|
20383
|
+
"connection",
|
|
20384
|
+
"content-length",
|
|
20385
|
+
"date",
|
|
20386
|
+
"keep-alive",
|
|
20387
|
+
"transfer-encoding"
|
|
20388
|
+
]);
|
|
20389
|
+
function recordLatencies(history, results) {
|
|
20390
|
+
return Object.fromEntries(
|
|
20391
|
+
results.map(({ name, status, skipped, latencyMs }) => {
|
|
20392
|
+
const before = history[name] ?? [];
|
|
20393
|
+
const answered = status !== void 0 && !skipped;
|
|
20394
|
+
return [name, answered ? [...before, latencyMs].slice(-DASHBOARD_HISTORY_LENGTH) : before];
|
|
20395
|
+
})
|
|
20396
|
+
);
|
|
20397
|
+
}
|
|
20398
|
+
function sparkline(values, unicode, width) {
|
|
20399
|
+
if (values.length === 0) return "";
|
|
20400
|
+
const low = Math.min(...values);
|
|
20401
|
+
const high = Math.max(...values);
|
|
20402
|
+
const range = low === high ? `${low}ms` : `${low}${unicode ? "\u2013" : "-"}${high}ms`;
|
|
20403
|
+
if (!unicode) return `${range} over ${values.length} ${values.length === 1 ? "check" : "checks"}`;
|
|
20404
|
+
const room = width - range.length - 1;
|
|
20405
|
+
if (room < 1) return range;
|
|
20406
|
+
const top = BLOCKS.length - 1;
|
|
20407
|
+
const bars = values.slice(-room).map((value) => BLOCKS[high === low ? 0 : Math.round((value - low) / (high - low) * top)]).join("");
|
|
20408
|
+
return `${bars} ${range}`;
|
|
20409
|
+
}
|
|
20410
|
+
function wrap(text, width, maxLines) {
|
|
20411
|
+
const lines = [];
|
|
20412
|
+
let line = "";
|
|
20413
|
+
for (const word of text.split(" ")) {
|
|
20414
|
+
if (line && line.length + 1 + word.length > width) {
|
|
20415
|
+
lines.push(line);
|
|
20416
|
+
line = word;
|
|
20417
|
+
} else line = line ? `${line} ${word}` : word;
|
|
20418
|
+
}
|
|
20419
|
+
lines.push(line);
|
|
20420
|
+
const kept = lines.slice(0, maxLines).map((kept2) => truncate(kept2, width));
|
|
20421
|
+
if (lines.length > maxLines) kept[maxLines - 1] = truncate(`${kept[maxLines - 1]} \u2026`, width);
|
|
20422
|
+
return kept;
|
|
20423
|
+
}
|
|
20424
|
+
function detailLines(result, history, { unicode, width, room }) {
|
|
20425
|
+
const dot = unicode ? "\xB7" : "-";
|
|
20426
|
+
const more = unicode ? "\u2026" : "...";
|
|
20427
|
+
const cut = (text) => truncate(text, width);
|
|
20428
|
+
if (result.skipped) return [{ id: "status", text: cut("skipped: not checked"), tone: "dim" }];
|
|
20429
|
+
const { response, status } = result;
|
|
20430
|
+
const tone = result.ok ? "good" : "bad";
|
|
20431
|
+
const lines = [];
|
|
20432
|
+
if (status === void 0) lines.push({ id: "status", text: cut(result.error ?? "down"), tone });
|
|
20433
|
+
else {
|
|
20434
|
+
const answer = `${status} ${response?.statusText ?? ""}`.trim();
|
|
20435
|
+
lines.push({ id: "status", text: cut(`${answer} ${dot} ${result.latencyMs}ms`), tone });
|
|
20436
|
+
if (result.error) lines.push({ id: "error", text: cut(result.error), tone });
|
|
20437
|
+
}
|
|
20438
|
+
if (result.hint) {
|
|
20439
|
+
for (const [index, text] of wrap(result.hint, width, HINT_LINES).entries())
|
|
20440
|
+
lines.push({ id: `hint ${index}`, text, tone: "dim" });
|
|
20441
|
+
}
|
|
20442
|
+
const latencies = sparkline(history, unicode, width);
|
|
20443
|
+
if (latencies) lines.push({ id: "latencies", text: cut(latencies), tone: "dim" });
|
|
20444
|
+
if (!response) return lines;
|
|
20445
|
+
const transport = ([name]) => TRANSPORT_HEADERS.has(name.toLowerCase());
|
|
20446
|
+
const headers = [
|
|
20447
|
+
...response.headers.filter((header) => !transport(header)),
|
|
20448
|
+
...response.headers.filter(transport)
|
|
20449
|
+
].map(([name, value]) => `${name}: ${value}`);
|
|
20450
|
+
const body = response.bodyNote ? [response.bodyNote] : response.body?.split("\n") ?? [];
|
|
20451
|
+
let headerRoom = Math.min(headers.length, HEADER_LINES);
|
|
20452
|
+
let bodyRoom = Math.min(body.length, BODY_LINES);
|
|
20453
|
+
const left = Math.max(0, room - lines.length);
|
|
20454
|
+
while (headerRoom + bodyRoom > left) {
|
|
20455
|
+
if (bodyRoom > 1 && bodyRoom >= headerRoom) bodyRoom -= 1;
|
|
20456
|
+
else if (headerRoom > 0) headerRoom -= 1;
|
|
20457
|
+
else bodyRoom -= 1;
|
|
20458
|
+
}
|
|
20459
|
+
const hiddenHeaders = headers.length - headerRoom;
|
|
20460
|
+
for (const [index, header] of headers.slice(0, headerRoom).entries()) {
|
|
20461
|
+
const last = index === headerRoom - 1 && hiddenHeaders > 0;
|
|
20462
|
+
lines.push(
|
|
20463
|
+
last ? {
|
|
20464
|
+
id: `header ${index}`,
|
|
20465
|
+
text: cut(`${more} ${hiddenHeaders + 1} more headers`),
|
|
20466
|
+
tone: "dim"
|
|
20467
|
+
} : { id: `header ${index}`, text: cut(header), tone: "dim" }
|
|
20468
|
+
);
|
|
20469
|
+
}
|
|
20470
|
+
const cutBody = body.length > bodyRoom || response.bodyCut;
|
|
20471
|
+
for (const [index, text] of body.slice(0, bodyRoom).entries()) {
|
|
20472
|
+
const last = index === bodyRoom - 1 && cutBody;
|
|
20473
|
+
lines.push({
|
|
20474
|
+
id: `body ${index}`,
|
|
20475
|
+
text: last ? `${truncate(text, Math.max(1, width - more.length - 1))} ${more}`.trim() : cut(text),
|
|
20476
|
+
tone: response.bodyNote ? "dim" : void 0
|
|
20477
|
+
});
|
|
20478
|
+
}
|
|
20479
|
+
return lines;
|
|
20480
|
+
}
|
|
20481
|
+
|
|
20315
20482
|
// src/dashboard/form.ts
|
|
20316
20483
|
var ROWS = [
|
|
20317
20484
|
{ key: "name", label: "Name", placeholder: "api" },
|
|
@@ -20357,16 +20524,24 @@ var list = (binding) => ({
|
|
|
20357
20524
|
...binding
|
|
20358
20525
|
});
|
|
20359
20526
|
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
|
-
|
|
20527
|
+
list({ keys: "\u2191\u2193 jk", ascii: "jk", action: "move", help: "Move", drop: 2 }),
|
|
20528
|
+
list({ keys: "r", action: "check", help: "Check now", barless: true, command: "check" }),
|
|
20529
|
+
list({
|
|
20530
|
+
keys: "\u21B5",
|
|
20531
|
+
ascii: "enter",
|
|
20532
|
+
action: "service",
|
|
20533
|
+
help: "Service detail",
|
|
20534
|
+
drop: 5,
|
|
20535
|
+
command: "detail"
|
|
20536
|
+
}),
|
|
20537
|
+
list({ keys: "u", action: "open", help: "Open its URL", barless: true, command: "open" }),
|
|
20538
|
+
list({ keys: "c", action: "copy", help: "Copy curl command", barless: true, command: "copy" }),
|
|
20539
|
+
list({ keys: "e", action: "edit", help: "Edit the service", barless: true, command: "edit" }),
|
|
20540
|
+
list({ keys: "d", action: "remove", help: "Remove it", barless: true, command: "remove" }),
|
|
20541
|
+
list({ keys: "a", action: "add", help: "Add a service", drop: 3, command: "add" }),
|
|
20542
|
+
list({ keys: "/", action: "filter", help: "Filter by name", drop: 4, command: "filter" }),
|
|
20543
|
+
list({ keys: ":", action: "commands", help: "Find a command", drop: 1 }),
|
|
20544
|
+
list({ keys: "o", action: "options", help: "Options", barless: true, command: "options" }),
|
|
20370
20545
|
{
|
|
20371
20546
|
screens: ["panel"],
|
|
20372
20547
|
section: "Panels",
|
|
@@ -20404,6 +20579,9 @@ var BINDINGS = [
|
|
|
20404
20579
|
help: "Keep the filter"
|
|
20405
20580
|
},
|
|
20406
20581
|
{ screens: ["filter"], section: "Filter", keys: "esc", action: "clear", help: "Clear it" },
|
|
20582
|
+
{ screens: ["palette"], keys: "\u2191\u2193", ascii: "arrows", action: "move", drop: 1 },
|
|
20583
|
+
{ screens: ["palette"], keys: "\u21B5", ascii: "enter", action: "run" },
|
|
20584
|
+
{ screens: ["palette"], keys: "esc", action: "close" },
|
|
20407
20585
|
{
|
|
20408
20586
|
screens: ["form"],
|
|
20409
20587
|
section: "Add and edit",
|
|
@@ -20430,14 +20608,16 @@ var BINDINGS = [
|
|
|
20430
20608
|
section: "Outside a text field",
|
|
20431
20609
|
keys: "?",
|
|
20432
20610
|
action: "help",
|
|
20433
|
-
help: "This help"
|
|
20611
|
+
help: "This help",
|
|
20612
|
+
command: "help"
|
|
20434
20613
|
},
|
|
20435
20614
|
{
|
|
20436
20615
|
screens: ["list", "panel", "help"],
|
|
20437
20616
|
section: "Outside a text field",
|
|
20438
20617
|
keys: "q",
|
|
20439
20618
|
action: "quit",
|
|
20440
|
-
help: "Quit"
|
|
20619
|
+
help: "Quit",
|
|
20620
|
+
command: "quit"
|
|
20441
20621
|
}
|
|
20442
20622
|
];
|
|
20443
20623
|
var SECTIONS = [
|
|
@@ -20459,6 +20639,17 @@ function helpBar(screen, unicode, columns) {
|
|
|
20459
20639
|
}
|
|
20460
20640
|
return line();
|
|
20461
20641
|
}
|
|
20642
|
+
function keyCommand(input, enter) {
|
|
20643
|
+
return BINDINGS.find(({ keys, command }) => command && (enter ? keys === "\u21B5" : keys === input))?.command;
|
|
20644
|
+
}
|
|
20645
|
+
function paletteEntries(unicode, query = "") {
|
|
20646
|
+
const wanted = query.trim().toLowerCase();
|
|
20647
|
+
return BINDINGS.flatMap((binding) => {
|
|
20648
|
+
const { command, help, action } = binding;
|
|
20649
|
+
if (!command || !help || !`${help} ${action}`.toLowerCase().includes(wanted)) return [];
|
|
20650
|
+
return [{ command, label: help, keys: written(binding, unicode) }];
|
|
20651
|
+
});
|
|
20652
|
+
}
|
|
20462
20653
|
var HELP_GAP = 3;
|
|
20463
20654
|
function sectionLines(sections, unicode) {
|
|
20464
20655
|
const described = BINDINGS.filter(({ help }) => help !== void 0);
|
|
@@ -20535,10 +20726,13 @@ function stepChoice(choices, current, direction) {
|
|
|
20535
20726
|
// src/dashboard/Dashboard.tsx
|
|
20536
20727
|
var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
|
|
20537
20728
|
var SERVICE_ROWS = ["Skip", "Timeout"];
|
|
20729
|
+
var NOTIFY_ROW = SETTING_ROWS.length;
|
|
20538
20730
|
var TIMEOUT_ROW = SERVICE_ROWS.indexOf("Timeout");
|
|
20539
20731
|
var NO_TIMEOUTS = {};
|
|
20540
20732
|
var MARKER_WIDTH = 2;
|
|
20541
20733
|
var PANEL_WIDTH = 30;
|
|
20734
|
+
var DETAIL_WIDTH = 60;
|
|
20735
|
+
var DEFAULT_ROWS = 24;
|
|
20542
20736
|
var FORM_WIDTH = 48;
|
|
20543
20737
|
var CURSOR = "_";
|
|
20544
20738
|
var PANEL_FRAME_WIDTH = 4;
|
|
@@ -20559,12 +20753,14 @@ function Dashboard({
|
|
|
20559
20753
|
onTimeoutChange,
|
|
20560
20754
|
onOpen,
|
|
20561
20755
|
onCopy,
|
|
20756
|
+
onNotify,
|
|
20562
20757
|
serviceFields,
|
|
20563
20758
|
onServiceWrite
|
|
20564
20759
|
}) {
|
|
20565
20760
|
const { exit } = useApp();
|
|
20566
|
-
const { columns } = use_window_size_default();
|
|
20761
|
+
const { columns, rows: windowRows } = use_window_size_default();
|
|
20567
20762
|
const [latest, setLatest] = (0, import_react36.useState)(null);
|
|
20763
|
+
const [history, setHistory] = (0, import_react36.useState)({});
|
|
20568
20764
|
const [selected, setSelected] = (0, import_react36.useState)(0);
|
|
20569
20765
|
const [settings, setSettings] = (0, import_react36.useState)(initial);
|
|
20570
20766
|
const [skipped, setSkipped] = (0, import_react36.useState)(() => new Set(initiallySkipped));
|
|
@@ -20573,6 +20769,7 @@ function Dashboard({
|
|
|
20573
20769
|
const [dialog, setDialog] = (0, import_react36.useState)(null);
|
|
20574
20770
|
const [filter, setFilter] = (0, import_react36.useState)("");
|
|
20575
20771
|
const [filtering, setFiltering] = (0, import_react36.useState)(false);
|
|
20772
|
+
const [palette, setPalette] = (0, import_react36.useState)(null);
|
|
20576
20773
|
const [writes, setWrites] = (0, import_react36.useState)(0);
|
|
20577
20774
|
const [help, setHelp] = (0, import_react36.useState)(false);
|
|
20578
20775
|
const [unsaved, setUnsaved] = (0, import_react36.useState)();
|
|
@@ -20583,6 +20780,20 @@ function Dashboard({
|
|
|
20583
20780
|
void saved?.then(setUnsaved);
|
|
20584
20781
|
};
|
|
20585
20782
|
const { intervalMs, timeoutMs } = settings;
|
|
20783
|
+
const known = (0, import_react36.useRef)(/* @__PURE__ */ new Map());
|
|
20784
|
+
const notifying = (0, import_react36.useRef)(false);
|
|
20785
|
+
notifying.current = settings.notify === true;
|
|
20786
|
+
const warned = (0, import_react36.useRef)(false);
|
|
20787
|
+
const tell = (next) => {
|
|
20788
|
+
const { known: now, changes } = stateChanges(known.current, next);
|
|
20789
|
+
known.current = now;
|
|
20790
|
+
if (!notifying.current || changes.length === 0) return;
|
|
20791
|
+
void onNotify?.(changeMessage(changes)).then((warning) => {
|
|
20792
|
+
if (warning === void 0 || warned.current) return;
|
|
20793
|
+
warned.current = true;
|
|
20794
|
+
setNotice(warning);
|
|
20795
|
+
});
|
|
20796
|
+
};
|
|
20586
20797
|
(0, import_react36.useEffect)(() => {
|
|
20587
20798
|
const controller = new AbortController();
|
|
20588
20799
|
const { signal } = controller;
|
|
@@ -20591,6 +20802,8 @@ function Dashboard({
|
|
|
20591
20802
|
const next = await check2(signal, timeoutMs, skipped, timeouts);
|
|
20592
20803
|
if (signal.aborted) return;
|
|
20593
20804
|
setLatest(next);
|
|
20805
|
+
tell(next);
|
|
20806
|
+
setHistory((before) => recordLatencies(before, next));
|
|
20594
20807
|
await new Promise((resolve) => {
|
|
20595
20808
|
const timer = setTimeout(resolve, intervalMs);
|
|
20596
20809
|
skipPause.current = () => {
|
|
@@ -20634,8 +20847,8 @@ ${outcome.warning}` : done);
|
|
|
20634
20847
|
else if (key === "backspace" && field) edit(form.values[field].slice(0, -1));
|
|
20635
20848
|
else if (typed && field) edit(form.values[field] + typed);
|
|
20636
20849
|
else if (key === "return") {
|
|
20637
|
-
const
|
|
20638
|
-
const problem = formProblem(rows2, form.values,
|
|
20850
|
+
const taken2 = (latest ?? []).map(({ name }) => name);
|
|
20851
|
+
const problem = formProblem(rows2, form.values, taken2);
|
|
20639
20852
|
if (problem) return setDialog({ ...form, row: problem.row, problem: problem.message });
|
|
20640
20853
|
setDialog(null);
|
|
20641
20854
|
const fields = formFields(form.values);
|
|
@@ -20658,10 +20871,9 @@ ${outcome.warning}` : done);
|
|
|
20658
20871
|
};
|
|
20659
20872
|
const change = (rowIndex, direction) => {
|
|
20660
20873
|
const row = SETTING_ROWS[rowIndex];
|
|
20661
|
-
if (!row) return;
|
|
20662
|
-
const
|
|
20663
|
-
if (
|
|
20664
|
-
const next = { ...settings, [row.key]: value };
|
|
20874
|
+
if (!row && rowIndex !== NOTIFY_ROW) return;
|
|
20875
|
+
const next = row ? { ...settings, [row.key]: stepChoice(row.choicesMs, settings[row.key], direction) } : { ...settings, notify: !settings.notify };
|
|
20876
|
+
if (row && next[row.key] === settings[row.key]) return;
|
|
20665
20877
|
setSettings(next);
|
|
20666
20878
|
track(onSettingsChange?.(next));
|
|
20667
20879
|
};
|
|
@@ -20678,6 +20890,54 @@ ${outcome.warning}` : done);
|
|
|
20678
20890
|
setTimeouts({ ...timeouts, [name]: value });
|
|
20679
20891
|
track(onTimeoutChange?.(name, value));
|
|
20680
20892
|
};
|
|
20893
|
+
const run = (command) => {
|
|
20894
|
+
const name = shown[current]?.name;
|
|
20895
|
+
switch (command) {
|
|
20896
|
+
case "check":
|
|
20897
|
+
return skipPause.current();
|
|
20898
|
+
case "options":
|
|
20899
|
+
return setPanel({ kind: "options", row: 0 });
|
|
20900
|
+
case "detail":
|
|
20901
|
+
return name ? setPanel({ kind: "service", name, row: 0 }) : void 0;
|
|
20902
|
+
case "open":
|
|
20903
|
+
return name ? void onOpen?.(name).then(setNotice) : void 0;
|
|
20904
|
+
case "copy":
|
|
20905
|
+
return name ? void onCopy?.(name, timeouts[name]).then(setNotice) : void 0;
|
|
20906
|
+
case "filter":
|
|
20907
|
+
return setFiltering(true);
|
|
20908
|
+
case "add":
|
|
20909
|
+
return onServiceWrite ? setDialog({ kind: "form", row: 0, values: EMPTY_FORM }) : void 0;
|
|
20910
|
+
case "edit": {
|
|
20911
|
+
const fields = onServiceWrite && name ? serviceFields?.(name) : void 0;
|
|
20912
|
+
if (fields) setDialog({ kind: "form", editing: name, row: 0, values: formValues(fields) });
|
|
20913
|
+
return;
|
|
20914
|
+
}
|
|
20915
|
+
case "remove":
|
|
20916
|
+
return onServiceWrite && name ? setDialog({ kind: "confirm", name }) : void 0;
|
|
20917
|
+
case "help":
|
|
20918
|
+
return setHelp(true);
|
|
20919
|
+
case "quit":
|
|
20920
|
+
return exit();
|
|
20921
|
+
default:
|
|
20922
|
+
return command;
|
|
20923
|
+
}
|
|
20924
|
+
};
|
|
20925
|
+
const typePalette = ({ query, row }, keystroke) => {
|
|
20926
|
+
const { key } = keystroke;
|
|
20927
|
+
const typed = typedCharacter(keystroke);
|
|
20928
|
+
const entries = paletteEntries(terminal.unicode, query);
|
|
20929
|
+
if (key === "escape") setPalette(null);
|
|
20930
|
+
else if (key === "return") {
|
|
20931
|
+
const entry = entries[row];
|
|
20932
|
+
if (!entry) return;
|
|
20933
|
+
setPalette(null);
|
|
20934
|
+
run(entry.command);
|
|
20935
|
+
} else if (key === "up") setPalette({ query, row: Math.max(0, row - 1) });
|
|
20936
|
+
else if (key === "down")
|
|
20937
|
+
setPalette({ query, row: Math.max(0, Math.min(entries.length - 1, row + 1)) });
|
|
20938
|
+
else if (key === "backspace") setPalette({ query: query.slice(0, -1), row: 0 });
|
|
20939
|
+
else if (typed) setPalette({ query: query + typed, row: 0 });
|
|
20940
|
+
};
|
|
20681
20941
|
const press = (keystroke) => {
|
|
20682
20942
|
const { input, key } = keystroke;
|
|
20683
20943
|
const up2 = key === "up" || input === "k";
|
|
@@ -20685,41 +20945,29 @@ ${outcome.warning}` : done);
|
|
|
20685
20945
|
const left = key === "left" || input === "h";
|
|
20686
20946
|
const right = key === "right" || input === "l";
|
|
20687
20947
|
const enter = key === "return";
|
|
20688
|
-
const selectedName = shown[current]?.name;
|
|
20689
20948
|
setNotice(void 0);
|
|
20690
20949
|
if (dialog?.kind === "form") typeInForm(dialog, keystroke);
|
|
20691
20950
|
else if (dialog) {
|
|
20692
20951
|
setDialog(null);
|
|
20693
20952
|
if (input === "y")
|
|
20694
20953
|
void write({ kind: "remove", name: dialog.name }, `Removed ${dialog.name}`);
|
|
20695
|
-
} else if (
|
|
20954
|
+
} else if (palette) typePalette(palette, keystroke);
|
|
20955
|
+
else if (filtering) typeFilter(keystroke);
|
|
20696
20956
|
else if (input === "q") exit();
|
|
20697
20957
|
else if (help) {
|
|
20698
20958
|
if (input === "?" || key === "escape") setHelp(false);
|
|
20699
20959
|
} else if (input === "?") setHelp(true);
|
|
20700
20960
|
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);
|
|
20961
|
+
const command = keyCommand(input, enter);
|
|
20962
|
+
if (command) run(command);
|
|
20963
|
+
else if (input === ":") setPalette({ query: "", row: 0 });
|
|
20708
20964
|
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
20965
|
else if (up2) setSelected(Math.max(0, current - 1));
|
|
20718
20966
|
else if (down) setSelected(Math.min(last, current + 1));
|
|
20719
20967
|
} else if (key === "escape" || (panel.kind === "options" ? input === "o" : enter))
|
|
20720
20968
|
setPanel(null);
|
|
20721
20969
|
else if (up2 || down) {
|
|
20722
|
-
const lastRow =
|
|
20970
|
+
const lastRow = panel.kind === "options" ? NOTIFY_ROW : SERVICE_ROWS.length - 1;
|
|
20723
20971
|
setPanel({ ...panel, row: Math.max(0, Math.min(lastRow, panel.row + (down ? 1 : -1))) });
|
|
20724
20972
|
} else if (left || right) {
|
|
20725
20973
|
if (panel.kind === "options") change(panel.row, right ? 1 : -1);
|
|
@@ -20775,11 +21023,12 @@ ${outcome.warning}` : done);
|
|
|
20775
21023
|
] });
|
|
20776
21024
|
}
|
|
20777
21025
|
if (!latest) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut("probe: checking\u2026") });
|
|
20778
|
-
const
|
|
21026
|
+
const asShown = (result) => {
|
|
20779
21027
|
if (result.skipped || !skipped.has(result.name)) return result;
|
|
20780
21028
|
const { name, type, target } = result;
|
|
20781
21029
|
return { name, type, target, ok: true, skipped: true, latencyMs: 0 };
|
|
20782
|
-
}
|
|
21030
|
+
};
|
|
21031
|
+
const results = shown.map(asShown);
|
|
20783
21032
|
const all = latest.filter((result) => !result.skipped && !skipped.has(result.name));
|
|
20784
21033
|
const up = all.filter((result) => result.ok).length;
|
|
20785
21034
|
const aside = latest.length - all.length;
|
|
@@ -20790,14 +21039,29 @@ ${outcome.warning}` : done);
|
|
|
20790
21039
|
`every ${seconds(intervalMs)}`,
|
|
20791
21040
|
...filter && !filtering ? [`filter: ${filter}`] : []
|
|
20792
21041
|
].join(` ${dot} `);
|
|
20793
|
-
const rows = results.length === 0 ? [] :
|
|
20794
|
-
|
|
20795
|
-
|
|
20796
|
-
|
|
21042
|
+
const rows = results.length === 0 ? [] : (
|
|
21043
|
+
// One line per service: the hint of the selected one goes under the list.
|
|
21044
|
+
formatCheckResults(
|
|
21045
|
+
results.map(({ hint: _, ...result }) => result),
|
|
21046
|
+
{ ...terminal, columns: Math.max(1, columns - MARKER_WIDTH) }
|
|
21047
|
+
).split("\n")
|
|
21048
|
+
);
|
|
20797
21049
|
const empty = filter ? `no service matches "${filter}"` : onServiceWrite ? "no service: press a to add one" : "no service";
|
|
20798
21050
|
const focused = results[current];
|
|
20799
|
-
const panelWidth = Math.min(PANEL_WIDTH, columns);
|
|
20800
|
-
const
|
|
21051
|
+
const panelWidth = Math.min(panel?.kind === "service" ? DETAIL_WIDTH : PANEL_WIDTH, columns);
|
|
21052
|
+
const panelRoom = Math.max(1, panelWidth - PANEL_FRAME_WIDTH);
|
|
21053
|
+
const detailed = panel?.kind === "service" ? latest.find(({ name }) => name === panel.name) : void 0;
|
|
21054
|
+
const taken = 1 + Math.max(1, rows.length) + 1 + 2 + 1 + 1 + SERVICE_ROWS.length + 1 + 1;
|
|
21055
|
+
const detail = detailed ? detailLines(asShown(detailed), history[detailed.name] ?? [], {
|
|
21056
|
+
unicode,
|
|
21057
|
+
width: panelRoom,
|
|
21058
|
+
room: (windowRows || DEFAULT_ROWS) - taken - (unsaved === void 0 ? 0 : 1)
|
|
21059
|
+
}) : [];
|
|
21060
|
+
const toned = ({ text, tone }) => tone === "good" ? colors.green(text) : tone === "bad" ? colors.red(text) : tone === "dim" ? colors.dim(text) : text;
|
|
21061
|
+
const panelRows = panel?.kind === "options" ? [
|
|
21062
|
+
...SETTING_ROWS.map((row) => [row.label, seconds(settings[row.key])]),
|
|
21063
|
+
["Notify", settings.notify ? "on" : "off"]
|
|
21064
|
+
] : panel ? [
|
|
20801
21065
|
["Skip", skipped.has(panel.name) ? "yes" : "no"],
|
|
20802
21066
|
[
|
|
20803
21067
|
"Timeout",
|
|
@@ -20805,8 +21069,15 @@ ${outcome.warning}` : done);
|
|
|
20805
21069
|
]
|
|
20806
21070
|
] : [];
|
|
20807
21071
|
const labelWidth = Math.max(0, ...panelRows.map(([label]) => label.length));
|
|
20808
|
-
const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : filtering ? "filter" : panel ? "panel" : "list";
|
|
21072
|
+
const screen = dialog?.kind === "form" ? "form" : dialog ? "confirm" : palette ? "palette" : filtering ? "filter" : panel ? "panel" : "list";
|
|
20809
21073
|
const hint = helpBar(screen, unicode, columns);
|
|
21074
|
+
const commands = palette ? paletteEntries(unicode, palette.query) : [];
|
|
21075
|
+
const commandWidth = Math.max(0, ...commands.map(({ label }) => label.length));
|
|
21076
|
+
const paletteRoom = Math.max(
|
|
21077
|
+
1,
|
|
21078
|
+
(windowRows || DEFAULT_ROWS) - (1 + Math.max(1, rows.length) + (focused ? 1 : 0) + (focused?.hint ? 1 : 0) + 1 + 1 + 1) - (unsaved === void 0 ? 0 : 1)
|
|
21079
|
+
);
|
|
21080
|
+
const firstCommand = Math.max(0, (palette?.row ?? 0) - paletteRoom + 1);
|
|
20810
21081
|
const formWidth = Math.min(FORM_WIDTH, columns);
|
|
20811
21082
|
const formLabels = dialog?.kind === "form" ? formRows(dialog.editing === void 0) : [];
|
|
20812
21083
|
const formLabelWidth = Math.max(0, ...formLabels.map(({ label }) => label.length));
|
|
@@ -20820,6 +21091,7 @@ ${outcome.warning}` : done);
|
|
|
20820
21091
|
focused && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(
|
|
20821
21092
|
cut(focused.error ? `${focused.target} ${dot} ${focused.error}` : focused.target)
|
|
20822
21093
|
) }),
|
|
21094
|
+
focused?.hint && panel?.kind !== "service" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(focused.hint)) }),
|
|
20823
21095
|
panel !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
20824
21096
|
Box_default,
|
|
20825
21097
|
{
|
|
@@ -20828,20 +21100,22 @@ ${outcome.warning}` : done);
|
|
|
20828
21100
|
borderStyle: unicode ? "single" : "classic",
|
|
20829
21101
|
paddingX: 1,
|
|
20830
21102
|
children: [
|
|
20831
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
Math.max(1, panelWidth - PANEL_FRAME_WIDTH)
|
|
20835
|
-
)
|
|
20836
|
-
) }),
|
|
21103
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(truncate(panel.kind === "options" ? "Options" : panel.name, panelRoom)) }),
|
|
21104
|
+
detail.map((line) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: toned(line) }, line.id)),
|
|
21105
|
+
detail.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim((unicode ? "\u2500" : "-").repeat(panelRoom)) }),
|
|
20837
21106
|
panelRows.map(([label, value], index) => {
|
|
20838
21107
|
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,
|
|
21108
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: truncate(line, panelRoom) }, label);
|
|
20840
21109
|
})
|
|
20841
21110
|
]
|
|
20842
21111
|
}
|
|
20843
21112
|
),
|
|
20844
21113
|
filtering && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`Filter: ${filter}${CURSOR}`) }),
|
|
21114
|
+
palette && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(`: ${palette.query}${CURSOR}`) }),
|
|
21115
|
+
palette && commands.slice(firstCommand, firstCommand + paletteRoom).map(({ command, label, keys }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: cut(
|
|
21116
|
+
`${commands[palette.row]?.command === command ? marker : " "}${label.padEnd(commandWidth)} ${keys}`
|
|
21117
|
+
) }, command)),
|
|
21118
|
+
palette && commands.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.dim(cut(`no command matches "${palette.query}"`)) }),
|
|
20845
21119
|
dialog?.kind === "confirm" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: colors.bold(cut(`Remove ${dialog.name} from the config?`)) }),
|
|
20846
21120
|
dialog?.kind === "form" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
20847
21121
|
Box_default,
|
|
@@ -20896,7 +21170,7 @@ async function openDashboard(initial, terminal, { onServiceWrite, ...state }) {
|
|
|
20896
21170
|
{ ...service, timeout: timeouts2[name] }
|
|
20897
21171
|
])
|
|
20898
21172
|
);
|
|
20899
|
-
return runChecks({ ...config, services }, [], { timeoutMs, signal }, skipped);
|
|
21173
|
+
return runChecks({ ...config, services }, [], { timeoutMs, signal, detail: true }, skipped);
|
|
20900
21174
|
};
|
|
20901
21175
|
const timeouts = Object.fromEntries(
|
|
20902
21176
|
Object.entries(config.services).flatMap(
|