@smithers-orchestrator/cli 0.20.1 → 0.20.3
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/package.json +17 -19
- package/src/index.js +52 -44
- package/src/workflow-pack.js +15 -6
- package/src/tui/app.jsx +0 -139
- package/src/tui/app.tsx +0 -5
- package/src/tui/components/AskModal.jsx +0 -109
- package/src/tui/components/AskModal.tsx +0 -3
- package/src/tui/components/AttentionPane.jsx +0 -112
- package/src/tui/components/AttentionPane.tsx +0 -6
- package/src/tui/components/ChatPane.jsx +0 -57
- package/src/tui/components/ChatPane.tsx +0 -7
- package/src/tui/components/CronList.jsx +0 -87
- package/src/tui/components/CronList.tsx +0 -5
- package/src/tui/components/DetailsPane.jsx +0 -96
- package/src/tui/components/DetailsPane.tsx +0 -7
- package/src/tui/components/FramesPane.jsx +0 -147
- package/src/tui/components/FramesPane.tsx +0 -8
- package/src/tui/components/LogsPane.jsx +0 -46
- package/src/tui/components/LogsPane.tsx +0 -6
- package/src/tui/components/MetricsPane.jsx +0 -108
- package/src/tui/components/MetricsPane.tsx +0 -5
- package/src/tui/components/NodeDetailView.jsx +0 -284
- package/src/tui/components/NodeDetailView.tsx +0 -7
- package/src/tui/components/NodeInspector.jsx +0 -51
- package/src/tui/components/NodeInspector.tsx +0 -7
- package/src/tui/components/RunDetailView.jsx +0 -190
- package/src/tui/components/RunDetailView.tsx +0 -7
- package/src/tui/components/RunsList.jsx +0 -184
- package/src/tui/components/RunsList.tsx +0 -7
- package/src/tui/components/SqliteBrowser.jsx +0 -131
- package/src/tui/components/SqliteBrowser.tsx +0 -5
- package/src/tui/components/WorkflowLauncher.jsx +0 -63
- package/src/tui/components/WorkflowLauncher.tsx +0 -3
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
2
|
-
export declare function ChatPane({ adapter, runId, focused, filterNodeId, }: {
|
|
3
|
-
adapter: SmithersDb;
|
|
4
|
-
runId: string;
|
|
5
|
-
focused: boolean;
|
|
6
|
-
filterNodeId?: string;
|
|
7
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
import { formatAge } from "../../format.js";
|
|
5
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {{ adapter: SmithersDb; onBack: () => void; }} value
|
|
9
|
-
*/
|
|
10
|
-
export function CronList({ adapter, onBack, }) {
|
|
11
|
-
const [crons, setCrons] = useState([]);
|
|
12
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
let mounted = true;
|
|
15
|
-
async function poll() {
|
|
16
|
-
try {
|
|
17
|
-
const jobs = await adapter.listCrons(false);
|
|
18
|
-
if (mounted)
|
|
19
|
-
setCrons(jobs);
|
|
20
|
-
}
|
|
21
|
-
catch { }
|
|
22
|
-
if (mounted)
|
|
23
|
-
setTimeout(poll, 2000);
|
|
24
|
-
}
|
|
25
|
-
poll();
|
|
26
|
-
return () => { mounted = false; };
|
|
27
|
-
}, [adapter]);
|
|
28
|
-
useKeyboard(async (key) => {
|
|
29
|
-
if (key.name === "escape") {
|
|
30
|
-
onBack();
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (crons.length > 0) {
|
|
34
|
-
if (key.name === "down" || key.name === "j") {
|
|
35
|
-
setSelectedIndex(Math.min(crons.length - 1, selectedIndex + 1));
|
|
36
|
-
}
|
|
37
|
-
else if (key.name === "up" || key.name === "k") {
|
|
38
|
-
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
|
39
|
-
}
|
|
40
|
-
else if (key.name === "backspace" || key.name === "delete") {
|
|
41
|
-
const id = crons[selectedIndex].cronId;
|
|
42
|
-
await adapter.deleteCron(id);
|
|
43
|
-
const next = await adapter.listCrons(false);
|
|
44
|
-
setCrons(next);
|
|
45
|
-
setSelectedIndex(Math.max(0, Math.min(selectedIndex, next.length - 1)));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
const selectedJob = crons[selectedIndex];
|
|
50
|
-
return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
|
|
51
|
-
{/* Left List */}
|
|
52
|
-
<box style={{ width: 45, height: "100%", borderRight: true, borderColor: "#34d399", flexDirection: "column" }}>
|
|
53
|
-
<text style={{ color: "gray", marginBottom: 1 }}> Active Cron Triggers: {crons.length} </text>
|
|
54
|
-
{crons.map((c, i) => (<text key={c.cronId} style={{ color: i === selectedIndex ? "#a7f3d0" : "white" }}>
|
|
55
|
-
{i === selectedIndex ? "▶ " : " "}{c.workflowPath.slice(0, 30)}
|
|
56
|
-
</text>))}
|
|
57
|
-
</box>
|
|
58
|
-
|
|
59
|
-
{/* Right Details */}
|
|
60
|
-
<box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
|
|
61
|
-
{selectedJob ? (<box style={{ flexDirection: "column" }}>
|
|
62
|
-
<text style={{ color: "yellow" }}> Workflow: {selectedJob.workflowPath} </text>
|
|
63
|
-
<text style={{ color: "#93c5fd" }}> Setup: {selectedJob.pattern} </text>
|
|
64
|
-
<text style={{ color: "white", marginTop: 1 }}>
|
|
65
|
-
Status: {selectedJob.enabled ? "ACTIVE" : "PAUSED"}
|
|
66
|
-
</text>
|
|
67
|
-
<text style={{ color: "white" }}>
|
|
68
|
-
Registered: {formatAge(selectedJob.createdAtMs)}
|
|
69
|
-
</text>
|
|
70
|
-
<text style={{ color: "white" }}>
|
|
71
|
-
Last Pired: {selectedJob.lastRunAtMs ? formatAge(selectedJob.lastRunAtMs) : "Never"}
|
|
72
|
-
</text>
|
|
73
|
-
<text style={{ color: "cyan" }}>
|
|
74
|
-
Next Fire: {selectedJob.nextRunAtMs ? formatAge(selectedJob.nextRunAtMs) : "Pending"}
|
|
75
|
-
</text>
|
|
76
|
-
|
|
77
|
-
<box style={{ marginTop: 2, flexDirection: "column", borderTop: true, borderColor: "gray", paddingTop: 1 }}>
|
|
78
|
-
<text style={{ color: "red" }}>[Backspace] Kill Trigger</text>
|
|
79
|
-
</box>
|
|
80
|
-
|
|
81
|
-
{selectedJob.errorJson && (<text style={{ color: "red", marginTop: 1 }}>
|
|
82
|
-
Last Error: {selectedJob.errorJson}
|
|
83
|
-
</text>)}
|
|
84
|
-
</box>) : (<text style={{ color: "gray" }}>No schedules found. Run `smithers cron add`.</text>)}
|
|
85
|
-
</box>
|
|
86
|
-
</box>);
|
|
87
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {{ adapter: SmithersDb; runId: string; focused: boolean; onInspectNode?: (node: any) => void; }} value
|
|
8
|
-
*/
|
|
9
|
-
export function DetailsPane({ adapter, runId, focused, onInspectNode, }) {
|
|
10
|
-
const [nodes, setNodes] = useState([]);
|
|
11
|
-
const [runData, setRunData] = useState(null);
|
|
12
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
let mounted = true;
|
|
15
|
-
let timeout;
|
|
16
|
-
async function fetchDetails() {
|
|
17
|
-
if (!mounted)
|
|
18
|
-
return;
|
|
19
|
-
try {
|
|
20
|
-
const run = await adapter.getRun(runId);
|
|
21
|
-
const fetchedNodes = await adapter.listNodes(runId);
|
|
22
|
-
if (mounted) {
|
|
23
|
-
setRunData(run);
|
|
24
|
-
setNodes(fetchedNodes);
|
|
25
|
-
setSelectedIndex((prev) => Math.min(prev, Math.max(0, fetchedNodes.length - 1)));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (err) { }
|
|
29
|
-
if (mounted)
|
|
30
|
-
timeout = setTimeout(fetchDetails, 1000);
|
|
31
|
-
}
|
|
32
|
-
fetchDetails();
|
|
33
|
-
return () => {
|
|
34
|
-
mounted = false;
|
|
35
|
-
clearTimeout(timeout);
|
|
36
|
-
};
|
|
37
|
-
}, [adapter, runId]);
|
|
38
|
-
useKeyboard((key) => {
|
|
39
|
-
if (!focused)
|
|
40
|
-
return;
|
|
41
|
-
if (key.name === "down" || key.name === "j") {
|
|
42
|
-
setSelectedIndex((s) => Math.min(s + 1, Math.max(0, nodes.length - 1)));
|
|
43
|
-
}
|
|
44
|
-
if (key.name === "up" || key.name === "k") {
|
|
45
|
-
setSelectedIndex((s) => Math.max(0, s - 1));
|
|
46
|
-
}
|
|
47
|
-
if (key.name === "enter" || key.name === "return") {
|
|
48
|
-
if (nodes[selectedIndex] && onInspectNode) {
|
|
49
|
-
onInspectNode(nodes[selectedIndex]);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
if (!runData) {
|
|
54
|
-
return <text style={{ margin: 1 }}>Loading details...</text>;
|
|
55
|
-
}
|
|
56
|
-
return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", padding: 1 }}>
|
|
57
|
-
<box flexDirection="column">
|
|
58
|
-
<text>
|
|
59
|
-
<strong>Status:</strong>{" "}
|
|
60
|
-
<span fg={runData.status === "finished"
|
|
61
|
-
? "green"
|
|
62
|
-
: runData.status === "failed"
|
|
63
|
-
? "red"
|
|
64
|
-
: runData.status === "running"
|
|
65
|
-
? "#34d399"
|
|
66
|
-
: "yellow"}>
|
|
67
|
-
{runData.status}
|
|
68
|
-
</span>
|
|
69
|
-
</text>
|
|
70
|
-
<text>
|
|
71
|
-
<strong>Input Payload:</strong>
|
|
72
|
-
</text>
|
|
73
|
-
<text>{runData.inputData ? runData.inputData.substring(0, 100) + "..." : "None"}</text>
|
|
74
|
-
|
|
75
|
-
{runData.outputData && (<>
|
|
76
|
-
<box style={{ height: 1 }}/>
|
|
77
|
-
<text>
|
|
78
|
-
<strong>Final Output:</strong>
|
|
79
|
-
</text>
|
|
80
|
-
<text>{runData.outputData.substring(0, 100) + "..."}</text>
|
|
81
|
-
</>)}
|
|
82
|
-
|
|
83
|
-
<box style={{ height: 1 }}/>
|
|
84
|
-
<text style={{ color: focused ? "yellow" : "white" }}>
|
|
85
|
-
<strong>Nodes [Press Enter on Node to Inspect]:</strong>
|
|
86
|
-
</text>
|
|
87
|
-
|
|
88
|
-
{nodes.map((node, i) => {
|
|
89
|
-
const isSelected = focused && selectedIndex === i;
|
|
90
|
-
return (<text key={`${node.runId}-${node.nodeId}-${node.iteration}`} style={{ color: isSelected ? "green" : "white" }}>
|
|
91
|
-
{isSelected ? "▶ " : " "}{node.nodeId}: {node.state} (Att: {node.attempts ?? 0}, Iter: {node.iteration})
|
|
92
|
-
</text>);
|
|
93
|
-
})}
|
|
94
|
-
</box>
|
|
95
|
-
</scrollbox>);
|
|
96
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
2
|
-
export declare function DetailsPane({ adapter, runId, focused, onInspectNode, }: {
|
|
3
|
-
adapter: SmithersDb;
|
|
4
|
-
runId: string;
|
|
5
|
-
focused: boolean;
|
|
6
|
-
onInspectNode?: (node: any) => void;
|
|
7
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {{ adapter: SmithersDb; runId: string; focused: boolean; filterNodeId?: string; nodeAttempt?: any; }} value
|
|
8
|
-
*/
|
|
9
|
-
export function FramesPane({ adapter, runId, focused, filterNodeId, nodeAttempt, }) {
|
|
10
|
-
const [frames, setFrames] = useState([]);
|
|
11
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
let mounted = true;
|
|
14
|
-
async function fetchFrames() {
|
|
15
|
-
if (!mounted)
|
|
16
|
-
return;
|
|
17
|
-
try {
|
|
18
|
-
// limit(500) to ensure we get a chunk safely, reverse to ASCENDING chronological order
|
|
19
|
-
const data = await adapter.listFrames(runId, 500);
|
|
20
|
-
if (mounted && data.length > 0) {
|
|
21
|
-
const ascData = data.slice().reverse();
|
|
22
|
-
setFrames(ascData);
|
|
23
|
-
if (frames.length === 0) {
|
|
24
|
-
setSelectedIndex(ascData.length - 1); // default to newest frame (bottom)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (err) { }
|
|
29
|
-
if (mounted)
|
|
30
|
-
setTimeout(fetchFrames, 2000); // Polling for live updates
|
|
31
|
-
}
|
|
32
|
-
fetchFrames();
|
|
33
|
-
return () => {
|
|
34
|
-
mounted = false;
|
|
35
|
-
};
|
|
36
|
-
}, [adapter, runId]);
|
|
37
|
-
let displayFrames = frames;
|
|
38
|
-
if (filterNodeId && nodeAttempt && frames.length > 0) {
|
|
39
|
-
const sMs = nodeAttempt.startedAtMs ?? 0;
|
|
40
|
-
const fMs = nodeAttempt.finishedAtMs;
|
|
41
|
-
const beforeF = frames.slice().reverse().find(f => f.createdAtMs <= sMs) || frames[0];
|
|
42
|
-
const afterF = fMs ? (frames.find(f => f.createdAtMs >= fMs) || frames[frames.length - 1]) : frames[frames.length - 1];
|
|
43
|
-
displayFrames = frames.map(f => {
|
|
44
|
-
if (f.frameNo === beforeF.frameNo && f.frameNo === afterF.frameNo) {
|
|
45
|
-
return { ...f, uiLabel: "Frame (Active)" };
|
|
46
|
-
}
|
|
47
|
-
if (f.frameNo === beforeF.frameNo)
|
|
48
|
-
return { ...f, uiLabel: "Frame (Before)" };
|
|
49
|
-
if (f.frameNo === afterF.frameNo)
|
|
50
|
-
return { ...f, uiLabel: "Frame (After)" };
|
|
51
|
-
return f;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
useKeyboard((key) => {
|
|
55
|
-
if (!focused || displayFrames.length === 0)
|
|
56
|
-
return;
|
|
57
|
-
if (key.name === "up" || key.name === "k") {
|
|
58
|
-
setSelectedIndex((s) => Math.max(0, s - 1));
|
|
59
|
-
}
|
|
60
|
-
if (key.name === "down" || key.name === "j") {
|
|
61
|
-
setSelectedIndex((s) => Math.min(s + 1, displayFrames.length - 1));
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
if (displayFrames.length === 0) {
|
|
65
|
-
return <text style={{ paddingLeft: 1 }}>No frame history available...</text>;
|
|
66
|
-
}
|
|
67
|
-
// Bound check in case selectedIndex drifted
|
|
68
|
-
const validIndex = Math.max(0, Math.min(selectedIndex, displayFrames.length - 1));
|
|
69
|
-
const selectedFrame = displayFrames[validIndex];
|
|
70
|
-
// Recursive formatter to convert `xmlJson` into a JSX code block
|
|
71
|
-
/**
|
|
72
|
-
* @param {any} node
|
|
73
|
-
* @param {number} [indent]
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
function formatJsxNode(node, indent = 0) {
|
|
77
|
-
if (!node || typeof node !== "object")
|
|
78
|
-
return "";
|
|
79
|
-
const space = " ".repeat(indent);
|
|
80
|
-
if (node.kind === "text") {
|
|
81
|
-
const escaped = String(node.text || "").replace(/\n/g, `\n${space}`);
|
|
82
|
-
return `${space}${escaped}`;
|
|
83
|
-
}
|
|
84
|
-
if (node.kind === "element" && typeof node.tag === "string") {
|
|
85
|
-
let propsStr = "";
|
|
86
|
-
if (node.props && typeof node.props === "object") {
|
|
87
|
-
for (const [k, v] of Object.entries(node.props)) {
|
|
88
|
-
if (typeof v === "string")
|
|
89
|
-
propsStr += ` ${k}="${v.replace(/"/g, '"')}"`;
|
|
90
|
-
else
|
|
91
|
-
propsStr += ` ${k}={${JSON.stringify(v)}}`;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const tag = node.tag;
|
|
95
|
-
const isTargetNode = filterNodeId && node.props?.id === filterNodeId;
|
|
96
|
-
const colorPrefix = isTargetNode ? "👉 " : "";
|
|
97
|
-
if (!Array.isArray(node.children) || node.children.length === 0) {
|
|
98
|
-
return `${space}${colorPrefix}<${tag}${propsStr} />`;
|
|
99
|
-
}
|
|
100
|
-
let res = `${space}${colorPrefix}<${tag}${propsStr}>\n`;
|
|
101
|
-
for (const child of node.children) {
|
|
102
|
-
res += formatJsxNode(child, indent + 1) + "\n";
|
|
103
|
-
}
|
|
104
|
-
res += `${space}</${tag}>`;
|
|
105
|
-
return res;
|
|
106
|
-
}
|
|
107
|
-
// Fallback if neither text nor element
|
|
108
|
-
return `${space}${JSON.stringify(node)}`;
|
|
109
|
-
}
|
|
110
|
-
let xmlString = "Empty Frame";
|
|
111
|
-
try {
|
|
112
|
-
if (selectedFrame?.xmlJson) {
|
|
113
|
-
const parsed = typeof selectedFrame.xmlJson === "string" ? JSON.parse(selectedFrame.xmlJson) : selectedFrame.xmlJson;
|
|
114
|
-
xmlString = formatJsxNode(parsed);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
xmlString = `[Format Error: ${err?.message}]\n\n${selectedFrame?.xmlJson ?? "Parse error"}`;
|
|
119
|
-
}
|
|
120
|
-
return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
|
|
121
|
-
{/* Left Sidebar: Frame List */}
|
|
122
|
-
<box style={{
|
|
123
|
-
width: 30,
|
|
124
|
-
height: "100%",
|
|
125
|
-
borderRight: true,
|
|
126
|
-
borderColor: "#34d399",
|
|
127
|
-
flexDirection: "column",
|
|
128
|
-
}} title={`Timeline [Up/Down]`}>
|
|
129
|
-
<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
|
|
130
|
-
{displayFrames.map((frame, i) => {
|
|
131
|
-
const isSelected = validIndex === i;
|
|
132
|
-
const label = frame.uiLabel ? `${frame.uiLabel}` : `Frame ${frame.frameNo}`;
|
|
133
|
-
return (<text key={frame.frameNo + label} style={{ color: isSelected ? "green" : "white" }}>
|
|
134
|
-
{isSelected ? "▶ " : " "}{label}
|
|
135
|
-
</text>);
|
|
136
|
-
})}
|
|
137
|
-
</scrollbox>
|
|
138
|
-
</box>
|
|
139
|
-
|
|
140
|
-
{/* Right Content: XML/JSX Dump */}
|
|
141
|
-
<box style={{ flexGrow: 1, height: "100%", flexDirection: "column" }}>
|
|
142
|
-
<scrollbox style={{ width: "100%", height: "100%", paddingLeft: 1 }}>
|
|
143
|
-
<text style={{ color: "#d8b4e2" }}>{xmlString}</text>
|
|
144
|
-
</scrollbox>
|
|
145
|
-
</box>
|
|
146
|
-
</box>);
|
|
147
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
|
|
2
|
-
export declare function FramesPane({ adapter, runId, focused, filterNodeId, nodeAttempt, }: {
|
|
3
|
-
adapter: SmithersDb;
|
|
4
|
-
runId: string;
|
|
5
|
-
focused: boolean;
|
|
6
|
-
filterNodeId?: string;
|
|
7
|
-
nodeAttempt?: any;
|
|
8
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { formatEventLine } from "../../format.js";
|
|
4
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {{ adapter: SmithersDb; runId: string; focused: boolean; }} value
|
|
8
|
-
*/
|
|
9
|
-
export function LogsPane({ adapter, runId, focused, }) {
|
|
10
|
-
const [logs, setLogs] = useState([]);
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
let mounted = true;
|
|
13
|
-
let lastSeq = -1;
|
|
14
|
-
async function fetchLogs() {
|
|
15
|
-
if (!mounted)
|
|
16
|
-
return;
|
|
17
|
-
try {
|
|
18
|
-
const events = await adapter.listEvents(runId, lastSeq, 200);
|
|
19
|
-
if (mounted && events.length > 0) {
|
|
20
|
-
const run = await adapter.getRun(runId);
|
|
21
|
-
const baseMs = run?.startedAtMs ?? run?.createdAtMs ?? Date.now();
|
|
22
|
-
const newLines = events.map((e) => formatEventLine(e, baseMs));
|
|
23
|
-
lastSeq = events[events.length - 1].seq;
|
|
24
|
-
setLogs((prev) => {
|
|
25
|
-
const updated = [...prev, ...newLines];
|
|
26
|
-
// keep the last 200 lines to avoid scrollbox lag
|
|
27
|
-
return updated.slice(-200);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (err) { }
|
|
32
|
-
if (mounted)
|
|
33
|
-
setTimeout(fetchLogs, 500);
|
|
34
|
-
}
|
|
35
|
-
fetchLogs();
|
|
36
|
-
return () => {
|
|
37
|
-
mounted = false;
|
|
38
|
-
};
|
|
39
|
-
}, [adapter, runId]);
|
|
40
|
-
return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", paddingLeft: 1, paddingRight: 1 }}>
|
|
41
|
-
<box flexDirection="column">
|
|
42
|
-
{logs.map((log, index) => (<text key={index}>{log}</text>))}
|
|
43
|
-
{logs.length === 0 && <text>Loading events...</text>}
|
|
44
|
-
</box>
|
|
45
|
-
</scrollbox>);
|
|
46
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {{ adapter: SmithersDb; onBack: () => void; }} value
|
|
8
|
-
*/
|
|
9
|
-
export function MetricsPane({ adapter, onBack, }) {
|
|
10
|
-
const [stats, setStats] = useState({
|
|
11
|
-
runsTotal: 0,
|
|
12
|
-
runsFinished: 0,
|
|
13
|
-
nodesTotal: 0,
|
|
14
|
-
tokensIn: 0,
|
|
15
|
-
tokensOut: 0,
|
|
16
|
-
tokensCache: 0,
|
|
17
|
-
series: []
|
|
18
|
-
});
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
let mounted = true;
|
|
21
|
-
async function fetchStats() {
|
|
22
|
-
try {
|
|
23
|
-
const [runStats] = await adapter.rawQuery(`SELECT count(*) as total, sum(case when status='finished' then 1 else 0 end) as finished FROM _smithers_runs`);
|
|
24
|
-
const [nodeStats] = await adapter.rawQuery(`SELECT count(*) as total FROM _smithers_nodes`);
|
|
25
|
-
const [tokenStats] = await adapter.rawQuery(`
|
|
26
|
-
SELECT
|
|
27
|
-
sum(cast(json_extract(payload_json, '$.inputTokens') as integer)) as tIn,
|
|
28
|
-
sum(cast(json_extract(payload_json, '$.outputTokens') as integer)) as tOut,
|
|
29
|
-
sum(cast(json_extract(payload_json, '$.cacheReadTokens') as integer)) as tCache
|
|
30
|
-
FROM _smithers_events
|
|
31
|
-
WHERE type = 'TokenUsageReported'
|
|
32
|
-
`);
|
|
33
|
-
// Last 24hr timeseries
|
|
34
|
-
const nowMs = Date.now();
|
|
35
|
-
const oneDayAgo = nowMs - (24 * 60 * 60 * 1000);
|
|
36
|
-
const series = await adapter.rawQuery(`
|
|
37
|
-
SELECT
|
|
38
|
-
strftime('%H:00', datetime(timestamp_ms/1000, 'unixepoch', 'localtime')) as hr,
|
|
39
|
-
sum(cast(json_extract(payload_json, '$.inputTokens') as integer) + cast(json_extract(payload_json, '$.outputTokens') as integer)) as totalTokens
|
|
40
|
-
FROM _smithers_events
|
|
41
|
-
WHERE type = 'TokenUsageReported' AND timestamp_ms > ${oneDayAgo}
|
|
42
|
-
GROUP BY hr
|
|
43
|
-
ORDER BY timestamp_ms ASC
|
|
44
|
-
LIMIT 24
|
|
45
|
-
`);
|
|
46
|
-
if (mounted) {
|
|
47
|
-
setStats({
|
|
48
|
-
runsTotal: runStats?.total || 0,
|
|
49
|
-
runsFinished: runStats?.finished || 0,
|
|
50
|
-
nodesTotal: nodeStats?.total || 0,
|
|
51
|
-
tokensIn: tokenStats?.tIn || 0,
|
|
52
|
-
tokensOut: tokenStats?.tOut || 0,
|
|
53
|
-
tokensCache: tokenStats?.tCache || 0,
|
|
54
|
-
series: series || []
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
// fail silently for telemetry
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
fetchStats();
|
|
63
|
-
const interval = setInterval(fetchStats, 5000);
|
|
64
|
-
return () => {
|
|
65
|
-
mounted = false;
|
|
66
|
-
clearInterval(interval);
|
|
67
|
-
};
|
|
68
|
-
}, [adapter]);
|
|
69
|
-
useKeyboard((key) => {
|
|
70
|
-
if (key.name === "escape") {
|
|
71
|
-
onBack();
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
// Render Sparkline
|
|
75
|
-
const maxTokens = Math.max(1, ...stats.series.map((s) => s.totalTokens || 0));
|
|
76
|
-
const blocks = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
77
|
-
const sparkline = stats.series.map((s) => {
|
|
78
|
-
const val = s.totalTokens || 0;
|
|
79
|
-
const idx = Math.floor((val / maxTokens) * (blocks.length - 1));
|
|
80
|
-
return blocks[idx];
|
|
81
|
-
}).join("");
|
|
82
|
-
const labels = stats.series.map((s) => s.hr).join(" ");
|
|
83
|
-
return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
|
|
84
|
-
<text style={{ color: "cyan", marginBottom: 1 }}> 📊 Smithers Global Telemetry (Prometheus Rollup) </text>
|
|
85
|
-
|
|
86
|
-
<box style={{ flexDirection: "row", marginBottom: 2 }}>
|
|
87
|
-
<box style={{ width: 30, flexDirection: "column", borderRight: true, borderColor: "gray" }}>
|
|
88
|
-
<text style={{ color: "white" }}> Lifetime Runs: </text>
|
|
89
|
-
<text style={{ color: "green" }}> {stats.runsTotal} ({stats.runsFinished} completed) </text>
|
|
90
|
-
</box>
|
|
91
|
-
<box style={{ width: 30, flexDirection: "column", borderRight: true, borderColor: "gray", paddingLeft: 1 }}>
|
|
92
|
-
<text style={{ color: "white" }}> Total Nodes Executed: </text>
|
|
93
|
-
<text style={{ color: "yellow" }}> {stats.nodesTotal} tasks </text>
|
|
94
|
-
</box>
|
|
95
|
-
<box style={{ width: 40, flexDirection: "column", paddingLeft: 1 }}>
|
|
96
|
-
<text style={{ color: "white" }}> LLM Token Throughput: </text>
|
|
97
|
-
<text style={{ color: "magenta" }}> IN: {stats.tokensIn} | OUT: {stats.tokensOut} </text>
|
|
98
|
-
</box>
|
|
99
|
-
</box>
|
|
100
|
-
|
|
101
|
-
<text style={{ color: "gray", marginTop: 1 }}> Token Usage (Last 24 Hours) </text>
|
|
102
|
-
<box style={{ height: 6, width: "100%", flexDirection: "column", marginTop: 1, border: true, borderColor: "#34d399", paddingLeft: 1 }}>
|
|
103
|
-
<text style={{ color: "cyan", marginTop: 1 }}> {sparkline || "No token telemetry to graph"} </text>
|
|
104
|
-
<text style={{ color: "gray" }}> {labels} </text>
|
|
105
|
-
</box>
|
|
106
|
-
|
|
107
|
-
</box>);
|
|
108
|
-
}
|