spinuptui 0.19.1 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,162 +11,70 @@
11
11
  Built with <a href="https://opentui.com">OpenTUI</a> and <a href="https://bun.sh">Bun</a>.
12
12
  </p>
13
13
 
14
- Once you're in, the dashboard looks like this:
14
+ ## Screenshots
15
15
 
16
- ```
17
- ◆ SpinupTUI v0.9.0 1 Dashboard 2 Servers 3 Stacks 4 Search 5 Events 20 servers · 171 sites
18
-
19
- ┌──────────────┐ ┌───────────────┐ ┌───────────────────┐ ┌──────────────────────┐
20
- │ Servers │ │ Sites │ │ Fleet Disk │ │ WP Updates │
21
- │ 20 │ │ 171 │ │ 22% │ │ 359 │
22
- │ 20 connected │ │ 139 WordPress │ │ 616.3 GB / 2.8 TB │ │ 217 plugin · 67 core │
23
- └──────────────┘ └───────────────┘ └───────────────────┘ └──────────────────────┘
24
- ┌─ Disk usage by server ───────────────┐ ┌─ Needs attention (27) ──────────────┐
25
- │ web1.example.com ██████░░░ 60% │ │ • db1.example.com — … │
26
- │ web2.example.com ██████░░░ 57% │ │ • web3.example.org — OS … │
27
- └──────────────────────────────────────┘ └─────────────────────────────────────┘
28
- ```
16
+ <p align="center">
17
+ <img src=".github/assets/screenshot-dashboard.png" alt="SpinupTUI Dashboard" width="100%">
18
+ </p>
19
+ <p align="center">
20
+ <img src=".github/assets/screenshot-servers.png" alt="SpinupTUI Servers" width="49%">
21
+ <img src=".github/assets/screenshot-stacks.png" alt="SpinupTUI Stacks" width="49%">
22
+ </p>
23
+ <p align="center">
24
+ <img src=".github/assets/screenshot-search.png" alt="SpinupTUI Search" width="49%">
25
+ <img src=".github/assets/screenshot-events.png" alt="SpinupTUI Events" width="49%">
26
+ </p>
29
27
 
30
28
  ## Features
31
29
 
32
- - **Fleet dashboard** — at-a-glance health of every server: connection status,
33
- disk usage bars, pending reboots/SpinupWP platform upgrades, WordPress update
34
- counts, and a recent activity feed. Servers running an **end-of-life Ubuntu
35
- release** are flagged too — a `⚠ os` badge in the Servers list, a red "Ubuntu …
36
- EOL" line in a server's detail panel, and an entry in the dashboard's "Needs
37
- attention" list pointing at the clone wizard (`C`).
38
- - **Server & site browser** — a three-pane navigator. Pick a server, see its
39
- sites, drill into full details (PHP version, HTTPS, page cache, backups, Git
40
- deployment, WP updates, and more). Each site row carries an at-a-glance **status
41
- column** — `👤🔑` granted SSH keys, `H` HTTPS, `C` page cache, `B` backups (lit
42
- when on, faint `·` when off), plus `◆` linked and `↑N` pending updates — aligned
43
- into columns across every row, with a compact key in the Sites panel title.
44
- - **Stack detection & fleet composition** — the Stacks tab classifies every site
45
- as Standard WP, Bedrock, or Non-WP, with a fleet-wide PHP version breakdown
46
- (EOL versions flagged). Press `d` to SSH-probe a site's actual stack — naming
47
- WHMCS, Laravel, Static HTML, and WordPress versions the API can't tell you —
48
- or `D` to probe a whole stack at once. (See "Stack detection" below.)
49
- - **Global search** — fuzzy search across every server and site at once by name,
50
- domain, or IP. Tab onto a result to act on it (open, SpinupWP, PHP upgrade,
51
- health) right from the results, without leaving the search.
52
- - **Events feed** — recent provisioning and operation activity, with per-event
53
- detail and output.
54
- - **Live server health** — press `h` on any server for a real-time view over
55
- SSH: CPU (aggregate + per-core + sparkline), load, memory/swap, disk mounts,
56
- and top processes. Polls every few seconds. (See "Server health" below.)
57
- - **Installed plugins & themes** — press `p` on a site for its real `wp plugin
58
- list` / `wp theme list` over SSH: every plugin and theme with its status,
59
- current version, and available update — the detail the API only exposes as bare
60
- counts. Read-only, combined scrollable list with per-section update badges.
61
- Detects the WordPress directory itself, so it works on `/public/` and Bedrock
62
- installs alike (even sites SpinupWP misclassifies as non-WordPress). (See
63
- "Installed plugins & themes" below.)
64
- - **Open in browser** — press `o` on any site to open it in your default browser.
65
- - **Link local working copies** — press `L` on a site to link its local checkout
66
- (a path plus the local URL where you serve it), then open it with `t` (a
67
- terminal there) or `v` (its local URL). The Stacks tab can **auto-discover**
68
- copies (`S`) by git remote / Bedrock `WP_HOME` / folder name, and **report**
69
- managed sites that still need one (`f`). Linked sites show a `◆` marker, and a
70
- linked checkout shows its local git drift (`⇡N unpushed` / `● uncommitted`).
71
- (See "Local working copies" below.)
72
- - **SSH into a site** — press `s` on a site to open a new terminal already running
73
- `ssh` into it (`{site_user}@{server_ip}`).
74
- - **DNS migration lens** — press `n` on a site (or `N` on a server) to see the DNS
75
- records that move a site to another server: each site's hostnames with their type,
76
- TTL (in seconds), and a `◀ here` flag when they point at this server. Connect a
77
- provider (AWS Route 53 / Cloudflare) and press `⏎` to **edit a record's TTL** — the
78
- prep step for a low-risk cutover — or `p` to **repoint the record**: pick another
79
- of your SpinupWP servers from a list (or enter an IP) and move the traffic. It only
80
- ever touches a site's own hosting records, never your MX/TXT/other records. (See
81
- "DNS hosts, access & editing" below.)
82
- - **Database backup & sync** — on a linked WordPress site (Search tab), press `d`
83
- to download a gzipped production database backup into the copy's `sql/` folder
84
- (**read-only on production**), or — opt-in — press `p` to pull production into
85
- your **local** database (backs up local first, rewrites URLs, runs your existing
86
- `sync.d` hook). Works with Standard WP and Bedrock, no per-site config. (See
87
- "Database backup & sync" below.)
88
- - **Production media fallback** — after a DB pull, your local site shows broken
89
- images because the (often huge) media library isn't synced. Press `m` on a
90
- linked WordPress site to drop a small mu-plugin that serves any **missing-locally**
91
- upload from production — so images resolve without copying a single file. Works
92
- on any local stack (it's pure PHP, not web-server config). (See "Production media
93
- fallback" below.)
94
- - **Upgrade a site's PHP version** — press `u` on a site to pick a new PHP
95
- version and apply it (`PUT /sites/{id}/php`), then watch the upgrade event run
96
- to completion. (See "Upgrading PHP" below.)
97
- - **Enable / disable HTTPS** — press `H` on a site to toggle its certificate.
98
- Enabling issues a free Let's Encrypt cert (the domain must already resolve to
99
- the server); disabling removes it — the confirm screen warns plainly that
100
- https:// visitors will see errors until it's re-enabled. Same background
101
- event-tracking as the PHP upgrade.
102
- - **Purge cache** — press `P` on a site to clear its page cache and WordPress
103
- object cache together (SpinupWP has no enable/disable for either on an
104
- existing site, only purge). Low-risk and fully reversible — both rebuild on
105
- the next page load.
106
- - **Server actions** — press `a` on a server to reboot it or restart a service
107
- (Nginx / PHP-FPM / MySQL / Redis). Servers needing a reboot show a `↻rbt`
108
- badge, and the overlay reads the box over SSH to show *why* (the pending
109
- kernel/security packages). (See "Server actions" below.)
110
- - **Create & connect a server** — press `c` on the Servers tab to provision a new
111
- server (DigitalOcean / Vultr / Linode / Hetzner), priced from the provider catalog
112
- and pre-filled to match a selected server. Then press `V` on any server that
113
- doesn't yet have a **site at its own hostname** to connect it end to end: SpinupTUI
114
- writes the DNS A record, creates a
115
- placeholder "vanity" site, enables HTTPS, hands you off to add your SSH key, and
116
- publishes a status page — turning a bare server into one you can actually work
117
- with. Long builds run in the background and survive a restart. (See "Creating &
118
- connecting servers" below.)
119
- - **Clone a whole server to a new one** — press `C` on a server to clone one or
120
- more of its sites onto a fresh (or existing) destination, **without touching DNS
121
- until you say so**. A guided wizard plans + sizes the sites, provisions or picks
122
- the destination, pulls each site server-to-server (Standard WP **and** Bedrock),
123
- verifies the clone against the source, and finally repoints DNS on your word.
124
- Runs in the background with a header badge. (See "Cloning a server" below.)
125
- - **Privileged writes over SSH (sudo + SSH keys)** — the SpinupWP API can't manage
126
- SSH keys or sudo users, so SpinupTUI does it directly. Press `S` on a server to
127
- **connect sudo** for the session (optionally **remembered in your macOS Keychain**
128
- so it auto-unlocks next time), then `K` on a site to **grant or revoke** your
129
- personal key and/or SpinupTUI's dedicated machine key — one site or every site on the
130
- server. (See "Privileged writes over SSH" below.)
131
- - **Uptime Kuma monitoring** — press `m` on a site to wire it into your own
132
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) instance (one-time
133
- connect, 2FA supported, verified by a real login). Vanity pages double as
134
- health endpoints (`?healthz` returns 503 under CPU pressure / low disk;
135
- `?format=json&key=…` serves full metrics) and get a load push monitor fed by a
136
- once-a-minute cron — Kuma graphs server load, and a silent cron acts as a
137
- dead-man's switch. Monitor status flows back in: a Monitor row in Details, a
138
- "▼ N monitors down" header badge, uptime/response/load sparklines in the
139
- Health view, and reboots automatically open a Kuma maintenance window so
140
- planned downtime never pages you. Regular sites get a homepage monitor
141
- (up/down + cert-expiry alerts) — client site files are never touched.
142
- Showed a push URL or health key on a screencast? `r` in the `m` overlay
143
- **rotates both secrets in place** — the old ones die immediately, the Kuma
144
- monitor (and its history) survives. Vanity sites are marked `⌂` in the
145
- Servers tab so they never hide among domain-named servers' sites.
146
- (See `docs/uptime-kuma.md` for recipes.)
147
- - **Site monitoring: fingerprints, a doctor, and a Redis sentinel** — press `M`
148
- on any site (Servers / Stacks / Search) for the monitoring overlay. `f`
149
- calibrates a **front-page check**: SpinupTUI reads your healthy front page,
150
- derives a template-identity fingerprint from what WordPress itself stamps
151
- into the markup (not a headline someone might edit), proves it discriminates,
152
- and registers a Kuma keyword monitor at your chosen window (5m–1h) — catching
153
- "the cache is serving the wrong page while HTTP stays 200". `d` runs the
154
- **site doctor**: a read-only cache differential (cached vs. bypassed render,
155
- proven by the server's own `fastcgi-cache` headers) plus a wp-admin door
156
- probe, diagnosing stale caches and **partial outages** — cached pages fine
157
- while everything uncached throws 500 (a dead Redis does exactly this) — and
158
- ending in a copyable runbook. `n` wires **alerts**: SpinupTUI detects the
159
- notification providers configured in Kuma by name and toggles them per site.
160
- And servers get a **Redis sentinel**: the heartbeat cron pings Redis every
161
- minute and reports up/down to its own `{server} redis` monitor.
162
- (See "Site monitoring" below.)
163
- - **Completion toasts** — background writes that take a while (a PHP upgrade, a
164
- server reboot/restart, resolving your fleet's DNS) raise a non-focus-stealing
165
- toast when they finish, so you're not left guessing after you've moved on.
166
- - **Release notes** — after SpinupTUI updates to a new version, a one-time overlay
167
- shows what changed (sourced straight from that version's GitHub release —
168
- no separate feed to maintain). Press `n` in `?` Help to see the current
169
- version's notes again any time.
30
+ - **Fleet dashboard** — connection status, disk usage, pending reboots/upgrades,
31
+ WP update counts, EOL Ubuntu flags, and a recent-activity feed for every server.
32
+ - **Server & site browser** — a three-pane navigator with an at-a-glance status
33
+ column (SSH keys, HTTPS, cache, backups, linked copy, pending updates).
34
+ - **Stack detection & fleet composition** — classifies every site as Standard WP,
35
+ Bedrock, or Non-WP, with a PHP-version breakdown and an on-demand SSH probe for
36
+ precise identification. → [docs/stack-detection.md](docs/stack-detection.md)
37
+ - **Global search** — fuzzy search across every server and site by name, domain,
38
+ or IP, with inline actions on the result.
39
+ - **Events feed** — recent provisioning/operation activity with per-event detail.
40
+ - **Live server health** (`h`) — real-time CPU/load/memory/disk over SSH.
41
+ → [docs/server-health.md](docs/server-health.md)
42
+ - **Installed plugins & themes** (`p`) — real `wp plugin`/`theme list` over SSH,
43
+ with per-item version and update status. → [docs/plugins-themes.md](docs/plugins-themes.md)
44
+ - **Open in browser** (`o`) / **SSH into a site** (`s`) — one-key shortcuts.
45
+ - **Link local working copies** (`L`) — link a site to its local checkout, with
46
+ auto-discovery and git-drift indicators. → [docs/local-working-copies.md](docs/local-working-copies.md)
47
+ - **DNS migration lens** (`n` / `N`) — see and edit a site's hosting records,
48
+ repoint it to another server. → [docs/dns-access-editing.md](docs/dns-access-editing.md)
49
+ - **Database backup & sync** (`d` / `p`, Search tab) — download or pull a
50
+ production DB into a linked local copy. → [docs/database-backup-sync.md](docs/database-backup-sync.md)
51
+ - **Production media fallback** (`m`, Search tab) — serve missing-locally images
52
+ from production after a DB pull. → [docs/production-media-fallback.md](docs/production-media-fallback.md)
53
+ - **Upgrade a site's PHP version** (`u`) — pick a version, apply it, watch the
54
+ event complete. → [docs/php-upgrade.md](docs/php-upgrade.md)
55
+ - **Enable / disable HTTPS** (`H`) — toggle a site's certificate.
56
+ - **Purge cache** (`P`) — clear page + object cache together.
57
+ - **Server actions** (`a`) — reboot or restart a service, with the real reason a
58
+ reboot is pending. → [docs/server-actions.md](docs/server-actions.md)
59
+ - **Create & connect a server** (`c` / `V`) — provision a server, then wire it up
60
+ end-to-end (DNS, a vanity site, HTTPS, SSH-key handoff).
61
+ → [docs/creating-connecting-servers.md](docs/creating-connecting-servers.md)
62
+ - **Clone a whole server to a new one** (`C`) — a guided wizard that plans,
63
+ copies, verifies, and cuts over DNS on your word.
64
+ → [docs/cloning-a-server.md](docs/cloning-a-server.md) ·
65
+ [how it works](docs/clone-wizard-explained.md)
66
+ - **Privileged writes over SSH** (`S` / `K`) — grant or revoke SSH keys directly,
67
+ since the API can't. → [docs/privileged-ssh-writes.md](docs/privileged-ssh-writes.md)
68
+ - **Uptime Kuma monitoring** (`m`) — wire a site into your own
69
+ [Uptime Kuma](https://github.com/louislam/uptime-kuma) for uptime, load, and
70
+ health checks. → [docs/uptime-kuma.md](docs/uptime-kuma.md)
71
+ - **Site monitoring: fingerprints, a doctor, and a Redis sentinel** (`M`) — a
72
+ front-page identity check, a cache/outage doctor, and alert wiring.
73
+ → [docs/site-monitoring.md](docs/site-monitoring.md)
74
+ - **Completion toasts** — a non-focus-stealing toast when a background write
75
+ (PHP upgrade, reboot, DNS resolve, …) finishes.
76
+ - **Release notes** — an in-app "what's new" after every update, sourced
77
+ straight from that version's GitHub release.
170
78
 
171
79
  > The tool is **read-only by default** and works great with a Read Only API
172
80
  > token. The write actions — creating a server, connecting it with a vanity site,
@@ -346,459 +254,95 @@ search box.
346
254
 
347
255
  ## Server health (SSH)
348
256
 
349
- The SpinupWP API exposes no live metrics, so the health view (`h` in the
350
- Servers tab) reaches the server directly over SSH using **your local SSH keys /
351
- agent** — the same way you'd `ssh in` and run `htop`. It runs a single batched,
352
- **read-only** command (`cat /proc/*`, `df`, `ps`) and renders the result.
353
-
354
- - **Connection target** is derived from the API: it connects as one of the
355
- server's `site_user`s at the server's IP (`site_user@ip`). No extra config
356
- needed if `ssh site_user@ip` already works from your terminal.
357
- - **Non-interactive:** it uses `BatchMode=yes`, so if key auth isn't already set
358
- up it fails fast with a hint rather than prompting for a password.
359
- - **Override the SSH user** (e.g. to use `root` or a sudo user) with the
360
- `SPINUPWP_SSH_USER` environment variable, or `"sshUser"` in the config file.
361
- - A persistent `ControlMaster` connection keeps repeated polls fast.
362
-
363
- Nothing is ever written to the server.
257
+ `h` on a server opens a real-time SSH view — CPU, load, memory/swap, disk, top
258
+ processes — read-only, using your local SSH keys. Full details:
259
+ [docs/server-health.md](docs/server-health.md).
364
260
 
365
261
  ## Stack detection
366
262
 
367
- The **Stacks** tab (`3`) breaks your fleet into buckets and helps you see what's
368
- actually running where. It works in two tiers:
369
-
370
- - **Tier 1 — instant, no SSH.** Every site is classified from data the API
371
- already returns: **Non-WP**, **Bedrock** (WordPress with a `/web/` webroot), or
372
- **Standard WP**. The left pane shows counts and bars; the right pane shows the
373
- fleet-wide **PHP version distribution** with end-of-life versions flagged.
374
-
375
- - **Tier 2 — on-demand SSH probe.** Press `d` on a site (in the Stacks or
376
- Servers tab) to inspect its filesystem **read-only** and identify it precisely:
377
- **WordPress** (with version), **Bedrock**, **WHMCS**, **Laravel**, or
378
- **Static HTML**. Press `D` to probe an entire stack in list order (bounded SSH
379
- concurrency). A conclusive probe **overrides** the Tier-1 guess — so a site the
380
- API mislabels (e.g. WordPress installed outside SpinupWP's installer reports
381
- `is_wordpress=false`) moves into its true bucket. The Non-WP bucket expands
382
- into named sub-rows (WHMCS / Laravel / Static HTML / Unknown / unprobed).
383
-
384
- Probes reuse the same SSH access as the health view (`site_user@ip`, your local
385
- keys, `BatchMode`) and are **read-only**. Results are cached to
386
- `~/.config/spinupwp-tui/stack-cache.json`, hydrated at startup, so detections
387
- survive restarts without re-running SSH.
263
+ The **Stacks** tab (`3`) classifies every site as Standard WP, Bedrock, or Non-WP
264
+ from API data alone, then lets you SSH-probe (`d` / `D`) for a precise ID (WHMCS,
265
+ Laravel, Static HTML, WordPress version) that overrides a mislabeled guess. Full
266
+ details: [docs/stack-detection.md](docs/stack-detection.md).
388
267
 
389
268
  ## Installed plugins & themes
390
269
 
391
- Press `p` on a site (Servers tab, sites pane) to list its installed plugins and
392
- themes over SSH — the `wp plugin list` / `wp theme list` detail the SpinupWP API
393
- never exposes (it only gives you *counts* of pending updates). You see every
394
- plugin and theme with its **status** (active / inactive / must-use / dropin),
395
- **current version**, and the **new version** when an update is available (`→ 1.2.3`
396
- in gold; `✓ current` otherwise), grouped into `PLUGINS` and `THEMES` sections each
397
- with an update badge.
398
-
399
- It's strictly **read-only**: it runs wp-cli as the site user using your local SSH
400
- keys (the same non-interactive auth the health view uses), and **detects the real
401
- WordPress directory itself** rather than trusting the `public_folder` setting — so
402
- it works on standard `/public/` installs and Bedrock (`web/wp`) alike, and even on
403
- sites SpinupWP misclassifies as non-WordPress (it finds WordPress core over SSH).
404
- Non-WordPress sites get a clear "no WordPress core found" message.
405
-
406
- `↑↓` / `jk` scroll, `r` re-reads over SSH, `Esc` / `q` / `p` closes.
270
+ `p` on a site (Servers tab) lists its real `wp plugin`/`theme list` over SSH —
271
+ status, version, and available updates — the detail the API only gives as bare
272
+ counts. Read-only, works on Bedrock and misclassified sites too. Full details:
273
+ [docs/plugins-themes.md](docs/plugins-themes.md).
407
274
 
408
275
  ## Upgrading PHP
409
276
 
410
- Press `u` on a selected site (in the **Servers** or **Stacks** tab) to change its
411
- PHP version. A picker lists the available versions — the current one is marked,
412
- end-of-life versions are flagged, and the list is sourced from the live PHP
413
- release schedule (so new versions like 8.5 appear automatically). After you
414
- confirm, the app calls `PUT /sites/{id}/php` and polls the resulting event until
415
- it finishes.
416
-
417
- - **Needs a Read/Write token.** SpinupWP exposes no token-scope endpoint, so a
418
- read-only token is detected when the upgrade comes back `403` — you'll get a
419
- clear "token is read-only" message and nothing changes. Swap in a Read/Write
420
- token (`spinuptui login`) to actually apply upgrades.
421
- - **On-demand install.** If the chosen version isn't installed on the server yet,
422
- SpinupWP installs it first; the event simply takes a little longer.
423
- - **Pending platform upgrade.** If the site's server has a pending SpinupWP
424
- platform upgrade, it can't be managed via the API until that runs — the picker
425
- is blocked and points you to open the server in the web app (`w`).
426
- - **Runs in the background.** The upgrade is tracked in the app's store, so you
427
- can press `Esc` to close the modal and it keeps going — the site's row shows a
428
- spinner and the target version (`→8.3`) until it settles, then refreshes to the
429
- new version (or flags `⬆!` if it failed). The SiteDetail "PHP" field shows the
430
- same in-progress state. You can launch upgrades on several sites at once. When an
431
- upgrade finishes, a **toast** confirms it (`example.com upgraded to PHP 8.3`) —
432
- useful since it often completes after you've closed the modal and moved on.
277
+ `u` on a site picks a new PHP version and applies it (`PUT /sites/{id}/php`),
278
+ tracking the event to completion in the background. Needs a Read/Write token.
279
+ Full details: [docs/php-upgrade.md](docs/php-upgrade.md).
433
280
 
434
281
  ## Server actions
435
282
 
436
- Press `a` on a selected server (in the **Servers** tab, or a result in **Search**)
437
- to open the server-actions overlay: **reboot** the server, or **restart** a single
438
- service (Nginx / PHP-FPM / MySQL / Redis). Pick → confirm → the app calls
439
- `POST /servers/{id}/reboot` or `/services/{svc}/restart` and tracks the event to
440
- completion — same background behavior as PHP upgrades (close the overlay and the
441
- server's row keeps a spinner).
442
-
443
- - **Needs a Read/Write token** (like PHP upgrades).
444
- - **Reboot visibility.** Servers with a pending reboot show a `↻rbt` badge in the
445
- Servers list and on the Dashboard's "Needs attention" panel.
446
- - **Why a reboot is pending.** The API only exposes a `reboot_required` boolean —
447
- no reason. So when you open the overlay on a flagged server, the app reads
448
- Ubuntu's `/var/run/reboot-required` + `.pkgs` over SSH (read-only, reusing the
449
- health view's connection) and shows the pending packages — typically a
450
- **kernel/security update**. This is labeled as OS-level context, not as
451
- SpinupWP's internal logic (a fleet-wide check confirmed the boolean tracks that
452
- file 1:1).
453
- - **Reboot is the big one** — its confirmation calls out that it takes the whole
454
- server down briefly (every site on it); a service restart is a brief blip.
455
- - **A toast on completion.** A reboot can take minutes; when it (or a restart)
456
- finishes, a non-focus-stealing toast tells you (`example.com rebooted` /
457
- `Nginx restarted on example.com`), so you don't have to keep checking.
283
+ `a` on a server reboots it or restarts a service (Nginx/PHP-FPM/MySQL/Redis),
284
+ and shows *why* a reboot is pending (the actual OS packages, read over SSH).
285
+ Needs a Read/Write token. Full details: [docs/server-actions.md](docs/server-actions.md).
458
286
 
459
287
  ## Site monitoring
460
288
 
461
- Everything hangs off one overlay: press `M` on a site in any view (the Servers
462
- tab also keeps its original `m`). It talks to your own
463
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) — Kuma watches 24/7 and
464
- sends the alerts; SpinupTUI creates the monitors *correctly* and diagnoses on
465
- demand.
466
-
467
- - **`a` — uptime monitors.** Regular sites get a homepage monitor (up/down +
468
- cert-expiry). Vanity sites get the full server treatment: healthz + load
469
- push + a **Redis sentinel** — the heartbeat cron runs `redis-cli ping` every
470
- minute and reports up/down to a `{server} redis` monitor (registered only
471
- after probing that Redis actually answers on that box). Why it matters: with
472
- SpinupWP's default object-cache drop-in, a dead Redis is **fatal (HTTP 500)
473
- for every request that misses the page cache** — cached pages keep serving
474
- 200, so only the sentinel notices, within a minute.
475
- - **`f` — the front-page check.** SpinupTUI reads your *healthy* front page,
476
- derives a template-identity fingerprint from WordPress's own `body_class()`
477
- output (`class="home`, `page-id-N`, …, falling back to the canonical link),
478
- validates it against a throwaway search render to prove it discriminates,
479
- and registers a Kuma keyword monitor at your chosen window (5m default —
480
- the check is served straight from the page cache, so it costs the site
481
- nothing). A red check means "answering 200 but serving the WRONG page" —
482
- the stale/corrupt-page-cache incident — and shows as `WRONG PAGE SERVED` in
483
- Details. Re-running `f` recalibrates the same monitor in place (history
484
- survives a redesign).
485
- - **`d` — the site doctor.** Read-only, zero setup, works without Kuma. It
486
- fetches the page twice — once normally, once with the cache-bypass cookie —
487
- and lets the server's `fastcgi-cache: HIT/BYPASS` headers prove which layer
488
- answered each request, then compares template identity between the two. It
489
- also knocks on the wp-admin door (the login page — never cached; Bedrock's
490
- relocated `/wp/` login handled). Verdicts: healthy · stale-cache (with a
491
- copyable runbook; the Elementor one-command flush is suggested only when
492
- Elementor is detected in the served markup) · **partial-outage** (cached
493
- pages 200, fresh renders or wp-admin 5xx — Redis or a PHP fatal; visitors
494
- look fine, admins are broken) · recalibrate (`f` jumps straight there) ·
495
- down · inconclusive.
496
- - **`n` — alert wiring.** SpinupTUI detects the notification providers configured
497
- in your Kuma (by name — Telegram, email, whatever you've set up) and shows
498
- whether each is attached to this site's monitors (`✓` all / `◐` some / `○`
499
- none); `⏎` toggles a provider across all of the site's checks at once.
500
- Creating the provider (bot tokens etc.) is the one step that stays in Kuma.
501
-
502
- The design principle across all of it: **the page cache makes sites look
503
- healthy while things break behind it** — every check watches either *through*
504
- the cache on purpose (is the right thing being served?) or deliberately
505
- *around* it (is the machinery behind it alive?).
289
+ `M` on a site opens the monitoring overlay — uptime/Redis monitors, a
290
+ template-identity front-page check, a cache/outage doctor, and Kuma alert
291
+ wiring, all against your own Uptime Kuma instance. Full details:
292
+ [docs/site-monitoring.md](docs/site-monitoring.md) ·
293
+ [docs/uptime-kuma.md](docs/uptime-kuma.md).
506
294
 
507
295
  ## Creating & connecting servers
508
296
 
509
- Two write actions on the **Servers** tab that stand up a new server and make it
510
- usable — both **need a Read/Write token**.
511
-
512
- **Create a server (`c`).** Opens a form pre-filled to **match** the selected
513
- server's provider, region, and size (or from scratch on an empty fleet). It prices
514
- the build from the provider's catalog (DigitalOcean / Vultr / Linode / Hetzner),
515
- suggests a hostname from your fleet's naming convention, and lets you switch
516
- provider (`p`), region (`g`), or size (`e`) and toggle backups (`b`) before
517
- confirming. Because the SpinupWP API can't list your configured server providers,
518
- the first time you build on a provider the overlay asks for its SpinupWP provider
519
- id (Account Settings → Server Providers) and saves it — once per provider. The
520
- build fires `POST /servers` and tracks the ~10-minute provision in the background.
521
-
522
- **Connect it with a vanity site (`V`).** A brand-new server has **no site**, so
523
- there's nothing to attach an SSH key to and no way for SpinupTUI to reach it — empty
524
- servers are flagged in **amber** in the Servers list. A busy server benefits from
525
- the same thing (a status page at its own hostname + a site user to hold your key),
526
- so `V` is offered on **any server that doesn't yet have a site at its own
527
- hostname** — it also appears under **Manage** in the server's Details panel. It
528
- builds the small placeholder ("vanity") site end to end:
529
-
530
- 1. **DNS** — writes an `A` record for the hostname → the server IP (AWS Route 53),
531
- using the connection from the DNS module.
532
- 2. **Propagate** — waits for the record to resolve (so Let's Encrypt can issue);
533
- after ~2 minutes it offers to skip SSL for now or keep waiting.
534
- 3. **Site** — creates a blank site (`installation_method: "blank"`).
535
- 4. **HTTPS** — enables a free Let's Encrypt certificate.
536
- 5. **SSH key** — deep-links you to the site's **SFTP & SSH → Site User** to add
537
- your key (the API can't do this), then waits for you to confirm.
538
- 6. **Publish** — seeds a minimal, brand-neutral status page (it reads its own
539
- hostname, so it's reusable on any server). Press `o` to open the live site.
540
-
541
- The whole build is a **resumable background job**: close the overlay and a header
542
- badge keeps tracking it; press `V` on the server to reopen it (even after the site
543
- exists). It survives quitting and relaunching the app. If a step fails, the overlay
544
- shows where, with `r` to retry or `x` to discard.
297
+ `c` on the Servers tab provisions a new server (priced from the provider
298
+ catalog); `V` connects any server with no hostname site end-to-end — DNS, a
299
+ placeholder site, HTTPS, and an SSH-key handoff. Needs a Read/Write token. Full
300
+ details: [docs/creating-connecting-servers.md](docs/creating-connecting-servers.md).
545
301
 
546
302
  ## Privileged writes over SSH (sudo & SSH keys)
547
303
 
548
- Some things the SpinupWP API simply can't do — it has **no** surface for SSH keys or
549
- sudo users. SpinupTUI does them directly over SSH instead, which means these actions use
550
- **your own SSH access**, not the API token.
551
-
552
- **Connect sudo (`S`).** Press `S` on a server and enter its SpinupWP **sudo user**
553
- and that user's **sudo password** once. SpinupTUI validates them against the live server
554
- (`sudo -S -p '' true`) and then holds the password **in memory for the session only**
555
- — the username persists to config, the password is never written to plaintext config.
556
- A connected server shows a green `● sudo` badge on its row; `S` again disconnects.
557
-
558
- - **Remember in the macOS Keychain (opt-in).** Tick the toggle when connecting and the
559
- password is saved to your **login Keychain** (service `spinup-sudo`, one item per
560
- server) — never to `config.json`, which only keeps the username and a `keychain`
561
- marker. Next time you press `S` on that server, sudo **auto-unlocks** with no
562
- retyping (the first read may show macOS's own "allow access" prompt — choose Always
563
- Allow). Press `f` to forget the saved password; disconnecting (`x`) a saved server
564
- offers a no-password **reconnect** rather than the credential form. Off macOS the
565
- toggle is absent and sudo stays in-memory per session.
566
-
567
- **Grant / revoke an SSH key (`K`).** With sudo connected, press `K` on a site to write
568
- keys into the site user's `authorized_keys`:
569
-
570
- - **Pick which keys** — any of **your personal keys** (discovered from `~/.ssh/*.pub`
571
- and your ssh-agent, so you can SSH/SFTP as yourself) and/or SpinupTUI's dedicated
572
- **`spinup-tui` machine key** (an ed25519 identity generated once into the config dir,
573
- deliberately **never added to your SpinupWP account** so SpinupWP's key reconciliation
574
- can't clobber it). Your selection is remembered.
575
- - **Pick the scope** — just this site, or **every site on the server** in one pass
576
- (per-site progress, with a retry for any that fail).
577
- - **Grant or remove** — `a`/`r` toggles the mode; removing pulls exactly the chosen
578
- key lines and leaves every other key (including SpinupWP-managed ones) untouched. The
579
- remote script is **idempotent** and a confirm overlay shows the exact command first.
580
- - Site rows show what's granted at a glance — **👤** (your key) and/or **🔑** (the
581
- machine key).
304
+ The API can't manage SSH keys or sudo users, so SpinupTUI does it directly:
305
+ `S` connects sudo on a server (optionally remembered in the macOS Keychain),
306
+ then `K` grants or revokes an SSH key on one site or the whole server. Full
307
+ details: [docs/privileged-ssh-writes.md](docs/privileged-ssh-writes.md).
582
308
 
583
309
  ## Cloning a server
584
310
 
585
- Press **`C`** on a server to clone one or more of its sites onto a **new or existing**
586
- destination server — a guided, two-pane wizard that lets you stage and verify a whole
587
- migration and **only repoints DNS when you say so**. It needs a **Read/Write token**
588
- (to create the destination sites) and **sudo connected on both ends** (the copy runs
589
- over SSH). The steps:
590
-
591
- 1. **Plan** — pick which of the source's sites to clone (all selected by default;
592
- `space` toggles). SpinupTUI sizes each one live (disk + database) into a payload total
593
- so you know what you're moving; a concurrency cap protects the busy source.
594
- 2. **Destination** — provision a fresh server pre-matched to the source (reusing the
595
- `c` flow), or `d` to pick an existing server as the target.
596
- 3. **Connect** — connect sudo on **both** servers. The clone is a server-to-server
597
- **pull**: the destination pulls each site directly from the source over SSH (no
598
- bytes routed through your laptop), authenticating with a key granted onto the source
599
- for the job and **revoked when it's done**.
600
- 4. **Git access** (only when a Bedrock site is selected) — each repo gets its **own
601
- read-only deploy key**, generated locally and never persisted: the public half
602
- goes on the repo (added for you via `gh` when it's installed and authed, or shown
603
- for a manual add — `o` opens the repo's deploy-key settings, `y` copies the key),
604
- and the pair rides the site create so SpinupWP installs it as the new site's git
605
- identity. Per-site keys are what let any number of Bedrock repos land on one
606
- server — GitHub allows a deploy key on only **one** repository account-wide, so a
607
- shared server key stops working at the second repo.
608
- 5. **Clone sites** — a live roster runs the sites concurrently, each advancing
609
- `create → pull → config → verify → done` with **live transfer progress** (bytes,
610
- rate, elapsed; database pulls show a true percent). Three stacks are handled:
611
- **Standard WP** (files + database, with `wp-config` re-stamped for the
612
- destination), **Bedrock** (git-native — created from the repo, `composer install`
613
- over SSH, uploads + secrets pulled, `.env` re-stamped), and **files-only** for
614
- non-WordPress sites (redirect shells, static/PHP — opt-in in Plan, no database).
615
- The pull **detects each site's real webroot** rather than trusting settings —
616
- `public/`-style layouts (with `wp-config.php` one level above the webroot) are
617
- preserved, and mid-move layouts are normalized on the destination. **Additional
618
- domains carry over** automatically (with their redirect settings), so the clone
619
- answers for every hostname the source did.
620
- 6. **Verify** — drill into any cloned site for a source-vs-clone comparison (wp-cli
621
- facts + an HTTP check that hits the **new** server while DNS still points at the old
622
- one; files-only sites compare file count, size, and HTTP instead).
623
- 7. **DNS cutover** — the wizard **waits for your explicit go** (`c`) after the roster
624
- settles, then repoints `A` records across each site's domains (apex + additional)
625
- to the new server in one batched, partial-aware pass; `↑↓`/`space` include or
626
- exclude individual records first. `www`-style records that follow the apex are
627
- skipped, not clobbered. Cloudflare **proxied** records repoint automatically too
628
- (their origin IP is always PATCHable even though their TTL stays fixed to
629
- automatic). Records in zones you can't edit show your DNS access note and `⏎`
630
- opens that zone's registrar console with the zone name copied — ready to paste.
631
-
632
- The clone runs in the **background** — pressing `Esc` doesn't abandon it; a header
633
- badge (`⠹ Cloning … — press C`) surfaces the in-flight job and `C` reopens the live
634
- roster. `←` (or `h`) steps **back a screen**: the setup steps go back freely, and
635
- once cloning has started the one back edge is DNS cutover → the clone roster — so
636
- you can re-verify or retry a site before flipping live traffic. **Every job writes a full log** (`~/.config/spinupwp-tui/logs/`, passwords
637
- redacted, self-pruning) — `⏎` on a failed site shows the complete error and `r`
638
- retries just that site. Pairs naturally with the DNS module: **lower the TTLs**
639
- (`n`/`N` → `⏎`) a day or two ahead so the cutover propagates fast.
311
+ `C` on a server opens a guided wizard that plans, sizes, and clones one or more
312
+ sites (Standard WP, Bedrock, or files-only) to a new or existing destination
313
+ over SSH, verifies each clone, and only repoints DNS when you approve it — all
314
+ in the background. Needs a Read/Write token + sudo on both ends. Full details:
315
+ [docs/cloning-a-server.md](docs/cloning-a-server.md) ·
316
+ [how it works](docs/clone-wizard-explained.md).
640
317
 
641
318
  ## Local working copies
642
319
 
643
- Bridge your SpinupWP sites to the local checkouts you actually edit. Press `L` on
644
- a site (Servers / Stacks / Search) to link a path and the local URL where you
645
- serve it; the site's details gain a "Local" field, and you can open the copy with
646
- `t` (a terminal at the path) or `v` (its local URL). All of this is **local-only**
647
- — no SpinupWP writes.
648
-
649
- - **Auto-discover (`S`, Stacks tab).** Scan one or more folders and match their
650
- subdirectories to sites — by git remote, Bedrock `WP_HOME`, or folder name —
651
- then batch-link the matches.
652
- - **"Needs a local copy" report (`f`, Stacks tab).** Lists the managed sites you
653
- have no usable local copy for (never linked, or a missing path), filterable by
654
- stack.
655
- - **Markers & drift.** Linked sites show `◆` in the lists; a linked, on-disk copy
656
- shows its local git drift (`⇡N unpushed` / `● uncommitted`), read from the repo
657
- with no network.
658
-
659
- Config keys: `localRoots` (folders to scan) and `localSites` (per-site path +
660
- local URL — tool-agnostic: Valet, Cove, LocalWP, Herd, DDEV, …).
320
+ `L` on a site links its local checkout (path + local URL); `t`/`v` open it. The
321
+ Stacks tab can auto-discover copies (`S`) and report sites still missing one
322
+ (`f`); linked copies show their local git drift. Full details:
323
+ [docs/local-working-copies.md](docs/local-working-copies.md).
661
324
 
662
325
  ## Database backup & sync
663
326
 
664
- For a WordPress site you've **linked** to a local copy, the Search tab can pull
665
- the production database down — the same idea as a hand-rolled `wp db export` +
666
- `scp`, without leaving the dashboard. Both actions are **read-only on production**
667
- (they export; they never write to the live site) and run `ssh`/`scp`
668
- non-interactively, so your key needs to be loaded in your agent.
669
-
670
- - **Download a backup (`d`).** Exports the production database with `wp-cli` into
671
- a stage file *outside* the public webroot, gzips it, downloads it into the linked
672
- copy's `sql/` folder, and removes the remote copy. Needs **no local WP-CLI** —
673
- the export runs on the server. Available whenever a WordPress site is linked. A
674
- spinner on the site's row tracks an in-flight download even if you close the
675
- overlay; the saved path and size are shown on completion.
676
- - **Pull production → local (`p`, opt-in).** A full refresh of your **local**
677
- database from production: it backs up the local DB first (to
678
- `sql/local_<timestamp>.sql.gz`), exports + downloads production, imports it
679
- locally, rewrites production URLs → your local URL (`wp search-replace`), and
680
- runs an optional `bin/sync.d/post-import.sh` hook if the project has one.
681
- **This overwrites your local database**, so it's **off by default** — enable it
682
- with `localSync` (see "Optional settings"). It needs a working local WP-CLI; if
683
- it's missing you get a clear error rather than a broken run.
684
-
685
- Everything is detected automatically, for Standard WP **and** Bedrock:
686
-
687
- - the **remote document root** from the API (`/sites/{domain}/files{public_folder}`),
688
- - the **SSH target** from the site/server (`{site_user}@{server_ip}`),
689
- - the **local WordPress root** from the linked path (where `wp` runs — wp-config
690
- for Standard WP, `wp-cli.yml` for Bedrock),
691
- - the **local URL** for the rewrite from the link's local URL, falling back to the
692
- project's `.env` `WP_HOME`.
693
-
694
- If your project already has a `bin/sync.d/post-import.sh` (e.g. Elementor URL
695
- swaps, plugin toggles), it runs with `WEB_DIR`, `SYNC_REMOTE_HOST`, and
696
- `SYNC_LOCAL_HOST` set — so existing per-project tweaks carry over with no extra
697
- configuration. When a project has **no** hook yet, the `p` confirm screen
698
- explains what the hook is and offers `s` to scaffold an **inert** sample
699
- `bin/sync.d/post-import.sh` (every example commented out, documented with the env
700
- vars above) for you to edit. Backups stay **gzipped** in `sql/`; decompress with
701
- `gunzip` when you need one.
702
-
703
- Both `d` and `p` show their progress as a **building checklist** inside the
704
- overlay — each step gets a `✓` as it completes, the running step spins, and a
705
- failure marks the exact step with `✕` — so you can see everything that happened,
706
- ending with the saved paths. It keeps running if you close the overlay (`Esc`);
707
- reopen with the same key to watch it through.
327
+ On a linked WordPress site (Search tab), `d` downloads a gzipped production DB
328
+ backup into `sql/`; `p` (opt-in via `localSync`) pulls production into your
329
+ **local** database, backing it up first and rewriting URLs. Works with
330
+ Standard WP and Bedrock. Full details:
331
+ [docs/database-backup-sync.md](docs/database-backup-sync.md).
708
332
 
709
333
  ## Production media fallback
710
334
 
711
- A `p` sync refreshes your local **database**, but not the media library — so the
712
- local site shows broken images. Syncing the files is often impractical (some
713
- libraries are hundreds of thousands of items). Instead, press **`m`** on a linked
714
- WordPress site to serve any image that's **missing locally** straight from
715
- production.
716
-
717
- It works by dropping a small, self-contained mu-plugin into your local copy
718
- (`wp-content/mu-plugins/` or Bedrock's `web/app/mu-plugins/`). The plugin rewrites
719
- any uploads URL whose file isn't present locally to the production origin —
720
- covering the standard media functions, page-builder output (e.g. Elementor inline
721
- CSS and gallery data), and **legacy paths** left over from a Standard-WP →
722
- Bedrock conversion. Because it decides "missing" from the real document root and
723
- redirects to the **same path** on production, your production routing (CDN/S3
724
- redirects included) resolves whatever it's handed.
725
-
726
- - **It runs in WordPress, not your web server**, so it works the same on Valet,
727
- Herd, LocalWP, DDEV, MAMP — anything. No nginx/Apache config to get right.
728
- - **Local-only and read-only on production** — it just hotlinks your own
729
- production images. It self-disables when running on the production domain, so
730
- it's inert if it ever gets deployed.
731
- - **The plugin's presence is the on/off state** (no config flag). Press `m` to
732
- toggle; `u` from the overlay updates it in place when a newer version ships.
733
- - Needs production reachable and hotlinking allowed (it won't work behind staging
734
- Basic-Auth). Images delivered by external CSS files or async JS/REST are the one
735
- thing server-side rewriting can't catch.
335
+ `m` on a linked WordPress site drops a small mu-plugin that serves any
336
+ missing-locally upload straight from production — no file copying, works on
337
+ any local stack. Full details:
338
+ [docs/production-media-fallback.md](docs/production-media-fallback.md).
736
339
 
737
340
  ## DNS hosts, access & editing
738
341
 
739
- A **server-migration lens** for DNS: see the records that move a site to another
740
- server, and edit them in place. It is deliberately **not** a full zone editor — it
741
- only ever shows and touches a site's own hosting records (its apex / `www` /
742
- subdomains and additional domains), so your MX, TXT, DKIM, and other zone records are
743
- never shown or changed. Moving a site can't take down its email.
744
-
745
- - **The view.** Press `n` on a site for just that site's records, or `N` on a server
746
- for every site on it; inside a site-scoped view, `a` expands to the whole server.
747
- Each **site** is a line, labeled by its own domain (even when it's a subdomain),
748
- with its hosting record's type, **TTL in seconds**, value, a `◀ here` flag when the
749
- record points at this server, and a `+www` tag when `www` simply follows the apex.
750
- A site's additional domains nest beneath it, so a domain portfolio reads as one
751
- site, not many. TTLs come from the zone's authoritative nameserver (the configured
752
- value, not a counted-down one), so they show even for hosts you haven't connected;
753
- `r` refreshes.
754
- - **Access (`✓ ↗ ○ ·`).** Each record's zone shows whether you can edit it: `✓`
755
- editable, `↗` web-only handoff, `○` the provider has an API but you haven't
756
- connected an account that holds the zone, `·` unknown. A zone is `✓` only when a
757
- connected account of the provider that actually serves it (its live nameservers)
758
- holds it — so a stale or duplicate zone elsewhere never shows a false green. With
759
- two or more accounts connected, an **ACCOUNT** column names the owning one.
760
- - **Edit a TTL (`⏎`).** On an editable record, `⏎` opens a focused editor — pick a
761
- preset or a custom value, confirm, and it's written to **AWS Route 53** or
762
- **Cloudflare** through your connected account. This is the prep step for a low-risk
763
- migration: lower the TTL, cut over, then restore it. Before writing, an edit-time
764
- check re-reads the live nameservers and **blocks** the change if the connected
765
- account's zone isn't the one actually serving the domain. Route 53 changes are
766
- followed to completion; the record shows an "updating" status that keeps ticking
767
- even if you leave the view. Cloudflare **proxied** records keep their TTL fixed
768
- to automatic, so it's not editable here — repointing still works (next).
769
- - **Repoint a record (`p`).** The same focused editor, on the record's **value** —
770
- where it points. The picker leads with **your SpinupWP servers** (name + IP, the
771
- record's current home tagged), because pointing a record at one of your own boxes
772
- is what this is for; a custom IP is the fallback. The same confirm and NS
773
- pre-flight gate apply, the write is followed to completion in the background, and
774
- the inventory row shows `→ new IP` while it applies. A Cloudflare **proxied**
775
- record repoints its **origin** behind the proxy (visitors keep resolving
776
- Cloudflare's IPs). CNAMEs aren't repointed — they follow their target, which is
777
- the record to edit. This is the standalone version of the clone wizard's DNS
778
- cutover: migrate a single site, finish a cutover by hand, or fix a stale record.
779
- - **Connect a provider (`c`).** Manage credentials for the selected zone's
780
- provider — **AWS Route 53** (an IAM access key), **Cloudflare** (a scoped token),
781
- or **GoDaddy** (a Production API key). Multiple accounts per provider are
782
- supported, with a drill-down into each account's zones. Credentials are verified
783
- before they're stored, kept in `config.json` (chmod 600), and the matching
784
- environment variables are honored (`CLOUDFLARE_API_TOKEN`, `AWS_ACCESS_KEY_ID` /
785
- `AWS_SECRET_ACCESS_KEY`, `GODADDY_API_KEY` / `GODADDY_API_SECRET`). Secrets are
786
- masked as you type. Listing hosts needs only read access (Cloudflare `Zone:Read`);
787
- **editing (a TTL or a repoint)** needs write access — Route 53 record writes, or a
788
- Cloudflare `Zone.DNS:Edit` token.
789
- - **Web-only hosts (GoDaddy, Namecheap, Network Solutions, …).** A registrar with
790
- no usable API shows `↗`; press `w` (in the inventory or the connect overlay) to
791
- open its web console — for GoDaddy specifically, your Clients hub, with the
792
- domain copied to your clipboard so you can paste it after logging in as the
793
- client. Every `↗` zone also shows an **access note** — "Delegate Access" by
794
- default (the assumed-normal case for a client's registrar), or your own
795
- per-zone override (e.g. an IT vendor's contact) when that default doesn't hold.
796
- Press `c` on any such zone to open **Manage Access**: it lists every zone
797
- already known at that host, `n` edits the selected zone's note, and `r`
798
- resolves your whole fleet's DNS to fill in any zones it hasn't seen yet.
799
-
800
- Provider credentials are optional — without them you still get the full host
801
- inventory and TTLs, just without the editable/account columns or in-place editing.
342
+ `n` (site) / `N` (server) show only a site's own hosting records — never
343
+ MX/TXT/other zone records — with TTLs and edit access. `⏎` edits a TTL, `p`
344
+ repoints a record to another of your servers (AWS Route 53 / Cloudflare). Full
345
+ details: [docs/dns-access-editing.md](docs/dns-access-editing.md).
802
346
 
803
347
  ## Development
804
348
 
@@ -813,15 +357,17 @@ bun run typecheck # tsc --noEmit
813
357
  SPINUP_DEV_MODE=1 bun run dev
814
358
  ```
815
359
 
816
- Boots straight into the dashboard against a small in-memory example fleet
817
- (`web1.example.com` … `web9.example.org`, a mix of Standard WP, Bedrock, and
818
- non-WP sites) — no API token, no network calls, nothing that can touch a real
819
- account. A purple **`DEV MODE`** badge in the header makes it unmistakable. Every
820
- write action (PHP upgrade, HTTPS toggle, purge cache, reboot, create a server/site)
821
- works against the fake data and shows the same in-progress/toast behavior as the
822
- real thing, so it's useful for screenshots, walkthroughs, and UI work without a
823
- live account on hand. The clone wizard's SSH-based file/DB pull is out of scope —
824
- it always talks to real servers over SSH, independent of the API client.
360
+ Boots straight into the dashboard against an in-memory example fleet (12
361
+ servers, ~80 sites — a deterministic mix of Standard WP, Bedrock, and non-WP
362
+ sites, EOL Ubuntu/PHP versions, and pending updates) — no API token, no network
363
+ calls, nothing that can touch a real account. A purple **`DEV MODE`** badge in
364
+ the header makes it unmistakable. Every write action (PHP upgrade, HTTPS
365
+ toggle, purge cache, reboot, create a server/site) works against the fake data
366
+ and shows the same in-progress/toast behavior as the real thing, so it's
367
+ useful for screenshots (the ones above were generated this way), walkthroughs,
368
+ and UI work without a live account on hand. The clone wizard's SSH-based
369
+ file/DB pull is out of scope — it always talks to real servers over SSH,
370
+ independent of the API client.
825
371
 
826
372
  The fixture fleet lives in `src/dev/fixtures.ts`; the fake client it's served
827
373
  through is `src/dev/mockClient.ts`.