spinuptui 0.24.3 → 0.25.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 +38 -1
- package/README.md +4 -2
- package/package.json +3 -8
- package/src/ui/App.tsx +2 -5
- package/src/ui/Splash.tsx +3 -0
- package/src/ui/ToastStack.tsx +33 -0
- package/src/ui/store.tsx +1 -1
- package/src/ui/toast.ts +49 -0
- package/src/ui/views/ServerActions.tsx +30 -4
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,41 @@ versions; such changes are called out here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.25.0] - 2026-08-12
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Server actions now surfaces a pending SpinupWP platform upgrade**, not just
|
|
18
|
+
a silent badge. The overlay (`a`) shows a banner explaining the API only
|
|
19
|
+
exposes it as a flag with no actionable detail, and `w` jumps straight to the
|
|
20
|
+
server's page in the SpinupWP web app to run it.
|
|
21
|
+
- **App version on the splash screen**, shown below the tagline while the first
|
|
22
|
+
batch of data loads.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **The reboot-pending detail line now shows the real reason when the SSH probe
|
|
26
|
+
fails**, instead of a generic "couldn't read detail over SSH" message — e.g.
|
|
27
|
+
an SSH auth failure now shows the actual error. Both this and the new upgrade
|
|
28
|
+
banner wrap across lines instead of overflowing the panel on longer text.
|
|
29
|
+
|
|
30
|
+
## [0.24.4] - 2026-08-06
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **The `Unknown component type: toaster` startup crash is now actually
|
|
34
|
+
fixed for real installs**, not just local project installs. `0.24.3`'s
|
|
35
|
+
`package.json` `overrides` only take effect when this repo's own
|
|
36
|
+
`package.json` is the root of the install — they're silently ignored when
|
|
37
|
+
spinuptui is installed as a dependency of something else, which is exactly
|
|
38
|
+
what a global `bun add -g` / `npm install -g` does, so the crash persisted
|
|
39
|
+
for every real user. Root cause traces back to the toast library
|
|
40
|
+
(`@opentui-ui/toast`) itself: it's stuck on a peer-dependency range from
|
|
41
|
+
opentui's `0.1.x` days, and no version pin on our side can bind how a
|
|
42
|
+
downstream install resolves *its* transitive dependencies. Every call site
|
|
43
|
+
in this app only ever fires a single-line success message, so it's
|
|
44
|
+
replaced with a small first-party toast implementation (`src/ui/toast.ts`
|
|
45
|
+
+ `src/ui/ToastStack.tsx`) built directly on the `@opentui/core`/
|
|
46
|
+
`@opentui/react` we already depend on — no external package, no peer-
|
|
47
|
+
dependency surface for a future version bump to break again.
|
|
48
|
+
|
|
14
49
|
## [0.24.3] - 2026-08-06
|
|
15
50
|
|
|
16
51
|
### Fixed
|
|
@@ -1280,7 +1315,9 @@ Initial tagged release.
|
|
|
1280
1315
|
### Notes
|
|
1281
1316
|
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
1282
1317
|
|
|
1283
|
-
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.
|
|
1318
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.25.0...HEAD
|
|
1319
|
+
[0.25.0]: https://github.com/mwender/spinupwp-tui/compare/v0.24.4...v0.25.0
|
|
1320
|
+
[0.24.4]: https://github.com/mwender/spinupwp-tui/compare/v0.24.3...v0.24.4
|
|
1284
1321
|
[0.24.3]: https://github.com/mwender/spinupwp-tui/compare/v0.24.2...v0.24.3
|
|
1285
1322
|
[0.24.2]: https://github.com/mwender/spinupwp-tui/compare/v0.24.1...v0.24.2
|
|
1286
1323
|
[0.24.1]: https://github.com/mwender/spinupwp-tui/compare/v0.24.0...v0.24.1
|
package/README.md
CHANGED
|
@@ -320,8 +320,10 @@ Full details: [docs/php-upgrade.md](docs/php-upgrade.md).
|
|
|
320
320
|
## Server actions
|
|
321
321
|
|
|
322
322
|
`a` on a server reboots it or restarts a service (Nginx/PHP-FPM/MySQL/Redis),
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
shows *why* a reboot is pending (the actual OS packages, read over SSH), and for
|
|
324
|
+
a pending SpinupWP platform upgrade — which the API can't act on — offers `w` to
|
|
325
|
+
jump straight to it in the SpinupWP web app. Needs a Read/Write token. Full
|
|
326
|
+
details: [docs/server-actions.md](docs/server-actions.md).
|
|
325
327
|
|
|
326
328
|
## Site monitoring
|
|
327
329
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spinuptui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "SpinupTUI — a terminal dashboard for browsing, monitoring, and managing your SpinupWP servers and sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,9 +41,8 @@
|
|
|
41
41
|
"author": "Michael Wender",
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@opentui
|
|
45
|
-
"@opentui/
|
|
46
|
-
"@opentui/react": "0.4.2",
|
|
44
|
+
"@opentui/core": "^0.4.1",
|
|
45
|
+
"@opentui/react": "^0.4.1",
|
|
47
46
|
"react": "^19.0.0",
|
|
48
47
|
"socket.io-client": "^4.8.3"
|
|
49
48
|
},
|
|
@@ -51,9 +50,5 @@
|
|
|
51
50
|
"@types/bun": "^1.3.14",
|
|
52
51
|
"@types/react": "^19.0.0",
|
|
53
52
|
"typescript": "^5.6.0"
|
|
54
|
-
},
|
|
55
|
-
"overrides": {
|
|
56
|
-
"@opentui/core": "0.4.2",
|
|
57
|
-
"@opentui/react": "0.4.2"
|
|
58
53
|
}
|
|
59
54
|
}
|
package/src/ui/App.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react"
|
|
4
4
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
5
|
-
import {
|
|
5
|
+
import { ToastStack } from "./ToastStack.tsx"
|
|
6
6
|
import { theme } from "../lib/theme.ts"
|
|
7
7
|
import { useStore } from "./store.tsx"
|
|
8
8
|
import { Splash } from "./Splash.tsx"
|
|
@@ -262,10 +262,7 @@ export function App() {
|
|
|
262
262
|
{store.connectZoneTarget && <ProviderConnect />}
|
|
263
263
|
{store.dnsRecordsTarget && <DnsRecords />}
|
|
264
264
|
{store.kumaSite && <KumaSite />}
|
|
265
|
-
|
|
266
|
-
so it draws over every view + overlay; top-right, nudged clear of the
|
|
267
|
-
2-row Header. It never takes keyboard focus. */}
|
|
268
|
-
<Toaster position="top-right" offset={{ top: 2, right: 2 }} />
|
|
265
|
+
<ToastStack />
|
|
269
266
|
</box>
|
|
270
267
|
)
|
|
271
268
|
}
|
package/src/ui/Splash.tsx
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { useEffect, useState } from "react"
|
|
5
5
|
import { theme } from "../lib/theme.ts"
|
|
6
6
|
import { Spinner } from "./components.tsx"
|
|
7
|
+
import { APP_VERSION } from "../version.ts"
|
|
7
8
|
|
|
8
9
|
const TAGLINE = "Terminal control center for your SpinupWP fleet"
|
|
9
10
|
|
|
@@ -40,6 +41,8 @@ export function Splash({ status }: { status: string }) {
|
|
|
40
41
|
<box style={{ height: 1 }} />
|
|
41
42
|
<text content={TAGLINE} fg={theme.text} />
|
|
42
43
|
<box style={{ height: 1 }} />
|
|
44
|
+
<text content={`Version ${APP_VERSION}`} fg={theme.brand} />
|
|
45
|
+
<box style={{ height: 1 }} />
|
|
43
46
|
<Pulse />
|
|
44
47
|
<box style={{ height: 2 }} />
|
|
45
48
|
<box style={{ flexDirection: "row" }}>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Renders the toast queue from ./toast.ts. See that file for why this is
|
|
2
|
+
// first-party instead of a dependency. Mounted last in App.tsx so it draws
|
|
3
|
+
// over every view + overlay; top-right, nudged clear of the 2-row Header.
|
|
4
|
+
// It never takes keyboard focus.
|
|
5
|
+
|
|
6
|
+
import { useSyncExternalStore } from "react"
|
|
7
|
+
import { theme } from "../lib/theme.ts"
|
|
8
|
+
import { getToasts, subscribeToasts } from "./toast.ts"
|
|
9
|
+
|
|
10
|
+
const TOAST_WIDTH = 60
|
|
11
|
+
|
|
12
|
+
export function ToastStack() {
|
|
13
|
+
const toasts = useSyncExternalStore(subscribeToasts, getToasts, getToasts)
|
|
14
|
+
|
|
15
|
+
if (toasts.length === 0) return null
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<box style={{ position: "absolute", top: 2, right: 2, flexDirection: "column", gap: 1, zIndex: 300 }}>
|
|
19
|
+
{toasts.map((t) => (
|
|
20
|
+
<box
|
|
21
|
+
key={t.id}
|
|
22
|
+
border
|
|
23
|
+
borderColor={theme.good}
|
|
24
|
+
backgroundColor={theme.bgPanel}
|
|
25
|
+
style={{ flexDirection: "row", gap: 1, maxWidth: TOAST_WIDTH, paddingLeft: 1, paddingRight: 1 }}
|
|
26
|
+
>
|
|
27
|
+
<text content="✓" fg={theme.good} />
|
|
28
|
+
<text content={t.message} fg={theme.text} />
|
|
29
|
+
</box>
|
|
30
|
+
))}
|
|
31
|
+
</box>
|
|
32
|
+
)
|
|
33
|
+
}
|
package/src/ui/store.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// trigger a refresh.
|
|
7
7
|
|
|
8
8
|
import { createContext, useContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"
|
|
9
|
-
import { toast } from "
|
|
9
|
+
import { toast } from "./toast.ts"
|
|
10
10
|
import { SpinupWPClient, ApiError, type ServerService, type SpinupWPClientLike } from "../api/client.ts"
|
|
11
11
|
import { isDevMode } from "../dev/devMode.ts"
|
|
12
12
|
import { createMockClient } from "../dev/mockClient.ts"
|
package/src/ui/toast.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Minimal first-party toast notifications.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the @opentui-ui/toast package, which is stuck on a peer-dependency
|
|
4
|
+
// range from opentui's 0.1.x days and breaks against the @opentui/core 0.4.x
|
|
5
|
+
// line we're on (see CHANGELOG v0.24.2/v0.24.3). Every call site in this app
|
|
6
|
+
// only ever fires a single-line success message, so a small first-party
|
|
7
|
+
// implementation is simpler — and immune to upstream drift — than depending
|
|
8
|
+
// on a package built for a much larger feature set we don't use.
|
|
9
|
+
//
|
|
10
|
+
// A plain module-level subscriber list (rather than React state) so callers
|
|
11
|
+
// outside the component tree — store.tsx's async completion handlers — can
|
|
12
|
+
// fire a toast without needing a ref or context. ToastStack.tsx reads this
|
|
13
|
+
// via useSyncExternalStore.
|
|
14
|
+
|
|
15
|
+
export interface ToastItem {
|
|
16
|
+
id: number
|
|
17
|
+
message: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TOAST_DURATION_MS = 4000
|
|
21
|
+
|
|
22
|
+
let nextId = 1
|
|
23
|
+
let toasts: ToastItem[] = []
|
|
24
|
+
const listeners = new Set<() => void>()
|
|
25
|
+
|
|
26
|
+
function notify() {
|
|
27
|
+
for (const listener of listeners) listener()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function subscribeToasts(listener: () => void): () => void {
|
|
31
|
+
listeners.add(listener)
|
|
32
|
+
return () => listeners.delete(listener)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getToasts(): ToastItem[] {
|
|
36
|
+
return toasts
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const toast = {
|
|
40
|
+
success(message: string) {
|
|
41
|
+
const id = nextId++
|
|
42
|
+
toasts = [...toasts, { id, message }]
|
|
43
|
+
notify()
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
toasts = toasts.filter((t) => t.id !== id)
|
|
46
|
+
notify()
|
|
47
|
+
}, TOAST_DURATION_MS)
|
|
48
|
+
},
|
|
49
|
+
}
|
|
@@ -15,6 +15,9 @@ import { Panel, Spinner, Centered } from "../components.tsx"
|
|
|
15
15
|
import { StatusBar } from "../StatusBar.tsx"
|
|
16
16
|
import { useStore } from "../store.tsx"
|
|
17
17
|
import { moveSelection } from "../List.tsx"
|
|
18
|
+
import { openUrl } from "../../lib/open.ts"
|
|
19
|
+
import { serverWebUrl } from "../../lib/spinupweb.ts"
|
|
20
|
+
import { toast } from "../toast.ts"
|
|
18
21
|
import type { RebootInfo } from "../../lib/ssh.ts"
|
|
19
22
|
import type { ServerOpKind } from "../store.tsx"
|
|
20
23
|
|
|
@@ -56,9 +59,11 @@ export function ServerActions() {
|
|
|
56
59
|
rebootInfoLoading,
|
|
57
60
|
rebootInfoErrors,
|
|
58
61
|
loadRebootInfo,
|
|
62
|
+
accountSlug,
|
|
59
63
|
} = store
|
|
60
64
|
|
|
61
65
|
const rebootRequired = !!server?.reboot_required
|
|
66
|
+
const upgradeRequired = !!server?.upgrade_required
|
|
62
67
|
const [phase, setPhase] = useState<Phase>("pick")
|
|
63
68
|
// Cursor starts on Reboot when one is pending, else on the first restart.
|
|
64
69
|
const [index, setIndex] = useState(() => (server?.reboot_required ? 0 : 1))
|
|
@@ -108,6 +113,12 @@ export function ServerActions() {
|
|
|
108
113
|
setTarget(ACTIONS[index])
|
|
109
114
|
setPhase("confirm")
|
|
110
115
|
return
|
|
116
|
+
case "w":
|
|
117
|
+
if (upgradeRequired && server) {
|
|
118
|
+
openUrl(serverWebUrl(server.id, accountSlug))
|
|
119
|
+
toast.success(accountSlug ? "Opening in SpinupWP…" : "Set accountSlug for deep links — opening dashboard")
|
|
120
|
+
}
|
|
121
|
+
return
|
|
111
122
|
}
|
|
112
123
|
return
|
|
113
124
|
}
|
|
@@ -179,17 +190,17 @@ export function ServerActions() {
|
|
|
179
190
|
{/* Reboot "why" context */}
|
|
180
191
|
{rebootRequired && (
|
|
181
192
|
<>
|
|
182
|
-
<box style={{ flexDirection: "
|
|
183
|
-
<text content="⚠ reboot pending
|
|
193
|
+
<box style={{ flexDirection: "column" }}>
|
|
194
|
+
<text content="⚠ reboot pending" fg={theme.warn} wrapMode="none" />
|
|
184
195
|
{infoLoading ? (
|
|
185
196
|
<box style={{ flexDirection: "row" }}>
|
|
186
197
|
<Spinner interval={120} />
|
|
187
198
|
<text content=" checking what's pending…" fg={theme.textDim} wrapMode="none" />
|
|
188
199
|
</box>
|
|
189
200
|
) : info ? (
|
|
190
|
-
<text content={rebootSummary(info)} fg={theme.textDim}
|
|
201
|
+
<text content={rebootSummary(info)} fg={theme.textDim} />
|
|
191
202
|
) : infoError ? (
|
|
192
|
-
<text content=
|
|
203
|
+
<text content={infoError} fg={theme.textFaint} />
|
|
193
204
|
) : (
|
|
194
205
|
<text content="reboot required" fg={theme.textDim} wrapMode="none" />
|
|
195
206
|
)}
|
|
@@ -197,6 +208,20 @@ export function ServerActions() {
|
|
|
197
208
|
<box style={{ height: 1 }} />
|
|
198
209
|
</>
|
|
199
210
|
)}
|
|
211
|
+
{/* SpinupWP platform upgrade — the API only exposes this as a flag,
|
|
212
|
+
no detail; the only actionable path is the web app. Two lines
|
|
213
|
+
(not a height:1 row) so the explanation can wrap instead of
|
|
214
|
+
overflowing the panel — see the flexGrow/flexShrink gotcha in
|
|
215
|
+
CLAUDE.md. */}
|
|
216
|
+
{upgradeRequired && (
|
|
217
|
+
<>
|
|
218
|
+
<box style={{ flexDirection: "column" }}>
|
|
219
|
+
<text content="⬆ SpinupWP upgrade pending" fg={theme.warn} wrapMode="none" />
|
|
220
|
+
<text content="Not doable from here — press w to open it in SpinupWP." fg={theme.textDim} />
|
|
221
|
+
</box>
|
|
222
|
+
<box style={{ height: 1 }} />
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
200
225
|
{ACTIONS.map((a, i) => {
|
|
201
226
|
const selected = i === index
|
|
202
227
|
const recommended = a.kind === "reboot" && rebootRequired
|
|
@@ -308,6 +333,7 @@ export function ServerActions() {
|
|
|
308
333
|
return [
|
|
309
334
|
{ key: "↑↓/jk", label: "action" },
|
|
310
335
|
{ key: "⏎", label: "choose" },
|
|
336
|
+
...(upgradeRequired ? [{ key: "w", label: "open in SpinupWP" }] : []),
|
|
311
337
|
{ key: "esc", label: "cancel" },
|
|
312
338
|
]
|
|
313
339
|
case "confirm":
|