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/README.md CHANGED
@@ -11,162 +11,77 @@
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
+ - **Site & server monitoring** (`m`) — a two-pane browser of a site's Uptime
69
+ Kuma monitors (health, front-page fingerprint, an opt-in cache-bypass
70
+ check, and, for vanity/server sites, load/Redis/PHP-fatal sentinels), each
71
+ with a live status dot, an in-context explanation, and one action key to
72
+ register/recalibrate/remove it — plus a cache/outage doctor and alert
73
+ wiring. → [docs/site-monitoring.md](docs/site-monitoring.md) ·
74
+ [docs/uptime-kuma.md](docs/uptime-kuma.md)
75
+ - **Non-interactive CLI subcommands for external tooling** — `spinuptui ssh <domain>`
76
+ resolves a domain to a live SSH target with a live connectivity probe;
77
+ `spinuptui incidents <domain>` / `--all` surfaces Uptime Kuma's down/up
78
+ history as JSON. Built so another agent/script can go from just a domain to
79
+ "what's wrong and how do I get in" with no manual lookup.
80
+ → [CLI subcommands](#cli-subcommands)
81
+ - **Completion toasts** — a non-focus-stealing toast when a background write
82
+ (PHP upgrade, reboot, DNS resolve, …) finishes.
83
+ - **Release notes** — an in-app "what's new" after every update, sourced
84
+ straight from that version's GitHub release.
170
85
 
171
86
  > The tool is **read-only by default** and works great with a Read Only API
172
87
  > token. The write actions — creating a server, connecting it with a vanity site,
@@ -248,6 +163,15 @@ to the version in the header (and in the `?` About panel).
248
163
  spinuptui Launch the dashboard
249
164
  spinuptui login Set or update your saved API token
250
165
  spinuptui where Show the config path and which source the token came from
166
+ spinuptui ssh <domain> Print SSH access info for a site as JSON (resolves the
167
+ site's server, builds its SSH target, and runs a live
168
+ connectivity probe — for external tooling, e.g. an
169
+ incident-diagnostics agent handed only a domain)
170
+ spinuptui incidents <domain> | --all [--hours N] Print Uptime Kuma down/up
171
+ incidents as JSON, scoped to sites SpinupTUI manages
172
+ monitoring for (config.json's kumaMonitors) — --all
173
+ sweeps every such site in one Kuma connection, --hours
174
+ sets the lookback window (default 24)
251
175
  spinuptui --version Print the version
252
176
  spinuptui --help Show help
253
177
  ```
@@ -324,8 +248,7 @@ These can be set in `config.json` or via an environment variable:
324
248
  | `u` | Upgrade a site's PHP version (Servers / Stacks / Search; needs a Read/Write token) |
325
249
  | `H` | Enable / disable HTTPS on a site (Servers / Stacks / Search; needs a Read/Write token) |
326
250
  | `P` | Purge a site's page cache + object cache (Servers / Stacks / Search; needs a Read/Write token) |
327
- | `m` | Site monitoring via Uptime Kuma (Servers tab, sites pane — same overlay as `M`) |
328
- | `M` | Site monitoring overlay — add monitors, `f` front-page check, `d` site doctor, `n` alert wiring, vanity refresh/rotation (Servers / Stacks / Search) |
251
+ | `m` / `M` | Site/server monitoring — a two-pane browser of this site's Uptime Kuma monitors (`M` is an alias, since lowercase `m` is taken in Search/Stacks). Inside: `↑`/`↓` selects a monitor, `a` registers/recalibrates/repairs it (Front page and Cache bypass open a check-window picker), `x` removes Front page or Cache bypass, `o` opens the selected monitor in Kuma, `d` runs the site doctor, `n` shows/edits alert wiring, and vanity sites add `R`/`r` for page refresh/secret rotation (Servers / Stacks / Search) |
329
252
  | `a` | Server actions: reboot / restart a service (Servers / Search; needs a Read/Write token) |
330
253
  | `c` | Create a new server (Servers tab; needs a Read/Write token) |
331
254
  | `V` | Add a vanity site at the server's own hostname — DNS + site + HTTPS + SSH-key handoff (Servers tab; offered when no hostname site exists; needs a Read/Write token) |
@@ -346,460 +269,98 @@ search box.
346
269
 
347
270
  ## Server health (SSH)
348
271
 
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.
272
+ `h` on a server opens a real-time SSH view — CPU, load, memory/swap, disk, top
273
+ processes — read-only, using your local SSH keys. Full details:
274
+ [docs/server-health.md](docs/server-health.md).
364
275
 
365
276
  ## Stack detection
366
277
 
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.
278
+ The **Stacks** tab (`3`) classifies every site as Standard WP, Bedrock, or Non-WP
279
+ from API data alone, then lets you SSH-probe (`d` / `D`) for a precise ID (WHMCS,
280
+ Laravel, Static HTML, WordPress version) that overrides a mislabeled guess. Full
281
+ details: [docs/stack-detection.md](docs/stack-detection.md).
388
282
 
389
283
  ## Installed plugins & themes
390
284
 
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.
285
+ `p` on a site (Servers tab) lists its real `wp plugin`/`theme list` over SSH —
286
+ status, version, and available updates — the detail the API only gives as bare
287
+ counts. Read-only, works on Bedrock and misclassified sites too. Full details:
288
+ [docs/plugins-themes.md](docs/plugins-themes.md).
407
289
 
408
290
  ## Upgrading PHP
409
291
 
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.
292
+ `u` on a site picks a new PHP version and applies it (`PUT /sites/{id}/php`),
293
+ tracking the event to completion in the background. Needs a Read/Write token.
294
+ Full details: [docs/php-upgrade.md](docs/php-upgrade.md).
433
295
 
434
296
  ## Server actions
435
297
 
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.
298
+ `a` on a server reboots it or restarts a service (Nginx/PHP-FPM/MySQL/Redis),
299
+ and shows *why* a reboot is pending (the actual OS packages, read over SSH).
300
+ Needs a Read/Write token. Full details: [docs/server-actions.md](docs/server-actions.md).
458
301
 
459
302
  ## Site monitoring
460
303
 
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?).
304
+ `m` on a site opens a full-screen two-pane monitor browser — health,
305
+ front-page fingerprint, an opt-in cache-bypass check, and (vanity/server
306
+ sites) load/Redis/PHP-fatal sentinels, each with live status, an in-context
307
+ explanation, and a single action key to register/recalibrate/remove it —
308
+ plus a cache/outage doctor and Kuma alert wiring, all against your own
309
+ Uptime Kuma instance. Full details:
310
+ [docs/site-monitoring.md](docs/site-monitoring.md) ·
311
+ [docs/uptime-kuma.md](docs/uptime-kuma.md).
506
312
 
507
313
  ## Creating & connecting servers
508
314
 
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.
315
+ `c` on the Servers tab provisions a new server (priced from the provider
316
+ catalog); `V` connects any server with no hostname site end-to-end — DNS, a
317
+ placeholder site, HTTPS, and an SSH-key handoff. Needs a Read/Write token. Full
318
+ details: [docs/creating-connecting-servers.md](docs/creating-connecting-servers.md).
545
319
 
546
320
  ## Privileged writes over SSH (sudo & SSH keys)
547
321
 
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).
322
+ The API can't manage SSH keys or sudo users, so SpinupTUI does it directly:
323
+ `S` connects sudo on a server (optionally remembered in the macOS Keychain),
324
+ then `K` grants or revokes an SSH key on one site or the whole server. Full
325
+ details: [docs/privileged-ssh-writes.md](docs/privileged-ssh-writes.md).
582
326
 
583
327
  ## Cloning a server
584
328
 
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 — each listed with its
596
- current site count, so you can see at a glance how busy a candidate already is.
597
- 3. **Connect** — connect sudo on **both** servers. The clone is a server-to-server
598
- **pull**: the destination pulls each site directly from the source over SSH (no
599
- bytes routed through your laptop), authenticating with a key granted onto the source
600
- for the job and **revoked when it's done**.
601
- 4. **Git access** (only when a Bedrock site is selected) — each repo gets its **own
602
- read-only deploy key**, generated locally and never persisted: the public half
603
- goes on the repo (added for you via `gh` when it's installed and authed, or shown
604
- for a manual add — `o` opens the repo's deploy-key settings, `y` copies the key),
605
- and the pair rides the site create so SpinupWP installs it as the new site's git
606
- identity. Per-site keys are what let any number of Bedrock repos land on one
607
- server — GitHub allows a deploy key on only **one** repository account-wide, so a
608
- shared server key stops working at the second repo.
609
- 5. **Clone sites** — a live roster runs the sites concurrently, each advancing
610
- `create → pull → config → verify → done` with **live transfer progress** (bytes,
611
- rate, elapsed; database pulls show a true percent). Three stacks are handled:
612
- **Standard WP** (files + database, with `wp-config` re-stamped for the
613
- destination), **Bedrock** (git-native — created from the repo, `composer install`
614
- over SSH, uploads + secrets pulled, `.env` re-stamped), and **files-only** for
615
- non-WordPress sites (redirect shells, static/PHP — opt-in in Plan, no database).
616
- The pull **detects each site's real webroot** rather than trusting settings —
617
- `public/`-style layouts (with `wp-config.php` one level above the webroot) are
618
- preserved, and mid-move layouts are normalized on the destination. **Additional
619
- domains carry over** automatically (with their redirect settings), so the clone
620
- answers for every hostname the source did.
621
- 6. **Verify** — drill into any cloned site for a source-vs-clone comparison (wp-cli
622
- facts + an HTTP check that hits the **new** server while DNS still points at the old
623
- one; files-only sites compare file count, size, and HTTP instead).
624
- 7. **DNS cutover** — the wizard **waits for your explicit go** (`c`) after the roster
625
- settles, then repoints `A` records across each site's domains (apex + additional)
626
- to the new server in one batched, partial-aware pass; `↑↓`/`space` include or
627
- exclude individual records first. `www`-style records that follow the apex are
628
- skipped, not clobbered. Cloudflare **proxied** records repoint automatically too
629
- (their origin IP is always PATCHable even though their TTL stays fixed to
630
- automatic). Records in zones you can't edit show your DNS access note and `⏎`
631
- opens that zone's registrar console with the zone name copied — ready to paste.
632
-
633
- The clone runs in the **background** — pressing `Esc` doesn't abandon it; a header
634
- badge (`⠹ Cloning … — press C`) surfaces the in-flight job and `C` reopens the live
635
- roster. `←` (or `h`) steps **back a screen**: the setup steps go back freely, and
636
- once cloning has started the one back edge is DNS cutover → the clone roster — so
637
- you can re-verify or retry a site before flipping live traffic. **Every job writes a full log** (`~/.config/spinupwp-tui/logs/`, passwords
638
- redacted, self-pruning) — `⏎` on a failed site shows the complete error and `r`
639
- retries just that site. Pairs naturally with the DNS module: **lower the TTLs**
640
- (`n`/`N` → `⏎`) a day or two ahead so the cutover propagates fast.
329
+ `C` on a server opens a guided wizard that plans, sizes, and clones one or more
330
+ sites (Standard WP, Bedrock, or files-only) to a new or existing destination
331
+ over SSH, verifies each clone, and only repoints DNS when you approve it — all
332
+ in the background. Needs a Read/Write token + sudo on both ends. Full details:
333
+ [docs/cloning-a-server.md](docs/cloning-a-server.md) ·
334
+ [how it works](docs/clone-wizard-explained.md).
641
335
 
642
336
  ## Local working copies
643
337
 
644
- Bridge your SpinupWP sites to the local checkouts you actually edit. Press `L` on
645
- a site (Servers / Stacks / Search) to link a path and the local URL where you
646
- serve it; the site's details gain a "Local" field, and you can open the copy with
647
- `t` (a terminal at the path) or `v` (its local URL). All of this is **local-only**
648
- — no SpinupWP writes.
649
-
650
- - **Auto-discover (`S`, Stacks tab).** Scan one or more folders and match their
651
- subdirectories to sites — by git remote, Bedrock `WP_HOME`, or folder name —
652
- then batch-link the matches.
653
- - **"Needs a local copy" report (`f`, Stacks tab).** Lists the managed sites you
654
- have no usable local copy for (never linked, or a missing path), filterable by
655
- stack.
656
- - **Markers & drift.** Linked sites show `◆` in the lists; a linked, on-disk copy
657
- shows its local git drift (`⇡N unpushed` / `● uncommitted`), read from the repo
658
- with no network.
659
-
660
- Config keys: `localRoots` (folders to scan) and `localSites` (per-site path +
661
- local URL — tool-agnostic: Valet, Cove, LocalWP, Herd, DDEV, …).
338
+ `L` on a site links its local checkout (path + local URL); `t`/`v` open it. The
339
+ Stacks tab can auto-discover copies (`S`) and report sites still missing one
340
+ (`f`); linked copies show their local git drift. Full details:
341
+ [docs/local-working-copies.md](docs/local-working-copies.md).
662
342
 
663
343
  ## Database backup & sync
664
344
 
665
- For a WordPress site you've **linked** to a local copy, the Search tab can pull
666
- the production database down — the same idea as a hand-rolled `wp db export` +
667
- `scp`, without leaving the dashboard. Both actions are **read-only on production**
668
- (they export; they never write to the live site) and run `ssh`/`scp`
669
- non-interactively, so your key needs to be loaded in your agent.
670
-
671
- - **Download a backup (`d`).** Exports the production database with `wp-cli` into
672
- a stage file *outside* the public webroot, gzips it, downloads it into the linked
673
- copy's `sql/` folder, and removes the remote copy. Needs **no local WP-CLI** —
674
- the export runs on the server. Available whenever a WordPress site is linked. A
675
- spinner on the site's row tracks an in-flight download even if you close the
676
- overlay; the saved path and size are shown on completion.
677
- - **Pull production → local (`p`, opt-in).** A full refresh of your **local**
678
- database from production: it backs up the local DB first (to
679
- `sql/local_<timestamp>.sql.gz`), exports + downloads production, imports it
680
- locally, rewrites production URLs → your local URL (`wp search-replace`), and
681
- runs an optional `bin/sync.d/post-import.sh` hook if the project has one.
682
- **This overwrites your local database**, so it's **off by default** — enable it
683
- with `localSync` (see "Optional settings"). It needs a working local WP-CLI; if
684
- it's missing you get a clear error rather than a broken run.
685
-
686
- Everything is detected automatically, for Standard WP **and** Bedrock:
687
-
688
- - the **remote document root** from the API (`/sites/{domain}/files{public_folder}`),
689
- - the **SSH target** from the site/server (`{site_user}@{server_ip}`),
690
- - the **local WordPress root** from the linked path (where `wp` runs — wp-config
691
- for Standard WP, `wp-cli.yml` for Bedrock),
692
- - the **local URL** for the rewrite from the link's local URL, falling back to the
693
- project's `.env` `WP_HOME`.
694
-
695
- If your project already has a `bin/sync.d/post-import.sh` (e.g. Elementor URL
696
- swaps, plugin toggles), it runs with `WEB_DIR`, `SYNC_REMOTE_HOST`, and
697
- `SYNC_LOCAL_HOST` set — so existing per-project tweaks carry over with no extra
698
- configuration. When a project has **no** hook yet, the `p` confirm screen
699
- explains what the hook is and offers `s` to scaffold an **inert** sample
700
- `bin/sync.d/post-import.sh` (every example commented out, documented with the env
701
- vars above) for you to edit. Backups stay **gzipped** in `sql/`; decompress with
702
- `gunzip` when you need one.
703
-
704
- Both `d` and `p` show their progress as a **building checklist** inside the
705
- overlay — each step gets a `✓` as it completes, the running step spins, and a
706
- failure marks the exact step with `✕` — so you can see everything that happened,
707
- ending with the saved paths. It keeps running if you close the overlay (`Esc`);
708
- reopen with the same key to watch it through.
345
+ On a linked WordPress site (Search tab), `d` downloads a gzipped production DB
346
+ backup into `sql/`; `p` (opt-in via `localSync`) pulls production into your
347
+ **local** database, backing it up first and rewriting URLs. Works with
348
+ Standard WP and Bedrock. Full details:
349
+ [docs/database-backup-sync.md](docs/database-backup-sync.md).
709
350
 
710
351
  ## Production media fallback
711
352
 
712
- A `p` sync refreshes your local **database**, but not the media library — so the
713
- local site shows broken images. Syncing the files is often impractical (some
714
- libraries are hundreds of thousands of items). Instead, press **`m`** on a linked
715
- WordPress site to serve any image that's **missing locally** straight from
716
- production.
717
-
718
- It works by dropping a small, self-contained mu-plugin into your local copy
719
- (`wp-content/mu-plugins/` or Bedrock's `web/app/mu-plugins/`). The plugin rewrites
720
- any uploads URL whose file isn't present locally to the production origin —
721
- covering the standard media functions, page-builder output (e.g. Elementor inline
722
- CSS and gallery data), and **legacy paths** left over from a Standard-WP →
723
- Bedrock conversion. Because it decides "missing" from the real document root and
724
- redirects to the **same path** on production, your production routing (CDN/S3
725
- redirects included) resolves whatever it's handed.
726
-
727
- - **It runs in WordPress, not your web server**, so it works the same on Valet,
728
- Herd, LocalWP, DDEV, MAMP — anything. No nginx/Apache config to get right.
729
- - **Local-only and read-only on production** — it just hotlinks your own
730
- production images. It self-disables when running on the production domain, so
731
- it's inert if it ever gets deployed.
732
- - **The plugin's presence is the on/off state** (no config flag). Press `m` to
733
- toggle; `u` from the overlay updates it in place when a newer version ships.
734
- - Needs production reachable and hotlinking allowed (it won't work behind staging
735
- Basic-Auth). Images delivered by external CSS files or async JS/REST are the one
736
- thing server-side rewriting can't catch.
353
+ `m` on a linked WordPress site drops a small mu-plugin that serves any
354
+ missing-locally upload straight from production — no file copying, works on
355
+ any local stack. Full details:
356
+ [docs/production-media-fallback.md](docs/production-media-fallback.md).
737
357
 
738
358
  ## DNS hosts, access & editing
739
359
 
740
- A **server-migration lens** for DNS: see the records that move a site to another
741
- server, and edit them in place. It is deliberately **not** a full zone editor — it
742
- only ever shows and touches a site's own hosting records (its apex / `www` /
743
- subdomains and additional domains), so your MX, TXT, DKIM, and other zone records are
744
- never shown or changed. Moving a site can't take down its email.
745
-
746
- - **The view.** Press `n` on a site for just that site's records, or `N` on a server
747
- for every site on it; inside a site-scoped view, `a` expands to the whole server.
748
- Each **site** is a line, labeled by its own domain (even when it's a subdomain),
749
- with its hosting record's type, **TTL in seconds**, value, a `◀ here` flag when the
750
- record points at this server, and a `+www` tag when `www` simply follows the apex.
751
- A site's additional domains nest beneath it, so a domain portfolio reads as one
752
- site, not many. TTLs come from the zone's authoritative nameserver (the configured
753
- value, not a counted-down one), so they show even for hosts you haven't connected;
754
- `r` refreshes.
755
- - **Access (`✓ ↗ ○ ·`).** Each record's zone shows whether you can edit it: `✓`
756
- editable, `↗` web-only handoff, `○` the provider has an API but you haven't
757
- connected an account that holds the zone, `·` unknown. A zone is `✓` only when a
758
- connected account of the provider that actually serves it (its live nameservers)
759
- holds it — so a stale or duplicate zone elsewhere never shows a false green. With
760
- two or more accounts connected, an **ACCOUNT** column names the owning one.
761
- - **Edit a TTL (`⏎`).** On an editable record, `⏎` opens a focused editor — pick a
762
- preset or a custom value, confirm, and it's written to **AWS Route 53** or
763
- **Cloudflare** through your connected account. This is the prep step for a low-risk
764
- migration: lower the TTL, cut over, then restore it. Before writing, an edit-time
765
- check re-reads the live nameservers and **blocks** the change if the connected
766
- account's zone isn't the one actually serving the domain. Route 53 changes are
767
- followed to completion; the record shows an "updating" status that keeps ticking
768
- even if you leave the view. Cloudflare **proxied** records keep their TTL fixed
769
- to automatic, so it's not editable here — repointing still works (next).
770
- - **Repoint a record (`p`).** The same focused editor, on the record's **value** —
771
- where it points. The picker leads with **your SpinupWP servers** (name + IP, the
772
- record's current home tagged), because pointing a record at one of your own boxes
773
- is what this is for; a custom IP is the fallback. The same confirm and NS
774
- pre-flight gate apply, the write is followed to completion in the background, and
775
- the inventory row shows `→ new IP` while it applies. A Cloudflare **proxied**
776
- record repoints its **origin** behind the proxy (visitors keep resolving
777
- Cloudflare's IPs). CNAMEs aren't repointed — they follow their target, which is
778
- the record to edit. This is the standalone version of the clone wizard's DNS
779
- cutover: migrate a single site, finish a cutover by hand, or fix a stale record.
780
- - **Connect a provider (`c`).** Manage credentials for the selected zone's
781
- provider — **AWS Route 53** (an IAM access key), **Cloudflare** (a scoped token),
782
- or **GoDaddy** (a Production API key). Multiple accounts per provider are
783
- supported, with a drill-down into each account's zones. Credentials are verified
784
- before they're stored, kept in `config.json` (chmod 600), and the matching
785
- environment variables are honored (`CLOUDFLARE_API_TOKEN`, `AWS_ACCESS_KEY_ID` /
786
- `AWS_SECRET_ACCESS_KEY`, `GODADDY_API_KEY` / `GODADDY_API_SECRET`). Secrets are
787
- masked as you type. Listing hosts needs only read access (Cloudflare `Zone:Read`);
788
- **editing (a TTL or a repoint)** needs write access — Route 53 record writes, or a
789
- Cloudflare `Zone.DNS:Edit` token.
790
- - **Web-only hosts (GoDaddy, Namecheap, Network Solutions, …).** A registrar with
791
- no usable API shows `↗`; press `w` (in the inventory or the connect overlay) to
792
- open its web console — for GoDaddy specifically, your Clients hub, with the
793
- domain copied to your clipboard so you can paste it after logging in as the
794
- client. Every `↗` zone also shows an **access note** — "Delegate Access" by
795
- default (the assumed-normal case for a client's registrar), or your own
796
- per-zone override (e.g. an IT vendor's contact) when that default doesn't hold.
797
- Press `c` on any such zone to open **Manage Access**: it lists every zone
798
- already known at that host, `n` edits the selected zone's note, and `r`
799
- resolves your whole fleet's DNS to fill in any zones it hasn't seen yet.
800
-
801
- Provider credentials are optional — without them you still get the full host
802
- inventory and TTLs, just without the editable/account columns or in-place editing.
360
+ `n` (site) / `N` (server) show only a site's own hosting records — never
361
+ MX/TXT/other zone records — with TTLs and edit access. `⏎` edits a TTL, `p`
362
+ repoints a record to another of your servers (AWS Route 53 / Cloudflare). Full
363
+ details: [docs/dns-access-editing.md](docs/dns-access-editing.md).
803
364
 
804
365
  ## Development
805
366
 
@@ -814,15 +375,17 @@ bun run typecheck # tsc --noEmit
814
375
  SPINUP_DEV_MODE=1 bun run dev
815
376
  ```
816
377
 
817
- Boots straight into the dashboard against a small in-memory example fleet
818
- (`web1.example.com` … `web9.example.org`, a mix of Standard WP, Bedrock, and
819
- non-WP sites) — no API token, no network calls, nothing that can touch a real
820
- account. A purple **`DEV MODE`** badge in the header makes it unmistakable. Every
821
- write action (PHP upgrade, HTTPS toggle, purge cache, reboot, create a server/site)
822
- works against the fake data and shows the same in-progress/toast behavior as the
823
- real thing, so it's useful for screenshots, walkthroughs, and UI work without a
824
- live account on hand. The clone wizard's SSH-based file/DB pull is out of scope —
825
- it always talks to real servers over SSH, independent of the API client.
378
+ Boots straight into the dashboard against an in-memory example fleet (12
379
+ servers, ~80 sites — a deterministic mix of Standard WP, Bedrock, and non-WP
380
+ sites, EOL Ubuntu/PHP versions, and pending updates) — no API token, no network
381
+ calls, nothing that can touch a real account. A purple **`DEV MODE`** badge in
382
+ the header makes it unmistakable. Every write action (PHP upgrade, HTTPS
383
+ toggle, purge cache, reboot, create a server/site) works against the fake data
384
+ and shows the same in-progress/toast behavior as the real thing, so it's
385
+ useful for screenshots (the ones above were generated this way), walkthroughs,
386
+ and UI work without a live account on hand. The clone wizard's SSH-based
387
+ file/DB pull is out of scope — it always talks to real servers over SSH,
388
+ independent of the API client.
826
389
 
827
390
  The fixture fleet lives in `src/dev/fixtures.ts`; the fake client it's served
828
391
  through is `src/dev/mockClient.ts`.