spinuptui 1.3.0-alpha.2 → 1.3.0-alpha.3

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.
@@ -17,6 +17,7 @@ import type { ModuleId, ModuleStatus } from "../../modules/contract.ts"
17
17
  import { beaconFromConfig, beaconSend, testMessage } from "../../lib/beacon.ts"
18
18
  import { loadConfig } from "../../config.ts"
19
19
  import { upcomingRuns } from "../../modules/scheduler.ts"
20
+ import { describeShare, homeStatus, tilde, type HomeStatus } from "../../lib/home.ts"
20
21
 
21
22
  interface Card {
22
23
  id: ModuleId
@@ -43,7 +44,7 @@ function stateLabel(card: Card, status: ModuleStatus | undefined): { dot: string
43
44
 
44
45
  export function Modules({ rows }: { rows: number }) {
45
46
  const store = useStore()
46
- const { route, inputMode, overlayOpen, beaconSetupOpen, setBeaconSetupOpen, schedulerInstallOpen, setSchedulerInstallOpen } = store
47
+ const { route, inputMode, overlayOpen, beaconSetupOpen, setBeaconSetupOpen, schedulerInstallOpen, setSchedulerInstallOpen, homeSyncOpen, setHomeSyncOpen, heartbeatSetupOpen, setHeartbeatSetupOpen } = store
47
48
  const [selected, setSelected] = useState(0)
48
49
  const [statuses, setStatuses] = useState<Map<ModuleId, ModuleStatus>>(new Map())
49
50
  const [testing, setTesting] = useState(false)
@@ -52,7 +53,7 @@ export function Modules({ rows }: { rows: number }) {
52
53
  // the config). status() is cheap and never sends anything.
53
54
  const [tick, setTick] = useState(0)
54
55
  useEffect(() => {
55
- if (beaconSetupOpen || schedulerInstallOpen) return
56
+ if (beaconSetupOpen || schedulerInstallOpen || homeSyncOpen || heartbeatSetupOpen) return
56
57
  let live = true
57
58
  void Promise.all(MODULES.map((m) => m.status())).then((list) => {
58
59
  if (live) setStatuses(new Map(list.map((s) => [s.id, s])))
@@ -60,7 +61,7 @@ export function Modules({ rows }: { rows: number }) {
60
61
  return () => {
61
62
  live = false
62
63
  }
63
- }, [beaconSetupOpen, schedulerInstallOpen, tick])
64
+ }, [beaconSetupOpen, schedulerInstallOpen, homeSyncOpen, heartbeatSetupOpen, tick])
64
65
 
65
66
  const isActive = route === "modules" && !inputMode && !overlayOpen
66
67
  const card = CARDS[selected]!
@@ -91,6 +92,12 @@ export function Modules({ rows }: { rows: number }) {
91
92
  if (card.id === "beacon") return setBeaconSetupOpen(true)
92
93
  if (card.id === "scheduler") return setSchedulerInstallOpen(true)
93
94
  return
95
+ case "f":
96
+ if (card.id === "scheduler") return setHomeSyncOpen(true)
97
+ return
98
+ case "h":
99
+ if (card.id === "scheduler") return setHeartbeatSetupOpen(true)
100
+ return
94
101
  case "t":
95
102
  if (card.id === "beacon" && status && status.state !== "not_configured") return sendTest()
96
103
  return
@@ -142,7 +149,13 @@ export function Modules({ rows }: { rows: number }) {
142
149
  hints={[
143
150
  { key: "↑↓/jk", label: "select" },
144
151
  ...(card.id === "beacon" ? [{ key: "⏎", label: beaconReady ? "reconfigure" : "set up" }] : []),
145
- ...(card.id === "scheduler" ? [{ key: "⏎", label: status && status.state !== "not_configured" ? "reinstall" : "install" }] : []),
152
+ ...(card.id === "scheduler"
153
+ ? [
154
+ { key: "⏎", label: status && status.state !== "not_configured" ? "reinstall" : "install" },
155
+ { key: "f", label: "your folder" },
156
+ { key: "h", label: "heartbeat" },
157
+ ]
158
+ : []),
146
159
  ...(beaconReady ? [{ key: "t", label: testing ? "sending…" : "test page" }] : []),
147
160
  ]}
148
161
  />
@@ -194,13 +207,23 @@ function ModuleDetail({ card, status }: { card: Card; status: ModuleStatus | und
194
207
  {card.id === "beacon" && status.state !== "not_configured" && (
195
208
  <text content="⏎ reconfigure the pager · t send a test page · CLI: spinuptui beacon status" fg={theme.textFaint} />
196
209
  )}
197
- {card.id === "scheduler" && <SchedulerDetail installed={status.state !== "not_configured"} />}
210
+ {card.id === "scheduler" && <SchedulerDetail installed={status.state !== "not_configured"} refreshKey={status} />}
198
211
  </box>
199
212
  )
200
213
  }
201
214
 
202
- function SchedulerDetail({ installed }: { installed: boolean }) {
215
+ function SchedulerDetail({ installed, refreshKey }: { installed: boolean; refreshKey: unknown }) {
203
216
  const [runs] = useState(() => upcomingRuns())
217
+ // Level 0 of the SpinupTUI folder: always say where the work lands.
218
+ const [home, setHome] = useState<HomeStatus | null>(null)
219
+ const heartbeatSet = !!loadConfig().scheduler?.heartbeat
220
+ useEffect(() => {
221
+ let live = true
222
+ void homeStatus().then((h) => live && setHome(h))
223
+ return () => {
224
+ live = false
225
+ }
226
+ }, [refreshKey])
204
227
  return (
205
228
  <>
206
229
  <box style={{ height: 1 }} />
@@ -223,6 +246,11 @@ function SchedulerDetail({ installed }: { installed: boolean }) {
223
246
  </box>
224
247
  ))}
225
248
  <box style={{ height: 1 }} />
249
+ {home && <text content={`Jobs and reports: ${tilde(home.path)}`} fg={theme.textDim} wrapMode="none" />}
250
+ {home && <text content={` ${describeShare(home)}`} fg={home.git && home.remote ? theme.good : theme.textDim} wrapMode="none" />}
251
+ <text content={home?.git && home.remote ? "f your folder" : "f share it across machines, so you can queue jobs from anywhere"} fg={theme.accent} wrapMode="none" />
252
+ <text content={heartbeatSet ? "h station heartbeat" : "h get alerted from elsewhere if this machine stops (station heartbeat)"} fg={theme.accent} wrapMode="none" />
253
+ <box style={{ height: 1 }} />
226
254
  <text
227
255
  content={`${installed ? "⏎ reinstall (repairs the unit) · " : ""}CLI: spinuptui scheduler add | list | status`}
228
256
  fg={theme.textFaint}
@@ -36,7 +36,7 @@ function score(haystack: string, q: string): number | null {
36
36
 
37
37
  export function Search({ rows }: { rows: number }) {
38
38
  const store = useStore()
39
- const { servers, sites, serverById, sitesForServer, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setSiteOfflineSite, setServerActionsServer, accountSlug, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe, setEnableLocalSyncSite, setVanityServer, vanityJob } = store
39
+ const { servers, sites, serverById, sitesForServer, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setSiteOfflineSite, setServerActionsServer, openInSpinupWP, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe, setEnableLocalSyncSite, setVanityServer, vanityJob } = store
40
40
  const [query, setQuery] = useState("")
41
41
  const [selected, setSelected] = useState(0)
42
42
  // "query" = typing/filtering (input focused); "actions" = input blurred so the
@@ -101,9 +101,8 @@ export function Search({ rows }: { rows: number }) {
101
101
  openUrl((s.https?.enabled ? "https://" : "http://") + s.domain)
102
102
  flashMsg(`Opening ${s.domain}…`)
103
103
  }
104
- const openInSpinup = (url: string) => {
105
- openUrl(url)
106
- flashMsg(accountSlug ? "Opening in SpinupWP…" : "Set accountSlug for deep links — opening dashboard")
104
+ const openInSpinup = (build: (slug: string) => string) => {
105
+ if (openInSpinupWP(build)) flashMsg("Opening in SpinupWP…")
107
106
  }
108
107
 
109
108
  useKeyboard((key) => {
@@ -141,8 +140,8 @@ export function Search({ rows }: { rows: number }) {
141
140
  if (current?.kind === "site") openSite(current.site)
142
141
  return
143
142
  case "w":
144
- if (current?.kind === "site") openInSpinup(siteWebUrl(current.site.id, accountSlug))
145
- else if (current?.kind === "server") openInSpinup(serverWebUrl(current.server.id, accountSlug))
143
+ if (current?.kind === "site") openInSpinup((slug) => siteWebUrl(current.site.id, slug))
144
+ else if (current?.kind === "server") openInSpinup((slug) => serverWebUrl(current.server.id, slug))
146
145
  return
147
146
  case "u":
148
147
  if (current?.kind === "site") setPhpUpgradeSite(current.site)
@@ -19,7 +19,6 @@ import { Panel, Spinner, Centered } from "../components.tsx"
19
19
  import { StatusBar } from "../StatusBar.tsx"
20
20
  import { useStore } from "../store.tsx"
21
21
  import { moveSelection } from "../List.tsx"
22
- import { openUrl } from "../../lib/open.ts"
23
22
  import { serverWebUrl } from "../../lib/spinupweb.ts"
24
23
  import { toast } from "../toast.ts"
25
24
  import type { RebootInfo } from "../../lib/ssh.ts"
@@ -67,7 +66,7 @@ export function ServerActions() {
67
66
  rebootInfoLoading,
68
67
  rebootInfoErrors,
69
68
  loadRebootInfo,
70
- accountSlug,
69
+ openInSpinupWP,
71
70
  refresh,
72
71
  } = store
73
72
 
@@ -161,8 +160,11 @@ export function ServerActions() {
161
160
  return
162
161
  case "w":
163
162
  if (upgradeRequired && server) {
164
- openUrl(serverWebUrl(server.id, accountSlug))
165
- toast.success(accountSlug ? "Opening in SpinupWP…" : "Set accountSlug for deep links — opening dashboard")
163
+ const id = server.id
164
+ if (openInSpinupWP((slug) => serverWebUrl(id, slug))) toast.success("Opening in SpinupWP…")
165
+ // No slug yet: the prompt takes over — close this overlay so its
166
+ // keys don't fire underneath.
167
+ else close()
166
168
  }
167
169
  return
168
170
  }
@@ -16,7 +16,8 @@
16
16
  import { useEffect, useMemo, useRef, useState } from "react"
17
17
  import { useKeyboard } from "@opentui/react"
18
18
  import { theme, statusColor, statusDot } from "../../lib/theme.ts"
19
- import { bar, truncate } from "../../lib/format.ts"
19
+ import { bar, timeAgo, truncate } from "../../lib/format.ts"
20
+ import { isVersionOld } from "../../lib/stackCache.ts"
20
21
  import { STACKS, effectiveStack, stackColor, type Stack } from "../../lib/stack.ts"
21
22
  import { phpSortKey } from "../../lib/phpEol.ts"
22
23
  import { probeKindColor, type ProbeKind } from "../../lib/probe.ts"
@@ -75,7 +76,7 @@ function nonWpSub(kind: ProbeKind | null, vanity: boolean): NonWpSub {
75
76
  export function Stacks({ rows }: { rows: number }) {
76
77
  const store = useStore()
77
78
  const wpCoreJobs = useWpCoreJobs()
78
- const { sites, serverById, route, inputMode, overlayOpen, probes, probingIds, probeErrors, runProbe, runProbeMany, isProbeStale, isPhpEol, accountSlug, setPhpUpgradeSite, setBulkWpGroup, phpUpgrades, setHttpsToggleSite, setPurgeCacheSite, setSiteOfflineSite, setLocalLinkSite, openLocalTerminal, openLocalUrl, localLinks, setDiscoverOpen, setForgottenOpen, setForgottenStack, sshSite, setKumaSite } =
79
+ const { sites, serverById, route, inputMode, overlayOpen, probes, probingIds, probeErrors, runProbe, runProbeMany, isProbeStale, isPhpEol, openInSpinupWP, setPhpUpgradeSite, setBulkWpGroup, phpUpgrades, setHttpsToggleSite, setPurgeCacheSite, setSiteOfflineSite, setLocalLinkSite, openLocalTerminal, openLocalUrl, localLinks, setDiscoverOpen, setForgottenOpen, setForgottenStack, sshSite, setKumaSite } =
79
80
  store
80
81
 
81
82
  const [groupIndex, setGroupIndex] = useState(0)
@@ -218,8 +219,8 @@ export function Stacks({ rows }: { rows: number }) {
218
219
  case "w":
219
220
  // Open the selected site in the SpinupWP web app.
220
221
  if (focus === "sites" && groupSites[siteIndex]) {
221
- openUrl(siteWebUrl(groupSites[siteIndex].id, accountSlug))
222
- flashMsg(accountSlug ? "Opening in SpinupWP…" : "Set accountSlug for deep links — opening dashboard")
222
+ const id = groupSites[siteIndex].id
223
+ if (openInSpinupWP((slug) => siteWebUrl(id, slug))) flashMsg("Opening in SpinupWP…")
223
224
  }
224
225
  return
225
226
  case "d":
@@ -421,11 +422,23 @@ export function Stacks({ rows }: { rows: number }) {
421
422
  ) : vanity ? (
422
423
  <text content="Vanity Site" fg={selected ? theme.text : VANITY_COLOR} wrapMode="none" />
423
424
  ) : cached ? (
424
- <text
425
- content={truncate(cached.result.label, 20) + (isProbeStale(s) ? "*" : "")}
426
- fg={probeKindColor(cached.result.kind, selected)}
427
- wrapMode="none"
428
- />
425
+ <>
426
+ <text
427
+ content={truncate(cached.result.label, 20) + (isProbeStale(s) ? "*" : "")}
428
+ fg={probeKindColor(cached.result.kind, selected)}
429
+ wrapMode="none"
430
+ />
431
+ {/* A version past its re-check age (the background re-probe
432
+ failed or hasn't run) says how old it is, so it can't
433
+ pass for current. */}
434
+ {isVersionOld(cached) && (
435
+ <text
436
+ content={" " + timeAgo(new Date(cached.detectedAt).toISOString()).replace(/ ago$/, "")}
437
+ fg={selected ? theme.text : theme.warn}
438
+ wrapMode="none"
439
+ />
440
+ )}
441
+ </>
429
442
  ) : errored ? (
430
443
  <text content="probe failed" fg={selected ? theme.text : theme.bad} wrapMode="none" />
431
444
  ) : (