spinuptui 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +943 -0
- package/LICENSE +21 -0
- package/README.md +856 -0
- package/bin/spinuptui.js +13 -0
- package/package.json +55 -0
- package/src/api/client.ts +354 -0
- package/src/api/types.ts +206 -0
- package/src/config.ts +309 -0
- package/src/dev/devMode.ts +7 -0
- package/src/dev/fixtures.ts +238 -0
- package/src/dev/mockClient.ts +214 -0
- package/src/index.tsx +68 -0
- package/src/lib/appUpdate.ts +206 -0
- package/src/lib/awsSigV4.ts +133 -0
- package/src/lib/cloneDomains.ts +112 -0
- package/src/lib/cloneLog.ts +85 -0
- package/src/lib/dbBackup.ts +198 -0
- package/src/lib/dbSync.ts +253 -0
- package/src/lib/discover.ts +165 -0
- package/src/lib/dns.ts +144 -0
- package/src/lib/dnsCache.ts +94 -0
- package/src/lib/dnsQuery.ts +180 -0
- package/src/lib/dnsRecords.ts +411 -0
- package/src/lib/format.ts +106 -0
- package/src/lib/gitDeployKey.ts +0 -0
- package/src/lib/gitStatus.ts +37 -0
- package/src/lib/jobs.ts +32 -0
- package/src/lib/keychain.ts +49 -0
- package/src/lib/local.ts +116 -0
- package/src/lib/mediaFallback.ts +288 -0
- package/src/lib/open.ts +103 -0
- package/src/lib/phpEol.ts +151 -0
- package/src/lib/probe.ts +183 -0
- package/src/lib/providers.ts +284 -0
- package/src/lib/providersCache.ts +105 -0
- package/src/lib/releaseNotes.ts +67 -0
- package/src/lib/serverClone.ts +782 -0
- package/src/lib/serverCreate.ts +161 -0
- package/src/lib/siteDoctor.ts +260 -0
- package/src/lib/siteFingerprint.ts +108 -0
- package/src/lib/spinupweb.ts +33 -0
- package/src/lib/ssh.ts +669 -0
- package/src/lib/stack.ts +78 -0
- package/src/lib/stackCache.ts +113 -0
- package/src/lib/theme.ts +54 -0
- package/src/lib/ubuntuEol.ts +124 -0
- package/src/lib/uptimeKuma.ts +551 -0
- package/src/lib/vanitySite.ts +297 -0
- package/src/lib/wpInventory.ts +153 -0
- package/src/ui/App.tsx +265 -0
- package/src/ui/Details.tsx +403 -0
- package/src/ui/Explain.tsx +158 -0
- package/src/ui/Header.tsx +117 -0
- package/src/ui/Help.tsx +296 -0
- package/src/ui/List.tsx +95 -0
- package/src/ui/Onboarding.tsx +93 -0
- package/src/ui/Splash.tsx +51 -0
- package/src/ui/StatusBar.tsx +60 -0
- package/src/ui/components.tsx +392 -0
- package/src/ui/store.tsx +4253 -0
- package/src/ui/views/Browser.tsx +365 -0
- package/src/ui/views/CloneWizard.tsx +932 -0
- package/src/ui/views/Dashboard.tsx +149 -0
- package/src/ui/views/DbBackup.tsx +228 -0
- package/src/ui/views/DbSync.tsx +307 -0
- package/src/ui/views/Discover.tsx +271 -0
- package/src/ui/views/DnsInventory.tsx +580 -0
- package/src/ui/views/DnsRecords.tsx +562 -0
- package/src/ui/views/Events.tsx +98 -0
- package/src/ui/views/Forgotten.tsx +165 -0
- package/src/ui/views/GrantKey.tsx +504 -0
- package/src/ui/views/Health.tsx +322 -0
- package/src/ui/views/HttpsToggle.tsx +195 -0
- package/src/ui/views/KumaSite.tsx +571 -0
- package/src/ui/views/LocalLink.tsx +220 -0
- package/src/ui/views/MediaFallback.tsx +180 -0
- package/src/ui/views/NewServer.tsx +750 -0
- package/src/ui/views/PhpUpgrade.tsx +301 -0
- package/src/ui/views/ProviderConnect.tsx +527 -0
- package/src/ui/views/PurgeCache.tsx +186 -0
- package/src/ui/views/ReleaseNotes.tsx +84 -0
- package/src/ui/views/Search.tsx +448 -0
- package/src/ui/views/ServerActions.tsx +328 -0
- package/src/ui/views/Stacks.tsx +371 -0
- package/src/ui/views/SudoConnect.tsx +358 -0
- package/src/ui/views/VanityNewSite.tsx +407 -0
- package/src/ui/views/WpInventory.tsx +218 -0
- package/src/version.ts +8 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
While the project is in `0.x`, the public surface (CLI subcommands/flags, config
|
|
9
|
+
file location and format, and token-resolution behavior) may change between minor
|
|
10
|
+
versions; such changes are called out here.
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.19.0] - 2026-07-08
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **The app is now "SpinupTUI", and the command is `spinuptui`.** The rename
|
|
18
|
+
distinguishes the app from the SpinupWP service it talks to (and `spinup` was
|
|
19
|
+
already taken on npm). Everything else is unchanged — the config file stays at
|
|
20
|
+
`~/.config/spinupwp-tui/config.json`, and no settings move. **Source-checkout
|
|
21
|
+
users:** re-run `bun run link-global` to get the `spinuptui` command (and
|
|
22
|
+
`bun unlink` the old `spinup` link if you had one).
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Installable from npm.** `bun install -g spinuptui` installs the global
|
|
26
|
+
`spinuptui` command; update any time with `bun update -g spinuptui`. The
|
|
27
|
+
package runs from source under Bun (OpenTUI needs Bun's FFI) — running the
|
|
28
|
+
bin under Node prints a friendly "install Bun" pointer instead of a crash.
|
|
29
|
+
The in-app update notice is now channel-aware: a package install is shown
|
|
30
|
+
the `bun update -g spinuptui` one-liner, while a git checkout keeps the
|
|
31
|
+
in-place `u` (git pull) updater.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **The header no longer shows a perpetual "Connecting {server}…" spinner when a
|
|
35
|
+
vanity-site build is just waiting for you.** The connect flow's SSH-key step is
|
|
36
|
+
manual by design (add your key, confirm in the overlay), but the resumable job
|
|
37
|
+
surfaced it with the same animated spinner as active work — so a build parked
|
|
38
|
+
at that step looked stuck-connecting on every launch, with no hint of what it
|
|
39
|
+
wanted. That state now gets its own badge: `○ {server} needs your SSH key —
|
|
40
|
+
press V`. The active "Connecting…" badge also gained the "press V" pointer the
|
|
41
|
+
clone badge always had.
|
|
42
|
+
- **Clone wizard no longer falsely fails a site when SpinupWP is slow to run its
|
|
43
|
+
add-domain event.** SpinupWP serializes events per server, so under a concurrent
|
|
44
|
+
clone an additional-domain add can sit queued for minutes behind site-creates.
|
|
45
|
+
The wizard's poll gave up after a flat 3 minutes and reported "failed on
|
|
46
|
+
SpinupWP" — aborting the site before any files were copied — even when the event
|
|
47
|
+
went on to succeed seconds later (2026-07-07 post-mortem, event 40454437). The
|
|
48
|
+
poll now waits out queueing with a generous 30-minute backstop, takes one final
|
|
49
|
+
look at the event before giving up, and when it does time out it says so
|
|
50
|
+
honestly ("gave up waiting … retry the site to re-check") instead of blaming
|
|
51
|
+
SpinupWP — a per-site retry safely resumes, since already-added domains are
|
|
52
|
+
skipped.
|
|
53
|
+
|
|
54
|
+
## [0.18.0] - 2026-07-07
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **At-a-glance status column in the Servers list.** Each site row now carries a
|
|
58
|
+
fixed-width capability strip — `👤🔑` granted SSH keys, `H` HTTPS, `C` page cache,
|
|
59
|
+
`B` backups (lit when on, faint `·` when off), plus `◆` linked and `↑N` pending
|
|
60
|
+
updates — aligned into clean columns across every row (the site stack tag is now
|
|
61
|
+
fixed-width so nothing jitters). A compact key sits in the Sites panel title.
|
|
62
|
+
- **Installed plugins & themes (`p`).** Press `p` on a site to list its real
|
|
63
|
+
`wp plugin list` / `wp theme list` over SSH: every plugin and theme with its
|
|
64
|
+
status, current version, and available update (`→ 1.2.3`) — the detail the API
|
|
65
|
+
only exposes as bare counts. Read-only, combined scrollable list with per-section
|
|
66
|
+
update badges. Detects the WordPress directory itself, so it works on `/public/`
|
|
67
|
+
and Bedrock installs alike. (See "Installed plugins & themes" in the README.)
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- **Site stack & type now follow the `d`-identify probe, not SpinupWP's flag.**
|
|
71
|
+
SpinupWP's `is_wordpress` is unreliable — false for Bedrock/git sites and for
|
|
72
|
+
Standard-WP `/public/` installs it imported as "Generic" — so a real WordPress
|
|
73
|
+
site could show as `app` in the Servers list, and its Details "Type" as Generic,
|
|
74
|
+
even after `d` detected WordPress. The row stack tag and the Details "Stack" /
|
|
75
|
+
"Type" fields now reflect the **detected** stack, and the plugins/themes view no
|
|
76
|
+
longer refuses a misclassified WordPress site (it confirms WordPress core over
|
|
77
|
+
SSH instead of trusting the flag).
|
|
78
|
+
|
|
79
|
+
## [0.17.0] - 2026-07-06
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- **Clone wizard: every Bedrock site now gets its own read-only deploy key.** The
|
|
83
|
+
Git-access step generates a unique keypair per repo locally (held in memory only,
|
|
84
|
+
redacted from job logs), puts the public half on the repo (auto via `gh`, or
|
|
85
|
+
manually — `o` opens the repo's deploy-key settings, `y` copies the key), and
|
|
86
|
+
sends the pair with the site create so SpinupWP installs it as the new site's
|
|
87
|
+
git identity. Verified live end to end against a private repo.
|
|
88
|
+
|
|
89
|
+
### Fixed
|
|
90
|
+
- **Adding deploy keys for a second Bedrock repo no longer fails.** The wizard
|
|
91
|
+
previously reused the destination server's single deploy key, but GitHub allows
|
|
92
|
+
a deploy key on only **one** repository account-wide — the first Bedrock repo
|
|
93
|
+
cloned to a server claimed it, and every later repo's add failed with
|
|
94
|
+
"key is already in use". Per-site keys remove the limit entirely.
|
|
95
|
+
|
|
96
|
+
## [0.16.1] - 2026-07-05
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- **Alert wiring (`n`) now really covers *all* of a site's monitors.** The
|
|
100
|
+
toggle attached/detached providers on a hardcoded list (healthz, load push,
|
|
101
|
+
front-page check) that predated the Redis sentinel, so `⏎` silently skipped
|
|
102
|
+
the `{server} redis` monitor. The site's monitors are now collected by
|
|
103
|
+
convention from the saved monitor refs, so the Redis sentinel — and any
|
|
104
|
+
monitor kind added in the future — is wired automatically. If you toggled a
|
|
105
|
+
provider on before this fix, open `n` again: it shows `◐ some monitors only`
|
|
106
|
+
and one `⏎` attaches it to the rest.
|
|
107
|
+
- The server-create wizard's monitor step no longer drops previously saved
|
|
108
|
+
Redis/front-page monitor references from the config when re-run.
|
|
109
|
+
|
|
110
|
+
## [0.16.0] - 2026-07-04
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
- **Front-page check (`f` in the site-monitoring overlay) — site monitoring
|
|
114
|
+
Phase 1.** Catches the failure plain up/down monitors sleep through: the page
|
|
115
|
+
cache serving the *wrong page* while HTTP stays 200 (two real incidents showed
|
|
116
|
+
a search-results template where the home page belonged). Instead of a
|
|
117
|
+
hand-picked headline keyword — which breaks the day someone edits the copy —
|
|
118
|
+
Spinup reads the live front page while it's healthy and derives a
|
|
119
|
+
*template-identity* fingerprint (WP's `body_class()` output: `home` /
|
|
120
|
+
`page-id-N` / `front-page`, falling back to the canonical link), validates it
|
|
121
|
+
against a throwaway search render to prove it actually discriminates, and
|
|
122
|
+
registers an Uptime Kuma keyword monitor asserting it. Check window is
|
|
123
|
+
selectable (5m default · 15m · 30m · 1h — the fetch is served straight from
|
|
124
|
+
the page cache, so even 5m costs the site nothing). Re-running `f`
|
|
125
|
+
recalibrates the *existing* monitor row in place (history and notification
|
|
126
|
+
wiring survive a redesign). A failing check surfaces as `WRONG PAGE SERVED`
|
|
127
|
+
in the site's Details pane and the overlay.
|
|
128
|
+
- **`M` opens site monitoring from every site view.** Search and Stacks now bind
|
|
129
|
+
capital `M` (their lowercase `m` was taken by media fallback); the Servers tab
|
|
130
|
+
keeps `m` and gains `M` as an alias, so one muscle-memory key works everywhere.
|
|
131
|
+
|
|
132
|
+
- **Alert wiring from inside Spinup (`n` in the site-monitoring overlay).**
|
|
133
|
+
Lists the notification providers actually configured in your Uptime Kuma —
|
|
134
|
+
detected by name (e.g. "Telegram Alerts"), not assumed — and shows whether
|
|
135
|
+
each is attached to this site's Spinup monitors (`✓` all / `◐` some / `○`
|
|
136
|
+
none). `⏎` toggles a provider across all of the site's checks at once,
|
|
137
|
+
editing each monitor in place (history, tags and other notification wiring
|
|
138
|
+
survive). If Kuma has no providers yet, the overlay says so and points to
|
|
139
|
+
Kuma → Settings → Notifications — creating the provider itself (bot tokens
|
|
140
|
+
etc.) is the one step that stays in Kuma.
|
|
141
|
+
|
|
142
|
+
- **The site doctor (`d` in the site-monitoring overlay) — site monitoring
|
|
143
|
+
Phase 2.** A read-only, zero-setup diagnosis of the "200 OK but wrong page"
|
|
144
|
+
failure class, built on the cache differential: the page is fetched twice —
|
|
145
|
+
once normally (cache-eligible) and once with the default
|
|
146
|
+
`wordpress_no_cache` bypass cookie — and WP's template identity is compared
|
|
147
|
+
between the two. SpinupWP's `fastcgi-cache: HIT/BYPASS` headers prove which
|
|
148
|
+
layer answered each request, so a mismatch is *proof* the page cache is
|
|
149
|
+
serving a different template than PHP renders (the purge-fixable condition),
|
|
150
|
+
not a guess. Verdicts: healthy · stale-cache (with a copyable runbook —
|
|
151
|
+
`wp elementor flush_css` appears only when Elementor is positively detected
|
|
152
|
+
in the served markup, and that one command also purges SpinupWP's caches via
|
|
153
|
+
the plugin's compat hook) · recalibrate (the page changed, not the cache —
|
|
154
|
+
`f` jumps straight there) · **partial-outage** · down · inconclusive. Works
|
|
155
|
+
without a Kuma connection, degrades honestly when the page cache is off, and
|
|
156
|
+
never writes anything — diagnosis ends at the runbook by design.
|
|
157
|
+
- **The doctor catches partial outages cached-page monitors sleep through.**
|
|
158
|
+
Two probes, two real failure shapes: (1) cached page 200 but a **fresh render
|
|
159
|
+
throws 5xx** — verified live: with SpinupWP's default object-cache drop-in a
|
|
160
|
+
dead Redis is *fatal* on every page-cache miss, and a plugin/theme fatal
|
|
161
|
+
looks identical, so visitors see cached pages while admins, logged-in users
|
|
162
|
+
and everything uncached fails; (2) the **wp-admin door probe** — the "site
|
|
163
|
+
serves fine but wp-admin shows the critical-error screen" incident shape —
|
|
164
|
+
fetches the login page (never page-cached; Bedrock's relocated `/wp/` login
|
|
165
|
+
handled), where a 5xx means an admin-facing fatal. Hardened sites that hide
|
|
166
|
+
or protect their login answer 401/403/404 and are reported as "can't judge",
|
|
167
|
+
never false-alarmed. Both produce the `partial-outage` verdict with a runbook
|
|
168
|
+
pointing at the site's error logs and the server's Redis monitor.
|
|
169
|
+
- **Server-wide Redis sentinel — site monitoring Phase 3a.** The vanity
|
|
170
|
+
heartbeat cron gains a second marker-managed line: `redis-cli ping` every
|
|
171
|
+
minute, pushed to a new `{server} redis` monitor in Kuma that alerts
|
|
172
|
+
independently (and rides the `r` secret rotation like everything else).
|
|
173
|
+
Registered automatically by `a`/`R` on a vanity site — after probing that
|
|
174
|
+
Redis actually answers on that box, so a Redis-less server never gets a
|
|
175
|
+
permanently-red monitor. A red sentinel while the server beats fine surfaces
|
|
176
|
+
as `REDIS DOWN` on the server's Details row. Live-verified the full cycle on
|
|
177
|
+
the test box: stop redis → down beat within the minute → restart → recovery
|
|
178
|
+
beat. That test also surfaced why this matters more than expected: with
|
|
179
|
+
SpinupWP's default object-cache drop-in, Redis being down is **fatal (HTTP
|
|
180
|
+
500) for every request that misses the page cache** — cached pages keep
|
|
181
|
+
serving 200, so page-watching monitors sleep through a real partial outage
|
|
182
|
+
that this sentinel catches in a minute.
|
|
183
|
+
|
|
184
|
+
### Fixed
|
|
185
|
+
- **The site-monitoring overlay no longer gets stuck on a finished op's
|
|
186
|
+
message.** A completed action's `✓`/`✕` result used to replace the action
|
|
187
|
+
menu for the rest of the session — reopening the overlay showed only the old
|
|
188
|
+
message, hiding `a`/`f`/`n` (the keys still worked, invisibly). Results now
|
|
189
|
+
render above the menu, and a settled result is forgotten when the overlay
|
|
190
|
+
closes, so it always reopens fresh.
|
|
191
|
+
- **Help/Explain overlays could deadlock open over a focused input.** Opening
|
|
192
|
+
`?` or `i` in the beat before a view's text input grabbed focus (easy to hit
|
|
193
|
+
by typing fast right after switching to Search) left an overlay that Esc
|
|
194
|
+
couldn't close — the global handler bailed on `inputMode` before reaching the
|
|
195
|
+
overlay's dismiss keys. Dismissal is now checked first.
|
|
196
|
+
|
|
197
|
+
## [0.15.0] - 2026-07-04
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
- **Rotate monitoring secrets (`r` in the `m` overlay, vanity sites).** Made for
|
|
201
|
+
screencasts: record with the push URL or metrics-JSON key on screen, then press
|
|
202
|
+
`r` right after and both secrets die. A new push token is edited into the
|
|
203
|
+
*existing* Kuma push monitor — same monitor row, so heartbeat history, uptime
|
|
204
|
+
stats and notification wiring all survive (no delete/re-create) — the heartbeat
|
|
205
|
+
cron is rewritten to the new URL over SSH, a new health key is re-seeded into
|
|
206
|
+
the vanity page, and any Kuma monitor URL still carrying the old key (the
|
|
207
|
+
JSON-query recipe) is re-keyed automatically. Confirm-gated; works without a
|
|
208
|
+
Kuma connection too (then just the health key rotates).
|
|
209
|
+
- **Vanity sites stand out in the Servers tab.** The server's own vanity/health
|
|
210
|
+
page is marked with a `⌂` glyph and brand-green tint in the sites list — with
|
|
211
|
+
servers named like domains (the vanity convention), it otherwise hides among
|
|
212
|
+
regular sites.
|
|
213
|
+
|
|
214
|
+
## [0.14.0] - 2026-07-03
|
|
215
|
+
|
|
216
|
+
### Added
|
|
217
|
+
- **Uptime Kuma integration — monitors register themselves.** Connect your Kuma
|
|
218
|
+
instance once (`m` on any site walks you through it; creds verified by a real
|
|
219
|
+
login and stored 0600, or via `SPINUP_KUMA_URL`/`_USERNAME`/`_PASSWORD`) and:
|
|
220
|
+
- The **vanity wizard grows two steps**: after publishing the page it registers
|
|
221
|
+
a healthz monitor + a load push monitor in Kuma, then installs a once-a-minute
|
|
222
|
+
heartbeat cron in the site user's crontab. The cron sends the 1-min load as
|
|
223
|
+
the push `ping`, so Kuma graphs server load — and a silent cron (server down,
|
|
224
|
+
cron dead, egress broken) flips the monitor: dead-man's-switch semantics.
|
|
225
|
+
Both steps auto-skip when Kuma isn't connected, and a failure there is
|
|
226
|
+
skippable — the site is already live.
|
|
227
|
+
- **`m` on any site** manages monitoring in place: vanity sites get the full
|
|
228
|
+
treatment (incl. `R` to re-publish pages seeded before the health-endpoint
|
|
229
|
+
feature existed), regular sites get a homepage monitor (up/down + cert-expiry
|
|
230
|
+
alerts) — client site files are never touched. **`R` works without Kuma
|
|
231
|
+
too**: unconnected, it simply pushes the current page (a vanity-page
|
|
232
|
+
refresh); connected, it also registers monitors + the cron. The connect
|
|
233
|
+
form is opt-in (`c`), never a gate.
|
|
234
|
+
- The client speaks Kuma's socket.io API (its only management API), adopts
|
|
235
|
+
same-named monitors instead of duplicating them, reuses push tokens, and
|
|
236
|
+
handles both Kuma 1.x and 2.x monitor schemas.
|
|
237
|
+
- **Kuma metrics live inside Spinup.** With a connection configured, the store
|
|
238
|
+
polls Kuma once a minute and the data surfaces where you already look:
|
|
239
|
+
- **Health view** gains a "Monitor (Uptime Kuma)" panel — up/down, 24h uptime %,
|
|
240
|
+
a response-time sparkline, and a server-load sparkline fed by the heartbeat
|
|
241
|
+
cron (real history that survives reopening the view, which the SSH-sampled
|
|
242
|
+
CPU history can't).
|
|
243
|
+
- **Details pane** shows a Monitor row per site (up/down + 24h uptime, or
|
|
244
|
+
"not monitored · m"); the **header** shows a "▼ N monitors down" badge —
|
|
245
|
+
only bad news earns header space.
|
|
246
|
+
- **Reboots never page you**: firing a reboot first wraps the server's
|
|
247
|
+
monitors in a Kuma maintenance window and removes it when the reboot event
|
|
248
|
+
settles. Strictly best-effort — Kuma being unreachable never blocks the
|
|
249
|
+
reboot — and the confirm screen says it's happening.
|
|
250
|
+
- Load pushes are sent ×100 as integers (some Kuma builds silently drop float
|
|
251
|
+
pings — found the hard way); Spinup's views scale them back.
|
|
252
|
+
- **Vanity pages are now health endpoints any uptime monitor can watch.** The page
|
|
253
|
+
the `V` wizard seeds gains two machine modes: `?healthz` returns plain
|
|
254
|
+
`200 ok` / `503 unhealthy: …` (1-min load per core > 2, or disk free < 10%) so a
|
|
255
|
+
plain HTTP monitor gets resource alerting for free, and `?format=json&key=…`
|
|
256
|
+
returns full metrics (load, cores, uptime, disk, memory, PHP version) guarded by
|
|
257
|
+
a per-site key generated at seed time (shown on the wizard's done screen, kept in
|
|
258
|
+
`vanityHealthKeys` in config.json so re-seeds keep monitor URLs stable). The
|
|
259
|
+
human page and its WHMCS-parseable `<load>`/`<uptime>` tags are unchanged.
|
|
260
|
+
Monitoring recipes — including Uptime Kuma keyword/JSON-query/cert-expiry
|
|
261
|
+
setups — live in `docs/uptime-kuma.md`.
|
|
262
|
+
|
|
263
|
+
## [0.13.0] - 2026-07-03
|
|
264
|
+
|
|
265
|
+
### Added
|
|
266
|
+
- **A vanity site can now be added to a server that already has sites.** `V` was
|
|
267
|
+
only offered on empty servers (its original "connect a fresh box" role), so a
|
|
268
|
+
busy server had no way to get its hostname page + key-holding site user. The
|
|
269
|
+
gate is now "does the server have a site at its own hostname yet" — and when
|
|
270
|
+
it doesn't, a `V — Vanity site at hostname` row appears under Manage in the
|
|
271
|
+
server's Details panel (or "Resume vanity-site build" for an unfinished one).
|
|
272
|
+
- **The DNS view can now repoint a record — press `p` to choose where it points.**
|
|
273
|
+
Alongside the TTL editor, the `N` view's single-record editor gains a "Point
|
|
274
|
+
at…" mode: pick from a list of your SpinupWP servers (name + IP, with the
|
|
275
|
+
current one tagged) — pointing a record at one of your own boxes is what this
|
|
276
|
+
is for — or enter a custom IP. Same confirm-before-firing flow as every other
|
|
277
|
+
live write, and the change keeps applying in the background (Route 53 polls to
|
|
278
|
+
INSYNC) if you close the editor. The inventory's VALUE column shows the
|
|
279
|
+
in-flight repoint (`→ new IP`) and the settled value, mirroring the TTL cell.
|
|
280
|
+
Guardrails carried over from the cutover work: a CNAME row explains it follows
|
|
281
|
+
its target instead of opening, a Cloudflare *proxied* record repoints its
|
|
282
|
+
origin (with a note that visitors keep resolving Cloudflare's IPs), a
|
|
283
|
+
multi-value record warns it'll be collapsed to the one new value, and the
|
|
284
|
+
NS-mismatch pre-flight blocks edits through a stale duplicate zone.
|
|
285
|
+
- **The clone wizard can now go back a screen with `←` (or `h`).** The setup
|
|
286
|
+
steps (Plan ↔ Destination ↔ Connect dest ↔ Git access) step back freely —
|
|
287
|
+
nothing has executed yet, so re-picking sites or the destination is safe.
|
|
288
|
+
Once the per-site fan-out has fired, the one back edge is **DNS cutover →
|
|
289
|
+
the clone roster**, so a verify can be re-run or a failed site retried
|
|
290
|
+
before flipping live traffic. Cutover state survives the round trip: rows
|
|
291
|
+
already checked, flipped, or mid-flight are never reset, and a site that
|
|
292
|
+
becomes done via retry gets its DNS records read on re-entry.
|
|
293
|
+
|
|
294
|
+
### Fixed
|
|
295
|
+
- **The DNS view's `◀ here` marker is now always relative to the server you're
|
|
296
|
+
viewing.** The "points here" flag was baked into each hostname's cached
|
|
297
|
+
resolution at lookup time, relative to whichever server's inventory triggered
|
|
298
|
+
it — so after checking DNS on the new server post-migration, reopening the
|
|
299
|
+
view on the OLD server showed records that point at the new box flagged
|
|
300
|
+
`◀ here` (with the wrong "N point here" tally). The cache now stores the
|
|
301
|
+
resolved IPs neutrally and computes "here" at render against the current
|
|
302
|
+
server; a just-repointed record also reads correctly immediately, before any
|
|
303
|
+
refresh.
|
|
304
|
+
- **Clone verification no longer reports a cut-off read as a mismatch.** The
|
|
305
|
+
source-vs-clone facts run as one wp-cli script per side; if it was
|
|
306
|
+
interrupted partway (a timeout, or a plugin stalling on an outbound call),
|
|
307
|
+
the missing tail rendered as `–` mismatch rows — reading as "the clone
|
|
308
|
+
differs" when nothing had been compared. Verification now runs wp-cli with
|
|
309
|
+
`--skip-plugins --skip-themes` (no plugin code stalls the read, and both
|
|
310
|
+
sides use identical flags so every count stays comparable), allows the
|
|
311
|
+
facts read twice the time, and a read that still comes back incomplete
|
|
312
|
+
fails loudly as a verify error naming the side, instead of masquerading as
|
|
313
|
+
differences.
|
|
314
|
+
|
|
315
|
+
## [0.12.0] - 2026-07-02
|
|
316
|
+
|
|
317
|
+
### Fixed
|
|
318
|
+
- **The clone wizard now detects each site's real webroot instead of assuming
|
|
319
|
+
it.** The first clone of a long-lived production server failed every site:
|
|
320
|
+
they all used a `/public/` public folder while the pull chain hardcoded
|
|
321
|
+
`files/` as the webroot. And the `public_folder` *setting* alone can't be
|
|
322
|
+
trusted either — SpinupWP never moves files when you configure one (its UI
|
|
323
|
+
tells you to move them yourself). The Standard-WP pull now finds where
|
|
324
|
+
WordPress actually lives on the source, and when a site is mid-move (core
|
|
325
|
+
still at the files root, setting pointing deeper) the clone completes the
|
|
326
|
+
move on the destination — placing `wp-config.php` one directory **above**
|
|
327
|
+
the webroot, the long-standing config-outside-the-docroot hardening Spinup
|
|
328
|
+
is deliberately opinionated about. Root-webroot sites keep the stock layout
|
|
329
|
+
untouched; subdirectory installs clone as-is; anything unrecognizable
|
|
330
|
+
refuses with a clear message instead of a mystery failure.
|
|
331
|
+
- **Sites SpinupWP marks as not-WordPress (redirect shells, static/PHP
|
|
332
|
+
sites) now clone as "files only"** — opt-in in the Plan step (tagged, default
|
|
333
|
+
off). The destination is created with no database, the whole files tree
|
|
334
|
+
transfers verbatim over the same hardened transport (with byte progress),
|
|
335
|
+
and verification compares file count, total size, and HTTP response instead
|
|
336
|
+
of WordPress facts. Previously these sites failed mid-pull with a cryptic
|
|
337
|
+
error; an interim fix merely excluded them.
|
|
338
|
+
- **Cloned databases keep the source's table prefix** — the destination DB
|
|
339
|
+
was previously always created with `wp_`, breaking sites with custom
|
|
340
|
+
prefixes.
|
|
341
|
+
- **Cloned sites now carry over the source's additional domains** (with their
|
|
342
|
+
redirect settings). A freshly created site only answers for its primary
|
|
343
|
+
domain, so `www.` and every extra hostname would have pointed at a server
|
|
344
|
+
that didn't serve them after DNS cutover. The wizard now re-creates the
|
|
345
|
+
full set on the destination right after the site is created — verified by
|
|
346
|
+
the destination's nginx `server_name` picking up every hostname.
|
|
347
|
+
|
|
348
|
+
### Fixed (during live migration hardening)
|
|
349
|
+
- **Concurrent clones no longer sabotage each other's SSH.** The server-to-server
|
|
350
|
+
pull authenticated every site with one shared temporary key, so with three
|
|
351
|
+
sites in flight each site's setup/cleanup replaced or deleted the key the
|
|
352
|
+
others were actively using — at most one site per run could survive. Each
|
|
353
|
+
site now gets its own key, created and revoked independently.
|
|
354
|
+
- **A file changing on the live source mid-transfer no longer kills the whole
|
|
355
|
+
site's clone.** tar reports "file changed as we read it" with a warning exit
|
|
356
|
+
code that was being treated as fatal — and the wizard's own flags suppressed
|
|
357
|
+
the message that would have explained it. Warnings are now tolerated (real
|
|
358
|
+
transfer errors still fail) and logged with the exact file named.
|
|
359
|
+
|
|
360
|
+
- **Large file transfers no longer get killed by the wizard's own timeout.**
|
|
361
|
+
A ~15-minute production pull was cut off at exactly 900s by two stacked
|
|
362
|
+
timeouts — and the error surfaced a harmless tar warning instead of the
|
|
363
|
+
real cause. The file-transfer budget is now 60 minutes, the outer session
|
|
364
|
+
timeout strictly exceeds the in-script one, and an actual timeout says
|
|
365
|
+
"file transfer timed out after 60 minutes" in so many words.
|
|
366
|
+
- **Manual DNS records in the cutover now show your access note** ("Delegate
|
|
367
|
+
Access" by default, or whatever you've written for that zone) instead of
|
|
368
|
+
wrongly telling you to connect an account for registrars that are managed
|
|
369
|
+
by hand on purpose. API-connectable hosts (Cloudflare, Route 53) keep the
|
|
370
|
+
actionable "connect an account" message.
|
|
371
|
+
|
|
372
|
+
### Changed
|
|
373
|
+
- **The DNS cutover screen grew real controls.** ↑↓ moves a cursor over the
|
|
374
|
+
records; space includes/excludes any ready record from the batch flip
|
|
375
|
+
(◉/◯, same as the Plan step); ⏎ on a record opens its zone's registrar
|
|
376
|
+
web console with the zone name copied to the clipboard (the
|
|
377
|
+
delegate-access flow); `c` cuts over exactly the included records; finish
|
|
378
|
+
moved to `f`.
|
|
379
|
+
- **In-flight clone stages show a live elapsed timer** in the roster, so a
|
|
380
|
+
long file pull reads as working rather than frozen.
|
|
381
|
+
- **File and database transfers show live byte progress** — the roster reads
|
|
382
|
+
`pull · files · 1.2 GB · 18 MB/s · 4m32s` while a transfer runs. Measured
|
|
383
|
+
by a read-only sidecar that stats the growing archive over its own SSH
|
|
384
|
+
session every few seconds, so the progress display can never interfere
|
|
385
|
+
with the transfer itself. File transfers show the Plan-measured size as a
|
|
386
|
+
soft ceiling (`1.2 GB of ~2.9 GB` — approximate because the archive
|
|
387
|
+
compresses in flight), and database pulls show a true percent (the dump
|
|
388
|
+
is staged and gzipped on the source first, so its final size is a fact).
|
|
389
|
+
- **The clone wizard no longer jumps to DNS cutover on its own.** When the
|
|
390
|
+
clone roster settles, the wizard now stays put so you can eyeball every
|
|
391
|
+
site's final state — press `c` to continue to the cutover step (which moves
|
|
392
|
+
live traffic) when you're ready.
|
|
393
|
+
|
|
394
|
+
### Added
|
|
395
|
+
- **Dev Mode (`SPINUP_DEV_MODE=1`).** Boots straight into the dashboard against
|
|
396
|
+
a small in-memory example fleet — no API token, no network calls, nothing
|
|
397
|
+
that can touch a real account. Every write action (PHP upgrade, HTTPS
|
|
398
|
+
toggle, purge cache, reboot, create a server/site) behaves the same way it
|
|
399
|
+
would against a real account — spinners, toasts, in-progress states — so
|
|
400
|
+
it's useful for demos, screenshots, and UI work without a live account on
|
|
401
|
+
hand. A purple `DEV MODE` badge in the header makes it unmistakable.
|
|
402
|
+
- **The API client is now rate-limit aware.** It watches
|
|
403
|
+
`X-RateLimit-Remaining` on every response and paces itself as the window
|
|
404
|
+
runs low, and a 429 is absorbed with a Retry-After backoff instead of
|
|
405
|
+
surfacing as a failure. This matters: during a real migration, rate-limited
|
|
406
|
+
polling made three *successful* operations report as failures. Event
|
|
407
|
+
polling is also slower-cadence and tolerates transient API errors, and a
|
|
408
|
+
genuinely failed domain-add now includes SpinupWP's own event output in the
|
|
409
|
+
error.
|
|
410
|
+
- **Every clone job now writes a full log** to `~/.config/spinupwp-tui/logs/`
|
|
411
|
+
(one JSONL file per job: every remote script, exit code, and complete
|
|
412
|
+
output, with passwords redacted). Previously errors were truncated to the
|
|
413
|
+
roster's width and lost when the app closed. Logs self-prune when a new
|
|
414
|
+
job starts: anything older than 30 days or beyond the 20 most recent job
|
|
415
|
+
logs is removed (copy a log elsewhere to keep it longer).
|
|
416
|
+
- **Press `⏎` on a failed site in the clone roster** to see the full
|
|
417
|
+
untruncated error, the failing step, and the log path — and `r` there
|
|
418
|
+
retries just that site.
|
|
419
|
+
|
|
420
|
+
## [0.11.0] - 2026-07-01
|
|
421
|
+
|
|
422
|
+
### Added
|
|
423
|
+
- **"What's new" release notes.** After Spinup updates to a new version, a
|
|
424
|
+
one-time overlay shows what changed — sourced straight from that version's
|
|
425
|
+
GitHub release (the same API the update-check hint already polls), so
|
|
426
|
+
there's no separate feed to maintain. Shows once per version, dismisses with
|
|
427
|
+
any key. Press `n` in `?` Help any time to replay the current version's notes.
|
|
428
|
+
- **Update in place with `u`.** When the gold `✦ vX.Y.Z` hint shows a newer
|
|
429
|
+
release, open `?` Help and press `u` to `git pull --ff-only` right there —
|
|
430
|
+
no more leaving the app to update manually. Refuses on a dirty checkout
|
|
431
|
+
(never overwrites uncommitted work) or diverged history (never merges/
|
|
432
|
+
rebases); since the running process can't hot-reload, it always tells you
|
|
433
|
+
plainly to restart afterward, with a `bun install` nudge when dependencies
|
|
434
|
+
changed. Manual `git pull` still works exactly as before.
|
|
435
|
+
- **Enable / disable HTTPS on a site (`H`).** The direction is auto-derived
|
|
436
|
+
from the site's current state — press `H`, confirm, and it's done, tracked
|
|
437
|
+
in the background the same way a PHP upgrade is. Disabling explicitly warns
|
|
438
|
+
that https:// visitors will see errors until it's re-enabled.
|
|
439
|
+
- **Purge page + object cache (`P`).** SpinupWP has no enable/disable for
|
|
440
|
+
either cache on an existing site (only at creation time) — purge is the one
|
|
441
|
+
available write, so `P` fires both together under a single confirm and
|
|
442
|
+
tracks them to completion. Low-risk and fully reversible by nature (both
|
|
443
|
+
caches just rebuild on the next page load).
|
|
444
|
+
|
|
445
|
+
### Fixed
|
|
446
|
+
- **Cloudflare proxied records no longer block DNS cutover.** The clone
|
|
447
|
+
wizard's cutover step used to mark every Cloudflare *proxied* (orange-cloud)
|
|
448
|
+
record as "manual" — but a proxied record's TTL is what's actually
|
|
449
|
+
uneditable (forced to automatic); its origin IP was always safely PATCHable
|
|
450
|
+
through the same partial write already used for cutover. Proxied records now
|
|
451
|
+
flow through the automatic batch flip like any other editable record; the
|
|
452
|
+
TTL editor is unaffected and still correctly refuses to touch a proxied
|
|
453
|
+
record's TTL.
|
|
454
|
+
- **Release notes overlay: text no longer overlaps on wrapped bullets.** The
|
|
455
|
+
box's height was computed from logical line count, undercounting bullets
|
|
456
|
+
that word-wrap into several visual rows — the box now sizes to whatever
|
|
457
|
+
content actually renders instead.
|
|
458
|
+
|
|
459
|
+
## [0.10.0] - 2026-07-01
|
|
460
|
+
|
|
461
|
+
### Added
|
|
462
|
+
- **Servers running an end-of-life Ubuntu release are flagged.** SpinupWP's own
|
|
463
|
+
web app warns when a site's Ubuntu version can no longer get new PHP installs —
|
|
464
|
+
Spinup now surfaces the same signal proactively, fleet-wide. A server whose
|
|
465
|
+
Ubuntu LTS release is past Canonical's published EOL date shows ` ⚠ os` in the
|
|
466
|
+
Servers list, a red-flagged "Ubuntu … EOL, clone to newer (C)" line in its detail
|
|
467
|
+
panel, and a "Needs attention" entry on the Dashboard pointing at the clone
|
|
468
|
+
wizard (`C`). EOL dates come from the same embedded-table-plus-endoflife.date-
|
|
469
|
+
refresh approach already used for PHP EOL, so it stays current without a
|
|
470
|
+
Spinup release (`src/lib/ubuntuEol.ts`).
|
|
471
|
+
- **Access notes for zones you can't reach via API — any `↗ web`-only host, not
|
|
472
|
+
just GoDaddy.** DNS providers with no API (GoDaddy's is gated to large/reseller
|
|
473
|
+
accounts; Namecheap, Network Solutions, and others never had one) used to show
|
|
474
|
+
as an undifferentiated `↗ web` in the DNS inventory, with no way to tell "I have
|
|
475
|
+
delegate access to this myself" from "a third party manages this domain." The
|
|
476
|
+
inventory's NOTE column now shows a global assumed default ("Delegate Access" —
|
|
477
|
+
most registrars are managed the same way) or, when set, a per-zone override in
|
|
478
|
+
amber — e.g. a note pointing at the third-party IT vendor for the one client
|
|
479
|
+
domain you don't manage directly. Set/clear the override from `c` "Manage
|
|
480
|
+
Access" on any inventory row (widened from GoDaddy-only to any host with a web
|
|
481
|
+
handoff): the previously-empty Zones pane lists every zone already known
|
|
482
|
+
fleet-wide for that host (from prior DNS lookups), landing the cursor on the
|
|
483
|
+
zone you opened it from; press `n` to edit its note, or `r` to resolve every
|
|
484
|
+
fleet domain and fill in any gaps. Only exceptions are ever stored — the common
|
|
485
|
+
case costs zero config.
|
|
486
|
+
|
|
487
|
+
## [0.9.1] - 2026-06-30
|
|
488
|
+
|
|
489
|
+
### Changed
|
|
490
|
+
- **The "update available" hint is easier to spot.** The `✦ vX.Y.Z` nudge in the
|
|
491
|
+
header (and the `?` About panel) is now **bright gold** and bold, instead of the
|
|
492
|
+
same green as the wordmark — so a newer release stands out at a glance.
|
|
493
|
+
|
|
494
|
+
## [0.9.0] - 2026-06-30
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
- **Completion toasts for background writes.** When a **PHP upgrade** or a **server
|
|
498
|
+
reboot / service restart** finishes, a non-focus-stealing toast slides in at the
|
|
499
|
+
top-right — `web3.example.com upgraded to PHP 8.3`, `web1.example.com rebooted`,
|
|
500
|
+
`Nginx restarted on web1.example.com`. These operations keep tracking in the
|
|
501
|
+
background after you close their overlay, so the toast is the "it's done" signal you
|
|
502
|
+
would otherwise have to go looking for; it auto-dismisses (~4s) and never takes
|
|
503
|
+
keyboard focus. Built on `@opentui-ui/toast`.
|
|
504
|
+
- **Privileged writes over SSH: connect sudo (`S`) + grant Spinup's SSH key (`K`).**
|
|
505
|
+
The first things Spinup writes that the SpinupWP API simply can't do (it has no
|
|
506
|
+
SSH-key or sudo-user surface). Press **`S`** on a server to **connect sudo** for the
|
|
507
|
+
session: enter the SpinupWP sudo user + its sudo password once, Spinup validates
|
|
508
|
+
them against the live server, and then holds them **in memory for the session
|
|
509
|
+
only** (the username persists to config; the **password is never written to
|
|
510
|
+
disk**). A connected server shows a green **● sudo** on its row; press `S` again to
|
|
511
|
+
disconnect. With sudo connected, press **`K`** on a site to drop Spinup's
|
|
512
|
+
**dedicated machine key**
|
|
513
|
+
into that site user's `authorized_keys` over sudo — an ed25519 identity generated
|
|
514
|
+
once into the config dir (`keys/spinup-tui`), commented `spinup-tui@<your-host>`,
|
|
515
|
+
and deliberately **never added to your SpinupWP account** so SpinupWP's
|
|
516
|
+
`authorized_keys` reconciliation leaves it untouched. The remote script is
|
|
517
|
+
**idempotent** (`grep -qxF` — re-running never duplicates the line), and a confirm
|
|
518
|
+
overlay shows the exact remote command before anything fires. **`K` shows a key
|
|
519
|
+
picker:** choose any of **your personal keys** (discovered from `~/.ssh/*.pub` and
|
|
520
|
+
the ssh-agent — so you can SSH/SFTP as yourself) and/or the **machine key**, deploy
|
|
521
|
+
several at once, and your selection is remembered for next time. **Choose the scope**
|
|
522
|
+
too: just this site, or **every site on the server** in one pass (the same idempotent
|
|
523
|
+
append runs on each, with a per-site progress readout and a retry for any that fail).
|
|
524
|
+
And when you **connect a new server (vanity flow)**, you can connect sudo right from
|
|
525
|
+
the flow (`S`, same as on a server); with sudo connected, the SSH-key step grants your
|
|
526
|
+
saved keys there (`g`) and publishes — no more manual "add your key in SpinupWP"
|
|
527
|
+
round-trip. The site list shows which keys are on a site at a glance — **👤 your key**
|
|
528
|
+
and/or **🔑 the spinup-tui machine key** — and the Details panel spells it out
|
|
529
|
+
(`Granted your key + spinup-tui`). The same `K` overlay can **remove** keys too
|
|
530
|
+
(`a`/`r` to toggle grant/remove) — the reverse of grant, leaving every other key (incl.
|
|
531
|
+
SpinupWP-managed ones) untouched. See `docs/2026-06-26_sudo-ssh-key-provisioning-spec.md`.
|
|
532
|
+
- **Save your sudo password to the macOS Keychain (opt-in).** Connecting sudo (`S`)
|
|
533
|
+
now offers a **"Remember in macOS Keychain"** toggle. Tick it and the next time you
|
|
534
|
+
open `S` on that server, sudo **auto-unlocks** — no re-typing the password. The
|
|
535
|
+
password lives **only** in your login Keychain (service `spinup-sudo`, one item per
|
|
536
|
+
server) and the in-memory session; **`config.json` never holds it** — only the
|
|
537
|
+
username and a `keychain: true` marker. Manage it from the connected panel: **`f`**
|
|
538
|
+
forgets the saved password (the live session stays connected), and disconnecting
|
|
539
|
+
(**`x`**) a saved server shows a **no-password reconnect** panel (`⏎` to reconnect,
|
|
540
|
+
`f` to forget) instead of the credential form — you never re-enter a password
|
|
541
|
+
Spinup already holds. The first auto-unlock may surface macOS's own "allow access"
|
|
542
|
+
prompt; choose **Always Allow** and it stays silent after. Off macOS this is absent
|
|
543
|
+
and sudo stays in-memory per session, exactly as before. Uses the built-in
|
|
544
|
+
`security` CLI — no new dependencies.
|
|
545
|
+
- **Create a new server (`c`).** Press `c` on a server in the Servers tab to
|
|
546
|
+
provision a new one, pre-filled to **match** the selected server's provider,
|
|
547
|
+
region, and size. The form prices the build from the provider's catalog
|
|
548
|
+
(DigitalOcean / Vultr / Linode / Hetzner) so you see a monthly cost before
|
|
549
|
+
confirming, suggests a hostname from your fleet's naming convention (e.g. the
|
|
550
|
+
next `webN.example.com`), and lets you switch provider (`p`), region (`g`), or
|
|
551
|
+
size (`e`) and toggle backups. The
|
|
552
|
+
build fires `POST /servers` and tracks the ~10-minute provisioning in the
|
|
553
|
+
background, so closing the overlay doesn't abandon it. Because the SpinupWP API
|
|
554
|
+
can't list your configured server providers, the first time you create on a
|
|
555
|
+
provider the overlay asks for its SpinupWP provider id (from Account Settings →
|
|
556
|
+
Server Providers) and saves it for you — once per provider, no hand-editing
|
|
557
|
+
config. First step of the clone-to-new-server workflow (see
|
|
558
|
+
`docs/2026-06-24_clone-to-server-spec.md`).
|
|
559
|
+
- **Clone a whole server to a new one (`C`).** Press `C` on a server to open the
|
|
560
|
+
clone wizard — a guided, two-pane journey that lifts one or more of a server's
|
|
561
|
+
sites onto a fresh (or existing) destination, **without touching DNS until you
|
|
562
|
+
say so**, so you can stage and verify a migration before cutting over. The steps:
|
|
563
|
+
**Plan** — pick which of the source's sites to clone (all selected by default;
|
|
564
|
+
`space` toggles), and Spinup sizes each one live (`du` + `wp db size`) into a
|
|
565
|
+
payload total so you know what you're moving; a concurrency throttle protects the
|
|
566
|
+
source. **Destination** — provision a new server pre-matched to the source, or
|
|
567
|
+
`d` to pick an existing server as the target. **Connect** — connect sudo on
|
|
568
|
+
**both** ends (the clone is a server-to-server **pull**: the destination pulls
|
|
569
|
+
from the source over SSH using a granted key + agent forwarding, and the source
|
|
570
|
+
key is revoked when it's done). **Clone sites** — a live roster runs the sites
|
|
571
|
+
concurrently, each advancing `create → pull → config → verify → done`. Both
|
|
572
|
+
stacks are handled: **Standard WP** (files via tar-over-ssh, DB via cat-over-ssh,
|
|
573
|
+
wp-config re-stamped for the dest) and **Bedrock** (git-native — create from the
|
|
574
|
+
repo, `composer install` over SSH, pull `uploads` + `auth.json`, re-stamp `.env`
|
|
575
|
+
keeping custom vars). **Verify** — a source-vs-clone drill-down (wp-cli value
|
|
576
|
+
diff + a `--resolve` HTTP check that hits the new server while DNS still points at
|
|
577
|
+
the old one). **DNS cutover** — when you're satisfied, repoint **every** A record
|
|
578
|
+
across each site's domains (apex + additional) to the new server in one batched,
|
|
579
|
+
partial-aware pass; www-style CNAMEs that follow the apex are skipped, not
|
|
580
|
+
clobbered. The clone runs in the **background** — `esc` doesn't abandon it; a
|
|
581
|
+
header badge (`⠹ Cloning <server> — press C`) surfaces the in-flight job and `C`
|
|
582
|
+
on the source reopens the live roster. See
|
|
583
|
+
`docs/2026-06-24_clone-to-server-spec.md`.
|
|
584
|
+
- **Connect a new server with a vanity site (`V`).** A freshly-created server has
|
|
585
|
+
no site, so there's nothing to attach an SSH key to and no way for Spinup to
|
|
586
|
+
reach it. Press `V` on a server with no sites to build a small placeholder site
|
|
587
|
+
at the server's own hostname: Spinup writes the DNS A record (Route 53), waits
|
|
588
|
+
for it to resolve, creates the site, enables HTTPS (a free Let's Encrypt
|
|
589
|
+
certificate), hands you off to add your SSH key (the site's **SFTP & SSH → Site
|
|
590
|
+
User**), then publishes a minimal, brand-neutral status page. Press `o` on the
|
|
591
|
+
success screen to open the live site. The whole build runs in the background and
|
|
592
|
+
survives closing the overlay — reopen it with `V`, and a header badge tracks it.
|
|
593
|
+
- **Background jobs survive a restart.** Long-running work — a server provision, a
|
|
594
|
+
PHP upgrade, a vanity-site build — is now persisted, so quitting and relaunching
|
|
595
|
+
Spinup reconnects to it instead of forgetting it. SSH-based jobs that can't be
|
|
596
|
+
reconnected (a production DB backup/sync) are clearly flagged as **interrupted**
|
|
597
|
+
on restart rather than silently lost.
|
|
598
|
+
|
|
599
|
+
### Changed
|
|
600
|
+
- **New-server progress shows a live elapsed timer**, and a header badge surfaces
|
|
601
|
+
an in-flight provision (or vanity-site build) from any tab.
|
|
602
|
+
- **Servers with no sites are flagged in amber** in the Servers list — an empty
|
|
603
|
+
server is a dead end until it has a site (press `V` to create one).
|
|
604
|
+
- **The vanity build's "Wait for DNS to propagate" step now shows a timer.** A
|
|
605
|
+
live count-down (`m:ss left`) ticks through the ~2-minute window; if it lapses you
|
|
606
|
+
can **keep waiting** (`w`) and the timer flips to a count-up (`m:ss elapsed`) from
|
|
607
|
+
that same point so you can see exactly how long it's been — without being prompted
|
|
608
|
+
again. From there, **`c` continues now** (create the site and move on) or **`s`
|
|
609
|
+
skips SSL** (publish over HTTP and add the certificate later).
|
|
610
|
+
- **DNS connection resolution re-checks itself instead of trusting a stale cache.**
|
|
611
|
+
Writing the vanity A record (and, later, the clone DNS cutover) used a cached list
|
|
612
|
+
of which zones each connected account serves; a zone you registered *today* wasn't
|
|
613
|
+
in it, so the build failed with a misleading "no editable DNS connection serves
|
|
614
|
+
this zone." Now, when no account matches but one *is* connected for that provider,
|
|
615
|
+
Spinup re-verifies that provider's accounts and retries before giving up — so a
|
|
616
|
+
freshly-registered zone just works, no manual refresh. The cache also expires
|
|
617
|
+
entries after 24h so they refresh on their own. The error messages now distinguish
|
|
618
|
+
"no account connected" from "connected but it doesn't serve this zone."
|
|
619
|
+
|
|
620
|
+
## [0.8.0] - 2026-06-23
|
|
621
|
+
|
|
622
|
+
### Added
|
|
623
|
+
- **Production media fallback (`m`).** After a `p` DB pull, your local site shows
|
|
624
|
+
broken images because the media library isn't synced. Press `m` on a linked
|
|
625
|
+
WordPress site to drop a small, self-contained mu-plugin that serves any
|
|
626
|
+
**missing-locally** upload straight from production — so images resolve without
|
|
627
|
+
copying a single file. It runs in WordPress (pure PHP), so it works the same on
|
|
628
|
+
any local stack (Valet / Herd / LocalWP / DDEV / MAMP) with no web-server config.
|
|
629
|
+
It decides "missing" from the real document root and redirects to the **same
|
|
630
|
+
path** on production, so it covers page-builder URLs (Elementor inline CSS &
|
|
631
|
+
gallery data), **legacy `/wp-content/uploads` paths** left from a Bedrock
|
|
632
|
+
conversion, and CDN/S3 redirects (production's own routing resolves them).
|
|
633
|
+
Local-only and read-only on production; self-disables on the production domain
|
|
634
|
+
so it's inert if ever deployed. The plugin's presence is its on/off state (no
|
|
635
|
+
config); `m` toggles it and the overlay offers an in-place update when a newer
|
|
636
|
+
version ships. (See "Production media fallback" in the README.)
|
|
637
|
+
- **App version, About, and update check.** The header now shows the running
|
|
638
|
+
version next to the wordmark (`◆ Spinup vX.Y.Z`). The `?` help overlay is
|
|
639
|
+
redesigned into a responsive multi-column layout with an **About** column
|
|
640
|
+
(version, how to update, repo). On launch the app checks the latest GitHub
|
|
641
|
+
release (cached on disk for 6h — no background polling) and, when a newer
|
|
642
|
+
release exists, shows a `✦ vX.Y.Z` hint in the header and the About panel.
|
|
643
|
+
|
|
644
|
+
### Changed
|
|
645
|
+
- **The app is now called "Spinup".** Renamed the app's own branding — the
|
|
646
|
+
header wordmark, the help/About panel, and the CLI banner — to **Spinup**, since
|
|
647
|
+
it's a control center for your *SpinupWP account*. References to the SpinupWP
|
|
648
|
+
*service* (the web app via `w`, the API, deep links) are unchanged.
|
|
649
|
+
|
|
650
|
+
## [0.7.1] - 2026-06-23
|
|
651
|
+
|
|
652
|
+
### Added
|
|
653
|
+
- **Scaffold a post-import hook from the sync screen.** When you press `p` on a
|
|
654
|
+
linked site that has no `bin/sync.d/post-import.sh`, the confirm screen now
|
|
655
|
+
explains what the hook is (it runs after the import + URL rewrite) and offers
|
|
656
|
+
`s` to write an **inert** sample for you — a documented `post-import.sh` with
|
|
657
|
+
the `WEB_DIR` / `SYNC_REMOTE_HOST` / `SYNC_LOCAL_HOST` env contract and common
|
|
658
|
+
examples (Elementor URL swaps, plugin toggles), all commented out so nothing
|
|
659
|
+
runs until you uncomment it. It never overwrites an existing hook.
|
|
660
|
+
|
|
661
|
+
### Changed
|
|
662
|
+
- **Backup (`d`) and sync (`p`) now show progress as a building checklist.** Each
|
|
663
|
+
step (back up local, export, download, import, rewrite URLs, run hook) is listed
|
|
664
|
+
in one bordered panel and gets a `✓` as it finishes, the running step spins, and
|
|
665
|
+
the final result (saved paths, size) appears below the completed stack — so the
|
|
666
|
+
whole operation, including the "done" summary, reads in a single frame. A
|
|
667
|
+
failure marks the exact step that broke with `✕` and shows the error inline.
|
|
668
|
+
|
|
669
|
+
## [0.7.0] - 2026-06-22
|
|
670
|
+
|
|
671
|
+
### Added
|
|
672
|
+
- **Edit DNS record TTLs — the first DNS write.** From the DNS view, press `⏎` on a
|
|
673
|
+
site's hosting record to open a focused editor and change its TTL (a preset or a
|
|
674
|
+
custom value). This is the prep step for a low-risk server migration: lower the TTL
|
|
675
|
+
before you cut over, then restore it after. Writes go to **AWS Route 53** or
|
|
676
|
+
**Cloudflare** through your connected account, behind a confirm step. An edit-time
|
|
677
|
+
safety check re-reads the live nameservers and **blocks the write** if the account's
|
|
678
|
+
zone isn't the one actually serving the domain — so you can't edit a stale or
|
|
679
|
+
duplicate copy. Route 53 changes are followed to completion; the record shows an
|
|
680
|
+
"updating" status that keeps ticking even if you leave the view.
|
|
681
|
+
- **Open the DNS view scoped to one site.** Press `n` on a site (Servers or Search)
|
|
682
|
+
to open the inventory for just that site's domains and records — the view you want
|
|
683
|
+
when migrating a single site. Press `a` inside to expand to the whole server.
|
|
684
|
+
- **Download a production database backup.** On a linked WordPress site, press
|
|
685
|
+
`d` in Search to export the production database with `wp-cli` (into a stage file
|
|
686
|
+
*outside* the public webroot), gzip it, download it into the linked copy's
|
|
687
|
+
`sql/` folder, and remove the remote copy. **Read-only on production** and needs
|
|
688
|
+
no local WP-CLI — the export runs on the server. The remote document root and
|
|
689
|
+
SSH target are derived from the API; SSH/scp run non-interactively. A spinner on
|
|
690
|
+
the site's row tracks an in-flight download even after the overlay is closed.
|
|
691
|
+
(See "Database backup & sync" in the README.)
|
|
692
|
+
- **Pull production → local DB sync (opt-in).** Press `p` on a linked WordPress
|
|
693
|
+
site to refresh your **local** database from production: it backs up the local
|
|
694
|
+
DB first, exports + downloads production, imports it locally, rewrites
|
|
695
|
+
production URLs → your local URL (`wp search-replace`), and runs an optional
|
|
696
|
+
`bin/sync.d/post-import.sh` hook if the project has one. Works with Standard WP
|
|
697
|
+
and Bedrock, detecting the local WordPress root, local URL (link or `.env`
|
|
698
|
+
`WP_HOME`), and table prefix automatically — no per-site config. **It overwrites
|
|
699
|
+
your local database, so it's off by default**; enable it with `localSync` (a
|
|
700
|
+
`config.json` field, preferred, or the `SPINUPWP_LOCAL_SYNC` env var). Read-only
|
|
701
|
+
on production.
|
|
702
|
+
- **Site Control panel.** The Search result's action menu is now grouped
|
|
703
|
+
(Open / Remote / Local / Server) so the growing set of per-site actions stays
|
|
704
|
+
scannable.
|
|
705
|
+
|
|
706
|
+
### Changed
|
|
707
|
+
- **The DNS view is now a migration lens, organized by site.** It shows only the
|
|
708
|
+
records that "count" when you move a site — each **site** on its own line (labeled
|
|
709
|
+
by the site's own domain, even when it's a subdomain), its hosting record's
|
|
710
|
+
type / TTL / value, a `◀ here` flag when the record points at this server, and a
|
|
711
|
+
`+www` tag when `www` simply follows the apex. A site's additional domains nest
|
|
712
|
+
beneath it, so a domain portfolio reads as one site, not many. TTLs are shown in
|
|
713
|
+
**seconds** (e.g. `3600`), and editing happens in place — there's no full-zone
|
|
714
|
+
record list to get lost in.
|
|
715
|
+
|
|
716
|
+
### Notes
|
|
717
|
+
- Only the **TTL** is editable so far (repointing a record's target is a later step).
|
|
718
|
+
Cloudflare **proxied** records are read-only (their TTL is managed by Cloudflare).
|
|
719
|
+
By design the DNS view only ever touches a site's own hosting records — your MX,
|
|
720
|
+
TXT, and other zone records are never shown or modified, so moving a site can't
|
|
721
|
+
take down its email.
|
|
722
|
+
- Editing a Cloudflare record needs a token scoped to `Zone.DNS:Edit` (the read-only
|
|
723
|
+
`Zone:Read` token from the host inventory isn't enough).
|
|
724
|
+
- The DB backup/sync actions only appear for **WordPress** sites (they use
|
|
725
|
+
`wp-cli`), and only when the site is **linked** to a local copy (the backup's
|
|
726
|
+
destination). The destructive sync additionally requires the `localSync` opt-in.
|
|
727
|
+
|
|
728
|
+
## [0.6.0] - 2026-06-20
|
|
729
|
+
|
|
730
|
+
### Added
|
|
731
|
+
- **DNS host inventory.** A new read-only DNS module that answers "where is each
|
|
732
|
+
domain's DNS hosted?" — the inventory you'd otherwise build by hand to migrate
|
|
733
|
+
or clone a site. Press `n` on a site to look up its domains' hosts; press `N`
|
|
734
|
+
on a server for a full, zone-keyed inventory of every domain on it. The unit is
|
|
735
|
+
the **zone**: `www` and the apex collapse together, and a separate-TLD redirect
|
|
736
|
+
(e.g. an alternate domain) surfaces as its own zone with its own host, so a full
|
|
737
|
+
portfolio move doesn't miss anything. Hosts are detected from a live nameserver
|
|
738
|
+
lookup (no `dig` dependency) and labeled (Cloudflare, AWS Route 53, GoDaddy,
|
|
739
|
+
…), falling back to the nameserver domain for anything unrecognized. Results
|
|
740
|
+
are cached with a visible age; `r` refreshes.
|
|
741
|
+
- **DNS access detection.** Each zone also shows whether you can **edit** it:
|
|
742
|
+
`✓` editable · `↗` web only · `○` needs key · `·` unknown. A zone is `✓` only
|
|
743
|
+
when a connected account of the provider that actually serves it (its live
|
|
744
|
+
nameservers) holds it — so a stale or duplicate zone in another account doesn't
|
|
745
|
+
give a false green. With more than one account connected, an **ACCOUNT** column
|
|
746
|
+
names the owning account.
|
|
747
|
+
- **Connect DNS providers.** Press `c` on a zone to manage credentials for its
|
|
748
|
+
provider — **AWS Route 53** (an access key for an IAM user scoped to Route 53
|
|
749
|
+
reads), **Cloudflare** (a `Zone:Read` scoped token), or **GoDaddy** (a
|
|
750
|
+
Production API key). Multiple accounts per provider are supported, with a
|
|
751
|
+
two-pane drill-down into each account's zones. Credentials are verified on the
|
|
752
|
+
spot (only stored if they work) and kept in `config.json` (chmod 600); standard
|
|
753
|
+
environment variables (`CLOUDFLARE_API_TOKEN`, `AWS_ACCESS_KEY_ID` /
|
|
754
|
+
`AWS_SECRET_ACCESS_KEY`, `GODADDY_API_KEY` / `GODADDY_API_SECRET`) are honored too.
|
|
755
|
+
- **GoDaddy web fallback.** GoDaddy's API is only available to larger accounts, so
|
|
756
|
+
a GoDaddy zone shows `↗` and `w` (from the inventory or the connect overlay)
|
|
757
|
+
opens your GoDaddy **Clients hub** and copies the domain to your clipboard —
|
|
758
|
+
with an in-context note on the assumed Delegate-Access workflow.
|
|
759
|
+
- **Masked secret entry.** Tokens and secret keys are masked as you type, in both
|
|
760
|
+
the DNS provider forms and the first-run **API-token onboarding** screen.
|
|
761
|
+
|
|
762
|
+
### Notes
|
|
763
|
+
- The DNS module is **entirely read-only** — it lists hosts and verifies access;
|
|
764
|
+
it does not change any DNS records (editing is a later phase). Provider
|
|
765
|
+
credentials are optional: without them you still get the full host inventory,
|
|
766
|
+
just without the editable/account columns.
|
|
767
|
+
|
|
768
|
+
## [0.5.0] - 2026-06-19
|
|
769
|
+
|
|
770
|
+
### Added
|
|
771
|
+
- **Link local working copies.** Press `L` on a site (Servers / Stacks / Search)
|
|
772
|
+
to link it to its local checkout — a path plus the local URL where you serve it.
|
|
773
|
+
The site's details gain a "Local" field, and you can open the copy with `t` (a
|
|
774
|
+
terminal at the path) or `v` (its local URL in your browser).
|
|
775
|
+
- **Auto-discover local copies.** Press `S` in the Stacks tab to scan one or more
|
|
776
|
+
folders and match their subdirectories to sites — by git remote, Bedrock
|
|
777
|
+
`WP_HOME`, or folder name — then batch-link the matches. Add scan folders inline
|
|
778
|
+
(comma-separated) the first time.
|
|
779
|
+
- **"Needs a local copy" report.** Press `f` in the Stacks tab for the managed
|
|
780
|
+
sites you have no usable local copy for (never linked, or a missing path),
|
|
781
|
+
alphabetical and filterable by stack with `←/→`; `a` trims to the ones needing
|
|
782
|
+
attention (pending updates).
|
|
783
|
+
- **Local drift at a glance.** A linked, on-disk site shows `⇡N unpushed` /
|
|
784
|
+
`● uncommitted` in the context strip (read from the local repo; no network).
|
|
785
|
+
- **SSH into a site.** Press `s` on a site to open a new terminal already running
|
|
786
|
+
`ssh` into it (`{site_user}@{server_ip}`).
|
|
787
|
+
- **Glanceable row markers.** `◆` marks sites with a linked local copy and `↑N`
|
|
788
|
+
shows pending WordPress updates, across the Servers / Stacks / Search lists.
|
|
789
|
+
- **"Explain this screen."** Press `i` on any view for a plain-language guide to
|
|
790
|
+
what each pane is and what every key does there.
|
|
791
|
+
- **Per-view subtitles** under the tab bar describing each screen and its key actions.
|
|
792
|
+
|
|
793
|
+
### Changed
|
|
794
|
+
- **Outcome-oriented key labels:** "detect" → "identify app", "scan local" →
|
|
795
|
+
"find local copies", "upgrade PHP" → "change PHP".
|
|
796
|
+
- **Terminal actions open *your* terminal**, detected from `$TERM_PROGRAM` (e.g.
|
|
797
|
+
iTerm), not a hardcoded Terminal.app; override with a `terminalApp` config/env value.
|
|
798
|
+
- **New config keys:** `localRoots` (folders to scan) and `localSites` (per-site
|
|
799
|
+
link: path + local URL). The local URL is tool-agnostic (Valet, Cove, LocalWP,
|
|
800
|
+
Herd, DDEV, …); an older `valetUrl` key is read and migrated automatically.
|
|
801
|
+
|
|
802
|
+
### Notes
|
|
803
|
+
- All new actions are read/local-only — no new SpinupWP API writes.
|
|
804
|
+
- "Open a terminal" and SSH script iTerm or Terminal on macOS; other terminals
|
|
805
|
+
fall back to Terminal.app.
|
|
806
|
+
|
|
807
|
+
## [0.4.0] - 2026-06-18
|
|
808
|
+
|
|
809
|
+
### Added
|
|
810
|
+
- **Server actions: reboot + service restarts.** Press `a` on a server (Servers
|
|
811
|
+
tab or a Search result) to reboot it (`POST /servers/{id}/reboot`) or restart a
|
|
812
|
+
service — Nginx / PHP-FPM / MySQL / Redis (`POST /servers/{id}/services/{svc}/restart`).
|
|
813
|
+
Pick → confirm → the event is tracked to completion in the store, so closing the
|
|
814
|
+
overlay leaves it running and the server's row keeps a spinner (same model as PHP
|
|
815
|
+
upgrades). Reboot's confirmation calls out whole-server downtime for all sites on
|
|
816
|
+
it; a service restart is a brief blip. Needs a Read/Write token.
|
|
817
|
+
- **Reboot visibility + "why".** Servers needing a reboot show a `↻rbt` badge in the
|
|
818
|
+
Servers list (the Dashboard already listed them). Because the API only exposes a
|
|
819
|
+
`reboot_required` boolean with no reason, the actions overlay reads Ubuntu's
|
|
820
|
+
`/var/run/reboot-required` + `.pkgs` over SSH (read-only, reusing the health
|
|
821
|
+
connection) and shows the pending packages — typically a kernel/security update —
|
|
822
|
+
labeled as OS-level context, not SpinupWP's internal logic. ServerDetail's
|
|
823
|
+
"Reboot" field shows the same summary once probed.
|
|
824
|
+
|
|
825
|
+
## [0.3.0] - 2026-06-18
|
|
826
|
+
|
|
827
|
+
### Added
|
|
828
|
+
- **Upgrade a site's PHP version — the first write action.** Press `u` on a site
|
|
829
|
+
(Servers / Stacks / Search) to pick a PHP version and apply it
|
|
830
|
+
(`PUT /sites/{id}/php`); the resulting upgrade event is polled to completion.
|
|
831
|
+
The picker is built dynamically from the (network-refreshed)
|
|
832
|
+
[endoflife.date](https://endoflife.date) schedule — so new releases appear on
|
|
833
|
+
their own — with the current version marked and EOL versions flagged; it is
|
|
834
|
+
**not** filtered to versions already installed, since SpinupWP installs one on
|
|
835
|
+
demand. Tracking lives in the app store, so closing the modal leaves the
|
|
836
|
+
upgrade running: the site's row shows a spinner and target version (`→8.3`)
|
|
837
|
+
until it settles (or flags `⬆!` on failure), and the detail panel mirrors the
|
|
838
|
+
in-progress state. A site whose server has a pending platform upgrade is
|
|
839
|
+
blocked with a pointer to the `w` web deep link.
|
|
840
|
+
- **Search actions mode.** In the Search tab, press `Tab` (or `→`) to hand focus
|
|
841
|
+
from the search box to the selected result's action menu — `o` open, `w`
|
|
842
|
+
SpinupWP, `u` PHP upgrade, `h` health — and `←` / `Esc` to return to typing.
|
|
843
|
+
- **Server upgrade visibility + SpinupWP deep links.** Servers with a pending
|
|
844
|
+
SpinupWP platform upgrade show an `⬆upg` badge in the Servers list and a
|
|
845
|
+
"SpinupWP upgrade" field in the detail panel. Press `w` to open the selected
|
|
846
|
+
server (or site) directly in the SpinupWP web app — useful for actions the API
|
|
847
|
+
can't perform, like running the upgrade. Requires `accountSlug` in config
|
|
848
|
+
(`SPINUPWP_ACCOUNT_SLUG`); falls back to the dashboard root if unset.
|
|
849
|
+
|
|
850
|
+
### Changed
|
|
851
|
+
- **PHP EOL flagging is now date-driven, not a hard-coded version cutoff.** A
|
|
852
|
+
version is flagged once its real end-of-life date is past the current date, so
|
|
853
|
+
it self-corrects as the calendar advances (e.g. 8.2 flips on 2027-01-01 with
|
|
854
|
+
no code change). Dates come from an embedded php.net table (offline default)
|
|
855
|
+
refreshed from [endoflife.date](https://endoflife.date) and cached to
|
|
856
|
+
`~/.config/spinupwp-tui/php-eol.json`; unknown versions are never guessed.
|
|
857
|
+
|
|
858
|
+
### Fixed
|
|
859
|
+
- The `upgrade_required` server field was mislabeled "OS upgrade"; it's a
|
|
860
|
+
SpinupWP **platform** upgrade (not Ubuntu), now labeled accordingly.
|
|
861
|
+
|
|
862
|
+
### Notes
|
|
863
|
+
- The PHP upgrade is the first **write** feature and needs a SpinupWP
|
|
864
|
+
**Read/Write** token. With a Read Only token the action returns a clear "token
|
|
865
|
+
is read-only" message and nothing changes; the entire read path keeps working.
|
|
866
|
+
|
|
867
|
+
## [0.2.0] - 2026-06-18
|
|
868
|
+
|
|
869
|
+
### Added
|
|
870
|
+
- **Site stack detection + Stacks tab (Tier 1).** Classifies every site as
|
|
871
|
+
Standard WP, Bedrock, or Non-WP from data already fetched (no SSH): non-WP →
|
|
872
|
+
Non-WP, WP with a `/web/` webroot → Bedrock, any other WP → Standard WP. Adds
|
|
873
|
+
a color-coded stack tag to the Servers and Search site lists, a "Stack" field
|
|
874
|
+
in the site detail panel, and a new **Stacks** tab (key `3`) showing fleet
|
|
875
|
+
composition (counts + bars), drill-down into the sites of a selected stack,
|
|
876
|
+
and the fleet-wide PHP version distribution with end-of-life versions flagged.
|
|
877
|
+
- **Tier-2 stack probe (`d` / `D`).** On-demand, read-only SSH probe that
|
|
878
|
+
inspects a site's filesystem to name what the API can't: WordPress (with
|
|
879
|
+
version), Bedrock, WHMCS, Laravel, and Static HTML. `d` probes the selected
|
|
880
|
+
site (in the Stacks and Servers tabs); `D` probes the entire selected stack,
|
|
881
|
+
in list order, with bounded SSH concurrency. Detected results show in the
|
|
882
|
+
Stacks list, the Non-WP breakdown, and the site detail panel.
|
|
883
|
+
- **Probe-aware composition.** A conclusive probe overrides the API's
|
|
884
|
+
classification, so sites the API mislabels (e.g. WordPress installed outside
|
|
885
|
+
SpinupWP's installer reports `is_wordpress=false`) move into their true
|
|
886
|
+
bucket; counts shift toward reality as you probe. The Non-WP bucket expands
|
|
887
|
+
into named sub-rows (WHMCS / Laravel / Static HTML / Unknown / unprobed).
|
|
888
|
+
- **Probe cache.** Results are cached to disk
|
|
889
|
+
(`~/.config/spinupwp-tui/stack-cache.json`), hydrated at startup and written
|
|
890
|
+
through (serialized) per probe, so detections survive restarts without
|
|
891
|
+
re-running SSH.
|
|
892
|
+
|
|
893
|
+
### Changed
|
|
894
|
+
- Tab keys shifted to make room for Stacks: Search is now `4`, Events is `5`.
|
|
895
|
+
|
|
896
|
+
### Fixed
|
|
897
|
+
- Capital-letter shortcuts (`D`, and the previously-dead `G` jump-to-bottom) now
|
|
898
|
+
register correctly — OpenTUI delivers letters lowercased with a shift flag, so
|
|
899
|
+
key matching is normalized accordingly.
|
|
900
|
+
|
|
901
|
+
## [0.1.0] - 2026-06-18
|
|
902
|
+
|
|
903
|
+
Initial tagged release.
|
|
904
|
+
|
|
905
|
+
### Added
|
|
906
|
+
- Fleet dashboard: connection status, disk-usage bars, pending reboots/OS
|
|
907
|
+
upgrades, WordPress update counts, and a recent activity feed.
|
|
908
|
+
- Server & site browser: three-pane navigator with full site details (PHP
|
|
909
|
+
version, HTTPS, page cache, backups, Git deployment, WP updates).
|
|
910
|
+
- Global fuzzy search across every server and site by name, domain, or IP.
|
|
911
|
+
- Events feed with per-event detail and output.
|
|
912
|
+
- Live server health view (press `h`): CPU aggregate/per-core/sparkline, load,
|
|
913
|
+
memory/swap, disk mounts, and top processes over SSH.
|
|
914
|
+
- Open-in-browser for sites (press `o`).
|
|
915
|
+
- Global `spinup` command with `login`, `where`, `help`, and `version`
|
|
916
|
+
subcommands, plus first-run onboarding that validates and saves a token.
|
|
917
|
+
|
|
918
|
+
### Notes
|
|
919
|
+
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
920
|
+
|
|
921
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.19.0...HEAD
|
|
922
|
+
[0.19.0]: https://github.com/mwender/spinupwp-tui/compare/v0.18.0...v0.19.0
|
|
923
|
+
[0.18.0]: https://github.com/mwender/spinupwp-tui/compare/v0.17.0...v0.18.0
|
|
924
|
+
[0.17.0]: https://github.com/mwender/spinupwp-tui/compare/v0.16.1...v0.17.0
|
|
925
|
+
[0.16.1]: https://github.com/mwender/spinupwp-tui/compare/v0.16.0...v0.16.1
|
|
926
|
+
[0.16.0]: https://github.com/mwender/spinupwp-tui/compare/v0.15.0...v0.16.0
|
|
927
|
+
[0.15.0]: https://github.com/mwender/spinupwp-tui/compare/v0.14.0...v0.15.0
|
|
928
|
+
[0.14.0]: https://github.com/mwender/spinupwp-tui/compare/v0.13.0...v0.14.0
|
|
929
|
+
[0.13.0]: https://github.com/mwender/spinupwp-tui/compare/v0.12.0...v0.13.0
|
|
930
|
+
[0.12.0]: https://github.com/mwender/spinupwp-tui/compare/v0.11.0...v0.12.0
|
|
931
|
+
[0.11.0]: https://github.com/mwender/spinupwp-tui/compare/v0.10.0...v0.11.0
|
|
932
|
+
[0.10.0]: https://github.com/mwender/spinupwp-tui/compare/v0.9.1...v0.10.0
|
|
933
|
+
[0.9.1]: https://github.com/mwender/spinupwp-tui/compare/v0.9.0...v0.9.1
|
|
934
|
+
[0.9.0]: https://github.com/mwender/spinupwp-tui/compare/v0.8.0...v0.9.0
|
|
935
|
+
[0.8.0]: https://github.com/mwender/spinupwp-tui/compare/v0.7.1...v0.8.0
|
|
936
|
+
[0.7.1]: https://github.com/mwender/spinupwp-tui/compare/v0.7.0...v0.7.1
|
|
937
|
+
[0.7.0]: https://github.com/mwender/spinupwp-tui/compare/v0.6.0...v0.7.0
|
|
938
|
+
[0.6.0]: https://github.com/mwender/spinupwp-tui/compare/v0.5.0...v0.6.0
|
|
939
|
+
[0.5.0]: https://github.com/mwender/spinupwp-tui/compare/v0.4.0...v0.5.0
|
|
940
|
+
[0.4.0]: https://github.com/mwender/spinupwp-tui/compare/v0.3.0...v0.4.0
|
|
941
|
+
[0.3.0]: https://github.com/mwender/spinupwp-tui/compare/v0.2.0...v0.3.0
|
|
942
|
+
[0.2.0]: https://github.com/mwender/spinupwp-tui/compare/v0.1.0...v0.2.0
|
|
943
|
+
[0.1.0]: https://github.com/mwender/spinupwp-tui/releases/tag/v0.1.0
|