spinuptui 0.27.0 → 0.28.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 +11 -1
- package/README.md +6 -2
- package/package.json +1 -1
- package/src/config.ts +232 -8
- package/src/index.tsx +25 -5
- package/src/lib/dnsCache.ts +9 -6
- package/src/lib/keychain.ts +11 -5
- package/src/lib/providersCache.ts +9 -6
- package/src/lib/stackCache.ts +9 -6
- package/src/lib/wpCli.ts +2 -2
- package/src/ui/App.tsx +10 -0
- package/src/ui/Header.tsx +5 -1
- package/src/ui/Help.tsx +1 -0
- package/src/ui/accounts.tsx +21 -0
- package/src/ui/store.tsx +65 -20
- package/src/ui/views/Accounts.tsx +370 -0
- package/src/ui/views/Stacks.tsx +31 -6
- package/src/ui/wpCoreJobs.tsx +18 -0
package/src/ui/App.tsx
CHANGED
|
@@ -39,6 +39,7 @@ import { WpInventory } from "./views/WpInventory.tsx"
|
|
|
39
39
|
import { ProviderConnect } from "./views/ProviderConnect.tsx"
|
|
40
40
|
import { DnsRecords } from "./views/DnsRecords.tsx"
|
|
41
41
|
import { KumaSite } from "./views/KumaSite.tsx"
|
|
42
|
+
import { Accounts } from "./views/Accounts.tsx"
|
|
42
43
|
|
|
43
44
|
const MIN_SPLASH_MS = 1200
|
|
44
45
|
|
|
@@ -49,6 +50,7 @@ export function App() {
|
|
|
49
50
|
const [splashDone, setSplashDone] = useState(false)
|
|
50
51
|
const [showHelp, setShowHelp] = useState(false)
|
|
51
52
|
const [showExplain, setShowExplain] = useState(false)
|
|
53
|
+
const [showAccounts, setShowAccounts] = useState(false)
|
|
52
54
|
|
|
53
55
|
// Enforce a minimum splash duration so the intro is visible even on fast loads.
|
|
54
56
|
useEffect(() => {
|
|
@@ -60,6 +62,7 @@ export function App() {
|
|
|
60
62
|
const overlayActive =
|
|
61
63
|
showHelp ||
|
|
62
64
|
showExplain ||
|
|
65
|
+
showAccounts ||
|
|
63
66
|
store.releaseNotesInfo !== null ||
|
|
64
67
|
store.healthServer !== null ||
|
|
65
68
|
store.wpInventorySite !== null ||
|
|
@@ -119,6 +122,9 @@ export function App() {
|
|
|
119
122
|
// While a text field is focused, let it consume everything else.
|
|
120
123
|
if (store.inputMode) return
|
|
121
124
|
|
|
125
|
+
// The Accounts overlay owns the keyboard while open.
|
|
126
|
+
if (showAccounts) return
|
|
127
|
+
|
|
122
128
|
// The release-notes overlay owns the keyboard while open (any key dismisses).
|
|
123
129
|
if (store.releaseNotesInfo) return
|
|
124
130
|
|
|
@@ -194,6 +200,9 @@ export function App() {
|
|
|
194
200
|
// The site-monitoring (Uptime Kuma) overlay owns the keyboard while open.
|
|
195
201
|
if (store.kumaSite) return
|
|
196
202
|
|
|
203
|
+
// A (Shift+a): the Accounts overlay — the header advertises it from every tab.
|
|
204
|
+
if (key.shift && key.name === "a") return setShowAccounts(true)
|
|
205
|
+
|
|
197
206
|
switch (key.name) {
|
|
198
207
|
case "q":
|
|
199
208
|
return quit()
|
|
@@ -274,6 +283,7 @@ export function App() {
|
|
|
274
283
|
{store.connectZoneTarget && <ProviderConnect />}
|
|
275
284
|
{store.dnsRecordsTarget && <DnsRecords />}
|
|
276
285
|
{store.kumaSite && <KumaSite />}
|
|
286
|
+
{showAccounts && <Accounts onClose={() => setShowAccounts(false)} />}
|
|
277
287
|
<ToastStack />
|
|
278
288
|
</box>
|
|
279
289
|
)
|
package/src/ui/Header.tsx
CHANGED
|
@@ -27,7 +27,7 @@ const SUBTITLES: Record<Route, string> = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function Header() {
|
|
30
|
-
const { route, servers, sites, loading, lastUpdated, updateInfo, newServerJob, vanityJob, cloneJob, cloneServer, kumaStatus } = useStore()
|
|
30
|
+
const { route, servers, sites, loading, lastUpdated, updateInfo, newServerJob, vanityJob, cloneJob, cloneServer, kumaStatus, profileLabel } = useStore()
|
|
31
31
|
// Name the domain(s) when there are few enough to fit the header row; fall
|
|
32
32
|
// back to a bare count once there are too many to list safely. Deliberately
|
|
33
33
|
// still just the plain up/down signal (kumaStatus.up) — not expanding what
|
|
@@ -119,6 +119,10 @@ export function Header() {
|
|
|
119
119
|
<box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1, alignItems: "center" }}>
|
|
120
120
|
<text content="💡 " fg={theme.accent} style={{ flexShrink: 0 }} />
|
|
121
121
|
<text content={SUBTITLES[route]} fg={theme.textDim} wrapMode="none" style={{ flexGrow: 1, flexShrink: 1 }} />
|
|
122
|
+
{/* Always visible, so it's never a guess which client's fleet this is. */}
|
|
123
|
+
<text content=" Account: " fg={theme.textFaint} style={{ flexShrink: 0 }} />
|
|
124
|
+
<text content={truncate(profileLabel, 32)} fg={theme.text} wrapMode="none" style={{ flexShrink: 0 }} />
|
|
125
|
+
<text content=" A switch" fg={theme.textFaint} style={{ flexShrink: 0 }} />
|
|
122
126
|
</box>
|
|
123
127
|
</box>
|
|
124
128
|
)
|
package/src/ui/Help.tsx
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Account switching lives ABOVE the store: switching remounts <StoreProvider>
|
|
2
|
+
// keyed by the account, which is what guarantees a clean slate — every effect,
|
|
3
|
+
// poller and interval of the outgoing account is torn down by React, and every
|
|
4
|
+
// id-keyed cache (servers, sites, probes, jobs…) starts empty for the new one.
|
|
5
|
+
// See config.ts "Accounts" for how the profiles are stored.
|
|
6
|
+
|
|
7
|
+
import { createContext, useContext } from "react"
|
|
8
|
+
|
|
9
|
+
export interface AccountsApi {
|
|
10
|
+
// Make `id` the active account and remount the app on it. The caller checks
|
|
11
|
+
// the store's switchBlocker() first — nothing here waits for running work.
|
|
12
|
+
switchAccount: (id: string) => Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AccountsContext = createContext<AccountsApi | null>(null)
|
|
16
|
+
|
|
17
|
+
export function useAccounts(): AccountsApi {
|
|
18
|
+
const ctx = useContext(AccountsContext)
|
|
19
|
+
if (!ctx) throw new Error("useAccounts must be used within <AccountsContext.Provider>")
|
|
20
|
+
return ctx
|
|
21
|
+
}
|
package/src/ui/store.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { SpinupWPClient, ApiError, type ServerService, type SpinupWPClientLike }
|
|
|
11
11
|
import { isDevMode } from "../dev/devMode.ts"
|
|
12
12
|
import { createMockClient } from "../dev/mockClient.ts"
|
|
13
13
|
import type { Server, Site, Event, ProviderMetadata, CreateServerPayload, CreateSitePayload, AdditionalDomain } from "../api/types.ts"
|
|
14
|
-
import { loadConfig, saveConfig, type ServerProviderRef, type KumaMonitorRef, type UptimeKumaConn } from "../config.ts"
|
|
14
|
+
import { loadConfig, saveConfig, type StoredConfig, type ServerProviderRef, type KumaMonitorRef, type UptimeKumaConn } from "../config.ts"
|
|
15
15
|
import { saveJob, removeJob } from "../lib/jobs.ts"
|
|
16
16
|
import { APP_VERSION } from "../version.ts"
|
|
17
17
|
import { cachedUpdateInfo, refreshUpdateInfo, type UpdateInfo } from "../lib/appUpdate.ts"
|
|
@@ -53,9 +53,10 @@ import { keychainAvailable, setSudoPassword, getSudoPassword, deleteSudoPassword
|
|
|
53
53
|
import { StackCache, siteSignature, type CachedProbe } from "../lib/stackCache.ts"
|
|
54
54
|
import { resolvePhpEolDates, refreshPhpEolDates, isPhpEol as isPhpEolWith, offeredPhpVersions as offeredPhpVersionsWith, type PhpEolDates } from "../lib/phpEol.ts"
|
|
55
55
|
import { resolveUbuntuEolDates, refreshUbuntuEolDates, isUbuntuEol as isUbuntuEolWith, type UbuntuEolDates } from "../lib/ubuntuEol.ts"
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
56
|
+
import { wpCoreBusy } from "./wpCoreJobs.tsx"
|
|
57
|
+
import { planDbBackup, runDbBackup, isDbBackupInFlight, type DbBackupProgress, type PlanResult } from "../lib/dbBackup.ts"
|
|
58
|
+
import { planDbSync, runDbSync, isDbSyncInFlight, type DbSyncProgress, type SyncPlanResult } from "../lib/dbSync.ts"
|
|
59
|
+
import { planLocalSetup, runLocalSetup, isLocalSetupInFlight, type LocalSetupProgress, type LocalSetupPlanResult } from "../lib/localSetup.ts"
|
|
59
60
|
import { planMediaFallback, type MediaFallbackResult } from "../lib/mediaFallback.ts"
|
|
60
61
|
|
|
61
62
|
export type Route = "dashboard" | "servers" | "stacks" | "search" | "events"
|
|
@@ -754,6 +755,18 @@ interface StoreValue extends DataState {
|
|
|
754
755
|
enableLocalSync: () => void
|
|
755
756
|
// SpinupWP account slug (from env/config) for building web deep links.
|
|
756
757
|
accountSlug: string | null
|
|
758
|
+
// The account (profile) this store is mounted for — see config.ts "Accounts".
|
|
759
|
+
// Switching remounts the whole store, so these never change under a view.
|
|
760
|
+
profileId: string
|
|
761
|
+
profileLabel: string
|
|
762
|
+
setProfileLabel: (label: string) => void // after a rename in the Accounts overlay
|
|
763
|
+
// The token came from SPINUPWP_ACCESS_TOKEN, which pins the account: switching
|
|
764
|
+
// would change nothing until it's unset.
|
|
765
|
+
tokenFromEnv: boolean
|
|
766
|
+
// What's still running that an account switch would orphan, in words — or
|
|
767
|
+
// null when it's safe to switch. Only work that writes (to SpinupWP, a server,
|
|
768
|
+
// DNS, or a local database) counts; reads and probes are simply dropped.
|
|
769
|
+
switchBlocker: () => string | null
|
|
757
770
|
sitesForServer: (serverId: number) => Site[]
|
|
758
771
|
serverById: (id: number | null | undefined) => Server | undefined
|
|
759
772
|
// Tier-2 stack probes (on-demand SSH), hydrated from disk at startup.
|
|
@@ -895,6 +908,9 @@ export function useStore(): StoreValue {
|
|
|
895
908
|
|
|
896
909
|
export function StoreProvider({ children }: { children: ReactNode }) {
|
|
897
910
|
const cfgRef = useRef(loadConfig())
|
|
911
|
+
// Every config write from this store targets the account it was mounted for —
|
|
912
|
+
// a late write from an async flow can't land in an account switched to since.
|
|
913
|
+
const saveProfileConfig = (partial: StoredConfig) => saveConfig(partial, cfgRef.current.profileId)
|
|
898
914
|
const clientRef = useRef<SpinupWPClientLike | null>(null)
|
|
899
915
|
if (!clientRef.current) {
|
|
900
916
|
clientRef.current = isDevMode() ? createMockClient() : new SpinupWPClient(cfgRef.current)
|
|
@@ -927,7 +943,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
927
943
|
// Fresh install, or upgrading from a pre-this-feature version — nothing
|
|
928
944
|
// meaningful to announce. Seed silently so future bumps trigger correctly.
|
|
929
945
|
cfgRef.current.lastSeenVersion = APP_VERSION
|
|
930
|
-
void
|
|
946
|
+
void saveProfileConfig({ lastSeenVersion: APP_VERSION })
|
|
931
947
|
return
|
|
932
948
|
}
|
|
933
949
|
void fetchReleaseNotes(APP_VERSION).then((notes) => {
|
|
@@ -939,7 +955,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
939
955
|
const dismissReleaseNotes = useCallback(() => {
|
|
940
956
|
setReleaseNotesInfo(null)
|
|
941
957
|
cfgRef.current.lastSeenVersion = APP_VERSION
|
|
942
|
-
void
|
|
958
|
+
void saveProfileConfig({ lastSeenVersion: APP_VERSION })
|
|
943
959
|
}, [])
|
|
944
960
|
// On-demand replay (Help overlay's `n`) — bypasses the "seen" gate entirely,
|
|
945
961
|
// since viewing the current version's notes again is always valid regardless
|
|
@@ -951,6 +967,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
951
967
|
if (notes) setReleaseNotesInfo(notes)
|
|
952
968
|
})
|
|
953
969
|
}, [])
|
|
970
|
+
const [profileLabel, setProfileLabel] = useState(cfgRef.current.profileLabel)
|
|
954
971
|
const [route, setRoute] = useState<Route>("dashboard")
|
|
955
972
|
const [inputMode, setInputMode] = useState(false)
|
|
956
973
|
const [overlayOpen, setOverlayOpen] = useState(false)
|
|
@@ -1640,7 +1657,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1640
1657
|
const next = { ...prev, [providerKey]: { ...(prev[providerKey] ?? {}), id } }
|
|
1641
1658
|
cfgRef.current.serverProviders = next
|
|
1642
1659
|
setServerProviders(next)
|
|
1643
|
-
void
|
|
1660
|
+
void saveProfileConfig({ serverProviders: next })
|
|
1644
1661
|
}, [])
|
|
1645
1662
|
|
|
1646
1663
|
// Poll a server-create event to completion, mirroring its status into the job
|
|
@@ -1734,7 +1751,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1734
1751
|
const record: Record<string, LocalLink> = {}
|
|
1735
1752
|
for (const [id, link] of next) record[String(id)] = link
|
|
1736
1753
|
cfgRef.current.localSites = record
|
|
1737
|
-
void
|
|
1754
|
+
void saveProfileConfig({ localSites: record })
|
|
1738
1755
|
}, [])
|
|
1739
1756
|
|
|
1740
1757
|
const linkSite = useCallback(
|
|
@@ -1766,14 +1783,14 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1766
1783
|
if (prev.includes(trimmed)) return prev
|
|
1767
1784
|
const next = [...prev, trimmed]
|
|
1768
1785
|
cfgRef.current.localRoots = next
|
|
1769
|
-
void
|
|
1786
|
+
void saveProfileConfig({ localRoots: next })
|
|
1770
1787
|
return next
|
|
1771
1788
|
})
|
|
1772
1789
|
}, [])
|
|
1773
1790
|
|
|
1774
1791
|
const enableLocalSync = useCallback(() => {
|
|
1775
1792
|
cfgRef.current.localSync = true
|
|
1776
|
-
void
|
|
1793
|
+
void saveProfileConfig({ localSync: true })
|
|
1777
1794
|
setLocalSyncState(true)
|
|
1778
1795
|
}, [])
|
|
1779
1796
|
|
|
@@ -1823,7 +1840,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1823
1840
|
const record: Record<string, { user: string; keychain?: boolean }> = {}
|
|
1824
1841
|
for (const [id, u] of users) record[String(id)] = saved.has(id) ? { user: u, keychain: true } : { user: u }
|
|
1825
1842
|
cfgRef.current.sudoUsers = record
|
|
1826
|
-
void
|
|
1843
|
+
void saveProfileConfig({ sudoUsers: record })
|
|
1827
1844
|
}, [])
|
|
1828
1845
|
const isSudoConnected = useCallback((serverId: number) => sudoConnected.has(serverId), [sudoConnected])
|
|
1829
1846
|
const sudoSavedFor = useCallback((serverId: number) => sudoSaved.has(serverId), [sudoSaved])
|
|
@@ -1920,7 +1937,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1920
1937
|
const setPreferredGrantKeys = useCallback((ids: string[]) => {
|
|
1921
1938
|
cfgRef.current.preferredGrantKeys = ids
|
|
1922
1939
|
setPreferredGrantKeysState(ids)
|
|
1923
|
-
void
|
|
1940
|
+
void saveProfileConfig({ preferredGrantKeys: ids })
|
|
1924
1941
|
}, [])
|
|
1925
1942
|
|
|
1926
1943
|
// Persist the granted-keys map (only non-empty entries) to config, write-through.
|
|
@@ -1928,7 +1945,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1928
1945
|
const rec: Record<string, string[]> = {}
|
|
1929
1946
|
for (const [id, set] of next) if (set.size) rec[String(id)] = [...set]
|
|
1930
1947
|
cfgRef.current.grantedKeys = rec
|
|
1931
|
-
void
|
|
1948
|
+
void saveProfileConfig({ grantedKeys: rec })
|
|
1932
1949
|
}, [])
|
|
1933
1950
|
|
|
1934
1951
|
// Record / forget which keys (by body) Spinup has granted on a site.
|
|
@@ -1970,7 +1987,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1970
1987
|
else next.delete(apex)
|
|
1971
1988
|
const rec = Object.fromEntries(next)
|
|
1972
1989
|
cfgRef.current.zoneAccessNotes = rec
|
|
1973
|
-
void
|
|
1990
|
+
void saveProfileConfig({ zoneAccessNotes: rec })
|
|
1974
1991
|
return next
|
|
1975
1992
|
})
|
|
1976
1993
|
}, [])
|
|
@@ -2447,7 +2464,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2447
2464
|
for (const p of ALL_PROVIDERS) {
|
|
2448
2465
|
providers[p] = next[p].filter((c) => !c.env).map((c) => ({ id: c.id, label: c.label, creds: c.creds }))
|
|
2449
2466
|
}
|
|
2450
|
-
void
|
|
2467
|
+
void saveProfileConfig({ providers })
|
|
2451
2468
|
}, [])
|
|
2452
2469
|
|
|
2453
2470
|
// Verify a connection and write the result through to the cache.
|
|
@@ -2682,7 +2699,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2682
2699
|
if (keys[domain] !== key) {
|
|
2683
2700
|
keys[domain] = key
|
|
2684
2701
|
cfgRef.current.vanityHealthKeys = keys
|
|
2685
|
-
void
|
|
2702
|
+
void saveProfileConfig({ vanityHealthKeys: keys })
|
|
2686
2703
|
}
|
|
2687
2704
|
return key
|
|
2688
2705
|
}, [])
|
|
@@ -2696,7 +2713,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2696
2713
|
const newKey = crypto.randomUUID().replace(/-/g, "")
|
|
2697
2714
|
keys[domain] = newKey
|
|
2698
2715
|
cfgRef.current.vanityHealthKeys = keys
|
|
2699
|
-
void
|
|
2716
|
+
void saveProfileConfig({ vanityHealthKeys: keys })
|
|
2700
2717
|
return { oldKey, newKey }
|
|
2701
2718
|
}, [])
|
|
2702
2719
|
|
|
@@ -2705,13 +2722,13 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2705
2722
|
const persistKumaAuth = useCallback((conn: UptimeKumaConn, jwt: string) => {
|
|
2706
2723
|
if (conn.env || jwt === conn.jwt) return
|
|
2707
2724
|
cfgRef.current.uptimeKuma = { ...conn, jwt }
|
|
2708
|
-
void
|
|
2725
|
+
void saveProfileConfig({ uptimeKuma: { ...conn, jwt } })
|
|
2709
2726
|
}, [])
|
|
2710
2727
|
|
|
2711
2728
|
const persistKumaMonitors = useCallback((domain: string, ref: KumaMonitorRef) => {
|
|
2712
2729
|
const map = { ...cfgRef.current.kumaMonitors, [domain]: ref }
|
|
2713
2730
|
cfgRef.current.kumaMonitors = map
|
|
2714
|
-
void
|
|
2731
|
+
void saveProfileConfig({ kumaMonitors: map })
|
|
2715
2732
|
}, [])
|
|
2716
2733
|
|
|
2717
2734
|
// The step machine. Each step performs its work then hands off to the next; it
|
|
@@ -3127,7 +3144,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
3127
3144
|
if (!login.ok) return { ok: false, error: login.error, tokenRequired: login.tokenRequired }
|
|
3128
3145
|
const stored = { url, username: conn.username, password: conn.password, jwt: login.jwt }
|
|
3129
3146
|
cfgRef.current.uptimeKuma = stored
|
|
3130
|
-
void
|
|
3147
|
+
void saveProfileConfig({ uptimeKuma: stored })
|
|
3131
3148
|
setKumaEpoch((e) => e + 1) // start the status poll loop now
|
|
3132
3149
|
await kuma.waitForVersion() // `info` can land after the login ack
|
|
3133
3150
|
return { ok: true, version: kuma.version }
|
|
@@ -4245,6 +4262,29 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4245
4262
|
}
|
|
4246
4263
|
}, [trackServerEvent, trackPhpUpgradeEvent, trackHttpsToggleEvent, driveVanity])
|
|
4247
4264
|
|
|
4265
|
+
const switchBlocker = (): string | null => {
|
|
4266
|
+
const count = <T,>(m: Map<unknown, T>, busy: (p: T) => boolean) => [...m.values()].filter(busy).length
|
|
4267
|
+
const running: [number, string][] = [
|
|
4268
|
+
[count(phpUpgrades, isUpgradeInFlight), "PHP upgrade"],
|
|
4269
|
+
[count(httpsToggles, isHttpsToggleInFlight), "HTTPS change"],
|
|
4270
|
+
[count(purgeCacheProgress, isPurgeCacheInFlight), "cache purge"],
|
|
4271
|
+
[count(serverOps, isServerOpInFlight), "server reboot/restart"],
|
|
4272
|
+
[count(keyGrants, isKeyGrantInFlight), "SSH key grant"],
|
|
4273
|
+
[count(dbBackups, isDbBackupInFlight), "database backup"],
|
|
4274
|
+
[count(dbSyncs, isDbSyncInFlight), "database sync"],
|
|
4275
|
+
[count(localSetups, isLocalSetupInFlight), "local copy setup"],
|
|
4276
|
+
[count(recordWrites, isRecordWriteInFlight), "DNS change"],
|
|
4277
|
+
[count(kumaOps, (o) => o.status === "running"), "monitoring change"],
|
|
4278
|
+
[isNewServerInFlight(newServerJob) ? 1 : 0, "server build"],
|
|
4279
|
+
[isVanityInFlight(vanityJob) ? 1 : 0, "vanity-site build"],
|
|
4280
|
+
// A clone still being planned hasn't touched anything yet — only a launched one counts.
|
|
4281
|
+
[isCloneInFlight(cloneJob) && cloneJob!.step !== "plan" && cloneJob!.step !== "server" ? 1 : 0, "server clone"],
|
|
4282
|
+
[wpCoreBusy() ? 1 : 0, "WordPress update"],
|
|
4283
|
+
]
|
|
4284
|
+
const busy = running.filter(([n]) => n > 0).map(([n, what]) => (n > 1 ? `${n} ${what}s` : `a ${what}`))
|
|
4285
|
+
return busy.length ? `Still running: ${busy.join(", ")}. Switch accounts once ${busy.length > 1 ? "they finish" : "it finishes"}.` : null
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4248
4288
|
const value: StoreValue = {
|
|
4249
4289
|
servers,
|
|
4250
4290
|
sites,
|
|
@@ -4437,6 +4477,11 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4437
4477
|
setEnableLocalSyncSite,
|
|
4438
4478
|
enableLocalSync,
|
|
4439
4479
|
accountSlug: cfgRef.current.accountSlug,
|
|
4480
|
+
profileId: cfgRef.current.profileId,
|
|
4481
|
+
profileLabel,
|
|
4482
|
+
setProfileLabel,
|
|
4483
|
+
tokenFromEnv: cfgRef.current.tokenSource === "env",
|
|
4484
|
+
switchBlocker,
|
|
4440
4485
|
sitesForServer,
|
|
4441
4486
|
serverById,
|
|
4442
4487
|
probes,
|