spinuptui 0.21.1 → 0.22.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.
@@ -562,7 +562,7 @@ export function CloneWizard() {
562
562
  case "adding":
563
563
  return "adding…"
564
564
  case "error":
565
- return truncate(k.error ?? "failed", 36)
565
+ return k.error ?? "failed"
566
566
  default:
567
567
  return "add the key by hand, then r to re-check"
568
568
  }
@@ -579,9 +579,8 @@ export function CloneWizard() {
579
579
  return (
580
580
  <box key={k.repo} style={{ flexDirection: "row", height: 1 }}>
581
581
  <text content={`${g} `} fg={c} style={{ flexShrink: 0 }} />
582
- <text content={`${k.owner}/${k.name}`} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
583
- <box style={{ flexGrow: 1 }} />
584
- <text content={label(k)} fg={k.status === "error" ? theme.bad : k.status === "present" || k.status === "added" ? theme.good : theme.textFaint} wrapMode="none" style={{ flexShrink: 0 }} />
582
+ <text content={`${k.owner}/${k.name}`} fg={theme.text} wrapMode="none" style={{ flexShrink: 0 }} />
583
+ <text content={" " + label(k)} fg={k.status === "error" ? theme.bad : k.status === "present" || k.status === "added" ? theme.good : theme.textFaint} wrapMode="none" style={{ flexGrow: 1, flexShrink: 1 }} />
585
584
  </box>
586
585
  )
587
586
  })}
@@ -682,15 +681,14 @@ export function CloneWizard() {
682
681
  : st === "done" ? `now → ${r.targetValue ?? targetIp}`
683
682
  : st === "ready" ? `${r.currentValue ?? "?"} → ${r.targetValue ?? targetIp}${r.excluded ? " (skipped)" : ""}`
684
683
  : st === "manual" ? `manual: ${r.reason ?? "edit by hand"}`
685
- : st === "error" ? truncate(r.error ?? "failed", 28)
684
+ : st === "error" ? (r.error ?? "failed")
686
685
  : "pending"
687
686
  const dcolor = st === "error" ? theme.bad : st === "done" ? theme.good : st === "manual" ? theme.warn : cur ? theme.text : theme.textFaint
688
687
  return (
689
688
  <box key={`${siteId}|${r.name}`} style={{ flexDirection: "row", height: 1, backgroundColor: cur ? theme.bgAlt : undefined }}>
690
689
  {busy ? <Spinner color={theme.brand} /> : <text content={glyph} fg={gcolor} style={{ flexShrink: 0 }} />}
691
- <text content={` ${r.name}`} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
692
- <box style={{ flexGrow: 1 }} />
693
- <text content={detail} fg={dcolor} wrapMode="none" style={{ flexShrink: 0 }} />
690
+ <text content={` ${r.name}`} fg={theme.text} wrapMode="none" style={{ flexShrink: 0 }} />
691
+ <text content={" " + detail} fg={dcolor} wrapMode="none" style={{ flexGrow: 1, flexShrink: 1 }} />
694
692
  </box>
695
693
  )
696
694
  }
@@ -732,13 +730,12 @@ export function CloneWizard() {
732
730
  return (
733
731
  <box key={s.sourceSiteId} style={{ flexDirection: "row", height: 1, backgroundColor: cur ? theme.bgAlt : undefined }}>
734
732
  {active ? <Spinner color={theme.brand} /> : <text content={s.step === "done" ? "✓" : s.step === "error" ? "✕" : "○"} fg={s.step === "done" ? theme.good : s.step === "error" ? theme.bad : theme.textFaint} style={{ flexShrink: 0 }} />}
735
- <text content={` ${s.domain}`} fg={cur || s.step === "error" || s.step === "done" ? theme.text : theme.textDim} wrapMode="none" style={{ flexShrink: 1 }} />
736
- <box style={{ flexGrow: 1 }} />
733
+ <text content={` ${s.domain}`} fg={cur || s.step === "error" || s.step === "done" ? theme.text : theme.textDim} wrapMode="none" style={{ flexShrink: 0 }} />
737
734
  <text
738
- content={(s.step === "error" ? truncate(s.error ?? "failed", 24) : s.step === "done" ? "done" : s.step === "queued" ? "queued" : `${s.step}${s.detail ? " · " + s.detail : ""}${s.transferBytes != null ? ` · ${formatBytes(s.transferBytes)}${s.transferTarget ? ` of ${s.transferExact ? "" : "~"}${formatBytes(s.transferTarget)}` : ""}${s.transferExact && s.transferTarget ? ` (${Math.min(100, Math.round((s.transferBytes / s.transferTarget) * 100))}%)` : ""}${s.transferRate ? ` · ${formatBytes(s.transferRate)}/s` : ""}` : ""}${s.stageStartedAt ? " · " + fmtElapsed(now - s.stageStartedAt) : ""}`) + vmark}
735
+ content={" " + ((s.step === "error" ? s.error ?? "failed" : s.step === "done" ? "done" : s.step === "queued" ? "queued" : `${s.step}${s.detail ? " · " + s.detail : ""}${s.transferBytes != null ? ` · ${formatBytes(s.transferBytes)}${s.transferTarget ? ` of ${s.transferExact ? "" : "~"}${formatBytes(s.transferTarget)}` : ""}${s.transferExact && s.transferTarget ? ` (${Math.min(100, Math.round((s.transferBytes / s.transferTarget) * 100))}%)` : ""}${s.transferRate ? ` · ${formatBytes(s.transferRate)}/s` : ""}` : ""}${s.stageStartedAt ? " · " + fmtElapsed(now - s.stageStartedAt) : ""}`) + vmark)}
739
736
  fg={s.step === "error" || (s.verify && !s.verify.ok) ? theme.bad : s.verify?.ok ? theme.good : s.step === "done" ? theme.good : theme.textFaint}
740
737
  wrapMode="none"
741
- style={{ flexShrink: 0 }}
738
+ style={{ flexGrow: 1, flexShrink: 1 }}
742
739
  />
743
740
  </box>
744
741
  )
@@ -9,7 +9,7 @@ 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 } from "../components.tsx"
12
+ import { Panel, Field, StatusBadge, SiteMetaCell, Spinner, ControlPanel, siteGroups } 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"
@@ -17,6 +17,7 @@ import { ServerDetail, SiteDetail, SERVER_CONTROL } 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"
20
+ import { isVanityPair } from "../../lib/vanitySite.ts"
20
21
  import { useStore } from "../store.tsx"
21
22
  import type { Server, Site } from "../../api/types.ts"
22
23
 
@@ -34,7 +35,7 @@ function score(haystack: string, q: string): number | null {
34
35
 
35
36
  export function Search({ rows }: { rows: number }) {
36
37
  const store = useStore()
37
- 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 } = 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 } = store
38
39
  const [query, setQuery] = useState("")
39
40
  const [selected, setSelected] = useState(0)
40
41
  // "query" = typing/filtering (input focused); "actions" = input blurred so the
@@ -195,6 +196,14 @@ export function Search({ rows }: { rows: number }) {
195
196
  // fallback here; M matches the same overlay's binding in Stacks.
196
197
  if (current?.kind === "site") setKumaSite(current.site)
197
198
  return
199
+ case "R":
200
+ // Refresh a vanity page's HTML (site-only, and only for the one site per
201
+ // server whose domain equals the server's own name). Same binding/behavior
202
+ // as the m-overlay's R (KumaSite.tsx) — fires directly, no confirm, since
203
+ // it just republishes the same bundled page that's already live.
204
+ if (current?.kind === "site" && isVanityPair(current.site.domain, serverById(current.site.server_id)?.name ?? ""))
205
+ return kumaConfigured ? startKumaSetup(current.site, { reseed: true }) : startVanityReseed(current.site)
206
+ return
198
207
  case "n":
199
208
  // DNS inventory scoped to this site (its domains + records) — the migrate
200
209
  // lens for moving one site to another server.
@@ -241,7 +250,7 @@ export function Search({ rows }: { rows: number }) {
241
250
  focus === "query"
242
251
  ? [
243
252
  { key: "↑↓", label: "select" },
244
- { key: "⏎", label: "open site" },
253
+ ...(current?.kind === "site" ? [{ key: "⏎", label: "open site" }] : []),
245
254
  { key: "Tab/→", label: "actions" },
246
255
  { key: "esc", label: "dashboard" },
247
256
  ]
@@ -254,15 +263,9 @@ export function Search({ rows }: { rows: number }) {
254
263
  { key: "←/esc", label: "back" },
255
264
  ]
256
265
  : [
266
+ // The Actions panel (Details pane) already lists every per-site key —
267
+ // this footer is nav-only now, so the two can't drift out of sync.
257
268
  { key: "↑↓", label: "select" },
258
- { key: "o", label: "open" },
259
- { key: "s", label: "SSH" },
260
- { key: "n", label: "DNS" },
261
- { key: "d", label: "DB backup" },
262
- ...(localSync ? [{ key: "p", label: "pull to local" }] : []),
263
- ...(current?.kind === "site" && current.site.is_wordpress && localLinks.has(current.site.id) ? [{ key: "m", label: "media fallback" }] : []),
264
- { key: "M", label: "monitoring" },
265
- { key: "a", label: "server actions" },
266
269
  { key: "←/esc", label: "back" },
267
270
  ]
268
271
 
@@ -352,7 +355,7 @@ export function Search({ rows }: { rows: number }) {
352
355
  />
353
356
  </Panel>
354
357
 
355
- <Panel title={focus === "actions" ? " Actions " : " Details "} active={focus === "actions"} width={44}>
358
+ <Panel title={focus === "actions" ? " Actions " : " Details "} active={focus === "actions"} width={64}>
356
359
  {!current ? (
357
360
  <text content="No selection" fg={theme.textFaint} />
358
361
  ) : focus === "actions" ? (
@@ -374,48 +377,12 @@ export function Search({ rows }: { rows: number }) {
374
377
  )
375
378
  }
376
379
 
377
- // Site Control panel shown in the Details pane while in "actions" focus. Groups
378
- // the live single-key actions (Open / Remote / Local / Server) so the suite is
379
- // discoverable and scales as more actions land. A site gets the full set; a
380
- // server gets just the open + server actions.
381
- type ActionGroup = { title: string; items: [string, string][] }
382
-
383
- // The DB backup/sync use wp-cli, so they only apply to WordPress sites — omitted
384
- // for non-WP sites (their `d`/`p` keypresses are also guarded in the handler). DNS
385
- // (`n`) and the rest apply to any site.
386
- function siteGroups(isWordpress: boolean, localSync: boolean): ActionGroup[] {
387
- const remote: [string, string][] = [
388
- ["s", "SSH into the site"],
389
- ["n", "DNS records (migrate lens)"],
390
- ]
391
- if (isWordpress) {
392
- remote.push(["d", "Download DB backup"])
393
- if (localSync) remote.push(["p", "Pull production DB to local"])
394
- }
395
- const local: [string, string][] = [
396
- ["t", "Open working copy in a terminal"],
397
- ["v", "Open the local URL"],
398
- ["L", "Link / edit local copy"],
399
- ]
400
- if (isWordpress) local.push(["m", "Media: serve missing images from production"])
401
- remote.push(["u", "Upgrade PHP version"])
402
- remote.push(["H", "Enable/disable HTTPS"])
403
- remote.push(["P", "Purge page + object cache"])
404
- remote.push(["M", "Monitoring (Uptime Kuma + front-page check)"])
405
- return [
406
- { title: "Open", items: [["o", "Open site in browser"], ["w", "Open in SpinupWP"]] },
407
- { title: "Remote", items: remote },
408
- { title: "Local", items: local },
409
- { title: "Server", items: [["a", "Server actions (reboot / restart)"], ["h", "Server health"]] },
410
- ]
411
- }
412
-
413
380
  function ActionsCard({ result, serverName, localSync }: { result: Result; serverName: string; localSync: boolean }) {
414
381
  const isSite = result.kind === "site"
415
382
  const name = isSite ? result.site.domain : result.server.name
416
383
  const status = isSite ? result.site.status : result.server.connection_status
417
384
  // Server results reuse the SAME Server Control suite as the Browser detail pane.
418
- const groups = isSite ? siteGroups(result.site.is_wordpress, localSync) : SERVER_CONTROL
385
+ const groups = isSite ? siteGroups(result.site.is_wordpress, localSync, isVanityPair(result.site.domain, serverName)) : SERVER_CONTROL
419
386
  return (
420
387
  <box style={{ flexDirection: "column" }}>
421
388
  <box style={{ flexDirection: "row" }}>