spinuptui 0.20.0 → 0.21.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/CHANGELOG.md +83 -1
- package/README.md +144 -581
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/dev/fixtures.ts +494 -5
- package/src/index.tsx +44 -1
- package/src/lib/cliIncidents.ts +126 -0
- package/src/lib/cliSsh.ts +161 -0
- package/src/lib/probe.ts +1 -1
- package/src/lib/ssh.ts +46 -0
- package/src/lib/uptimeKuma.ts +99 -3
- package/src/lib/vanitySite.ts +74 -0
- package/src/ui/Header.tsx +10 -2
- package/src/ui/store.tsx +123 -6
- package/src/ui/views/KumaSite.tsx +508 -127
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
// Site monitoring overlay — opened with `m` in the sites pane.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// - `
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
3
|
+
// Full-screen two-pane browser (mirrors ProviderConnect.tsx): a left list of
|
|
4
|
+
// this site's monitor kinds (source: MONITOR_GLOSSARY, filtered by vanity vs
|
|
5
|
+
// regular) with a status dot, and a right detail pane showing whichever one
|
|
6
|
+
// is highlighted — its one-liner, longer mechanism description, live status,
|
|
7
|
+
// and how to act on it. The Kuma connect form is opt-in (`c`, or pressing
|
|
8
|
+
// `a` while unconnected), so the vanity-page refresh works with no Uptime
|
|
9
|
+
// Kuma at all:
|
|
10
|
+
// - `↑↓`/`jk` move the highlight in the left list.
|
|
11
|
+
// - `a` acts on WHICHEVER monitor is currently selected — one key, not one
|
|
12
|
+
// per kind. Front page selected → opens the calibration window picker;
|
|
13
|
+
// Cache bypass selected → opens its window picker; anything else →
|
|
14
|
+
// register/repair (vanity: healthz + load push + cron + Redis/PHP-fatal
|
|
15
|
+
// sentinels if sudo is connected; regular site: homepage monitor only —
|
|
16
|
+
// client site files are never touched).
|
|
17
|
+
// - `R` on a vanity site (domain = server name) re-publishes the embedded
|
|
18
|
+
// page — the upgrade path for pages seeded by older Spinup versions.
|
|
19
|
+
// Without a Kuma connection that's ALL it does; with one it also
|
|
20
|
+
// registers monitors and installs the heartbeat cron.
|
|
17
21
|
// - `n` (alerts) lists Kuma's notification providers by name (Telegram, email,
|
|
18
22
|
// …) and toggles them per site across all of the site's Spinup monitors —
|
|
19
23
|
// detection is real (providers ride the post-login event burst), only the
|
|
@@ -21,17 +25,20 @@
|
|
|
21
25
|
// - `r` (vanity, confirm-gated) rotates the monitoring secrets: new push token
|
|
22
26
|
// edited into the existing monitor (history kept), cron rewritten, new health
|
|
23
27
|
// key re-seeded — so secrets shown on a screencast can be killed right after.
|
|
28
|
+
// - `d` (regular sites) runs the doctor — a pure-HTTP cache-vs-fresh-render
|
|
29
|
+
// diagnosis, deliberately not gated on a Kuma connection.
|
|
24
30
|
// - The connect form verifies by actually logging in before anything persists;
|
|
25
31
|
// the minted JWT is stored so later sessions (and 2FA accounts) log in by
|
|
26
32
|
// token. Env-sourced connections (SPINUP_KUMA_*) never see the form.
|
|
27
33
|
|
|
28
34
|
import { useEffect, useState } from "react"
|
|
29
35
|
import { useKeyboard } from "@opentui/react"
|
|
30
|
-
import { theme } from "../../lib/theme.ts"
|
|
36
|
+
import { theme, statusDot, statusColor } from "../../lib/theme.ts"
|
|
31
37
|
import { Panel, Centered, Field, SecretInput, Spinner } from "../components.tsx"
|
|
32
38
|
import { StatusBar } from "../StatusBar.tsx"
|
|
33
39
|
import { useStore, type KumaAlertProvider } from "../store.tsx"
|
|
34
40
|
import { isVanityPair } from "../../lib/vanitySite.ts"
|
|
41
|
+
import { openUrl } from "../../lib/open.ts"
|
|
35
42
|
import { runSiteDoctor, type DoctorReport } from "../../lib/siteDoctor.ts"
|
|
36
43
|
import { classifyStack } from "../../lib/stack.ts"
|
|
37
44
|
|
|
@@ -50,6 +57,88 @@ const FP_WINDOWS = [
|
|
|
50
57
|
{ label: "1h", sec: 3600 },
|
|
51
58
|
] as const
|
|
52
59
|
|
|
60
|
+
// Check windows for the cache-bypass monitor. Unlike the fingerprint check
|
|
61
|
+
// (served from cache, costs nothing), this forces a real PHP render every
|
|
62
|
+
// time — the window is really "how much added load is acceptable", so it
|
|
63
|
+
// skews much longer. Defaults to 1h.
|
|
64
|
+
const BYPASS_WINDOWS = [
|
|
65
|
+
{ label: "30m", sec: 1800 },
|
|
66
|
+
{ label: "1h", sec: 3600 },
|
|
67
|
+
{ label: "2h", sec: 7200 },
|
|
68
|
+
{ label: "4h", sec: 14400 },
|
|
69
|
+
{ label: "6h", sec: 21600 },
|
|
70
|
+
] as const
|
|
71
|
+
const BYPASS_DEFAULT_IDX = 1 // "1h"
|
|
72
|
+
|
|
73
|
+
// The `i` glossary overlay's content — one entry per monitor kind this view
|
|
74
|
+
// can register, accurate to the actual mechanism (not an approximation of
|
|
75
|
+
// it). `contexts` gates which site kind sees each entry.
|
|
76
|
+
interface GlossaryEntry {
|
|
77
|
+
key: string
|
|
78
|
+
label: string
|
|
79
|
+
tagline: string
|
|
80
|
+
detail: string
|
|
81
|
+
contexts: Array<"vanity" | "regular">
|
|
82
|
+
}
|
|
83
|
+
const MONITOR_GLOSSARY: GlossaryEntry[] = [
|
|
84
|
+
{
|
|
85
|
+
key: "health-site",
|
|
86
|
+
label: "Site health check",
|
|
87
|
+
tagline: "Is this specific website up right now?",
|
|
88
|
+
detail:
|
|
89
|
+
"A plain HTTP check on this site's own homepage, plus certificate-expiry alerting. Served straight from the page cache when one exists — cheap, but for that exact reason it can't see a PHP fatal hiding behind a still-warm cache. This is about whether THIS site answers — independent of whether the server it lives on is under strain.",
|
|
90
|
+
contexts: ["regular"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "health-server",
|
|
94
|
+
label: "Server health check",
|
|
95
|
+
tagline: "Is the server itself under strain?",
|
|
96
|
+
detail:
|
|
97
|
+
"Hits the vanity page's /?healthz endpoint — a small script that reports the server's own resource state (load, disk) and returns 503 when it's strained. This is about the server's health, not any one site's: a customer site can be completely broken while this stays green, and the server can be strained while every site still limps out a 200.",
|
|
98
|
+
contexts: ["vanity"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "push",
|
|
102
|
+
label: "Load heartbeat",
|
|
103
|
+
tagline: "Is the server itself still alive?",
|
|
104
|
+
detail:
|
|
105
|
+
"A once-a-minute cron on the server pushes its 1-minute load average to Kuma. It's a dead-man's-switch: the cron never reports \"down\" itself — it just goes silent if the server, the cron, or its network egress dies, and Kuma's own missed-heartbeat timeout is what raises the alarm.",
|
|
106
|
+
contexts: ["vanity"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "redis",
|
|
110
|
+
label: "Redis sentinel",
|
|
111
|
+
tagline: "Is Redis actually answering?",
|
|
112
|
+
detail:
|
|
113
|
+
"The same heartbeat cron also runs redis-cli ping every minute and actively reports up/down. Unlike the load heartbeat, this alerts immediately when Redis stops answering while the server itself is fine — important because SpinupWP's default object-cache drop-in makes a dead Redis fatal on every page-cache miss.",
|
|
114
|
+
contexts: ["vanity"],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "fatal",
|
|
118
|
+
label: "PHP-fatal sentinel",
|
|
119
|
+
tagline: "Did any site on this server just start fataling?",
|
|
120
|
+
detail:
|
|
121
|
+
"A root-level cron scans every site's error/debug logs each minute for new \"PHP Fatal error\" lines. Catches the exact blind spot where a fatal happens behind a still-warm page cache and the plain per-site checks never notice. One monitor per server (not per site) to avoid pileup — the specific affected domain is named in the down alert's own message.",
|
|
122
|
+
contexts: ["vanity"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "fingerprint",
|
|
126
|
+
label: "Front page",
|
|
127
|
+
tagline: "Is the cache serving the right page?",
|
|
128
|
+
detail:
|
|
129
|
+
"At setup time, reads the live homepage and derives a template-identity fingerprint from whatever's actually there — a body-class token if one exists, otherwise the canonical link tag. From then on it asserts that same fingerprint stays present. Reads straight from the page cache, so it catches the cache serving a stale or wrong template even though the page still answers 200 — a failure a plain up/down check can't see.",
|
|
130
|
+
contexts: ["regular"],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "bypass",
|
|
134
|
+
label: "Cache bypass",
|
|
135
|
+
tagline: "Can PHP actually render this page right now?",
|
|
136
|
+
detail:
|
|
137
|
+
"The same homepage URL, but with a Cookie: wordpress_no_cache=1 header that forces the page cache to be skipped — so unlike the checks above, this one genuinely exercises PHP on every check. That's also why it costs the site something (a full render each time) and is opt-in rather than automatic, meant for sites with an actual history of PHP fatals hiding behind the cache.",
|
|
138
|
+
contexts: ["regular"],
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
|
|
53
142
|
// The `n` (alerts) step's state machine: load → list providers → toggle.
|
|
54
143
|
type AlertsState =
|
|
55
144
|
| null
|
|
@@ -57,8 +146,13 @@ type AlertsState =
|
|
|
57
146
|
| { kind: "error"; error: string }
|
|
58
147
|
| { kind: "ready"; providers: KumaAlertProvider[]; monitorIds: number[]; idx: number; busy: boolean; flash: string | null }
|
|
59
148
|
|
|
149
|
+
// A passive, header-line summary of alert wiring — fetched once when the
|
|
150
|
+
// overlay opens (not polled), kept in sync with `n`'s own toggles. Separate
|
|
151
|
+
// from AlertsState since it must survive after `n`'s interactive view closes.
|
|
152
|
+
type AlertSummary = null | { kind: "loading" } | { kind: "error"; error: string } | { kind: "ready"; providers: KumaAlertProvider[] }
|
|
153
|
+
|
|
60
154
|
export function KumaSite() {
|
|
61
|
-
const { kumaSite: site, setKumaSite, kumaConfigured, connectKuma, kumaMonitorFor, kumaOps, startKumaSetup, startVanityReseed, startKumaRotate, startFingerprintSetup, fetchKumaAlerts, toggleKumaAlert, clearKumaOp, kumaStatus, servers, setInputMode } = useStore()
|
|
155
|
+
const { kumaSite: site, setKumaSite, kumaConfigured, kumaUrl, connectKuma, kumaMonitorFor, kumaOps, startKumaSetup, startVanityReseed, startKumaRotate, startFingerprintSetup, startBypassMonitorSetup, removeSiteMonitor, fetchKumaAlerts, toggleKumaAlert, clearKumaOp, kumaStatus, servers, setInputMode } = useStore()
|
|
62
156
|
|
|
63
157
|
const [draft, setDraft] = useState({ url: "", username: "", password: "" })
|
|
64
158
|
const [fieldIdx, setFieldIdx] = useState(0)
|
|
@@ -69,12 +163,26 @@ export function KumaSite() {
|
|
|
69
163
|
// `r` arms a confirm (rotation kills the live push URL / health key the moment
|
|
70
164
|
// it fires — never do that on a stray keypress); y/⏎ fires, Esc disarms.
|
|
71
165
|
const [confirmRotate, setConfirmRotate] = useState(false)
|
|
166
|
+
// `x` arms a confirm before deleting an opt-in monitor (front page / cache
|
|
167
|
+
// bypass) — remembers WHICH kind, unlike confirmRotate (only one target).
|
|
168
|
+
const [confirmRemove, setConfirmRemove] = useState<"fingerprint" | "bypass" | null>(null)
|
|
72
169
|
// `f` opens the front-page check-window picker; ⏎ calibrates & registers.
|
|
73
170
|
const [fpPick, setFpPick] = useState(false)
|
|
74
171
|
const [fpIdx, setFpIdx] = useState(0)
|
|
172
|
+
// `b` opens the cache-bypass check-window picker; ⏎ registers/recalibrates.
|
|
173
|
+
// Own state (not shared with fpPick) — different window array and default.
|
|
174
|
+
const [bypassPick, setBypassPick] = useState(false)
|
|
175
|
+
const [bypassIdx, setBypassIdx] = useState(BYPASS_DEFAULT_IDX)
|
|
75
176
|
// `n` opens the alerts step — a live read of Kuma's notification providers and
|
|
76
177
|
// which are attached to this site's monitors; ⏎ toggles the selected one.
|
|
77
178
|
const [alerts, setAlerts] = useState<AlertsState>(null)
|
|
179
|
+
// Passive header-line summary of alert wiring — fetched once per site open
|
|
180
|
+
// (see the effect below), not polled; kept in sync with `n`'s own toggles.
|
|
181
|
+
const [alertSummary, setAlertSummary] = useState<AlertSummary>(null)
|
|
182
|
+
// Which monitor kind is highlighted in the left-hand list (full-screen
|
|
183
|
+
// browser). Clamped against `entries.length` at read time, not here — the
|
|
184
|
+
// list's length depends on isVanity, computed after this point.
|
|
185
|
+
const [selectedIdx, setSelectedIdx] = useState(0)
|
|
78
186
|
// `d` runs the doctor — read-only HTTP diagnosis (works without Kuma).
|
|
79
187
|
const [doctor, setDoctor] = useState<null | { kind: "loading" } | { kind: "ready"; report: DoctorReport }>(null)
|
|
80
188
|
// 2FA: revealed only after Kuma answers `tokenRequired` to a correct password.
|
|
@@ -92,6 +200,139 @@ export function KumaSite() {
|
|
|
92
200
|
// without this the only fix would be hand-editing config.json).
|
|
93
201
|
const connecting = showConnect
|
|
94
202
|
|
|
203
|
+
// The left-hand list's rows for this site's context, and the currently
|
|
204
|
+
// highlighted one — clamped so a stale index (e.g. from a previous site
|
|
205
|
+
// with more rows) never indexes past the end.
|
|
206
|
+
const entries = MONITOR_GLOSSARY.filter((e) => e.contexts.includes(isVanity ? "vanity" : "regular"))
|
|
207
|
+
const safeIdx = Math.min(selectedIdx, Math.max(entries.length - 1, 0))
|
|
208
|
+
const selected = entries[safeIdx]
|
|
209
|
+
|
|
210
|
+
const win = (sec: number) => (sec % 3600 === 0 ? `${sec / 3600}h` : `${Math.round(sec / 60)}m`)
|
|
211
|
+
const healthUp = site ? (kumaStatus.get(site.domain)?.up ?? null) : null
|
|
212
|
+
const fpUp = site ? (kumaStatus.get(site.domain)?.fingerprintUp ?? null) : null
|
|
213
|
+
const redisUp = site ? (kumaStatus.get(site.domain)?.redisUp ?? null) : null
|
|
214
|
+
const fatalUp = site ? (kumaStatus.get(site.domain)?.fatalUp ?? null) : null
|
|
215
|
+
const bypassUp = site ? (kumaStatus.get(site.domain)?.bypassUp ?? null) : null
|
|
216
|
+
const fp = registered?.fingerprint
|
|
217
|
+
const fpValue = fp
|
|
218
|
+
? `${fp.detail} · every ${win(fp.interval)}${fpUp === false ? " · WRONG PAGE SERVED" : fpUp === true ? " · ok" : ""}`
|
|
219
|
+
: registered?.fingerprintId
|
|
220
|
+
? "registered (details unknown) · a recalibrates"
|
|
221
|
+
: "not calibrated · a"
|
|
222
|
+
const fpColor = fpUp === false ? theme.bad : fp ? (fpUp === true ? theme.good : theme.text) : theme.textFaint
|
|
223
|
+
const redisValue = !registered?.redisId
|
|
224
|
+
? "not registered · a adds it"
|
|
225
|
+
: redisUp === false
|
|
226
|
+
? "down"
|
|
227
|
+
: redisUp === true
|
|
228
|
+
? "up"
|
|
229
|
+
: "registered (status unknown)"
|
|
230
|
+
const redisColor = redisUp === false ? theme.bad : registered?.redisId ? (redisUp === true ? theme.good : theme.text) : theme.textFaint
|
|
231
|
+
const fatalValue = !registered?.fatalId
|
|
232
|
+
? "not registered · a adds it"
|
|
233
|
+
: fatalUp === false
|
|
234
|
+
? "down"
|
|
235
|
+
: fatalUp === true
|
|
236
|
+
? "up"
|
|
237
|
+
: "registered (status unknown)"
|
|
238
|
+
const fatalColor = fatalUp === false ? theme.bad : registered?.fatalId ? (fatalUp === true ? theme.good : theme.text) : theme.textFaint
|
|
239
|
+
const bypassValue = !registered?.bypassId
|
|
240
|
+
? "not registered"
|
|
241
|
+
: bypassUp === false
|
|
242
|
+
? "down · a recalibrates"
|
|
243
|
+
: bypassUp === true
|
|
244
|
+
? "up · a recalibrates"
|
|
245
|
+
: "registered (status unknown) · a recalibrates"
|
|
246
|
+
const bypassColor = bypassUp === false ? theme.bad : registered?.bypassId ? (bypassUp === true ? theme.good : theme.text) : theme.textFaint
|
|
247
|
+
|
|
248
|
+
// Per-entry status, normalized into the { registered, upState } shape
|
|
249
|
+
// statusDot/statusColor's connection-style vocabulary maps onto (neither
|
|
250
|
+
// accepts "up"/"down" directly — see theme.ts).
|
|
251
|
+
function statusFor(key: string): { registered: boolean; upState: boolean | null; value: string; color: string } {
|
|
252
|
+
switch (key) {
|
|
253
|
+
case "health-site":
|
|
254
|
+
case "health-server":
|
|
255
|
+
return {
|
|
256
|
+
registered: !!registered?.healthId,
|
|
257
|
+
upState: healthUp,
|
|
258
|
+
value: !registered?.healthId
|
|
259
|
+
? registered?.fingerprintId
|
|
260
|
+
? "front-page check only"
|
|
261
|
+
: "not registered"
|
|
262
|
+
: healthUp === false
|
|
263
|
+
? "down"
|
|
264
|
+
: healthUp === true
|
|
265
|
+
? "up"
|
|
266
|
+
: "registered (status unknown)",
|
|
267
|
+
color: healthUp === false ? theme.bad : registered?.healthId ? (healthUp === true ? theme.good : theme.text) : theme.textFaint,
|
|
268
|
+
}
|
|
269
|
+
case "push":
|
|
270
|
+
// The load-heartbeat cron feeds the SAME shared `up` field as the
|
|
271
|
+
// health check (store.tsx's poll loop only sets it from push when
|
|
272
|
+
// health hasn't already) — there's no independent push-only signal.
|
|
273
|
+
return {
|
|
274
|
+
registered: !!registered?.pushId,
|
|
275
|
+
upState: healthUp,
|
|
276
|
+
value: !registered?.pushId ? "not registered" : healthUp === false ? "down" : healthUp === true ? "up" : "registered (status unknown)",
|
|
277
|
+
color: healthUp === false ? theme.bad : registered?.pushId ? (healthUp === true ? theme.good : theme.text) : theme.textFaint,
|
|
278
|
+
}
|
|
279
|
+
case "redis":
|
|
280
|
+
return { registered: !!registered?.redisId, upState: redisUp, value: redisValue, color: redisColor }
|
|
281
|
+
case "fatal":
|
|
282
|
+
return { registered: !!registered?.fatalId, upState: fatalUp, value: fatalValue, color: fatalColor }
|
|
283
|
+
case "fingerprint":
|
|
284
|
+
return { registered: !!registered?.fingerprintId, upState: fpUp, value: fpValue, color: fpColor }
|
|
285
|
+
case "bypass":
|
|
286
|
+
return { registered: !!registered?.bypassId, upState: bypassUp, value: bypassValue, color: bypassColor }
|
|
287
|
+
default:
|
|
288
|
+
return { registered: false, upState: null, value: "unknown", color: theme.textFaint }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// statusDot/statusColor's own vocabulary (theme.ts:26-47) is connection/
|
|
292
|
+
// deploy-style strings, not up/down — normalize here rather than there.
|
|
293
|
+
function dotStatus(s: { registered: boolean; upState: boolean | null }): string | null {
|
|
294
|
+
if (!s.registered) return null
|
|
295
|
+
if (s.upState === true) return "active"
|
|
296
|
+
if (s.upState === false) return "failed"
|
|
297
|
+
return "pending"
|
|
298
|
+
}
|
|
299
|
+
// The specific Kuma monitor id behind an entry, for the `o` deep-link — null
|
|
300
|
+
// when not yet registered (nothing to link to).
|
|
301
|
+
function monitorIdFor(key: string): number | null {
|
|
302
|
+
switch (key) {
|
|
303
|
+
case "health-site":
|
|
304
|
+
case "health-server":
|
|
305
|
+
return registered?.healthId ?? null
|
|
306
|
+
case "push":
|
|
307
|
+
return registered?.pushId ?? null
|
|
308
|
+
case "redis":
|
|
309
|
+
return registered?.redisId ?? null
|
|
310
|
+
case "fatal":
|
|
311
|
+
return registered?.fatalId ?? null
|
|
312
|
+
case "fingerprint":
|
|
313
|
+
return registered?.fingerprintId ?? null
|
|
314
|
+
case "bypass":
|
|
315
|
+
return registered?.bypassId ?? null
|
|
316
|
+
default:
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Compresses alertSummary into one header line. Only flags the ABNORMAL
|
|
322
|
+
// case explicitly (partial wiring) — full wiring is the expected state and
|
|
323
|
+
// doesn't need its own callout.
|
|
324
|
+
function formatAlertSummary(): { text: string; color: string } {
|
|
325
|
+
if (!kumaConfigured) return { text: "Alerts: connect Kuma first · c", color: theme.textFaint }
|
|
326
|
+
if (!alertSummary || alertSummary.kind === "loading") return { text: "Alerts: checking…", color: theme.textFaint }
|
|
327
|
+
if (alertSummary.kind === "error") return { text: "Alerts: unavailable · n to retry", color: theme.textFaint }
|
|
328
|
+
if (alertSummary.providers.length === 0) return { text: "Alerts: none configured in Kuma · n to add one", color: theme.textFaint }
|
|
329
|
+
const attached = alertSummary.providers.filter((p) => p.attachedAny)
|
|
330
|
+
if (attached.length === 0) return { text: "Alerts: not wired to any provider · n to choose", color: theme.warn }
|
|
331
|
+
const names = attached.map((p) => p.name).join(", ")
|
|
332
|
+
const anyPartial = attached.some((p) => p.attachedAny && !p.attachedAll)
|
|
333
|
+
return { text: `Alerts: ${names}${anyPartial ? " (some monitors only)" : ""} · n to manage`, color: theme.good }
|
|
334
|
+
}
|
|
335
|
+
|
|
95
336
|
// While the connect form is up, its inputs own the keyboard (suppresses the
|
|
96
337
|
// global single-key shortcuts); cleared on connect/close.
|
|
97
338
|
useEffect(() => {
|
|
@@ -99,6 +340,27 @@ export function KumaSite() {
|
|
|
99
340
|
return () => setInputMode(false)
|
|
100
341
|
}, [connecting, setInputMode])
|
|
101
342
|
|
|
343
|
+
// Silently fetch alert-wiring status once per site open (not polled — this
|
|
344
|
+
// needs a getMonitor round-trip per monitor, unlike the passive poll loop's
|
|
345
|
+
// beat reads, so it isn't "free" the way up/down status is). Re-fires on a
|
|
346
|
+
// genuinely different site (site?.domain toggles through undefined when the
|
|
347
|
+
// overlay closes, so reopening the same site re-fetches too).
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!site || !kumaConfigured) {
|
|
350
|
+
setAlertSummary(null)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
let cancelled = false
|
|
354
|
+
setAlertSummary({ kind: "loading" })
|
|
355
|
+
void fetchKumaAlerts(site).then((r) => {
|
|
356
|
+
if (cancelled) return
|
|
357
|
+
setAlertSummary(r.ok ? { kind: "ready", providers: r.providers } : { kind: "error", error: r.error })
|
|
358
|
+
})
|
|
359
|
+
return () => {
|
|
360
|
+
cancelled = true
|
|
361
|
+
}
|
|
362
|
+
}, [site?.domain, kumaConfigured, fetchKumaAlerts])
|
|
363
|
+
|
|
102
364
|
const runDoctor = () => {
|
|
103
365
|
if (!site) return
|
|
104
366
|
setDoctor({ kind: "loading" })
|
|
@@ -180,6 +442,15 @@ export function KumaSite() {
|
|
|
180
442
|
if (name === "escape" || name === "n" || name === "q") return setConfirmRotate(false)
|
|
181
443
|
return
|
|
182
444
|
}
|
|
445
|
+
if (confirmRemove) {
|
|
446
|
+
if ((name === "y" || name === "return") && site) {
|
|
447
|
+
const kind = confirmRemove
|
|
448
|
+
setConfirmRemove(null)
|
|
449
|
+
return removeSiteMonitor(site, kind)
|
|
450
|
+
}
|
|
451
|
+
if (name === "escape" || name === "n" || name === "q") return setConfirmRemove(null)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
183
454
|
if (doctor) {
|
|
184
455
|
if (name === "escape" || name === "q") return setDoctor(null)
|
|
185
456
|
if (doctor.kind !== "ready") return
|
|
@@ -212,6 +483,12 @@ export function KumaSite() {
|
|
|
212
483
|
const n = prev.monitorIds.length
|
|
213
484
|
return { ...prev, providers, busy: false, flash: `✓ ${p.name} ${on ? "now alerts" : "no longer alerts"} for ${n} monitor${n === 1 ? "" : "s"}` }
|
|
214
485
|
})
|
|
486
|
+
// Keep the header's passive summary in sync without a re-fetch.
|
|
487
|
+
if (r.ok) {
|
|
488
|
+
setAlertSummary((prev) =>
|
|
489
|
+
prev?.kind === "ready" ? { kind: "ready", providers: prev.providers.map((q) => (q.id === p.id ? { ...q, attachedAll: on, attachedAny: on } : q)) } : prev,
|
|
490
|
+
)
|
|
491
|
+
}
|
|
215
492
|
})
|
|
216
493
|
return
|
|
217
494
|
}
|
|
@@ -228,15 +505,27 @@ export function KumaSite() {
|
|
|
228
505
|
if (name === "escape" || name === "q") return setFpPick(false)
|
|
229
506
|
return
|
|
230
507
|
}
|
|
508
|
+
if (bypassPick) {
|
|
509
|
+
if (name === "left" || name === "up" || name === "h" || name === "k") return setBypassIdx((i) => Math.max(i - 1, 0))
|
|
510
|
+
if (name === "right" || name === "down" || name === "l" || name === "j" || name === "tab") return setBypassIdx((i) => Math.min(i + 1, BYPASS_WINDOWS.length - 1))
|
|
511
|
+
if (/^[1-5]$/.test(name)) return setBypassIdx(Number(name) - 1)
|
|
512
|
+
if (name === "return" && site) {
|
|
513
|
+
setBypassPick(false)
|
|
514
|
+
return startBypassMonitorSetup(site, BYPASS_WINDOWS[bypassIdx]!.sec)
|
|
515
|
+
}
|
|
516
|
+
if (name === "escape" || name === "q") return setBypassPick(false)
|
|
517
|
+
return
|
|
518
|
+
}
|
|
231
519
|
if (name === "c") return setShowConnect(true)
|
|
232
|
-
if (name === "
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const idx = FP_WINDOWS.findIndex((w) => w.sec === storedSec)
|
|
237
|
-
setFpIdx(idx >= 0 ? idx : 0)
|
|
238
|
-
return setFpPick(true)
|
|
520
|
+
if (name === "o" && site && op?.status !== "running") {
|
|
521
|
+
const id = selected ? monitorIdFor(selected.key) : null
|
|
522
|
+
if (kumaUrl && id != null) openUrl(`${kumaUrl}/dashboard/${id}`)
|
|
523
|
+
return
|
|
239
524
|
}
|
|
525
|
+
// Move the left list's highlight — only reachable here since every modal
|
|
526
|
+
// state above already returned.
|
|
527
|
+
if ((name === "up" || name === "k") && site && op?.status !== "running") return setSelectedIdx((i) => Math.max(i - 1, 0))
|
|
528
|
+
if ((name === "down" || name === "j") && site && op?.status !== "running") return setSelectedIdx((i) => Math.min(i + 1, entries.length - 1))
|
|
240
529
|
if (name === "d" && !isVanity && site && op?.status !== "running") {
|
|
241
530
|
// Pure HTTP — deliberately NOT gated on a Kuma connection.
|
|
242
531
|
return runDoctor()
|
|
@@ -250,8 +539,29 @@ export function KumaSite() {
|
|
|
250
539
|
return
|
|
251
540
|
}
|
|
252
541
|
if (name === "r" && isVanity && site && op?.status !== "running") return setConfirmRotate(true)
|
|
542
|
+
if (
|
|
543
|
+
name === "x" &&
|
|
544
|
+
site &&
|
|
545
|
+
op?.status !== "running" &&
|
|
546
|
+
(selected?.key === "fingerprint" || selected?.key === "bypass") &&
|
|
547
|
+
monitorIdFor(selected.key) != null
|
|
548
|
+
) {
|
|
549
|
+
return setConfirmRemove(selected.key)
|
|
550
|
+
}
|
|
551
|
+
// The single "act on whichever monitor is selected" key — replaces the old
|
|
552
|
+
// per-kind f/b keys now that the right pane shows one monitor at a time.
|
|
253
553
|
if (name === "a" && site && op?.status !== "running") {
|
|
254
554
|
if (!kumaConfigured) return setShowConnect(true) // monitors need a connection
|
|
555
|
+
if (selected?.key === "fingerprint") {
|
|
556
|
+
const storedSec = registered?.fingerprint?.interval
|
|
557
|
+
const idx = FP_WINDOWS.findIndex((w) => w.sec === storedSec)
|
|
558
|
+
setFpIdx(idx >= 0 ? idx : 0)
|
|
559
|
+
return setFpPick(true)
|
|
560
|
+
}
|
|
561
|
+
if (selected?.key === "bypass") {
|
|
562
|
+
setBypassIdx(BYPASS_DEFAULT_IDX)
|
|
563
|
+
return setBypassPick(true)
|
|
564
|
+
}
|
|
255
565
|
return startKumaSetup(site)
|
|
256
566
|
}
|
|
257
567
|
if (name === "R" && isVanity && site && op?.status !== "running") {
|
|
@@ -270,7 +580,27 @@ export function KumaSite() {
|
|
|
270
580
|
<text content={site.domain} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
271
581
|
</box>
|
|
272
582
|
|
|
273
|
-
|
|
583
|
+
{connecting ? (
|
|
584
|
+
<Centered>{renderConnect()}</Centered>
|
|
585
|
+
) : confirmRotate ? (
|
|
586
|
+
<Centered>{renderRotateConfirm()}</Centered>
|
|
587
|
+
) : confirmRemove ? (
|
|
588
|
+
<Centered>{renderRemoveConfirm()}</Centered>
|
|
589
|
+
) : doctor ? (
|
|
590
|
+
<Centered>
|
|
591
|
+
<Panel title=" Doctor — cache diagnosis " active>
|
|
592
|
+
<box style={{ flexDirection: "column", width: 68, paddingTop: 1, paddingBottom: 1 }}>{renderDoctor()}</box>
|
|
593
|
+
</Panel>
|
|
594
|
+
</Centered>
|
|
595
|
+
) : alerts ? (
|
|
596
|
+
<Centered>
|
|
597
|
+
<Panel title=" Alerts " active>
|
|
598
|
+
<box style={{ flexDirection: "column", width: 68, paddingTop: 1, paddingBottom: 1 }}>{renderAlerts()}</box>
|
|
599
|
+
</Panel>
|
|
600
|
+
</Centered>
|
|
601
|
+
) : (
|
|
602
|
+
renderMonitorBrowser()
|
|
603
|
+
)}
|
|
274
604
|
|
|
275
605
|
<StatusBar hints={hints()} showGlobal={false} />
|
|
276
606
|
</box>
|
|
@@ -326,77 +656,41 @@ export function KumaSite() {
|
|
|
326
656
|
)
|
|
327
657
|
}
|
|
328
658
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
? `${fp.detail} · every ${win(fp.interval)}${fpUp === false ? " · WRONG PAGE SERVED" : fpUp === true ? " · ok" : ""}`
|
|
335
|
-
: registered?.fingerprintId
|
|
336
|
-
? "registered (details unknown) · f recalibrates"
|
|
337
|
-
: "not calibrated · f"
|
|
338
|
-
const fpColor = fpUp === false ? theme.bad : fp ? (fpUp === true ? theme.good : theme.text) : theme.textFaint
|
|
659
|
+
// Full-screen browser: a narrow left list of this site's monitor kinds
|
|
660
|
+
// (status dot + label) and a wide right detail pane for whichever one is
|
|
661
|
+
// selected — mirrors ProviderConnect.tsx's two-Panel-in-a-row layout.
|
|
662
|
+
function renderMonitorBrowser() {
|
|
663
|
+
const alertLine = formatAlertSummary()
|
|
339
664
|
return (
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
: "
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
) : alerts ? (
|
|
366
|
-
renderAlerts()
|
|
367
|
-
) : fpPick ? (
|
|
368
|
-
<>
|
|
369
|
-
<text content="Front-page check — alerts when the wrong page is served." fg={theme.text} wrapMode="none" />
|
|
370
|
-
<text content="Reads the live page now (while it's healthy), derives a template" fg={theme.textDim} wrapMode="none" />
|
|
371
|
-
<text content="fingerprint (body class / canonical — survives copy edits), and" fg={theme.textDim} wrapMode="none" />
|
|
372
|
-
<text content="registers a Kuma keyword monitor asserting it." fg={theme.textDim} wrapMode="none" />
|
|
373
|
-
<box style={{ height: 1 }} />
|
|
374
|
-
<box style={{ flexDirection: "row" }}>
|
|
375
|
-
<text content="Check window:" fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
376
|
-
{FP_WINDOWS.map((w, i) => (
|
|
377
|
-
<text key={w.label} content={i === fpIdx ? ` ▸${w.label}` : ` ${w.label}`} fg={i === fpIdx ? theme.brand : theme.textFaint} style={{ flexShrink: 0 }} />
|
|
378
|
-
))}
|
|
379
|
-
</box>
|
|
380
|
-
<box style={{ height: 1 }} />
|
|
381
|
-
<text content="←/→ or 1-4 window · ⏎ calibrate & register · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
382
|
-
</>
|
|
383
|
-
) : confirmRotate ? (
|
|
384
|
-
<>
|
|
385
|
-
<text content="Rotate this site's monitoring secrets?" fg={theme.warn} wrapMode="none" />
|
|
386
|
-
<text content="A new push URL and health key are minted; the old ones stop" fg={theme.textDim} wrapMode="none" />
|
|
387
|
-
<text content="working immediately. Kuma monitor history is kept." fg={theme.textDim} wrapMode="none" />
|
|
388
|
-
<box style={{ height: 1 }} />
|
|
389
|
-
<text content="y / ⏎ rotate · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
390
|
-
</>
|
|
391
|
-
) : op?.status === "running" ? (
|
|
392
|
-
<box style={{ flexDirection: "row" }}>
|
|
393
|
-
<Spinner />
|
|
394
|
-
<text content={` ${op.detail}`} fg={theme.textDim} wrapMode="none" />
|
|
665
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}>
|
|
666
|
+
{connectedVersion && (
|
|
667
|
+
<>
|
|
668
|
+
<text content={`● Uptime Kuma connected (${connectedVersion}).`} fg={theme.good} wrapMode="none" />
|
|
669
|
+
<box style={{ height: 1 }} />
|
|
670
|
+
</>
|
|
671
|
+
)}
|
|
672
|
+
<text content={isVanity ? "vanity page (full server monitoring)" : "site homepage (up/down + cert expiry)"} fg={theme.textDim} wrapMode="none" />
|
|
673
|
+
<text content={kumaConfigured ? "Uptime Kuma: connected · c to reconnect" : "Uptime Kuma: not connected · c"} fg={kumaConfigured ? theme.good : theme.textFaint} wrapMode="none" />
|
|
674
|
+
<text content={alertLine.text} fg={alertLine.color} wrapMode="none" />
|
|
675
|
+
<box style={{ height: 1 }} />
|
|
676
|
+
<box style={{ flexGrow: 1, flexDirection: "row", gap: 1 }}>
|
|
677
|
+
<Panel title=" Monitors " active width={30}>
|
|
678
|
+
<box style={{ flexDirection: "column", paddingTop: 1, paddingBottom: 1 }}>
|
|
679
|
+
{entries.map((e, i) => {
|
|
680
|
+
const s = statusFor(e.key)
|
|
681
|
+
const sel = i === safeIdx
|
|
682
|
+
return (
|
|
683
|
+
<box key={e.key} style={{ flexDirection: "row", backgroundColor: sel ? theme.selectedBg : undefined }}>
|
|
684
|
+
<text content={sel ? "› " : " "} fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
685
|
+
<text content={`${statusDot(dotStatus(s))} `} fg={statusColor(dotStatus(s))} style={{ flexShrink: 0 }} />
|
|
686
|
+
<text content={e.label} fg={sel ? theme.text : theme.textDim} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
687
|
+
</box>
|
|
688
|
+
)
|
|
689
|
+
})}
|
|
395
690
|
</box>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
the overlay must always offer its next moves. */}
|
|
691
|
+
</Panel>
|
|
692
|
+
<Panel title={selected ? ` ${selected.label} ` : " Monitor "} flexGrow={1} active>
|
|
693
|
+
<box style={{ flexDirection: "column", width: 90, paddingTop: 1, paddingBottom: 1, paddingLeft: 1 }}>
|
|
400
694
|
{op?.status === "error" && (
|
|
401
695
|
<>
|
|
402
696
|
<text content={`✕ ${op.error}`} fg={theme.bad} />
|
|
@@ -409,43 +703,111 @@ export function KumaSite() {
|
|
|
409
703
|
<box style={{ height: 1 }} />
|
|
410
704
|
</>
|
|
411
705
|
)}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
content={kumaConfigured ? "a — register monitors & cron without touching the page" : "a — register Kuma monitors (connects Uptime Kuma first)"}
|
|
419
|
-
fg={theme.textDim}
|
|
420
|
-
wrapMode="none"
|
|
421
|
-
/>
|
|
422
|
-
<text content="r — rotate secrets: new push URL + health key (old ones die)" fg={theme.textDim} wrapMode="none" />
|
|
423
|
-
<text content="n — alerts: choose where this server's checks notify" fg={theme.textDim} wrapMode="none" />
|
|
424
|
-
</>
|
|
425
|
-
) : (
|
|
426
|
-
<>
|
|
427
|
-
{op?.status === "error" && (
|
|
706
|
+
{op?.status === "running" ? (
|
|
707
|
+
<box style={{ flexDirection: "row" }}>
|
|
708
|
+
<Spinner />
|
|
709
|
+
<text content={` ${op.detail}`} fg={theme.textDim} wrapMode="none" />
|
|
710
|
+
</box>
|
|
711
|
+
) : fpPick && selected?.key === "fingerprint" ? (
|
|
428
712
|
<>
|
|
429
|
-
<text content=
|
|
713
|
+
<text content="Reads the live page now (while it's healthy), derives a template" fg={theme.textDim} wrapMode="none" />
|
|
714
|
+
<text content="fingerprint (body class / canonical — survives copy edits), and" fg={theme.textDim} wrapMode="none" />
|
|
715
|
+
<text content="registers a Kuma keyword monitor asserting it." fg={theme.textDim} wrapMode="none" />
|
|
430
716
|
<box style={{ height: 1 }} />
|
|
717
|
+
<box style={{ flexDirection: "row" }}>
|
|
718
|
+
<text content="Check window:" fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
719
|
+
{FP_WINDOWS.map((w, i) => (
|
|
720
|
+
<text key={w.label} content={i === fpIdx ? ` ▸${w.label}` : ` ${w.label}`} fg={i === fpIdx ? theme.brand : theme.textFaint} style={{ flexShrink: 0 }} />
|
|
721
|
+
))}
|
|
722
|
+
</box>
|
|
723
|
+
<box style={{ height: 1 }} />
|
|
724
|
+
<text content="←/→ or 1-4 window · ⏎ calibrate & register · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
431
725
|
</>
|
|
432
|
-
)
|
|
433
|
-
{op?.status === "done" && (
|
|
726
|
+
) : bypassPick && selected?.key === "bypass" ? (
|
|
434
727
|
<>
|
|
435
|
-
<text content=
|
|
728
|
+
<text content="Forces a real PHP render, catching a fatal hidden behind a still-" fg={theme.textDim} wrapMode="none" />
|
|
729
|
+
<text content="warm page cache. Costs the site a real render on every check, so" fg={theme.textDim} wrapMode="none" />
|
|
730
|
+
<text content="pick a loose window; use only on sites with a history of this." fg={theme.textDim} wrapMode="none" />
|
|
731
|
+
<box style={{ height: 1 }} />
|
|
732
|
+
<box style={{ flexDirection: "row" }}>
|
|
733
|
+
<text content="Check window:" fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
734
|
+
{BYPASS_WINDOWS.map((w, i) => (
|
|
735
|
+
<text key={w.label} content={i === bypassIdx ? ` ▸${w.label}` : ` ${w.label}`} fg={i === bypassIdx ? theme.brand : theme.textFaint} style={{ flexShrink: 0 }} />
|
|
736
|
+
))}
|
|
737
|
+
</box>
|
|
436
738
|
<box style={{ height: 1 }} />
|
|
739
|
+
<text content="←/→ or 1-5 window · ⏎ register · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
437
740
|
</>
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
741
|
+
) : selected ? (
|
|
742
|
+
<>
|
|
743
|
+
<text content={selected.tagline} fg={theme.text} wrapMode="none" />
|
|
744
|
+
<box style={{ height: 1 }} />
|
|
745
|
+
<text content={selected.detail} fg={theme.textDim} />
|
|
746
|
+
<box style={{ height: 1 }} />
|
|
747
|
+
<Field label="Status" value={statusFor(selected.key).value} valueColor={statusFor(selected.key).color} />
|
|
748
|
+
<Field
|
|
749
|
+
label="In Kuma"
|
|
750
|
+
value={monitorIdFor(selected.key) != null ? `#${monitorIdFor(selected.key)} · o opens it` : "not registered yet"}
|
|
751
|
+
valueColor={monitorIdFor(selected.key) != null ? theme.text : theme.textFaint}
|
|
752
|
+
/>
|
|
753
|
+
<Field
|
|
754
|
+
label="Action"
|
|
755
|
+
value={(() => {
|
|
756
|
+
const removable = (selected.key === "fingerprint" || selected.key === "bypass") && monitorIdFor(selected.key) != null
|
|
757
|
+
const base =
|
|
758
|
+
selected.key === "fingerprint"
|
|
759
|
+
? registered?.fingerprintId
|
|
760
|
+
? "a — recalibrate"
|
|
761
|
+
: "a — calibrate & register"
|
|
762
|
+
: selected.key === "bypass"
|
|
763
|
+
? registered?.bypassId
|
|
764
|
+
? "a — recalibrate"
|
|
765
|
+
: "a — register (opt-in)"
|
|
766
|
+
: statusFor(selected.key).registered
|
|
767
|
+
? "a — repair / re-register"
|
|
768
|
+
: "a — register"
|
|
769
|
+
return removable ? `${base} · x — remove` : base
|
|
770
|
+
})()}
|
|
771
|
+
/>
|
|
772
|
+
{selected.key !== "fingerprint" && selected.key !== "bypass" && statusFor(selected.key).registered && (
|
|
773
|
+
<text content="(safe to re-run any time — re-syncs with Kuma, e.g. if a monitor was" fg={theme.textFaint} wrapMode="none" />
|
|
774
|
+
)}
|
|
775
|
+
{selected.key !== "fingerprint" && selected.key !== "bypass" && statusFor(selected.key).registered && (
|
|
776
|
+
<text content="deleted directly in Kuma, or sudo just got connected for PHP-fatal)" fg={theme.textFaint} wrapMode="none" />
|
|
777
|
+
)}
|
|
778
|
+
</>
|
|
779
|
+
) : null}
|
|
780
|
+
</box>
|
|
781
|
+
</Panel>
|
|
782
|
+
</box>
|
|
783
|
+
</box>
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function renderRotateConfirm() {
|
|
788
|
+
return (
|
|
789
|
+
<Panel title=" Rotate monitoring secrets " active>
|
|
790
|
+
<box style={{ flexDirection: "column", width: 68, paddingTop: 1, paddingBottom: 1 }}>
|
|
791
|
+
<text content="Rotate this site's monitoring secrets?" fg={theme.warn} wrapMode="none" />
|
|
792
|
+
<text content="A new push URL and health key are minted; the old ones stop" fg={theme.textDim} wrapMode="none" />
|
|
793
|
+
<text content="working immediately. Kuma monitor history is kept." fg={theme.textDim} wrapMode="none" />
|
|
794
|
+
<box style={{ height: 1 }} />
|
|
795
|
+
<text content="y / ⏎ rotate · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
796
|
+
</box>
|
|
797
|
+
</Panel>
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function renderRemoveConfirm() {
|
|
802
|
+
const label = confirmRemove === "fingerprint" ? "front-page" : "cache-bypass"
|
|
803
|
+
return (
|
|
804
|
+
<Panel title=" Remove monitor " active>
|
|
805
|
+
<box style={{ flexDirection: "column", width: 68, paddingTop: 1, paddingBottom: 1 }}>
|
|
806
|
+
<text content={`Remove the ${label} monitor?`} fg={theme.warn} wrapMode="none" />
|
|
807
|
+
<text content="Deletes it in Kuma — its history is gone for good. Pressing" fg={theme.textDim} wrapMode="none" />
|
|
808
|
+
<text content="a afterward re-opens the picker to add it back from scratch." fg={theme.textDim} wrapMode="none" />
|
|
809
|
+
<box style={{ height: 1 }} />
|
|
810
|
+
<text content="y / ⏎ remove · esc cancel" fg={theme.textFaint} wrapMode="none" />
|
|
449
811
|
</box>
|
|
450
812
|
</Panel>
|
|
451
813
|
)
|
|
@@ -554,16 +916,35 @@ export function KumaSite() {
|
|
|
554
916
|
function hints() {
|
|
555
917
|
if (connecting) return [{ key: "⏎", label: "next / connect" }, { key: "esc", label: "back" }]
|
|
556
918
|
if (confirmRotate) return [{ key: "y/⏎", label: "rotate" }, { key: "esc", label: "cancel" }]
|
|
919
|
+
if (confirmRemove) return [{ key: "y/⏎", label: "remove" }, { key: "esc", label: "cancel" }]
|
|
557
920
|
if (doctor) return doctor.kind === "ready" ? [{ key: "d", label: "re-run" }, { key: "esc", label: "back" }] : [{ key: "esc", label: "back" }]
|
|
558
921
|
if (alerts) return alerts.kind === "ready" && alerts.providers.length > 0 ? [{ key: "↑↓", label: "select" }, { key: "⏎", label: "toggle" }, { key: "esc", label: "done" }] : [{ key: "esc", label: "back" }]
|
|
559
922
|
if (fpPick) return [{ key: "←/→", label: "window" }, { key: "⏎", label: "calibrate" }, { key: "esc", label: "cancel" }]
|
|
923
|
+
if (bypassPick) return [{ key: "←/→", label: "window" }, { key: "⏎", label: "register" }, { key: "esc", label: "cancel" }]
|
|
560
924
|
if (op?.status === "running") return [{ key: "esc", label: "background" }]
|
|
561
|
-
const base: { key: string; label: string }[] = []
|
|
925
|
+
const base: { key: string; label: string }[] = [{ key: "↑↓", label: "select monitor" }]
|
|
562
926
|
if (isVanity) base.push({ key: "R", label: kumaConfigured ? "refresh page + monitors" : "refresh page" })
|
|
563
927
|
if (isVanity) base.push({ key: "r", label: "rotate secrets" })
|
|
564
|
-
base.push({
|
|
565
|
-
|
|
928
|
+
base.push({
|
|
929
|
+
key: "a",
|
|
930
|
+
label:
|
|
931
|
+
selected?.key === "fingerprint"
|
|
932
|
+
? registered?.fingerprintId
|
|
933
|
+
? "recalibrate front page"
|
|
934
|
+
: "front-page check"
|
|
935
|
+
: selected?.key === "bypass"
|
|
936
|
+
? registered?.bypassId
|
|
937
|
+
? "recalibrate cache-bypass"
|
|
938
|
+
: "cache-bypass check"
|
|
939
|
+
: statusFor(selected?.key ?? "").registered
|
|
940
|
+
? "repair monitors"
|
|
941
|
+
: "add monitors",
|
|
942
|
+
})
|
|
943
|
+
if (selected && (selected.key === "fingerprint" || selected.key === "bypass") && monitorIdFor(selected.key) != null) {
|
|
944
|
+
base.push({ key: "x", label: "remove monitor" })
|
|
945
|
+
}
|
|
566
946
|
if (!isVanity) base.push({ key: "d", label: "doctor" })
|
|
947
|
+
if (selected && monitorIdFor(selected.key) != null) base.push({ key: "o", label: "open in Kuma" })
|
|
567
948
|
base.push({ key: "n", label: "alerts" })
|
|
568
949
|
base.push({ key: "c", label: kumaConfigured ? "reconnect Kuma" : "connect Kuma" })
|
|
569
950
|
return [...base, { key: "esc", label: "close" }]
|