@hoox-sh/hoox-tui 0.2.0 → 0.3.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.
- package/README.md +50 -31
- package/dist/main.js +4239 -1743
- package/package.json +8 -5
- package/src/app.tsx +45 -30
- package/src/cli-bridge-test-double.ts +48 -1
- package/src/components/layout/sidebar.tsx +27 -11
- package/src/components/layout/statusbar.test.tsx +19 -5
- package/src/components/layout/statusbar.tsx +34 -18
- package/src/components/shared/crash-screen.tsx +24 -2
- package/src/components/shared/error-boundary.tsx +56 -33
- package/src/components/ui/connection-toasts.test.ts +58 -1
- package/src/components/ui/toast.tsx +44 -92
- package/src/components/views/config-editor.test.tsx +4 -2
- package/src/components/views/config-editor.tsx +31 -3
- package/src/components/views/dashboard/alerts-panel.tsx +36 -8
- package/src/components/views/dashboard/auto-repair-helpers.test.ts +23 -0
- package/src/components/views/dashboard.test.tsx +2 -1
- package/src/components/views/dashboard.tsx +126 -41
- package/src/components/views/db-query.test.tsx +7 -0
- package/src/components/views/db-query.tsx +5 -4
- package/src/components/views/edge-topology.tsx +3 -2
- package/src/components/views/kv-viewer.test.tsx +28 -0
- package/src/components/views/kv-viewer.tsx +4 -3
- package/src/components/views/logs-viewer.tsx +46 -19
- package/src/components/views/queue-depth.test.tsx +39 -0
- package/src/components/views/queue-depth.tsx +3 -2
- package/src/components/views/secrets-viewer.test.tsx +2 -2
- package/src/components/views/secrets-viewer.tsx +4 -3
- package/src/components/views/service-manager.tsx +147 -53
- package/src/components/views/settings.tsx +50 -10
- package/src/components/views/setup-wizard.test.tsx +13 -0
- package/src/components/views/setup-wizard.tsx +53 -22
- package/src/components/views/trade-monitor.tsx +20 -13
- package/src/components/views/worker-detail.tsx +27 -12
- package/src/components/views/worker-settings.test.tsx +5 -6
- package/src/components/views/worker-settings.tsx +13 -11
- package/src/components/views/workers-overview.tsx +27 -6
- package/src/hooks/hooks.test.ts +303 -9
- package/src/hooks/index.ts +7 -1
- package/src/hooks/shell-overlay.ts +41 -0
- package/src/hooks/use-polling.ts +91 -35
- package/src/hooks/use-service-data.ts +21 -4
- package/src/main.tsx +6 -0
- package/src/services/cli-bridge/index.ts +28 -22
- package/src/services/cli-bridge/standalone.ts +29 -2
- package/src/services/cli-bridge.test.ts +848 -1
- package/src/services/clipboard.test.ts +217 -3
- package/src/services/dashboard-settings-loader.test.ts +50 -0
- package/src/services/tui-connection.test.ts +21 -0
- package/src/stores/service-store.test.ts +89 -3
- package/src/stores/ui-store.test.ts +23 -0
- package/src/test-setup.ts +11 -2
- package/src/types.ts +1 -0
- package/src/view-registry.test.ts +3 -1
- package/src/view-registry.tsx +14 -1
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* Provides toast helper functions (success, error, warning, info, loading)
|
|
9
|
-
* pre-configured with Hoox design system colors and durations.
|
|
7
|
+
* Fail-closed toast helpers.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* `@opentui-ui/toast@0.0.5` peers on old OpenTUI and often installs a nested
|
|
10
|
+
* `@opentui/core` that re-registers env vars (e.g. OPENTUI_FORCE_WCWIDTH)
|
|
11
|
+
* with a conflicting schema — that hard-crashes `hoox-tui` on global install.
|
|
12
|
+
*
|
|
13
|
+
* We therefore never import `@opentui-ui/toast` at module load. Helpers are
|
|
14
|
+
* pure no-ops (return a stub id). Connection/auth UX uses the status bar
|
|
15
|
+
* and service-store alerts instead.
|
|
14
16
|
*/
|
|
15
|
-
import { toast, TOAST_DURATION } from "@opentui-ui/toast";
|
|
16
17
|
import { Colors } from "@hoox-sh/hoox-shared";
|
|
17
18
|
|
|
18
19
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
@@ -29,128 +30,79 @@ export interface ToastOptions {
|
|
|
29
30
|
action?: { label: string; onClick: () => void };
|
|
30
31
|
/** Show a close button on the toast (default: false) */
|
|
31
32
|
closeButton?: boolean;
|
|
33
|
+
/** Optional id when updating an existing toast */
|
|
34
|
+
id?: ToastId;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** Returned toast ID for programmatic control */
|
|
35
38
|
export type ToastId = string | number;
|
|
36
39
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const baseStyle = {
|
|
41
|
-
backgroundColor: Colors.card,
|
|
42
|
-
foregroundColor: Colors.foreground,
|
|
43
|
-
borderColor: Colors.border,
|
|
44
|
-
paddingX: 1,
|
|
45
|
-
paddingY: 0,
|
|
46
|
-
};
|
|
40
|
+
// Keep style tokens referenced so callers/tests can still import Colors path
|
|
41
|
+
// and so future re-enable of a compatible toast lib has defaults ready.
|
|
42
|
+
void Colors;
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
info: { borderColor: Colors.highlight },
|
|
54
|
-
loading: { borderColor: Colors.dim },
|
|
55
|
-
};
|
|
44
|
+
let toastSeq = 0;
|
|
45
|
+
function nextId(): ToastId {
|
|
46
|
+
toastSeq += 1;
|
|
47
|
+
return `toast-noop-${toastSeq}`;
|
|
48
|
+
}
|
|
56
49
|
|
|
57
|
-
// ── Toast helper functions
|
|
50
|
+
// ── Toast helper functions (no-op, fail-closed) ────────────────────────────
|
|
58
51
|
|
|
59
52
|
/**
|
|
60
53
|
* Show a success toast notification.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* toastSuccess("Worker deployed successfully")
|
|
54
|
+
* Currently a no-op (see module doc) — status bar / alerts carry feedback.
|
|
65
55
|
*/
|
|
66
|
-
export function toastSuccess(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
56
|
+
export function toastSuccess(
|
|
57
|
+
_message: string,
|
|
58
|
+
_options?: ToastOptions
|
|
59
|
+
): ToastId {
|
|
60
|
+
return nextId();
|
|
72
61
|
}
|
|
73
62
|
|
|
74
63
|
/**
|
|
75
64
|
* Show an error toast notification.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* toastError("Failed to connect to API", { description: "Check your network" })
|
|
65
|
+
* Currently a no-op (see module doc).
|
|
80
66
|
*/
|
|
81
|
-
export function toastError(
|
|
82
|
-
return
|
|
83
|
-
...options,
|
|
84
|
-
style: { ...baseStyle, ...hooxTypeStyles.error },
|
|
85
|
-
duration: options?.duration ?? TOAST_DURATION.LONG,
|
|
86
|
-
});
|
|
67
|
+
export function toastError(_message: string, _options?: ToastOptions): ToastId {
|
|
68
|
+
return nextId();
|
|
87
69
|
}
|
|
88
70
|
|
|
89
71
|
/**
|
|
90
72
|
* Show a warning toast notification.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* toastWarning("Rate limit approaching", { description: "Slow down requests" })
|
|
73
|
+
* Currently a no-op (see module doc).
|
|
95
74
|
*/
|
|
96
|
-
export function toastWarning(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
75
|
+
export function toastWarning(
|
|
76
|
+
_message: string,
|
|
77
|
+
_options?: ToastOptions
|
|
78
|
+
): ToastId {
|
|
79
|
+
return nextId();
|
|
102
80
|
}
|
|
103
81
|
|
|
104
82
|
/**
|
|
105
83
|
* Show an info toast notification.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* toastInfo("New version available", { action: { label: "Update", onClick: () => {} } })
|
|
84
|
+
* Currently a no-op (see module doc).
|
|
110
85
|
*/
|
|
111
|
-
export function toastInfo(
|
|
112
|
-
return
|
|
113
|
-
...options,
|
|
114
|
-
style: { ...baseStyle, ...hooxTypeStyles.info },
|
|
115
|
-
duration: options?.duration ?? TOAST_DURATION.DEFAULT,
|
|
116
|
-
});
|
|
86
|
+
export function toastInfo(_message: string, _options?: ToastOptions): ToastId {
|
|
87
|
+
return nextId();
|
|
117
88
|
}
|
|
118
89
|
|
|
119
90
|
/**
|
|
120
91
|
* Show a persistent loading toast notification.
|
|
121
|
-
*
|
|
122
|
-
* Returns the toast ID so it can be updated to success/error later.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* const id = toastLoading("Deploying worker...")
|
|
126
|
-
* try {
|
|
127
|
-
* await deploy()
|
|
128
|
-
* toastSuccess("Deployed!", { id })
|
|
129
|
-
* } catch {
|
|
130
|
-
* toastError("Deploy failed", { id })
|
|
131
|
-
* }
|
|
92
|
+
* Currently a no-op (see module doc).
|
|
132
93
|
*/
|
|
133
94
|
export function toastLoading(
|
|
134
|
-
|
|
135
|
-
|
|
95
|
+
_message: string,
|
|
96
|
+
_options?: Omit<ToastOptions, "duration">
|
|
136
97
|
): ToastId {
|
|
137
|
-
return
|
|
138
|
-
...options,
|
|
139
|
-
style: { ...baseStyle, ...hooxTypeStyles.loading },
|
|
140
|
-
duration: TOAST_DURATION.PERSISTENT,
|
|
141
|
-
} as ToastOptions);
|
|
98
|
+
return nextId();
|
|
142
99
|
}
|
|
143
100
|
|
|
144
101
|
/**
|
|
145
|
-
* Dismiss a specific toast or all toasts.
|
|
146
|
-
* Thin wrapper around toast.dismiss().
|
|
147
|
-
*
|
|
148
|
-
* @example
|
|
149
|
-
* dismissToast(id) // dismiss specific
|
|
150
|
-
* dismissToast() // dismiss all
|
|
102
|
+
* Dismiss a specific toast or all toasts (no-op).
|
|
151
103
|
*/
|
|
152
|
-
export function dismissToast(
|
|
153
|
-
|
|
104
|
+
export function dismissToast(_id?: ToastId): void {
|
|
105
|
+
// no-op
|
|
154
106
|
}
|
|
155
107
|
|
|
156
108
|
// ── Clipboard copy feedback ────────────────────────────────────────────────
|
|
@@ -105,9 +105,11 @@ describe("ConfigEditor", () => {
|
|
|
105
105
|
expect(CONFIG_TREE_BLUEPRINT[0].type).toBe("directory");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
it("contains
|
|
108
|
+
it("contains config files without secret-like .env entries", async () => {
|
|
109
109
|
const children = CONFIG_TREE_BLUEPRINT[0].children!;
|
|
110
|
-
|
|
110
|
+
// wrangler.toml, trade.config.json, risk.config.json, strategies (no .env)
|
|
111
|
+
expect(children.length).toBe(4);
|
|
112
|
+
expect(children.some((c) => c.name === ".env")).toBe(false);
|
|
111
113
|
});
|
|
112
114
|
|
|
113
115
|
it("strategies directory has 3 config files", async () => {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { existsSync } from "node:fs";
|
|
34
34
|
import { isAbsolute, relative, resolve as pathResolve } from "node:path";
|
|
35
35
|
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
import {
|
|
38
38
|
Colors,
|
|
39
39
|
getHooxRepoPath,
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
formatContent,
|
|
61
61
|
} from "./config-editor/code-editor";
|
|
62
62
|
import { ActionBar } from "./config-editor/action-bar";
|
|
63
|
+
import { useViewKeyboard } from "../../hooks/shell-overlay";
|
|
63
64
|
|
|
64
65
|
// Re-export extracted types and functions for backward compatibility
|
|
65
66
|
export type {
|
|
@@ -134,7 +135,8 @@ export const CONFIG_TREE_BLUEPRINT: FileNode[] = [
|
|
|
134
135
|
},
|
|
135
136
|
],
|
|
136
137
|
},
|
|
137
|
-
|
|
138
|
+
// .env intentionally omitted — secret material must not be shown in-pane.
|
|
139
|
+
// Operators edit env files externally (or via `hoox config secrets …`).
|
|
138
140
|
],
|
|
139
141
|
},
|
|
140
142
|
];
|
|
@@ -272,7 +274,28 @@ export function resolveConfigFilePath(relativePath: string): string {
|
|
|
272
274
|
* Read a config file from disk.
|
|
273
275
|
* Enforces path safety + max size. Returns empty string if missing.
|
|
274
276
|
*/
|
|
277
|
+
/** True for secret-like paths that must never be rendered in the TUI. */
|
|
278
|
+
export function isSecretConfigPath(relativePath: string): boolean {
|
|
279
|
+
const base = relativePath.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
280
|
+
const lower = base.toLowerCase();
|
|
281
|
+
return (
|
|
282
|
+
lower === ".env" ||
|
|
283
|
+
lower.startsWith(".env.") ||
|
|
284
|
+
lower.endsWith(".pem") ||
|
|
285
|
+
lower.endsWith(".key") ||
|
|
286
|
+
lower.includes("secret") ||
|
|
287
|
+
lower.includes("credentials")
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
async function loadFileContent(relativePath: string): Promise<string> {
|
|
292
|
+
if (isSecretConfigPath(relativePath)) {
|
|
293
|
+
return (
|
|
294
|
+
`# ${relativePath}\n` +
|
|
295
|
+
`# BLOCKED: secret-like files are not displayed in the TUI.\n` +
|
|
296
|
+
`# Edit externally or use: hoox config secrets set <NAME>\n`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
276
299
|
let fullPath: string;
|
|
277
300
|
try {
|
|
278
301
|
fullPath = resolveConfigFilePath(relativePath);
|
|
@@ -311,6 +334,11 @@ async function saveFileContent(
|
|
|
311
334
|
relativePath: string,
|
|
312
335
|
content: string
|
|
313
336
|
): Promise<void> {
|
|
337
|
+
if (isSecretConfigPath(relativePath)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Refuse to write secret-like path from TUI: ${relativePath}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
314
342
|
if (content.length > MAX_CONFIG_FILE_BYTES) {
|
|
315
343
|
throw new Error(
|
|
316
344
|
`Content exceeds max size (${MAX_CONFIG_FILE_BYTES} bytes); refuse to write`
|
|
@@ -548,7 +576,7 @@ export function ConfigEditor() {
|
|
|
548
576
|
|
|
549
577
|
// ── Keyboard (only when this view is active) ───────────────────────────
|
|
550
578
|
|
|
551
|
-
|
|
579
|
+
useViewKeyboard((key) => {
|
|
552
580
|
if (!isActive) return;
|
|
553
581
|
if (key.ctrl && key.name === "z") {
|
|
554
582
|
handleUndo();
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useMemo, useEffect } from "react";
|
|
7
|
-
import { useKeyboard } from "@opentui/react";
|
|
8
7
|
import {
|
|
9
8
|
Colors,
|
|
10
9
|
AlertSeverityColor,
|
|
11
10
|
useServiceStore,
|
|
11
|
+
useUIStore,
|
|
12
12
|
} from "@hoox-sh/hoox-shared";
|
|
13
13
|
import type { AlertSeverity } from "@hoox-sh/hoox-shared";
|
|
14
|
+
import { useViewKeyboard } from "../../../hooks/shell-overlay";
|
|
14
15
|
|
|
15
16
|
/** Severity label prefix */
|
|
16
17
|
const SEVERITY_LABEL: Record<AlertSeverity, string> = {
|
|
@@ -38,10 +39,17 @@ function formatTime(ts: number): string {
|
|
|
38
39
|
/**
|
|
39
40
|
* AlertsPanel — scrollable list of recent alerts, newest first.
|
|
40
41
|
* Each alert row shows: [SEV] HH:MM:SS — message
|
|
41
|
-
* Color-coded by severity.
|
|
42
|
+
* Color-coded by severity.
|
|
43
|
+
*
|
|
44
|
+
* Keyboard (when Dashboard is active):
|
|
45
|
+
* ↑↓ — move selection
|
|
46
|
+
* Enter / Space — acknowledge selected
|
|
47
|
+
* x / Delete — dismiss (remove) selected
|
|
42
48
|
*/
|
|
43
49
|
function AlertsPanel() {
|
|
44
50
|
const alerts = useServiceStore((s) => s.alerts);
|
|
51
|
+
const activeView = useUIStore((s) => s.activeView);
|
|
52
|
+
const isActive = activeView === "dashboard";
|
|
45
53
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
46
54
|
|
|
47
55
|
// Newest first, limited
|
|
@@ -56,14 +64,33 @@ function AlertsPanel() {
|
|
|
56
64
|
setScrollOffset((o) => Math.min(o, Math.max(0, sortedAlerts.length - 1)));
|
|
57
65
|
}, [sortedAlerts.length]);
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
useViewKeyboard((key) => {
|
|
68
|
+
if (!isActive) return;
|
|
69
|
+
const name = String(key.name ?? "").toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (name === "up") {
|
|
62
72
|
setScrollOffset((o) => Math.max(0, o - 1));
|
|
63
|
-
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (name === "down") {
|
|
64
76
|
setScrollOffset((o) =>
|
|
65
77
|
Math.min(Math.max(0, sortedAlerts.length - 1), o + 1)
|
|
66
78
|
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const selected = sortedAlerts[scrollOffset];
|
|
83
|
+
if (!selected) return;
|
|
84
|
+
|
|
85
|
+
if (name === "return" || name === "enter" || name === "space") {
|
|
86
|
+
useServiceStore.getState().acknowledgeAlert(selected.id);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (name === "x" || name === "delete" || name === "backspace") {
|
|
90
|
+
useServiceStore.getState().dismissAlert(selected.id);
|
|
91
|
+
setScrollOffset((o) =>
|
|
92
|
+
Math.max(0, o - (o > 0 && o >= sortedAlerts.length - 1 ? 1 : 0))
|
|
93
|
+
);
|
|
67
94
|
}
|
|
68
95
|
});
|
|
69
96
|
|
|
@@ -139,11 +166,12 @@ function AlertsPanel() {
|
|
|
139
166
|
</scrollbox>
|
|
140
167
|
)}
|
|
141
168
|
|
|
142
|
-
{/* Scroll hint */}
|
|
169
|
+
{/* Scroll / action hint */}
|
|
143
170
|
{sortedAlerts.length > 0 && (
|
|
144
171
|
<box>
|
|
145
172
|
<text fg={Colors.dim} dim>
|
|
146
|
-
↑↓
|
|
173
|
+
↑↓ · Enter:ack · x:dismiss · {scrollOffset + 1}/
|
|
174
|
+
{sortedAlerts.length}
|
|
147
175
|
</text>
|
|
148
176
|
</box>
|
|
149
177
|
)}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from "bun:test";
|
|
7
|
+
import { formatRepairTime } from "./auto-repair-panel";
|
|
8
|
+
|
|
9
|
+
describe("formatRepairTime", () => {
|
|
10
|
+
it("formats as HH:MM:SS with zero padding", () => {
|
|
11
|
+
// Use local Date so test is timezone-stable for the chosen components
|
|
12
|
+
const d = new Date(2026, 0, 1, 9, 5, 7);
|
|
13
|
+
const out = formatRepairTime(d.getTime());
|
|
14
|
+
expect(out).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
|
15
|
+
expect(out).toBe(
|
|
16
|
+
[
|
|
17
|
+
d.getHours().toString().padStart(2, "0"),
|
|
18
|
+
d.getMinutes().toString().padStart(2, "0"),
|
|
19
|
+
d.getSeconds().toString().padStart(2, "0"),
|
|
20
|
+
].join(":")
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -145,7 +145,8 @@ describe("DashboardView", () => {
|
|
|
145
145
|
});
|
|
146
146
|
const output = await renderDashboard();
|
|
147
147
|
expect(output).toContain("only-worker");
|
|
148
|
-
|
|
148
|
+
// Empty slots render single-dash placeholders
|
|
149
|
+
const dashCount = (output.match(/(?:^|\s)-\s/g) ?? []).length;
|
|
149
150
|
expect(dashCount).toBeGreaterThan(0);
|
|
150
151
|
});
|
|
151
152
|
|