spinuptui 0.25.0 → 0.26.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.
@@ -9,11 +9,11 @@ import { useKeyboard } from "@opentui/react"
9
9
  import { theme, statusColor, statusDot } from "../../lib/theme.ts"
10
10
  import { classifyStack, stackColor, stackTag } from "../../lib/stack.ts"
11
11
  import { truncate } from "../../lib/format.ts"
12
- import { Panel, Field, StatusBadge, SiteMetaCell, Spinner, ControlPanel, siteGroups } from "../components.tsx"
12
+ import { Panel, SiteMetaCell, Spinner } from "../components.tsx"
13
13
  import { isDbBackupInFlight } from "../../lib/dbBackup.ts"
14
14
  import { isDbSyncInFlight } from "../../lib/dbSync.ts"
15
15
  import { List, moveSelection } from "../List.tsx"
16
- import { ServerDetail, SiteDetail, SERVER_CONTROL } from "../Details.tsx"
16
+ import { ServerDetail, SiteDetail, ControlStrip, CONTROL_STRIP_HEIGHT } from "../Details.tsx"
17
17
  import { StatusBar } from "../StatusBar.tsx"
18
18
  import { openUrl } from "../../lib/open.ts"
19
19
  import { serverWebUrl, siteWebUrl } from "../../lib/spinupweb.ts"
@@ -35,11 +35,12 @@ 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, setEnableLocalSyncSite } = store
38
+ const { servers, sites, serverById, sitesForServer, 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, setVanityServer, vanityJob } = 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
42
- // selected result's single-key actions (o/w/u/h) fire. Tab/→ enters, ←/Esc exits.
42
+ // selected result's single-key actions fire — listed in the Site/Server
43
+ // Control drawer below, same as the Servers tab. Tab/→ enters, ←/Esc exits.
43
44
  const [focus, setFocus] = useState<"query" | "actions">("query")
44
45
  const [flash, setFlash] = useState<string | null>(null)
45
46
  const flashMsg = (m: string) => {
@@ -160,11 +161,15 @@ export function Search({ rows }: { rows: number }) {
160
161
  case "L":
161
162
  if (current?.kind === "site") setLocalLinkSite(current.site)
162
163
  return
163
- case "K":
164
- // Grant Spinup's machine key to the selected site over SSH — matches
165
- // Browser's K binding, which was already in the shared legend here.
166
- if (current?.kind === "site") setGrantKeySite(current.site)
164
+ case "K": {
165
+ // Grant Spinup's machine key over SSH — matches Browser's K binding,
166
+ // which is site-anchored but works on a server result too (falls back
167
+ // to the server's first key-eligible site, mirroring Browser.tsx).
168
+ const anchor =
169
+ current?.kind === "site" ? current.site : current?.kind === "server" ? sitesForServer(current.server.id).find((s) => s.site_user) : undefined
170
+ if (anchor) setGrantKeySite(anchor)
167
171
  return
172
+ }
168
173
  case "s":
169
174
  if (current?.kind === "site") flashMsg(sshSite(current.site.id))
170
175
  return
@@ -262,6 +267,17 @@ export function Search({ rows }: { rows: number }) {
262
267
  case "N":
263
268
  if (current?.kind === "server") setDnsInventoryServer(current.server)
264
269
  return
270
+ case "V": {
271
+ // Create the vanity site at the server's own hostname, or resume an
272
+ // unfinished build — same gating as Browser's V.
273
+ if (current?.kind === "server") {
274
+ const s = current.server
275
+ const resumable = vanityJob && vanityJob.step !== "done" && vanityJob.serverId === s.id
276
+ const hasVanity = sitesForServer(s.id).some((site) => site.domain.toLowerCase() === s.name.toLowerCase())
277
+ if (resumable || !hasVanity) setVanityServer(s)
278
+ }
279
+ return
280
+ }
265
281
  }
266
282
  })
267
283
 
@@ -273,22 +289,14 @@ export function Search({ rows }: { rows: number }) {
273
289
  { key: "Tab/→", label: "actions" },
274
290
  { key: "esc", label: "dashboard" },
275
291
  ]
276
- : current?.kind === "server"
277
- ? [
278
- { key: "↑↓", label: "select" },
279
- { key: "w", label: "SpinupWP" },
280
- { key: "a", label: "actions" },
281
- { key: "h", label: "health" },
282
- { key: "←/esc", label: "back" },
283
- ]
284
- : [
285
- // The Actions panel (Details pane) already lists every per-site key —
286
- // this footer is nav-only now, so the two can't drift out of sync.
287
- { key: "↑↓", label: "select" },
288
- { key: "←/esc", label: "back" },
289
- ]
292
+ : [
293
+ // The Control drawer below already lists every key for the selected
294
+ // result — this footer stays nav-only so the two can't drift out of sync.
295
+ { key: "↑↓", label: "select" },
296
+ { key: "←/esc", label: "back" },
297
+ ]
290
298
 
291
- const listRows = Math.max(3, rows - 8) // input box (3) + status bar (1) + chrome
299
+ const listRows = Math.max(3, rows - 9 - CONTROL_STRIP_HEIGHT) // input box (3) + status bar (1) + chrome + control drawer
292
300
 
293
301
  return (
294
302
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
@@ -374,14 +382,9 @@ export function Search({ rows }: { rows: number }) {
374
382
  />
375
383
  </Panel>
376
384
 
377
- <Panel title={focus === "actions" ? " Actions " : " Details "} active={focus === "actions"} width={64}>
385
+ <Panel title=" Details " width={64}>
378
386
  {!current ? (
379
387
  <text content="No selection" fg={theme.textFaint} />
380
- ) : focus === "actions" ? (
381
- <ActionsCard
382
- result={current}
383
- serverName={current.kind === "site" ? (serverById(current.site.server_id)?.name ?? "—") : current.server.name}
384
- />
385
388
  ) : current.kind === "server" ? (
386
389
  <ServerDetail server={current.server} siteCount={store.sitesForServer(current.server.id).length} />
387
390
  ) : (
@@ -390,44 +393,13 @@ export function Search({ rows }: { rows: number }) {
390
393
  </Panel>
391
394
  </box>
392
395
 
393
- <StatusBar hints={hints} message={flash ?? undefined} showGlobal={false} />
394
- </box>
395
- )
396
- }
396
+ <ControlStrip
397
+ site={current?.kind === "site" ? current.site : null}
398
+ server={current?.kind === "server" ? current.server : null}
399
+ serverName={current?.kind === "site" ? (serverById(current.site.server_id)?.name ?? "—") : current?.kind === "server" ? current.server.name : "—"}
400
+ />
397
401
 
398
- function ActionsCard({ result, serverName }: { result: Result; serverName: string }) {
399
- const isSite = result.kind === "site"
400
- const name = isSite ? result.site.domain : result.server.name
401
- const status = isSite ? result.site.status : result.server.connection_status
402
- // Server results reuse the SAME Server Control suite as the Browser detail pane.
403
- const groups = isSite ? siteGroups(result.site.is_wordpress, isVanityPair(result.site.domain, serverName)) : SERVER_CONTROL
404
- return (
405
- <box style={{ flexDirection: "column" }}>
406
- <box style={{ flexDirection: "row" }}>
407
- <text content={truncate(name, 30)} fg={theme.text} attributes={1} wrapMode="none" />
408
- <box style={{ flexGrow: 1 }} />
409
- <StatusBadge status={status} />
410
- </box>
411
- {isSite ? (
412
- <>
413
- <text content={(result.site.https?.enabled ? "https://" : "http://") + result.site.domain} fg={theme.accent} wrapMode="none" />
414
- <box style={{ height: 1 }} />
415
- <Field label="Server" value={truncate(serverName, 28)} labelWidth={8} />
416
- <Field label="PHP" value={result.site.php_version ?? "—"} labelWidth={8} />
417
- <Field label="Stack" value={classifyStack(result.site)} valueColor={stackColor(classifyStack(result.site))} labelWidth={8} />
418
- </>
419
- ) : (
420
- <>
421
- <text content={result.server.ip_address ?? "—"} fg={theme.accent} wrapMode="none" />
422
- <box style={{ height: 1 }} />
423
- <Field label="Provider" value={result.server.provider_name ?? "—"} labelWidth={9} />
424
- <Field label="Region" value={result.server.region ?? "—"} labelWidth={9} />
425
- </>
426
- )}
427
- <box style={{ height: 1 }} />
428
- <ControlPanel heading={isSite ? "Site Control" : "Server Control"} groups={groups} />
429
- <box style={{ height: 1 }} />
430
- <text content="↑↓ select · ←/Esc back to search" fg={theme.textFaint} wrapMode="none" />
402
+ <StatusBar hints={hints} message={flash ?? undefined} showGlobal={false} />
431
403
  </box>
432
404
  )
433
405
  }