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
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -116,6 +116,9 @@ export interface KumaMonitorRef {
|
|
|
116
116
|
pushToken?: string
|
|
117
117
|
redisId?: number // "{server} redis" push monitor fed by the cron's redis-cli ping (vanity/server domains)
|
|
118
118
|
redisToken?: string
|
|
119
|
+
fatalId?: number // "{server} php-fatal" server-wide sentinel fed by the root cron's error/debug.log tail (vanity/server domains)
|
|
120
|
+
fatalToken?: string
|
|
121
|
+
bypassId?: number // "{domain} cache-bypass" opt-in http monitor with a page-cache-bypass Cookie header (per-site, no token — Kuma polls it directly)
|
|
119
122
|
fingerprintId?: number // keyword monitor asserting the front page serves its own template
|
|
120
123
|
// What fingerprint calibration derived (lib/siteFingerprint.ts) — shown in the
|
|
121
124
|
// overlay and kept so a recalibration can explain what it's replacing.
|
package/src/dev/fixtures.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
// Example fleet for Dev Mode (SPINUP_DEV_MODE=1) — a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Example fleet for Dev Mode (SPINUP_DEV_MODE=1) — a deliberately varied fleet (12
|
|
2
|
+
// servers / ~80 sites) so every view has plenty to show: EOL Ubuntu and EOL PHP
|
|
3
|
+
// servers/sites, Bedrock sites, non-WP apps, an empty server, a mid-provisioning
|
|
4
|
+
// server, a self-hosted/"custom" server, disabled-HTTPS sites, stale/missing
|
|
5
|
+
// backups, and a spread of disk usage / pending-update counts. All domains are
|
|
5
6
|
// example.com/.org/.net — never a real client's.
|
|
6
7
|
//
|
|
8
|
+
// Sites 1001-1006 are hand-authored (kept stable so anything that references
|
|
9
|
+
// them by id stays meaningful); sites 1007+ are generated deterministically
|
|
10
|
+
// (see GENERATED_SITE_PLAN below) — no Math.random/Date.now, so the fleet is
|
|
11
|
+
// identical across runs (reproducible screenshots, stable diffs).
|
|
12
|
+
//
|
|
7
13
|
// freshFixtures() returns a deep clone so MockSpinupWPClient can mutate its own copy
|
|
8
14
|
// freely without leaking state across StoreProvider remounts.
|
|
9
15
|
|
|
@@ -84,9 +90,153 @@ const SERVERS: Server[] = [
|
|
|
84
90
|
created_at: "2025-01-08T16:45:00Z",
|
|
85
91
|
status: "active",
|
|
86
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: 105,
|
|
95
|
+
name: "web4.example.com",
|
|
96
|
+
provider_name: "hetzner",
|
|
97
|
+
ubuntu_version: "24.04",
|
|
98
|
+
ip_address: "203.0.113.24",
|
|
99
|
+
ssh_port: 22,
|
|
100
|
+
timezone: "UTC",
|
|
101
|
+
region: "fsn1",
|
|
102
|
+
size: "cx41",
|
|
103
|
+
disk_space: { total: 120 * GB, available: 80 * GB, used: 40 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
104
|
+
database: null,
|
|
105
|
+
connection_status: "connected",
|
|
106
|
+
reboot_required: false,
|
|
107
|
+
upgrade_required: false,
|
|
108
|
+
created_at: "2024-05-12T12:00:00Z",
|
|
109
|
+
status: "active",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 106,
|
|
113
|
+
name: "web5.example.net",
|
|
114
|
+
provider_name: "digitalocean",
|
|
115
|
+
ubuntu_version: "22.04",
|
|
116
|
+
ip_address: "203.0.113.25",
|
|
117
|
+
ssh_port: 22,
|
|
118
|
+
timezone: "UTC",
|
|
119
|
+
region: "sfo3",
|
|
120
|
+
size: "s-2vcpu-4gb",
|
|
121
|
+
disk_space: { total: 60 * GB, available: 42 * GB, used: 18 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
122
|
+
database: null,
|
|
123
|
+
connection_status: "connected",
|
|
124
|
+
reboot_required: false,
|
|
125
|
+
upgrade_required: false,
|
|
126
|
+
created_at: "2025-09-19T10:30:00Z",
|
|
127
|
+
status: "active",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 107,
|
|
131
|
+
name: "web6.example.com",
|
|
132
|
+
provider_name: "vultr",
|
|
133
|
+
ubuntu_version: "24.04",
|
|
134
|
+
ip_address: "203.0.113.26",
|
|
135
|
+
ssh_port: 22,
|
|
136
|
+
timezone: "UTC",
|
|
137
|
+
region: "lax",
|
|
138
|
+
size: "vc2-4c-8gb",
|
|
139
|
+
disk_space: { total: 100 * GB, available: 75 * GB, used: 25 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
140
|
+
database: null,
|
|
141
|
+
connection_status: "connected",
|
|
142
|
+
reboot_required: false,
|
|
143
|
+
upgrade_required: false,
|
|
144
|
+
created_at: "2023-11-03T08:15:00Z",
|
|
145
|
+
status: "active",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 108,
|
|
149
|
+
name: "web7.example.net",
|
|
150
|
+
provider_name: "linode",
|
|
151
|
+
ubuntu_version: "22.04",
|
|
152
|
+
ip_address: "203.0.113.27",
|
|
153
|
+
ssh_port: 22,
|
|
154
|
+
timezone: "UTC",
|
|
155
|
+
region: "eu-west",
|
|
156
|
+
size: "g6-standard-4",
|
|
157
|
+
disk_space: { total: 100 * GB, available: 45 * GB, used: 55 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
158
|
+
database: null,
|
|
159
|
+
connection_status: "connected",
|
|
160
|
+
reboot_required: true,
|
|
161
|
+
upgrade_required: false,
|
|
162
|
+
created_at: "2024-02-27T09:00:00Z",
|
|
163
|
+
status: "active",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 109,
|
|
167
|
+
name: "web8.example.org",
|
|
168
|
+
provider_name: "hetzner",
|
|
169
|
+
ubuntu_version: "18.04",
|
|
170
|
+
ip_address: "203.0.113.28",
|
|
171
|
+
ssh_port: 22,
|
|
172
|
+
timezone: "UTC",
|
|
173
|
+
region: "hel1",
|
|
174
|
+
size: "cx31",
|
|
175
|
+
disk_space: { total: 40 * GB, available: 2 * GB, used: 38 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
176
|
+
database: null,
|
|
177
|
+
connection_status: "connected",
|
|
178
|
+
reboot_required: false,
|
|
179
|
+
upgrade_required: true,
|
|
180
|
+
created_at: "2021-09-14T11:00:00Z",
|
|
181
|
+
status: "active",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 110,
|
|
185
|
+
name: "web10.example.com",
|
|
186
|
+
provider_name: "digitalocean",
|
|
187
|
+
ubuntu_version: "24.04",
|
|
188
|
+
ip_address: "203.0.113.30",
|
|
189
|
+
ssh_port: 22,
|
|
190
|
+
timezone: "UTC",
|
|
191
|
+
region: "ams3",
|
|
192
|
+
size: "s-4vcpu-8gb",
|
|
193
|
+
disk_space: { total: 160 * GB, available: 90 * GB, used: 70 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
194
|
+
database: null,
|
|
195
|
+
connection_status: "connected",
|
|
196
|
+
reboot_required: false,
|
|
197
|
+
upgrade_required: false,
|
|
198
|
+
created_at: "2024-08-30T14:20:00Z",
|
|
199
|
+
status: "active",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 111,
|
|
203
|
+
name: "web11.example.org",
|
|
204
|
+
provider_name: "vultr",
|
|
205
|
+
ubuntu_version: "24.04",
|
|
206
|
+
ip_address: "203.0.113.31",
|
|
207
|
+
ssh_port: 22,
|
|
208
|
+
timezone: "UTC",
|
|
209
|
+
region: "syd",
|
|
210
|
+
size: "vc2-2c-4gb",
|
|
211
|
+
disk_space: null,
|
|
212
|
+
database: null,
|
|
213
|
+
connection_status: "connecting",
|
|
214
|
+
reboot_required: false,
|
|
215
|
+
upgrade_required: false,
|
|
216
|
+
created_at: "2026-07-08T07:50:00Z",
|
|
217
|
+
status: "active",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 112,
|
|
221
|
+
name: "edge1.example.net",
|
|
222
|
+
provider_name: null,
|
|
223
|
+
ubuntu_version: "22.04",
|
|
224
|
+
ip_address: "203.0.113.32",
|
|
225
|
+
ssh_port: 2222,
|
|
226
|
+
timezone: "UTC",
|
|
227
|
+
region: null,
|
|
228
|
+
size: null,
|
|
229
|
+
disk_space: { total: 80 * GB, available: 71 * GB, used: 9 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
230
|
+
database: null,
|
|
231
|
+
connection_status: "connected",
|
|
232
|
+
reboot_required: false,
|
|
233
|
+
upgrade_required: false,
|
|
234
|
+
created_at: "2025-04-02T13:10:00Z",
|
|
235
|
+
status: "active",
|
|
236
|
+
},
|
|
87
237
|
]
|
|
88
238
|
|
|
89
|
-
const
|
|
239
|
+
const HAND_SITES: Site[] = [
|
|
90
240
|
{
|
|
91
241
|
id: 1001,
|
|
92
242
|
server_id: 101,
|
|
@@ -193,6 +343,169 @@ const SITES: Site[] = [
|
|
|
193
343
|
},
|
|
194
344
|
]
|
|
195
345
|
|
|
346
|
+
// --- Generated sites (1007+) ------------------------------------------------
|
|
347
|
+
//
|
|
348
|
+
// One realistic short business-site word per generated site, so domains read as
|
|
349
|
+
// a real-looking fleet rather than "site1.example.com". Words already used by
|
|
350
|
+
// HAND_SITES (shop/blog/docs/app) are excluded to avoid collisions.
|
|
351
|
+
const WORDS = [
|
|
352
|
+
"status", "portal", "members", "support", "academy", "press", "forum", "store",
|
|
353
|
+
"news", "events", "clinic", "studio", "gallery", "market", "travel", "fitness",
|
|
354
|
+
"realty", "law", "dental", "vet", "bakery", "garage", "garden", "kitchen",
|
|
355
|
+
"media", "agency", "consult", "finance", "insure", "charity", "church",
|
|
356
|
+
"school", "camp", "yoga", "spa", "salon", "barber", "tailor", "florist",
|
|
357
|
+
"nursery", "daycare", "tutoring", "coaching", "photography", "videography",
|
|
358
|
+
"catering", "brewery", "winery", "coffee", "bistro", "diner", "grill",
|
|
359
|
+
"pizzeria", "patisserie", "boutique", "jewelry", "antiques", "hardware",
|
|
360
|
+
"plumbing", "electric", "roofing", "landscaping", "painting", "moving",
|
|
361
|
+
"cleaning", "security", "accounting", "marketing", "printing", "repair",
|
|
362
|
+
"rental", "storage", "notary", "locksmith",
|
|
363
|
+
]
|
|
364
|
+
const TLDS = ["com", "org", "net"]
|
|
365
|
+
|
|
366
|
+
// A handful of dates to cycle through deterministically (spread over ~3 years
|
|
367
|
+
// up to "today" in Dev Mode, 2026-07-08) — real-looking site ages without
|
|
368
|
+
// touching Date.now().
|
|
369
|
+
const CREATED_DATES = [
|
|
370
|
+
"2022-09-14T10:00:00Z", "2023-01-22T14:30:00Z", "2023-05-09T08:45:00Z",
|
|
371
|
+
"2023-08-30T16:00:00Z", "2023-11-17T11:20:00Z", "2024-02-06T09:15:00Z",
|
|
372
|
+
"2024-04-19T13:40:00Z", "2024-06-25T10:05:00Z", "2024-09-03T15:50:00Z",
|
|
373
|
+
"2024-12-11T12:00:00Z", "2025-02-14T09:30:00Z", "2025-04-28T17:10:00Z",
|
|
374
|
+
"2025-06-16T08:20:00Z", "2025-08-22T14:00:00Z", "2025-10-05T11:45:00Z",
|
|
375
|
+
"2025-12-01T09:00:00Z", "2026-02-09T13:15:00Z", "2026-04-14T10:30:00Z",
|
|
376
|
+
"2026-05-20T15:40:00Z", "2026-06-10T08:00:00Z",
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
// One "kind" per site, cycled by index — covers every stack the app classifies
|
|
380
|
+
// (Standard WP / Bedrock / Non-WP), plus the health signals the Dashboard,
|
|
381
|
+
// Stacks, and Browser views surface: EOL PHP, disabled HTTPS, disabled/absent
|
|
382
|
+
// backups, pending core/theme/plugin updates, and a non-Bedrock WP site that
|
|
383
|
+
// still deploys via git (a case cloneStackFor's git.repo-precedence handles).
|
|
384
|
+
type SiteKind = (site: { id: number; server_id: number; domain: string; site_user: string; created_at: string }) => Site
|
|
385
|
+
|
|
386
|
+
const KINDS: SiteKind[] = [
|
|
387
|
+
// Standard WP, healthy, current PHP.
|
|
388
|
+
(s) => ({
|
|
389
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
390
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
391
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
392
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
393
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 2, status: "active",
|
|
394
|
+
}),
|
|
395
|
+
// Bedrock, healthy, current PHP.
|
|
396
|
+
(s) => ({
|
|
397
|
+
...s, php_version: "8.2", public_folder: "/web/", is_wordpress: true,
|
|
398
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
399
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
400
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
401
|
+
git: { repo: `git@github.com:example/${s.site_user}-bedrock.git`, branch: "main", deploy_script: "composer install -o --no-dev", push_enabled: true, deployment_url: null },
|
|
402
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 0, status: "active",
|
|
403
|
+
}),
|
|
404
|
+
// Non-WP static site.
|
|
405
|
+
(s) => ({
|
|
406
|
+
...s, php_version: "8.3", public_folder: "/public/", is_wordpress: false,
|
|
407
|
+
https: { enabled: true }, status: "active",
|
|
408
|
+
}),
|
|
409
|
+
// WP on EOL PHP, no backups, plenty of updates pending.
|
|
410
|
+
(s) => ({
|
|
411
|
+
...s, php_version: "7.4", public_folder: "/", is_wordpress: true,
|
|
412
|
+
page_cache: { enabled: false }, https: { enabled: true },
|
|
413
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
414
|
+
backups: { files: false, database: false },
|
|
415
|
+
wp_core_update: true, wp_theme_updates: 2, wp_plugin_updates: 7, status: "active",
|
|
416
|
+
}),
|
|
417
|
+
// Non-WP app (custom PHP app, not WordPress at all).
|
|
418
|
+
(s) => ({
|
|
419
|
+
...s, php_version: "8.1", public_folder: "/public/", is_wordpress: false,
|
|
420
|
+
https: { enabled: true }, status: "active",
|
|
421
|
+
}),
|
|
422
|
+
// Standard WP with HTTPS disabled.
|
|
423
|
+
(s) => ({
|
|
424
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
425
|
+
page_cache: { enabled: true }, https: { enabled: false },
|
|
426
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
427
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
428
|
+
wp_core_update: false, wp_theme_updates: 1, wp_plugin_updates: 1, status: "active",
|
|
429
|
+
}),
|
|
430
|
+
// Bedrock on EOL PHP (8.1 aged out of security support 2025-12-31).
|
|
431
|
+
(s) => ({
|
|
432
|
+
...s, php_version: "8.1", public_folder: "/web/", is_wordpress: true,
|
|
433
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
434
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
435
|
+
backups: { files: true, database: true, retention_period: 7, next_run_time: "2026-07-09T03:00:00Z" },
|
|
436
|
+
git: { repo: `git@github.com:example/${s.site_user}-bedrock.git`, branch: "main", deploy_script: "composer install -o --no-dev", push_enabled: true, deployment_url: null },
|
|
437
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 1, status: "active",
|
|
438
|
+
}),
|
|
439
|
+
// Standard WP, heavy pending updates + EOL PHP.
|
|
440
|
+
(s) => ({
|
|
441
|
+
...s, php_version: "8.0", public_folder: "/", is_wordpress: true,
|
|
442
|
+
page_cache: { enabled: false }, https: { enabled: true },
|
|
443
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
444
|
+
backups: { files: true, database: true, retention_period: 7, next_run_time: "2026-07-09T03:00:00Z" },
|
|
445
|
+
wp_core_update: true, wp_theme_updates: 3, wp_plugin_updates: 11, status: "active",
|
|
446
|
+
}),
|
|
447
|
+
// Fresh Standard WP site, newest PHP, everything current.
|
|
448
|
+
(s) => ({
|
|
449
|
+
...s, php_version: "8.4", public_folder: "/", is_wordpress: true,
|
|
450
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
451
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
452
|
+
backups: { files: true, database: true, retention_period: 30, next_run_time: "2026-07-09T03:00:00Z" },
|
|
453
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 0, status: "active",
|
|
454
|
+
}),
|
|
455
|
+
// Standard WP (not Bedrock) that still deploys via git.
|
|
456
|
+
(s) => ({
|
|
457
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
458
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
459
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
460
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
461
|
+
git: { repo: `git@github.com:example/${s.site_user}.git`, branch: "main", deploy_script: null, push_enabled: false, deployment_url: null },
|
|
462
|
+
wp_core_update: false, wp_theme_updates: 1, wp_plugin_updates: 4, status: "active",
|
|
463
|
+
}),
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
// How many generated sites land on each active server. 102 (empty) and 111
|
|
467
|
+
// (still connecting) deliberately get none — see the SERVERS list above.
|
|
468
|
+
const GENERATED_SITE_PLAN: { serverId: number; count: number }[] = [
|
|
469
|
+
{ serverId: 101, count: 3 },
|
|
470
|
+
{ serverId: 103, count: 3 },
|
|
471
|
+
{ serverId: 104, count: 3 },
|
|
472
|
+
{ serverId: 105, count: 12 },
|
|
473
|
+
{ serverId: 106, count: 3 },
|
|
474
|
+
{ serverId: 107, count: 15 },
|
|
475
|
+
{ serverId: 108, count: 8 },
|
|
476
|
+
{ serverId: 109, count: 5 },
|
|
477
|
+
{ serverId: 110, count: 20 },
|
|
478
|
+
{ serverId: 112, count: 2 },
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
function generateSites(): Site[] {
|
|
482
|
+
const out: Site[] = []
|
|
483
|
+
let wordIdx = 0
|
|
484
|
+
let id = 1007
|
|
485
|
+
for (const { serverId, count } of GENERATED_SITE_PLAN) {
|
|
486
|
+
for (let i = 0; i < count; i++) {
|
|
487
|
+
const word = WORDS[wordIdx % WORDS.length]!
|
|
488
|
+
const tld = TLDS[wordIdx % TLDS.length]!
|
|
489
|
+
const domain = `${word}.example.${tld}`
|
|
490
|
+
const kind = KINDS[wordIdx % KINDS.length]!
|
|
491
|
+
out.push(
|
|
492
|
+
kind({
|
|
493
|
+
id,
|
|
494
|
+
server_id: serverId,
|
|
495
|
+
domain,
|
|
496
|
+
site_user: word,
|
|
497
|
+
created_at: CREATED_DATES[wordIdx % CREATED_DATES.length]!,
|
|
498
|
+
}),
|
|
499
|
+
)
|
|
500
|
+
wordIdx++
|
|
501
|
+
id++
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return out
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const SITES: Site[] = [...HAND_SITES, ...generateSites()]
|
|
508
|
+
|
|
196
509
|
const EVENTS: Event[] = [
|
|
197
510
|
{
|
|
198
511
|
id: 9001,
|
|
@@ -227,6 +540,182 @@ const EVENTS: Event[] = [
|
|
|
227
540
|
started_at: "2026-06-27T04:00:02Z",
|
|
228
541
|
finished_at: "2026-06-27T04:01:15Z",
|
|
229
542
|
},
|
|
543
|
+
{
|
|
544
|
+
id: 9004,
|
|
545
|
+
initiated_by: "dev-mode",
|
|
546
|
+
server_id: 111,
|
|
547
|
+
name: "server.provisioned",
|
|
548
|
+
status: "running",
|
|
549
|
+
output: "Provisioning web11.example.org on Vultr (syd)…",
|
|
550
|
+
created_at: "2026-07-08T07:50:00Z",
|
|
551
|
+
started_at: "2026-07-08T07:50:03Z",
|
|
552
|
+
finished_at: null,
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: 9005,
|
|
556
|
+
initiated_by: "dev-mode",
|
|
557
|
+
server_id: 107,
|
|
558
|
+
name: "site.https.enabled",
|
|
559
|
+
status: "failed",
|
|
560
|
+
output: "HTTPS request failed for realty.example.com — DNS does not yet resolve to this server",
|
|
561
|
+
created_at: "2026-07-07T15:22:00Z",
|
|
562
|
+
started_at: "2026-07-07T15:22:01Z",
|
|
563
|
+
finished_at: "2026-07-07T15:22:18Z",
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: 9006,
|
|
567
|
+
initiated_by: "dev-mode",
|
|
568
|
+
server_id: 105,
|
|
569
|
+
name: "site.php_version.updated",
|
|
570
|
+
status: "deployed",
|
|
571
|
+
output: "PHP updated to 8.4 for coffee.example.net",
|
|
572
|
+
created_at: "2026-07-06T10:05:00Z",
|
|
573
|
+
started_at: "2026-07-06T10:05:04Z",
|
|
574
|
+
finished_at: "2026-07-06T10:05:39Z",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
id: 9007,
|
|
578
|
+
initiated_by: "dev-mode",
|
|
579
|
+
server_id: 110,
|
|
580
|
+
name: "site.domains.updated",
|
|
581
|
+
status: "deployed",
|
|
582
|
+
output: "Added domain www.marketing.example.com",
|
|
583
|
+
created_at: "2026-07-05T13:40:00Z",
|
|
584
|
+
started_at: "2026-07-05T13:40:02Z",
|
|
585
|
+
finished_at: "2026-07-05T13:40:27Z",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 9008,
|
|
589
|
+
initiated_by: "dev-mode",
|
|
590
|
+
server_id: 108,
|
|
591
|
+
name: "server.service.mysql.restarted",
|
|
592
|
+
status: "deployed",
|
|
593
|
+
output: "mysql restarted on web7.example.net",
|
|
594
|
+
created_at: "2026-07-04T22:10:00Z",
|
|
595
|
+
started_at: "2026-07-04T22:10:01Z",
|
|
596
|
+
finished_at: "2026-07-04T22:10:12Z",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
id: 9009,
|
|
600
|
+
initiated_by: "dev-mode",
|
|
601
|
+
server_id: 109,
|
|
602
|
+
name: "site.object_cache.purged",
|
|
603
|
+
status: "deployed",
|
|
604
|
+
output: "Object cache purged for repair.example.net",
|
|
605
|
+
created_at: "2026-07-04T09:00:00Z",
|
|
606
|
+
started_at: "2026-07-04T09:00:01Z",
|
|
607
|
+
finished_at: "2026-07-04T09:00:06Z",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: 9010,
|
|
611
|
+
initiated_by: "dev-mode",
|
|
612
|
+
server_id: 106,
|
|
613
|
+
name: "site.https.disabled",
|
|
614
|
+
status: "deployed",
|
|
615
|
+
output: "HTTPS disabled for insure.example.org pending certificate reissue",
|
|
616
|
+
created_at: "2026-07-02T18:30:00Z",
|
|
617
|
+
started_at: "2026-07-02T18:30:02Z",
|
|
618
|
+
finished_at: "2026-07-02T18:30:20Z",
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
id: 9011,
|
|
622
|
+
initiated_by: "dev-mode",
|
|
623
|
+
server_id: 107,
|
|
624
|
+
name: "server.rebooted",
|
|
625
|
+
status: "deployed",
|
|
626
|
+
output: "Server rebooted successfully",
|
|
627
|
+
created_at: "2026-06-28T04:00:00Z",
|
|
628
|
+
started_at: "2026-06-28T04:00:02Z",
|
|
629
|
+
finished_at: "2026-06-28T04:01:08Z",
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: 9012,
|
|
633
|
+
initiated_by: "dev-mode",
|
|
634
|
+
server_id: 110,
|
|
635
|
+
name: "site.created",
|
|
636
|
+
status: "deployed",
|
|
637
|
+
output: "Site salon.example.com created",
|
|
638
|
+
created_at: "2026-06-24T11:15:00Z",
|
|
639
|
+
started_at: "2026-06-24T11:15:02Z",
|
|
640
|
+
finished_at: "2026-06-24T11:16:40Z",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: 9013,
|
|
644
|
+
initiated_by: "dev-mode",
|
|
645
|
+
server_id: 105,
|
|
646
|
+
name: "server.service.nginx.restarted",
|
|
647
|
+
status: "deployed",
|
|
648
|
+
output: "nginx restarted on web4.example.com",
|
|
649
|
+
created_at: "2026-06-20T06:45:00Z",
|
|
650
|
+
started_at: "2026-06-20T06:45:01Z",
|
|
651
|
+
finished_at: "2026-06-20T06:45:09Z",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: 9014,
|
|
655
|
+
initiated_by: "dev-mode",
|
|
656
|
+
server_id: 109,
|
|
657
|
+
name: "server.upgrade_required",
|
|
658
|
+
status: "deployed",
|
|
659
|
+
output: "Security updates available on web8.example.org (Ubuntu 18.04, EOL)",
|
|
660
|
+
created_at: "2026-06-18T05:00:00Z",
|
|
661
|
+
started_at: "2026-06-18T05:00:00Z",
|
|
662
|
+
finished_at: "2026-06-18T05:00:00Z",
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 9015,
|
|
666
|
+
initiated_by: "dev-mode",
|
|
667
|
+
server_id: 112,
|
|
668
|
+
name: "site.cache.purged",
|
|
669
|
+
status: "deployed",
|
|
670
|
+
output: "Page cache purged for notary.example.com",
|
|
671
|
+
created_at: "2026-06-15T14:00:00Z",
|
|
672
|
+
started_at: "2026-06-15T14:00:01Z",
|
|
673
|
+
finished_at: "2026-06-15T14:00:07Z",
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: 9016,
|
|
677
|
+
initiated_by: "dev-mode",
|
|
678
|
+
server_id: 108,
|
|
679
|
+
name: "site.php_version.updated",
|
|
680
|
+
status: "deployed",
|
|
681
|
+
output: "PHP updated to 8.2 for tailor.example.net",
|
|
682
|
+
created_at: "2026-06-10T09:20:00Z",
|
|
683
|
+
started_at: "2026-06-10T09:20:03Z",
|
|
684
|
+
finished_at: "2026-06-10T09:20:51Z",
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: 9017,
|
|
688
|
+
initiated_by: "dev-mode",
|
|
689
|
+
server_id: 106,
|
|
690
|
+
name: "server.provisioned",
|
|
691
|
+
status: "deployed",
|
|
692
|
+
output: "Server web5.example.net provisioned",
|
|
693
|
+
created_at: "2025-09-19T10:30:00Z",
|
|
694
|
+
started_at: "2025-09-19T10:30:04Z",
|
|
695
|
+
finished_at: "2025-09-19T10:34:12Z",
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
id: 9018,
|
|
699
|
+
initiated_by: "dev-mode",
|
|
700
|
+
server_id: 105,
|
|
701
|
+
name: "server.provisioned",
|
|
702
|
+
status: "deployed",
|
|
703
|
+
output: "Server web4.example.com provisioned",
|
|
704
|
+
created_at: "2024-05-12T12:00:00Z",
|
|
705
|
+
started_at: "2024-05-12T12:00:05Z",
|
|
706
|
+
finished_at: "2024-05-12T12:04:30Z",
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: 9019,
|
|
710
|
+
initiated_by: "dev-mode",
|
|
711
|
+
server_id: 112,
|
|
712
|
+
name: "server.service.redis.restarted",
|
|
713
|
+
status: "deployed",
|
|
714
|
+
output: "redis restarted on edge1.example.net",
|
|
715
|
+
created_at: "2024-01-08T20:00:00Z",
|
|
716
|
+
started_at: "2024-01-08T20:00:01Z",
|
|
717
|
+
finished_at: "2024-01-08T20:00:06Z",
|
|
718
|
+
},
|
|
230
719
|
]
|
|
231
720
|
|
|
232
721
|
function clone<T>(value: T): T {
|
package/src/index.tsx
CHANGED
|
@@ -12,9 +12,13 @@ import { StoreProvider } from "./ui/store.tsx"
|
|
|
12
12
|
import { App } from "./ui/App.tsx"
|
|
13
13
|
import { Onboarding } from "./ui/Onboarding.tsx"
|
|
14
14
|
import { isDevMode } from "./dev/devMode.ts"
|
|
15
|
+
import { SpinupWPClient } from "./api/client.ts"
|
|
16
|
+
import { resolveSshAccess } from "./lib/cliSsh.ts"
|
|
17
|
+
import { resolveIncidents } from "./lib/cliIncidents.ts"
|
|
15
18
|
|
|
16
19
|
const args = process.argv.slice(2)
|
|
17
|
-
const
|
|
20
|
+
const positionals = args.filter((a) => !a.startsWith("-"))
|
|
21
|
+
const command = positionals[0]
|
|
18
22
|
|
|
19
23
|
if (args.includes("-h") || args.includes("--help") || command === "help") {
|
|
20
24
|
const cfg = loadConfig()
|
|
@@ -24,6 +28,9 @@ Usage:
|
|
|
24
28
|
spinuptui Launch the dashboard
|
|
25
29
|
spinuptui login Set or update your saved API token
|
|
26
30
|
spinuptui where Print the config file path and token source
|
|
31
|
+
spinuptui ssh <domain> Print SSH access info for a site (JSON)
|
|
32
|
+
spinuptui incidents <domain> | --all [--hours N] Print Uptime Kuma
|
|
33
|
+
down/up incidents for a site or the whole fleet (JSON)
|
|
27
34
|
spinuptui --version Print the version
|
|
28
35
|
spinuptui --help Show this help
|
|
29
36
|
|
|
@@ -48,6 +55,42 @@ if (command === "where") {
|
|
|
48
55
|
process.exit(0)
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
if (command === "ssh") {
|
|
59
|
+
const domain = positionals[1]
|
|
60
|
+
if (!domain) {
|
|
61
|
+
console.error(JSON.stringify({ ok: false, reason: "usage", message: "Usage: spinuptui ssh <domain>" }))
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
const cfg = loadConfig()
|
|
65
|
+
const client = new SpinupWPClient(cfg)
|
|
66
|
+
const result = await resolveSshAccess(domain, client, cfg)
|
|
67
|
+
console.log(JSON.stringify(result))
|
|
68
|
+
process.exit(result.ok ? 0 : 1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (command === "incidents") {
|
|
72
|
+
const rest = args.slice(1)
|
|
73
|
+
const hoursIdx = rest.indexOf("--hours")
|
|
74
|
+
const hours = hoursIdx !== -1 ? Number(rest[hoursIdx + 1]) : 24
|
|
75
|
+
const allFlag = rest.includes("--all")
|
|
76
|
+
const domainArg = rest.find((a, i) => !a.startsWith("-") && !(hoursIdx !== -1 && i === hoursIdx + 1))
|
|
77
|
+
|
|
78
|
+
if ((!domainArg && !allFlag) || (domainArg && allFlag) || !Number.isFinite(hours) || hours <= 0) {
|
|
79
|
+
console.error(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: "usage",
|
|
83
|
+
message: "Usage: spinuptui incidents <domain> | spinuptui incidents --all [--hours N]",
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
const cfg = loadConfig()
|
|
89
|
+
const result = await resolveIncidents(cfg, { domain: domainArg, hours })
|
|
90
|
+
console.log(JSON.stringify(result))
|
|
91
|
+
process.exit(result.ok ? 0 : 1)
|
|
92
|
+
}
|
|
93
|
+
|
|
51
94
|
// `spinuptui login` forces the onboarding wizard even when a token already exists,
|
|
52
95
|
// so the token can be (re)saved to the config file for global use.
|
|
53
96
|
const forceLogin = command === "login"
|