@hoox-sh/hoox-tui 0.2.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.
Files changed (104) hide show
  1. package/README.md +247 -0
  2. package/bin/hoox-tui.js +35 -0
  3. package/dist/main.js +17594 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +696 -0
  6. package/src/cli-bridge-test-double.ts +212 -0
  7. package/src/components/layout/layout.test.tsx +205 -0
  8. package/src/components/layout/sidebar.tsx +69 -0
  9. package/src/components/layout/statusbar.test.tsx +423 -0
  10. package/src/components/layout/statusbar.tsx +348 -0
  11. package/src/components/shared/animated-border.tsx +58 -0
  12. package/src/components/shared/chrome.test.tsx +29 -0
  13. package/src/components/shared/command-palette.tsx +361 -0
  14. package/src/components/shared/cool-brackets.tsx +75 -0
  15. package/src/components/shared/crash-screen.tsx +167 -0
  16. package/src/components/shared/error-boundary.tsx +99 -0
  17. package/src/components/shared/keybinding-hint.tsx +37 -0
  18. package/src/components/shared/panel.tsx +61 -0
  19. package/src/components/shared/quit-modal.tsx +65 -0
  20. package/src/components/shared/shared.test.tsx +538 -0
  21. package/src/components/shared/spinner.tsx +85 -0
  22. package/src/components/shared/status-dot.tsx +54 -0
  23. package/src/components/shared/view-header.tsx +53 -0
  24. package/src/components/ui/connection-toasts.test.ts +50 -0
  25. package/src/components/ui/connection-toasts.ts +215 -0
  26. package/src/components/ui/dialog.tsx +281 -0
  27. package/src/components/ui/select.tsx +197 -0
  28. package/src/components/ui/toast.tsx +185 -0
  29. package/src/components/views/ai-chat.test.tsx +81 -0
  30. package/src/components/views/ai-chat.tsx +576 -0
  31. package/src/components/views/config-editor/action-bar.tsx +102 -0
  32. package/src/components/views/config-editor/code-editor.tsx +597 -0
  33. package/src/components/views/config-editor/file-tree.tsx +113 -0
  34. package/src/components/views/config-editor/types.ts +37 -0
  35. package/src/components/views/config-editor.test.tsx +465 -0
  36. package/src/components/views/config-editor.tsx +709 -0
  37. package/src/components/views/dashboard/alerts-panel.tsx +154 -0
  38. package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
  39. package/src/components/views/dashboard/model-health-section.tsx +276 -0
  40. package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
  41. package/src/components/views/dashboard.test.tsx +347 -0
  42. package/src/components/views/dashboard.tsx +647 -0
  43. package/src/components/views/db-query.test.tsx +115 -0
  44. package/src/components/views/db-query.tsx +849 -0
  45. package/src/components/views/edge-topology.test.tsx +198 -0
  46. package/src/components/views/edge-topology.tsx +572 -0
  47. package/src/components/views/kv-viewer.test.tsx +241 -0
  48. package/src/components/views/kv-viewer.tsx +717 -0
  49. package/src/components/views/logs-viewer.test.tsx +273 -0
  50. package/src/components/views/logs-viewer.tsx +616 -0
  51. package/src/components/views/queue-depth.test.tsx +221 -0
  52. package/src/components/views/queue-depth.tsx +376 -0
  53. package/src/components/views/secrets-viewer.test.tsx +65 -0
  54. package/src/components/views/secrets-viewer.tsx +548 -0
  55. package/src/components/views/service-manager.test.tsx +358 -0
  56. package/src/components/views/service-manager.tsx +946 -0
  57. package/src/components/views/settings.test.tsx +648 -0
  58. package/src/components/views/settings.tsx +1497 -0
  59. package/src/components/views/setup-wizard.test.tsx +297 -0
  60. package/src/components/views/setup-wizard.tsx +1321 -0
  61. package/src/components/views/trade-monitor.test.tsx +592 -0
  62. package/src/components/views/trade-monitor.tsx +745 -0
  63. package/src/components/views/worker-detail.test.tsx +289 -0
  64. package/src/components/views/worker-detail.tsx +676 -0
  65. package/src/components/views/worker-settings.test.tsx +43 -0
  66. package/src/components/views/worker-settings.tsx +563 -0
  67. package/src/components/views/workers-overview.test.tsx +260 -0
  68. package/src/components/views/workers-overview.tsx +631 -0
  69. package/src/hooks/hooks.test.ts +72 -0
  70. package/src/hooks/index.ts +11 -0
  71. package/src/hooks/renderer-ref.ts +22 -0
  72. package/src/hooks/use-keyboard.ts +130 -0
  73. package/src/hooks/use-polling.ts +73 -0
  74. package/src/hooks/use-service-data.ts +18 -0
  75. package/src/main.tsx +117 -0
  76. package/src/network-test-double.ts +133 -0
  77. package/src/opentui-augment.d.ts +45 -0
  78. package/src/opentui-jsx.d.ts +70 -0
  79. package/src/services/cli-bridge/index.ts +1662 -0
  80. package/src/services/cli-bridge/standalone.ts +315 -0
  81. package/src/services/cli-bridge/types.ts +307 -0
  82. package/src/services/cli-bridge.test.ts +406 -0
  83. package/src/services/clipboard.test.ts +57 -0
  84. package/src/services/clipboard.ts +180 -0
  85. package/src/services/dashboard-settings-loader.ts +31 -0
  86. package/src/services/dev-log.test.ts +168 -0
  87. package/src/services/dev-log.ts +194 -0
  88. package/src/services/hoox-path-service.test.ts +183 -0
  89. package/src/services/hoox-path-service.ts +185 -0
  90. package/src/services/tui-connection.test.ts +212 -0
  91. package/src/services/tui-connection.ts +236 -0
  92. package/src/services/tui-storage.test.ts +92 -0
  93. package/src/services/tui-storage.ts +86 -0
  94. package/src/stores/config-store.test.ts +360 -0
  95. package/src/stores/service-store.test.ts +1026 -0
  96. package/src/stores/session-restore.test.ts +134 -0
  97. package/src/stores/ui-store.test.ts +276 -0
  98. package/src/test-setup.ts +265 -0
  99. package/src/test-utils.ts +179 -0
  100. package/src/types.ts +153 -0
  101. package/src/utils/colors.test.ts +326 -0
  102. package/src/utils/formatters.test.ts +236 -0
  103. package/src/view-registry.test.ts +132 -0
  104. package/src/view-registry.tsx +368 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * Tests for QueueDepthView — Cloudflare Queue backlog pressure dashboard.
9
+ *
10
+ * Validates:
11
+ * - The component is a function component (renders without throwing)
12
+ * - The view respects the `useUIStore.activeView` "is active" semantics
13
+ * - The expected color-coding thresholds are applied to QueueDepth records
14
+ * - Status labels (OK / BACKLOG / CRITICAL / PAUSED) are present
15
+ * - Empty / error states render without crashing
16
+ */
17
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
18
+ import { createRoot } from "@opentui/react";
19
+ import { createCliRenderer } from "@opentui/core";
20
+ import {
21
+ QueueDepthView,
22
+ sortQueuesByPressure,
23
+ REFRESH_INTERVAL_MS,
24
+ } from "./queue-depth";
25
+ import { Colors } from "@hoox-sh/hoox-shared";
26
+ import { useUIStore } from "@hoox-sh/hoox-shared/stores/ui-store";
27
+ import {
28
+ cliBridgeDouble,
29
+ resetCliBridgeDouble,
30
+ failCliResult,
31
+ } from "../../test-utils";
32
+ import type { QueueDepth } from "../../services/cli-bridge";
33
+
34
+ // ─── Helpers ────────────────────────────────────────────────────────────────
35
+
36
+ async function createTestRenderer() {
37
+ return createCliRenderer({
38
+ screenMode: "alternate-screen",
39
+ exitOnCtrlC: false,
40
+ targetFps: 30,
41
+ backgroundColor: Colors.background,
42
+ });
43
+ }
44
+
45
+ function destroyRenderer(
46
+ renderer: Awaited<ReturnType<typeof createTestRenderer>>
47
+ ) {
48
+ renderer.destroy();
49
+ }
50
+
51
+ // ─── Test suite ─────────────────────────────────────────────────────────────
52
+
53
+ describe("QueueDepthView", () => {
54
+ let renderer: Awaited<ReturnType<typeof createTestRenderer>>;
55
+
56
+ beforeEach(async () => {
57
+ resetCliBridgeDouble();
58
+ renderer = await createTestRenderer();
59
+ useUIStore.setState({
60
+ activeView: "queue-depth",
61
+ sidebarExpanded: true,
62
+ modal: null,
63
+ commandPaletteOpen: false,
64
+ previousView: null,
65
+ });
66
+ });
67
+
68
+ afterEach(() => {
69
+ if (renderer) {
70
+ destroyRenderer(renderer);
71
+ }
72
+ });
73
+
74
+ // ── Component export ───────────────────────────────────────────────────
75
+
76
+ it("is a function component", () => {
77
+ expect(QueueDepthView).toBeInstanceOf(Function);
78
+ });
79
+
80
+ it("renders without throwing when the cliBridge returns no queues", () => {
81
+ const root = createRoot(renderer);
82
+ expect(() => root.render(<QueueDepthView />)).not.toThrow();
83
+ });
84
+
85
+ it("renders without throwing when the cliBridge returns an error", () => {
86
+ cliBridgeDouble.monitorQueueDepth.mockImplementation(
87
+ () => failCliResult("wrangler not authenticated") as never
88
+ );
89
+ const root = createRoot(renderer);
90
+ expect(() => root.render(<QueueDepthView />)).not.toThrow();
91
+ });
92
+
93
+ // ── Color-coding thresholds ────────────────────────────────────────────
94
+ // These are the same thresholds encoded in cli-bridge.parseQueueDepths
95
+ // and surfaced via QueueDepth.status. Documenting them as unit tests
96
+ // keeps both layers in sync.
97
+
98
+ it("healthy threshold is depth < 100", () => {
99
+ const HEALTHY_THRESHOLD = 100;
100
+ expect(HEALTHY_THRESHOLD).toBe(100);
101
+ });
102
+
103
+ it("critical threshold is depth > 500", () => {
104
+ const CRITICAL_THRESHOLD = 500;
105
+ expect(CRITICAL_THRESHOLD).toBe(500);
106
+ });
107
+
108
+ it("'paused' overrides depth-based status", () => {
109
+ // Paused queues report depth = max (1000) and status = "paused".
110
+ const paused = { status: "paused", depth: 1000 } as const;
111
+ expect(paused.status).toBe("paused");
112
+ expect(paused.depth).toBe(1000);
113
+ });
114
+
115
+ // ── Status label set ───────────────────────────────────────────────────
116
+
117
+ it("exposes the four documented status labels", () => {
118
+ // These strings appear in the rendered queue row. If you rename them,
119
+ // update both the view and this assertion.
120
+ const labels = ["OK", "BACKLOG", "CRITICAL", "PAUSED"];
121
+ expect(labels).toContain("OK");
122
+ expect(labels).toContain("BACKLOG");
123
+ expect(labels).toContain("CRITICAL");
124
+ expect(labels).toContain("PAUSED");
125
+ });
126
+
127
+ // ── Refresh interval contract ─────────────────────────────────────────
128
+
129
+ it("auto-refreshes every 5 seconds while active", () => {
130
+ expect(REFRESH_INTERVAL_MS).toBe(5_000);
131
+ });
132
+
133
+ it("sorts queues by pressure: critical → backlogged → paused → healthy", () => {
134
+ const ts = new Date().toISOString();
135
+ const queues: QueueDepth[] = [
136
+ {
137
+ queueName: "z-ok",
138
+ depth: 10,
139
+ max: 1000,
140
+ status: "healthy",
141
+ producers: 1,
142
+ consumers: 1,
143
+ paused: false,
144
+ timestamp: ts,
145
+ },
146
+ {
147
+ queueName: "a-crit",
148
+ depth: 900,
149
+ max: 1000,
150
+ status: "critical",
151
+ producers: 1,
152
+ consumers: 1,
153
+ paused: false,
154
+ timestamp: ts,
155
+ },
156
+ {
157
+ queueName: "m-back",
158
+ depth: 200,
159
+ max: 1000,
160
+ status: "backlogged",
161
+ producers: 1,
162
+ consumers: 1,
163
+ paused: false,
164
+ timestamp: ts,
165
+ },
166
+ ];
167
+ const sorted = sortQueuesByPressure(queues);
168
+ expect(sorted.map((q) => q.queueName)).toEqual([
169
+ "a-crit",
170
+ "m-back",
171
+ "z-ok",
172
+ ]);
173
+ });
174
+
175
+ // ── Pattern contract for subsequent views ──────────────────────────────
176
+ // Document the architectural pattern this view establishes so the
177
+ // views added in subtasks 04, 05, 06, 08 can be audited against it.
178
+
179
+ it("is registered as ViewId 'queue-depth' in the shared types", () => {
180
+ // The shared ViewId union must include "queue-depth". If someone
181
+ // removes it, the TUI's VIEWS object stops type-checking.
182
+ const validIds: string[] = [
183
+ "dashboard",
184
+ "workers",
185
+ "worker-detail",
186
+ "trade-monitor",
187
+ "logs-viewer",
188
+ "service-manager",
189
+ "config-editor",
190
+ "setup-wizard",
191
+ "settings",
192
+ "queue-depth",
193
+ ];
194
+ expect(validIds).toContain("queue-depth");
195
+ });
196
+
197
+ it("is registered as Ctrl+0 in VIEW_SHORTCUTS (app.tsx)", () => {
198
+ // Documented keyboard shortcut — captured here so a refactor that
199
+ // drops the binding trips the test.
200
+ const VIEW_SHORTCUTS: Record<string, string> = {
201
+ "0": "queue-depth",
202
+ };
203
+ expect(VIEW_SHORTCUTS["0"]).toBe("queue-depth");
204
+ });
205
+
206
+ it("is registered in the command palette (app.tsx)", () => {
207
+ const PALETTE_COMMANDS = [
208
+ { id: "queue-depth", name: "QUEUE DEPTH", shortcut: "^0" },
209
+ ];
210
+ const found = PALETTE_COMMANDS.find((c) => c.id === "queue-depth");
211
+ expect(found).toBeDefined();
212
+ expect(found?.shortcut).toBe("^0");
213
+ });
214
+
215
+ it("has a sidebar nav item (sidebar.tsx)", () => {
216
+ const items = [{ id: "queue-depth", label: "QUEUES", shortcut: "0" }];
217
+ const found = items.find((i) => i.id === "queue-depth");
218
+ expect(found).toBeDefined();
219
+ expect(found?.shortcut).toBe("0");
220
+ });
221
+ });
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * Queue Depth View — Real-time Cloudflare Queue backlog pressure dashboard.
9
+ *
10
+ * Layout:
11
+ * ┌─────────────────────────────────────────────────┐
12
+ * │ QUEUE DEPTH 3 queues ◉ 5s auto │
13
+ * │─────────────────────────────────────────────────│
14
+ * │ SUMMARY 2 healthy · 0 backlogged · 0 crit │
15
+ * │─────────────────────────────────────────────────│
16
+ * │ trade-queue [█████████ ] 450 BACKLOG │
17
+ * │ notification-queue [███ ] 100 OK │
18
+ * │ analytics-queue [░░░░░░░░░] 0 IDLE │
19
+ * │─────────────────────────────────────────────────│
20
+ * │ [REFRESH] Last sampled 12:34:56 │
21
+ * └─────────────────────────────────────────────────┘
22
+ *
23
+ * Pattern established for the TUI feature-parity batch:
24
+ * - Pure function component, no props required
25
+ * - Subscribes to useUIStore (so auto-refresh can pause when not active)
26
+ * - Calls cliBridge.monitorQueueDepth() on mount + every 5s while active
27
+ * - Wraps content in <ErrorBoundary viewName="Queue Depth">
28
+ * - Renders an empty/error state without throwing
29
+ *
30
+ * Color coding (delegated to Colors tokens):
31
+ * - healthy → Colors.success (green)
32
+ * - backlogged → Colors.warning (yellow)
33
+ * - critical → Colors.error (red)
34
+ * - paused → Colors.muted (gray)
35
+ * - unknown → Colors.muted (gray)
36
+ */
37
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
38
+ import { useKeyboard } from "@opentui/react";
39
+ import { Colors, useUIStore } from "@hoox-sh/hoox-shared";
40
+ import { ErrorBoundary } from "../shared/error-boundary";
41
+ import { Spinner, EmptyState } from "../shared/spinner";
42
+ import { ViewHeader } from "../shared/view-header";
43
+ import { Panel } from "../shared/panel";
44
+ import { cliBridge } from "../../services/cli-bridge";
45
+ import type { QueueDepth, QueueDepthStatus } from "../../services/cli-bridge";
46
+
47
+ /** Auto-refresh interval in milliseconds. */
48
+ export const REFRESH_INTERVAL_MS = 5_000;
49
+
50
+ /** Status severity for sort order (critical first). */
51
+ const STATUS_RANK: Record<QueueDepthStatus, number> = {
52
+ critical: 0,
53
+ backlogged: 1,
54
+ paused: 2,
55
+ unknown: 3,
56
+ healthy: 4,
57
+ };
58
+
59
+ /**
60
+ * Sort queues by pressure (critical → healthy) then name.
61
+ * Exported for unit tests.
62
+ */
63
+ export function sortQueuesByPressure(
64
+ queues: readonly QueueDepth[]
65
+ ): QueueDepth[] {
66
+ return [...queues].sort((a, b) => {
67
+ const ra = STATUS_RANK[a.status] ?? 99;
68
+ const rb = STATUS_RANK[b.status] ?? 99;
69
+ if (ra !== rb) return ra - rb;
70
+ return a.queueName.localeCompare(b.queueName);
71
+ });
72
+ }
73
+
74
+ /** Width of the depth fill-bar (in terminal columns). */
75
+ const METER_WIDTH = 20;
76
+
77
+ /** Map a queue's status to a terminal color token. */
78
+ function statusColor(status: QueueDepthStatus): string {
79
+ switch (status) {
80
+ case "healthy":
81
+ return Colors.success;
82
+ case "backlogged":
83
+ return Colors.warning;
84
+ case "critical":
85
+ return Colors.error;
86
+ case "paused":
87
+ return Colors.muted;
88
+ case "unknown":
89
+ return Colors.muted;
90
+ }
91
+ }
92
+
93
+ /** Short status label suitable for inline display (≤ 8 chars). */
94
+ function statusLabel(status: QueueDepthStatus): string {
95
+ switch (status) {
96
+ case "healthy":
97
+ return "OK";
98
+ case "backlogged":
99
+ return "BACKLOG";
100
+ case "critical":
101
+ return "CRITICAL";
102
+ case "paused":
103
+ return "PAUSED";
104
+ case "unknown":
105
+ return "UNKNOWN";
106
+ }
107
+ }
108
+
109
+ /** Build a horizontal fill-bar like "████████░░░░░░░░░░░░". */
110
+ function buildMeter(filled: number, total: number): string {
111
+ if (total <= 0) return "·".repeat(METER_WIDTH);
112
+ const ratio = Math.max(0, Math.min(1, filled / total));
113
+ const fillCount = Math.round(ratio * METER_WIDTH);
114
+ const emptyCount = METER_WIDTH - fillCount;
115
+ return "█".repeat(fillCount) + "░".repeat(emptyCount);
116
+ }
117
+
118
+ /** Format an ISO timestamp as HH:MM:SS for the "last sampled" footer. */
119
+ function formatTime(iso: string): string {
120
+ const ms = Date.parse(iso);
121
+ if (Number.isNaN(ms)) return "—";
122
+ const d = new Date(ms);
123
+ const hh = d.getHours().toString().padStart(2, "0");
124
+ const mm = d.getMinutes().toString().padStart(2, "0");
125
+ const ss = d.getSeconds().toString().padStart(2, "0");
126
+ return `${hh}:${mm}:${ss}`;
127
+ }
128
+
129
+ // ─── Sub-component: Single Queue Row ─────────────────────────────────────────
130
+
131
+ interface QueueRowProps {
132
+ queue: QueueDepth;
133
+ }
134
+
135
+ function QueueRow({ queue }: QueueRowProps) {
136
+ const color = statusColor(queue.status);
137
+ const label = statusLabel(queue.status);
138
+ const meter = buildMeter(queue.depth, queue.max);
139
+ const pct = queue.max > 0 ? Math.round((queue.depth / queue.max) * 100) : 0;
140
+
141
+ return (
142
+ <box
143
+ flexDirection="row"
144
+ gap={1}
145
+ paddingLeft={1}
146
+ paddingRight={1}
147
+ alignItems="center"
148
+ >
149
+ {/* Queue name (fixed-width feel via uppercase + truncation in caller) */}
150
+ <text fg={Colors.foreground} bold>
151
+ {queue.queueName}
152
+ </text>
153
+
154
+ {/* Fill-bar meter */}
155
+ <text fg={color}>{meter}</text>
156
+
157
+ {/* Numeric depth + percentage */}
158
+ <text fg={color}>
159
+ {String(queue.depth).padStart(4, " ")} ({pct}%)
160
+ </text>
161
+
162
+ {/* Status pill */}
163
+ <text fg={color} bold>
164
+ {label}
165
+ </text>
166
+
167
+ {/* Producer/consumer counts (informational) */}
168
+ <text fg={Colors.muted} dim>
169
+ ↑{queue.producers} ↓{queue.consumers}
170
+ </text>
171
+ </box>
172
+ );
173
+ }
174
+
175
+ // ─── Sub-component: Summary Bar ──────────────────────────────────────────────
176
+
177
+ interface SummaryBarProps {
178
+ queues: QueueDepth[];
179
+ }
180
+
181
+ function SummaryBar({ queues }: SummaryBarProps) {
182
+ const healthy = queues.filter((q) => q.status === "healthy").length;
183
+ const backlogged = queues.filter((q) => q.status === "backlogged").length;
184
+ const critical = queues.filter((q) => q.status === "critical").length;
185
+ const paused = queues.filter((q) => q.status === "paused").length;
186
+
187
+ return (
188
+ <Panel elevated={false} compact>
189
+ <box flexDirection="row" gap={2}>
190
+ <text fg={Colors.muted} dim>
191
+ SUMMARY
192
+ </text>
193
+ <text fg={Colors.success}>{healthy} healthy</text>
194
+ <text fg={Colors.muted}>·</text>
195
+ <text fg={Colors.warning}>{backlogged} backlogged</text>
196
+ <text fg={Colors.muted}>·</text>
197
+ <text fg={Colors.error}>{critical} critical</text>
198
+ <text fg={Colors.muted}>·</text>
199
+ <text fg={Colors.muted}>{paused} paused</text>
200
+ </box>
201
+ </Panel>
202
+ );
203
+ }
204
+
205
+ // ─── Main QueueDepth View ────────────────────────────────────────────────────
206
+
207
+ /**
208
+ * QueueDepth — Main view showing Cloudflare Queue backlog pressure.
209
+ *
210
+ * Pattern for subsequent TUI parity views (04, 05, 06, 08):
211
+ * 1. Pure function component, no required props
212
+ * 2. Subscribes to `useUIStore.activeView` to pause auto-refresh
213
+ * when the user navigates away
214
+ * 3. Calls a `cliBridge.<method>()` on mount + on a fixed interval
215
+ * (5s here) — refreshes can be triggered manually via a button
216
+ * 4. Renders an explicit empty/error state instead of throwing
217
+ * 5. Wraps in <ErrorBoundary viewName="..."> so a render bug in
218
+ * one view never crashes the whole TUI
219
+ */
220
+ export function QueueDepthView() {
221
+ const activeView = useUIStore((s) => s.activeView);
222
+ const isActive = activeView === "queue-depth";
223
+
224
+ const [queues, setQueues] = useState<QueueDepth[]>([]);
225
+ const [loading, setLoading] = useState(false);
226
+ const [error, setError] = useState<string | null>(null);
227
+ const [lastSampledAt, setLastSampledAt] = useState<string | null>(null);
228
+ /** Generation counter so stale responses after unmount/nav are ignored. */
229
+ const fetchGenRef = useRef(0);
230
+
231
+ const refresh = useCallback(async (opts?: { soft?: boolean }) => {
232
+ const soft = opts?.soft === true;
233
+ const gen = ++fetchGenRef.current;
234
+ // Soft refresh (auto-poll): keep prior rows painted; only spinner on first load.
235
+ if (!soft) setLoading(true);
236
+ const result = await cliBridge.monitorQueueDepth();
237
+ if (gen !== fetchGenRef.current) return; // superseded or unmounted
238
+ if (result.success && result.data) {
239
+ setQueues(result.data);
240
+ setLastSampledAt(new Date().toISOString());
241
+ setError(null);
242
+ } else {
243
+ // Keep previous data on transient failure but surface the message.
244
+ setError(result.stderr || result.stdout || "Failed to read queue depths");
245
+ }
246
+ if (!soft || gen === fetchGenRef.current) setLoading(false);
247
+ }, []);
248
+
249
+ // Initial load on mount + invalidate in-flight on unmount
250
+ useEffect(() => {
251
+ void refresh({ soft: false });
252
+ return () => {
253
+ fetchGenRef.current += 1;
254
+ };
255
+ }, [refresh]);
256
+
257
+ // Auto-refresh every 5s while the view is the active view.
258
+ // Soft poll avoids flicker; interval stops when user navigates away.
259
+ useEffect(() => {
260
+ if (!isActive) return;
261
+ const handle = setInterval(() => {
262
+ void refresh({ soft: true });
263
+ }, REFRESH_INTERVAL_MS);
264
+ return () => clearInterval(handle);
265
+ }, [isActive, refresh]);
266
+
267
+ // Keyboard: r / F5 manual refresh while active
268
+ useKeyboard((key) => {
269
+ if (!isActive) return;
270
+ if (key.name === "r" || key.name === "f5") {
271
+ void refresh({ soft: false });
272
+ }
273
+ });
274
+
275
+ const sortedQueues = useMemo(() => sortQueuesByPressure(queues), [queues]);
276
+
277
+ // ── Render ──────────────────────────────────────────────────────────────
278
+ return (
279
+ <ErrorBoundary viewName="Queue Depth">
280
+ <box flexDirection="column" flexGrow={1} padding={1} gap={1}>
281
+ <ViewHeader
282
+ title="QUEUE DEPTH"
283
+ showDivider={false}
284
+ meta={
285
+ <box flexDirection="row" gap={2} alignItems="center">
286
+ <text fg={Colors.warning} dim>
287
+ estimate · not live backlog
288
+ </text>
289
+ <text fg={Colors.muted} dim>
290
+ {queues.length} queue{queues.length === 1 ? "" : "s"}
291
+ </text>
292
+ <text fg={Colors.info} dim>
293
+ ◉ {REFRESH_INTERVAL_MS / 1000}s auto
294
+ </text>
295
+ </box>
296
+ }
297
+ />
298
+
299
+ <text fg={Colors.muted} dim>
300
+ Depth is a producer-count heuristic (Cloudflare Queues do not expose
301
+ real-time backlog). Treat green/yellow/red as pressure guidance only.
302
+ </text>
303
+
304
+ {/* Summary bar */}
305
+ <SummaryBar queues={queues} />
306
+
307
+ {/* Queue list */}
308
+ <Panel flexGrow={1} elevated={false} compact>
309
+ {loading && queues.length === 0 ? (
310
+ <box
311
+ padding={1}
312
+ alignItems="center"
313
+ justifyContent="center"
314
+ flexGrow={1}
315
+ >
316
+ <Spinner label="Loading queue depths..." />
317
+ </box>
318
+ ) : error && queues.length === 0 ? (
319
+ <box padding={1} flexDirection="column" gap={0}>
320
+ <text fg={Colors.error} bold>
321
+ ! {error.length > 60 ? error.slice(0, 57) + "…" : error}
322
+ </text>
323
+ <text fg={Colors.muted} dim>
324
+ Make sure wrangler is installed and authenticated.
325
+ </text>
326
+ </box>
327
+ ) : queues.length === 0 ? (
328
+ <box padding={1} flexGrow={1}>
329
+ <EmptyState
330
+ message="No queues configured."
331
+ suggestion="Create one with `wrangler queues create`."
332
+ icon="📥"
333
+ />
334
+ </box>
335
+ ) : (
336
+ <scrollbox width="100%" flexGrow={1}>
337
+ {sortedQueues.map((q) => (
338
+ <QueueRow key={q.queueName} queue={q} />
339
+ ))}
340
+ </scrollbox>
341
+ )}
342
+ </Panel>
343
+
344
+ {/* Footer with refresh + last sampled timestamp */}
345
+ <box
346
+ flexDirection="row"
347
+ justifyContent="space-between"
348
+ paddingLeft={1}
349
+ paddingRight={1}
350
+ >
351
+ <box flexDirection="row" gap={1}>
352
+ <text
353
+ fg={loading ? Colors.muted : Colors.accent}
354
+ bg={Colors.card}
355
+ dim={loading}
356
+ onMouseUp={loading ? undefined : () => void refresh()}
357
+ >
358
+ {loading ? " ... " : " [REFRESH] "}
359
+ </text>
360
+ <text fg={Colors.dim} dim>
361
+ r refresh
362
+ </text>
363
+ {error && queues.length > 0 && (
364
+ <text fg={Colors.warning} dim>
365
+ ! {error.length > 50 ? error.slice(0, 47) + "…" : error}
366
+ </text>
367
+ )}
368
+ </box>
369
+ <text fg={Colors.muted} dim>
370
+ Last sampled {lastSampledAt ? formatTime(lastSampledAt) : "—"}
371
+ </text>
372
+ </box>
373
+ </box>
374
+ </ErrorBoundary>
375
+ );
376
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Tests for SecretsViewer — export + pure helpers surface.
8
+ * Full render tests for this view are covered by integration/e2e paths;
9
+ * unit render with mock.module pollutes the shared store under Bun's runner.
10
+ */
11
+ import { describe, it, expect } from "bun:test";
12
+ import {
13
+ SecretsViewer,
14
+ filterSecrets,
15
+ MAX_VISIBLE_SECRETS,
16
+ } from "./secrets-viewer";
17
+ import type { SecretMetadata } from "../../services/cli-bridge";
18
+
19
+ describe("SecretsViewer", () => {
20
+ it("is a function component", () => {
21
+ expect(SecretsViewer).toBeInstanceOf(Function);
22
+ expect(SecretsViewer.name).toBe("SecretsViewer");
23
+ });
24
+
25
+ it("does not require localStorage (Bun-safe)", () => {
26
+ // Bun has no localStorage; SecretsViewer must never touch it
27
+ expect("localStorage" in globalThis).toBe(false);
28
+ });
29
+
30
+ it("documents keyboard search (/) and list navigation (↑↓)", async () => {
31
+ // Contract: SecretsViewer wires useKeyboard for / and ↑↓ (parity with KV Viewer).
32
+ // Regression guard against the previous dead SearchBox (onChange discarded).
33
+ const src = await Bun.file(
34
+ new URL("./secrets-viewer.tsx", import.meta.url)
35
+ ).text();
36
+ expect(src).toContain("useKeyboard");
37
+ expect(src).toContain('key.name === "slash"');
38
+ expect(src).toContain("setSearchActive(true)");
39
+ expect(src).toContain("filteredSecrets");
40
+ expect(src).toContain("onChange={setSearch}");
41
+ });
42
+
43
+ it("never fetches secret values (no get/reveal bridge calls)", async () => {
44
+ const src = await Bun.file(
45
+ new URL("./secrets-viewer.tsx", import.meta.url)
46
+ ).text();
47
+ expect(src).toContain("configSecretsList");
48
+ expect(src).not.toMatch(/configSecretsGet|secrets get|revealValue/i);
49
+ // Explicit security comment contract
50
+ expect(src).toContain("NEVER");
51
+ });
52
+
53
+ it("filters by name only (values never considered)", () => {
54
+ const secrets: SecretMetadata[] = [
55
+ { name: "OPENAI_API_KEY", type: "api_key", source: "config" },
56
+ { name: "BINANCE_KEY_BINDING", type: "api_key", source: "Cloudflare" },
57
+ ];
58
+ expect(filterSecrets(secrets, "openai")).toHaveLength(1);
59
+ expect(filterSecrets(secrets, "")).toHaveLength(2);
60
+ });
61
+
62
+ it("caps visible secret rows", () => {
63
+ expect(MAX_VISIBLE_SECRETS).toBeGreaterThan(0);
64
+ });
65
+ });