spinuptui 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/lib/appUpdate.ts +11 -1
- package/src/ui/App.tsx +6 -0
- package/src/ui/Details.tsx +2 -2
- package/src/ui/components.tsx +9 -2
- package/src/ui/store.tsx +18 -1
- package/src/ui/views/Browser.tsx +6 -3
- package/src/ui/views/EnableLocalSync.tsx +88 -0
- package/src/ui/views/Search.tsx +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,30 @@ versions; such changes are called out here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.24.1] - 2026-08-06
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`bun update -g spinuptui` no longer silently fails to update.** While
|
|
18
|
+
spinuptui is pre-1.0, a package-manager global install records a caret
|
|
19
|
+
version range (e.g. `^0.22.2`) that, under semver, only matches patch
|
|
20
|
+
releases — every later minor release fell outside it, so `update -g`
|
|
21
|
+
reported success without actually updating anything. The documented and
|
|
22
|
+
in-app update command (Help/About panel, README) is now
|
|
23
|
+
`bun add -g spinuptui@latest`, which always pulls the true latest version.
|
|
24
|
+
|
|
25
|
+
## [0.24.0] - 2026-07-15
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`p` (Pull prod. DB) is now discoverable even when `localSync` is off.**
|
|
29
|
+
Previously the Site Control legend hid the entry entirely until `localSync`
|
|
30
|
+
was enabled in config — the only one of `p`'s three preconditions
|
|
31
|
+
(WordPress, linked copy, localSync) that hid the entry instead of flashing
|
|
32
|
+
a message on press. Now `p` always shows for WordPress sites; pressing it
|
|
33
|
+
with `localSync` off opens an in-app "Enable local sync?" confirm instead.
|
|
34
|
+
Confirming just saves the setting — you press `p` again afterward to
|
|
35
|
+
actually pull, so the destructive action itself stays behind two
|
|
36
|
+
deliberate presses, same as before.
|
|
37
|
+
|
|
14
38
|
## [0.23.0] - 2026-07-15
|
|
15
39
|
|
|
16
40
|
### Added
|
|
@@ -1226,7 +1250,9 @@ Initial tagged release.
|
|
|
1226
1250
|
### Notes
|
|
1227
1251
|
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
1228
1252
|
|
|
1229
|
-
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.
|
|
1253
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.24.1...HEAD
|
|
1254
|
+
[0.24.1]: https://github.com/mwender/spinupwp-tui/compare/v0.24.0...v0.24.1
|
|
1255
|
+
[0.24.0]: https://github.com/mwender/spinupwp-tui/compare/v0.23.0...v0.24.0
|
|
1230
1256
|
[0.23.0]: https://github.com/mwender/spinupwp-tui/compare/v0.22.3...v0.23.0
|
|
1231
1257
|
[0.22.3]: https://github.com/mwender/spinupwp-tui/compare/v0.22.2...v0.22.3
|
|
1232
1258
|
[0.22.2]: https://github.com/mwender/spinupwp-tui/compare/v0.22.1...v0.22.2
|
package/README.md
CHANGED
|
@@ -150,8 +150,10 @@ bun run build:binary # produces ./spinuptui — move it onto your PATH
|
|
|
150
150
|
The app tells you when a newer release exists — a gold `✦ vX.Y.Z` appears next
|
|
151
151
|
to the version in the header (and in the `?` About panel).
|
|
152
152
|
|
|
153
|
-
- **Package install:** `bun
|
|
154
|
-
same command).
|
|
153
|
+
- **Package install:** `bun add -g spinuptui@latest` (the About panel shows
|
|
154
|
+
the same command). Don't use `bun update -g` — while spinuptui is pre-1.0,
|
|
155
|
+
it records a caret range (e.g. `^0.22.2`) that excludes every later minor
|
|
156
|
+
release, so `update -g` reports success but silently doesn't update.
|
|
155
157
|
- **Source checkout:** press **`u`** in the About panel to update in place
|
|
156
158
|
(`git pull --ff-only`; refuses if you have uncommitted changes, and never
|
|
157
159
|
merges/rebases). It can't hot-reload the already-running process, so it tells
|
|
@@ -220,6 +222,10 @@ These can be set in `config.json` or via an environment variable:
|
|
|
220
222
|
and stack probes (see "Server health" below).
|
|
221
223
|
- **`localSync`** / `SPINUPWP_LOCAL_SYNC` — opt-in for the **Pull production →
|
|
222
224
|
local** DB sync (`p`); off by default because it overwrites your local database.
|
|
225
|
+
You don't need to set this by hand: pressing `p` on a WordPress site while
|
|
226
|
+
it's off opens an in-app "Enable local sync?" prompt that flips and saves it
|
|
227
|
+
for you — press `p` again afterward to actually pull. Setting it directly is
|
|
228
|
+
still supported for scripted/first-run setups.
|
|
223
229
|
**Prefer `"localSync": true` in `config.json`** — it's read from a fixed path
|
|
224
230
|
(`~/.config/spinupwp-tui/config.json`) so it applies wherever you launch `spinuptui`
|
|
225
231
|
from. The environment variable works too, but note a `.env` is only loaded when
|
package/package.json
CHANGED
package/src/lib/appUpdate.ts
CHANGED
|
@@ -139,7 +139,17 @@ export function installChannel(): InstallChannel {
|
|
|
139
139
|
|
|
140
140
|
// The one-liner Help shows for a package install (also used by runSelfUpdate's
|
|
141
141
|
// "not a checkout" message so every path names the same command).
|
|
142
|
-
|
|
142
|
+
//
|
|
143
|
+
// Deliberately `add ... @latest`, not `update -g`: while spinuptui is pre-1.0,
|
|
144
|
+
// `bun add -g spinuptui` (no version) records a caret range like `^0.22.2`,
|
|
145
|
+
// which per semver only matches 0.22.x. Every minor release (0.23.0, 0.24.0, ...)
|
|
146
|
+
// falls outside that range, so `bun update -g` silently no-ops forever — it
|
|
147
|
+
// re-resolves within the stale range and finds nothing newer. `@latest` bypasses
|
|
148
|
+
// the recorded range entirely and rewrites it to match, so the *next* update
|
|
149
|
+
// hits the same wall until the following minor bump. Verified live against a
|
|
150
|
+
// real stale install (0.22.2 -> 0.24.0): `update -g` exited 0 without updating,
|
|
151
|
+
// `add -g spinuptui@latest` updated correctly.
|
|
152
|
+
export const PACKAGE_UPDATE_CMD = "bun add -g spinuptui@latest"
|
|
143
153
|
|
|
144
154
|
// The real checkout directory, resolved through Bun's module system rather
|
|
145
155
|
// than process.cwd() — `spinuptui` is typically a global symlink invoked from
|
package/src/ui/App.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import { GrantKey } from "./views/GrantKey.tsx"
|
|
|
23
23
|
import { SudoConnect } from "./views/SudoConnect.tsx"
|
|
24
24
|
import { DbBackup } from "./views/DbBackup.tsx"
|
|
25
25
|
import { DbSync } from "./views/DbSync.tsx"
|
|
26
|
+
import { EnableLocalSync } from "./views/EnableLocalSync.tsx"
|
|
26
27
|
import { MediaFallback } from "./views/MediaFallback.tsx"
|
|
27
28
|
import { ServerActions } from "./views/ServerActions.tsx"
|
|
28
29
|
import { NewServer } from "./views/NewServer.tsx"
|
|
@@ -67,6 +68,7 @@ export function App() {
|
|
|
67
68
|
store.sudoConnectServer !== null ||
|
|
68
69
|
store.dbBackupSite !== null ||
|
|
69
70
|
store.dbSyncSite !== null ||
|
|
71
|
+
store.enableLocalSyncSite !== null ||
|
|
70
72
|
store.mediaFallbackSite !== null ||
|
|
71
73
|
store.serverActionsServer !== null ||
|
|
72
74
|
store.newServerOpen ||
|
|
@@ -143,6 +145,9 @@ export function App() {
|
|
|
143
145
|
// The DB-sync overlay owns the keyboard while open.
|
|
144
146
|
if (store.dbSyncSite) return
|
|
145
147
|
|
|
148
|
+
// The "enable local sync?" confirm overlay owns the keyboard while open.
|
|
149
|
+
if (store.enableLocalSyncSite) return
|
|
150
|
+
|
|
146
151
|
// The media-fallback overlay owns the keyboard while open.
|
|
147
152
|
if (store.mediaFallbackSite) return
|
|
148
153
|
|
|
@@ -244,6 +249,7 @@ export function App() {
|
|
|
244
249
|
{store.sudoConnectServer && <SudoConnect />}
|
|
245
250
|
{store.dbBackupSite && <DbBackup />}
|
|
246
251
|
{store.dbSyncSite && <DbSync />}
|
|
252
|
+
{store.enableLocalSyncSite && <EnableLocalSync />}
|
|
247
253
|
{store.mediaFallbackSite && <MediaFallback />}
|
|
248
254
|
{store.serverActionsServer && <ServerActions />}
|
|
249
255
|
{store.newServerOpen && <NewServer />}
|
package/src/ui/Details.tsx
CHANGED
|
@@ -276,11 +276,11 @@ export function SiteDetail({ site, serverName }: { site: Site; serverName: strin
|
|
|
276
276
|
// Site Control groups — see ServerControl above. Recomputes stack/vanity
|
|
277
277
|
// itself since it's not a child of SiteDetail; both derivations are cheap.
|
|
278
278
|
export function SiteControl({ site, serverName, layout }: { site: Site; serverName: string; layout?: "list" | "flow" }) {
|
|
279
|
-
const { probes
|
|
279
|
+
const { probes } = useStore()
|
|
280
280
|
const stack = effectiveStack(site, probes.get(site.id)?.result.kind)
|
|
281
281
|
const isWordPress = stack !== "Non-WP"
|
|
282
282
|
const isVanity = isVanityPair(site.domain, serverName)
|
|
283
|
-
return <ControlPanel heading="Site Control" groups={siteGroups(isWordPress,
|
|
283
|
+
return <ControlPanel heading="Site Control" groups={siteGroups(isWordPress, isVanity)} layout={layout} />
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
// DNS zone-host lines for a site's domains, populated on demand (key `n`). Each
|
package/src/ui/components.tsx
CHANGED
|
@@ -434,7 +434,14 @@ export function ControlPanel({ heading, groups, layout = "list" }: { heading: st
|
|
|
434
434
|
// own server's name, per `isVanityPair`) adds the one-off page-refresh action —
|
|
435
435
|
// vanity pages seeded before this app existed need a way to pick up the current
|
|
436
436
|
// bundled HTML.
|
|
437
|
-
|
|
437
|
+
//
|
|
438
|
+
// `p` (Pull prod. DB) is always shown for WordPress sites, even when the
|
|
439
|
+
// localSync config flag is off — pressing it then opens a confirm overlay to
|
|
440
|
+
// enable the flag, rather than hiding the entry entirely. That matches how
|
|
441
|
+
// the OTHER two gates on this same action already behave (is_wordpress and
|
|
442
|
+
// "not linked" both flash on press instead of hiding the entry); localSync
|
|
443
|
+
// used to be the odd one out.
|
|
444
|
+
export function siteGroups(isWordpress: boolean, isVanity: boolean): ActionGroup[] {
|
|
438
445
|
const remote: [string, string][] = [
|
|
439
446
|
["s", "SSH"],
|
|
440
447
|
["K", "Grant SSH key"],
|
|
@@ -443,7 +450,7 @@ export function siteGroups(isWordpress: boolean, localSync: boolean, isVanity: b
|
|
|
443
450
|
]
|
|
444
451
|
if (isWordpress) {
|
|
445
452
|
remote.push(["d", "↓ DB backup"])
|
|
446
|
-
|
|
453
|
+
remote.push(["p", "Pull prod. DB"])
|
|
447
454
|
remote.push(["e", "Plugins & themes"])
|
|
448
455
|
}
|
|
449
456
|
const local: [string, string][] = [
|
package/src/ui/store.tsx
CHANGED
|
@@ -728,6 +728,12 @@ interface StoreValue extends DataState {
|
|
|
728
728
|
sshUser: string | null
|
|
729
729
|
// Opt-in for the destructive production→local DB sync (`p`); default off.
|
|
730
730
|
localSync: boolean
|
|
731
|
+
// The site `p` was pressed on while localSync was off — drives the "enable
|
|
732
|
+
// local sync?" confirm overlay. null when the overlay is closed.
|
|
733
|
+
enableLocalSyncSite: Site | null
|
|
734
|
+
setEnableLocalSyncSite: (s: Site | null) => void
|
|
735
|
+
// Persists localSync=true to config.json and flips the reactive flag above.
|
|
736
|
+
enableLocalSync: () => void
|
|
731
737
|
// SpinupWP account slug (from env/config) for building web deep links.
|
|
732
738
|
accountSlug: string | null
|
|
733
739
|
sitesForServer: (serverId: number) => Site[]
|
|
@@ -997,6 +1003,8 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
997
1003
|
const [dbBackups, setDbBackups] = useState<Map<number, DbBackupProgress>>(new Map())
|
|
998
1004
|
const [dbSyncSite, setDbSyncSite] = useState<Site | null>(null)
|
|
999
1005
|
const [dbSyncs, setDbSyncs] = useState<Map<number, DbSyncProgress>>(new Map())
|
|
1006
|
+
const [localSync, setLocalSyncState] = useState<boolean>(() => cfgRef.current.localSync)
|
|
1007
|
+
const [enableLocalSyncSite, setEnableLocalSyncSite] = useState<Site | null>(null)
|
|
1000
1008
|
const [mediaFallbackSite, setMediaFallbackSite] = useState<Site | null>(null)
|
|
1001
1009
|
const [localRoots, setLocalRoots] = useState<string[]>(() => [...cfgRef.current.localRoots])
|
|
1002
1010
|
const [discoverOpen, setDiscoverOpen] = useState(false)
|
|
@@ -1728,6 +1736,12 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1728
1736
|
})
|
|
1729
1737
|
}, [])
|
|
1730
1738
|
|
|
1739
|
+
const enableLocalSync = useCallback(() => {
|
|
1740
|
+
cfgRef.current.localSync = true
|
|
1741
|
+
void saveConfig({ localSync: true })
|
|
1742
|
+
setLocalSyncState(true)
|
|
1743
|
+
}, [])
|
|
1744
|
+
|
|
1731
1745
|
const openLocalTerminal = useCallback(
|
|
1732
1746
|
(siteId: number) => {
|
|
1733
1747
|
const link = localLinks.get(siteId)
|
|
@@ -4326,7 +4340,10 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4326
4340
|
rebootInfoErrors,
|
|
4327
4341
|
loadRebootInfo,
|
|
4328
4342
|
sshUser: cfgRef.current.sshUser,
|
|
4329
|
-
localSync
|
|
4343
|
+
localSync,
|
|
4344
|
+
enableLocalSyncSite,
|
|
4345
|
+
setEnableLocalSyncSite,
|
|
4346
|
+
enableLocalSync,
|
|
4330
4347
|
accountSlug: cfgRef.current.accountSlug,
|
|
4331
4348
|
sitesForServer,
|
|
4332
4349
|
serverById,
|
package/src/ui/views/Browser.tsx
CHANGED
|
@@ -23,7 +23,7 @@ type Focus = "servers" | "sites"
|
|
|
23
23
|
|
|
24
24
|
export function Browser({ rows }: { rows: number }) {
|
|
25
25
|
const store = useStore()
|
|
26
|
-
const { servers, sitesForServer, route, inputMode, overlayOpen, setHealthServer, setWpInventorySite, runProbe, probes, accountSlug, setPhpUpgradeSite, phpUpgrades, setHttpsToggleSite, setPurgeCacheSite, setGrantKeySite, setSudoConnectServer, isSudoConnected, setServerActionsServer, serverOps, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setNewServerSource, setNewServerOpen, setVanityServer, vanityJob, beginClone, isServerOsEol, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setDbSyncSite, localSync, localLinks, setDbBackupSite, setMediaFallbackSite } = store
|
|
26
|
+
const { servers, sitesForServer, route, inputMode, overlayOpen, setHealthServer, setWpInventorySite, runProbe, probes, accountSlug, setPhpUpgradeSite, phpUpgrades, setHttpsToggleSite, setPurgeCacheSite, setGrantKeySite, setSudoConnectServer, isSudoConnected, setServerActionsServer, serverOps, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setNewServerSource, setNewServerOpen, setVanityServer, vanityJob, beginClone, isServerOsEol, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setDbSyncSite, localSync, localLinks, setDbBackupSite, setMediaFallbackSite, setEnableLocalSyncSite } = store
|
|
27
27
|
|
|
28
28
|
const [serverIndex, setServerIndex] = useState(0)
|
|
29
29
|
const [siteIndex, setSiteIndex] = useState(0)
|
|
@@ -203,11 +203,14 @@ export function Browser({ rows }: { rows: number }) {
|
|
|
203
203
|
// Pull production → local (opt-in, overwrites the local DB): needs a
|
|
204
204
|
// WordPress site, localSync enabled, and a linked local copy. Same
|
|
205
205
|
// gate as Search's `p` (Search.tsx) so the Site Control legend's
|
|
206
|
-
// "Pull prod. DB" label is accurate here too.
|
|
206
|
+
// "Pull prod. DB" label is accurate here too. A localSync-off site
|
|
207
|
+
// opens a confirm overlay to enable it (rather than a dead-end flash)
|
|
208
|
+
// since it's now always shown in the legend, not hidden until enabled.
|
|
207
209
|
const s = focus === "sites" ? sites[siteIndex] : undefined
|
|
208
210
|
if (!s) return
|
|
209
211
|
if (!localSync) {
|
|
210
|
-
|
|
212
|
+
setEnableLocalSyncSite(s)
|
|
213
|
+
return
|
|
211
214
|
} else if (!s.is_wordpress) {
|
|
212
215
|
setFlash("Sync needs WordPress (wp-cli) — this isn't a WP site")
|
|
213
216
|
} else if (!localLinks.has(s.id)) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// "Enable local sync?" confirm overlay — opened when `p` (Pull prod. DB) is
|
|
2
|
+
// pressed on a site while the localSync config flag is off. localSync is a
|
|
3
|
+
// deliberate opt-in (pulling production overwrites the local database), so
|
|
4
|
+
// confirming here only flips the flag and saves it — it does NOT chain into
|
|
5
|
+
// the pull itself. The user presses `p` again afterward to actually run it,
|
|
6
|
+
// which still has its own confirm-before-firing step. Keeps the destructive
|
|
7
|
+
// action behind two deliberate presses, same safety margin as before, while
|
|
8
|
+
// making the feature discoverable instead of a silent config-file-only switch.
|
|
9
|
+
|
|
10
|
+
import { useState } from "react"
|
|
11
|
+
import { useKeyboard } from "@opentui/react"
|
|
12
|
+
import { theme } from "../../lib/theme.ts"
|
|
13
|
+
import { truncate } from "../../lib/format.ts"
|
|
14
|
+
import { Panel, Centered } from "../components.tsx"
|
|
15
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
16
|
+
import { useStore } from "../store.tsx"
|
|
17
|
+
|
|
18
|
+
type Phase = "confirm" | "done"
|
|
19
|
+
|
|
20
|
+
export function EnableLocalSync() {
|
|
21
|
+
const { enableLocalSyncSite: site, setEnableLocalSyncSite, enableLocalSync } = useStore()
|
|
22
|
+
const [phase, setPhase] = useState<Phase>("confirm")
|
|
23
|
+
|
|
24
|
+
const close = () => setEnableLocalSyncSite(null)
|
|
25
|
+
|
|
26
|
+
useKeyboard((key) => {
|
|
27
|
+
const name = key.name ?? ""
|
|
28
|
+
if (phase === "confirm") {
|
|
29
|
+
if (name === "y") {
|
|
30
|
+
enableLocalSync()
|
|
31
|
+
setPhase("done")
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if (name === "escape" || name === "q") return close()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
// "done" — any key closes.
|
|
38
|
+
return close()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!site) return null
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<box style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", flexDirection: "column", backgroundColor: theme.bg, zIndex: 210 }}>
|
|
45
|
+
<box style={{ flexDirection: "row", height: 1, backgroundColor: theme.bgAlt, paddingLeft: 1, paddingRight: 1, alignItems: "center" }}>
|
|
46
|
+
<text content="↓ Local sync " fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
47
|
+
<text content={truncate(site.domain, 40)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
48
|
+
</box>
|
|
49
|
+
|
|
50
|
+
<Centered>{renderBody()}</Centered>
|
|
51
|
+
|
|
52
|
+
<StatusBar hints={phase === "confirm" ? [{ key: "y", label: "enable" }, { key: "esc", label: "cancel" }] : [{ key: "any", label: "close" }]} showGlobal={false} />
|
|
53
|
+
</box>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
function renderBody() {
|
|
57
|
+
if (phase === "confirm") {
|
|
58
|
+
return (
|
|
59
|
+
<Panel title=" Enable local sync? " active>
|
|
60
|
+
<box style={{ flexDirection: "column", width: 62, paddingTop: 1, paddingBottom: 1 }}>
|
|
61
|
+
<text content="Local sync is off — it's opt-in because pulling production" fg={theme.text} wrapMode="none" />
|
|
62
|
+
<text content="overwrites your LOCAL database." fg={theme.text} wrapMode="none" />
|
|
63
|
+
<box style={{ height: 1 }} />
|
|
64
|
+
<text content="Enabling it just flips the setting — it won't pull anything" fg={theme.textDim} wrapMode="none" />
|
|
65
|
+
<text content="yet. Press p again afterward to actually pull production." fg={theme.textDim} wrapMode="none" />
|
|
66
|
+
<box style={{ height: 1 }} />
|
|
67
|
+
<text content="Press y to enable · Esc to cancel" fg={theme.textFaint} wrapMode="none" />
|
|
68
|
+
</box>
|
|
69
|
+
</Panel>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Panel title=" Done " active>
|
|
75
|
+
<box style={{ flexDirection: "column", width: 56, paddingTop: 1, paddingBottom: 1 }}>
|
|
76
|
+
<box style={{ flexDirection: "row" }}>
|
|
77
|
+
<text content="✓ " fg={theme.good} />
|
|
78
|
+
<text content="Local sync enabled" fg={theme.text} wrapMode="none" />
|
|
79
|
+
</box>
|
|
80
|
+
<box style={{ height: 1 }} />
|
|
81
|
+
<text content="Press p again to pull production into your local copy." fg={theme.textDim} wrapMode="none" />
|
|
82
|
+
<box style={{ height: 1 }} />
|
|
83
|
+
<text content="Press any key to close" fg={theme.textFaint} />
|
|
84
|
+
</box>
|
|
85
|
+
</Panel>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/ui/views/Search.tsx
CHANGED
|
@@ -35,7 +35,7 @@ function score(haystack: string, q: string): number | null {
|
|
|
35
35
|
|
|
36
36
|
export function Search({ rows }: { rows: number }) {
|
|
37
37
|
const store = useStore()
|
|
38
|
-
const { servers, sites, serverById, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setServerActionsServer, accountSlug, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe } = store
|
|
38
|
+
const { servers, sites, serverById, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setServerActionsServer, accountSlug, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe, setEnableLocalSyncSite } = store
|
|
39
39
|
const [query, setQuery] = useState("")
|
|
40
40
|
const [selected, setSelected] = useState(0)
|
|
41
41
|
// "query" = typing/filtering (input focused); "actions" = input blurred so the
|
|
@@ -179,9 +179,11 @@ export function Search({ rows }: { rows: number }) {
|
|
|
179
179
|
return
|
|
180
180
|
case "p":
|
|
181
181
|
// Pull production → local: opt-in (it overwrites the local DB), and needs
|
|
182
|
-
// a WordPress site + a local link.
|
|
182
|
+
// a WordPress site + a local link. A localSync-off site opens a confirm
|
|
183
|
+
// overlay to enable it (rather than a dead-end flash) since it's now
|
|
184
|
+
// always shown in the legend, not hidden until enabled.
|
|
183
185
|
if (current?.kind === "site") {
|
|
184
|
-
if (!localSync)
|
|
186
|
+
if (!localSync) setEnableLocalSyncSite(current.site)
|
|
185
187
|
else if (!current.site.is_wordpress) flashMsg("Sync needs WordPress (wp-cli) — this isn't a WP site")
|
|
186
188
|
else if (!localLinks.has(current.site.id)) flashMsg("Not linked — press L to link a local copy")
|
|
187
189
|
else setDbSyncSite(current.site)
|
|
@@ -379,7 +381,6 @@ export function Search({ rows }: { rows: number }) {
|
|
|
379
381
|
<ActionsCard
|
|
380
382
|
result={current}
|
|
381
383
|
serverName={current.kind === "site" ? (serverById(current.site.server_id)?.name ?? "—") : current.server.name}
|
|
382
|
-
localSync={localSync}
|
|
383
384
|
/>
|
|
384
385
|
) : current.kind === "server" ? (
|
|
385
386
|
<ServerDetail server={current.server} siteCount={store.sitesForServer(current.server.id).length} />
|
|
@@ -394,12 +395,12 @@ export function Search({ rows }: { rows: number }) {
|
|
|
394
395
|
)
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
function ActionsCard({ result, serverName
|
|
398
|
+
function ActionsCard({ result, serverName }: { result: Result; serverName: string }) {
|
|
398
399
|
const isSite = result.kind === "site"
|
|
399
400
|
const name = isSite ? result.site.domain : result.server.name
|
|
400
401
|
const status = isSite ? result.site.status : result.server.connection_status
|
|
401
402
|
// Server results reuse the SAME Server Control suite as the Browser detail pane.
|
|
402
|
-
const groups = isSite ? siteGroups(result.site.is_wordpress,
|
|
403
|
+
const groups = isSite ? siteGroups(result.site.is_wordpress, isVanityPair(result.site.domain, serverName)) : SERVER_CONTROL
|
|
403
404
|
return (
|
|
404
405
|
<box style={{ flexDirection: "column" }}>
|
|
405
406
|
<box style={{ flexDirection: "row" }}>
|