station-kit 1.0.2 → 1.0.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/dist/cli-main.js +3 -29
- package/dist/cli-main.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +89 -9
- package/dist/server/index.js.map +1 -1
- package/next.config.ts +1 -1
- package/out/404.html +1 -0
- package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_buildManifest.js +1 -0
- package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/580-f007f4d4c050db4e.js +1 -0
- package/out/_next/static/chunks/743-5bb03adbb0e4ddec.js +1 -0
- package/out/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +1 -0
- package/out/_next/static/chunks/app/_not-found/page-ce21b4ba9038a5a7.js +1 -0
- package/out/_next/static/chunks/app/broadcasts/[id]/page-057eeaa51d28cbfd.js +1 -0
- package/out/_next/static/chunks/app/broadcasts/page-ac768ee4bcf3086f.js +1 -0
- package/out/_next/static/chunks/app/layout-a5f4d2f2e87939b2.js +1 -0
- package/out/_next/static/chunks/app/page-62d1dbcfdc93b566.js +1 -0
- package/out/_next/static/chunks/app/runs/[id]/page-7f726f2f4ea8f616.js +1 -0
- package/out/_next/static/chunks/app/signals/[name]/page-8f9f032eb0171ded.js +1 -0
- package/out/_next/static/chunks/app/signals/page-fb42d9c0368bbc58.js +1 -0
- package/out/_next/static/chunks/framework-077b27ad7787463c.js +1 -0
- package/out/_next/static/chunks/main-app-a9f19d5831b41b19.js +1 -0
- package/out/_next/static/chunks/main-f1c74cefd4965abf.js +1 -0
- package/out/_next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +1 -0
- package/out/_next/static/chunks/pages/_error-273a093c18b5ed0f.js +1 -0
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/out/_next/static/chunks/webpack-e40b05d6bdcb3589.js +1 -0
- package/out/_next/static/css/0be0e65a5f561f37.css +1 -0
- package/out/broadcasts/_.html +1 -0
- package/out/broadcasts/_.txt +22 -0
- package/out/broadcasts.html +1 -0
- package/out/broadcasts.txt +25 -0
- package/out/index.html +1 -0
- package/out/index.txt +25 -0
- package/out/runs/_.html +1 -0
- package/out/runs/_.txt +22 -0
- package/out/signals/_.html +1 -0
- package/out/signals/_.txt +22 -0
- package/out/signals.html +1 -0
- package/out/signals.txt +25 -0
- package/package.json +16 -9
- package/src/app/broadcasts/[id]/broadcast-detail.tsx +511 -0
- package/src/app/broadcasts/[id]/page.tsx +4 -506
- package/src/app/hooks/use-api.ts +1 -1
- package/src/app/hooks/use-realtime.ts +2 -3
- package/src/app/runs/[id]/page.tsx +4 -272
- package/src/app/runs/[id]/run-detail.tsx +277 -0
- package/src/app/signals/[name]/page.tsx +4 -245
- package/src/app/signals/[name]/signal-detail.tsx +250 -0
- package/src/cli-main.ts +3 -36
- package/src/cli.ts +1 -1
- package/src/server/index.ts +94 -10
- package/next-env.d.ts +0 -6
- package/station.config.example.ts +0 -16
- package/tsconfig.json +0 -12
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,250 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { SignalDetail } from "./signal-detail";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { useApi, type SignalMeta, type SchemaField } from "../../hooks/use-api";
|
|
6
|
-
import { useStation } from "../../hooks/use-station";
|
|
7
|
-
import { useBreadcrumb } from "../../hooks/use-breadcrumb";
|
|
8
|
-
import { RunTable } from "../../components/run-table";
|
|
9
|
-
import { SchemaForm } from "../../components/schema-form";
|
|
10
|
-
|
|
11
|
-
function formatMs(ms: number): string {
|
|
12
|
-
if (ms < 1000) return `${ms}ms`;
|
|
13
|
-
if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`;
|
|
14
|
-
if (ms < 3_600_000) return `${(ms / 60_000).toFixed(0)}m`;
|
|
15
|
-
return `${(ms / 3_600_000).toFixed(0)}h`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function renderSchemaInline(schema: SchemaField): string {
|
|
19
|
-
if (schema.type === "object" && schema.properties) {
|
|
20
|
-
const fields = Object.entries(schema.properties)
|
|
21
|
-
.map(([key, field]) => `${key}: ${field.type}${field.required ? "" : "?"}`)
|
|
22
|
-
.join(", ");
|
|
23
|
-
return `{ ${fields} }`;
|
|
24
|
-
}
|
|
25
|
-
if (schema.type === "array" && schema.items) {
|
|
26
|
-
return `${renderSchemaInline(schema.items)}[]`;
|
|
27
|
-
}
|
|
28
|
-
if (schema.type === "enum" && schema.values) {
|
|
29
|
-
return schema.values.map((v) => `"${v}"`).join(" | ");
|
|
30
|
-
}
|
|
31
|
-
return schema.type;
|
|
3
|
+
export function generateStaticParams() {
|
|
4
|
+
return [{ name: "_" }];
|
|
32
5
|
}
|
|
33
6
|
|
|
34
|
-
const STATUS_FILTERS = ["all", "completed", "failed", "running", "pending"] as const;
|
|
35
|
-
type StatusFilter = (typeof STATUS_FILTERS)[number];
|
|
36
|
-
|
|
37
7
|
export default function SignalDetailPage() {
|
|
38
|
-
|
|
39
|
-
const name = params.name as string;
|
|
40
|
-
const decodedName = decodeURIComponent(name);
|
|
41
|
-
const api = useApi();
|
|
42
|
-
const { events } = useStation();
|
|
43
|
-
|
|
44
|
-
const [signal, setSignal] = useState<SignalMeta | null>(null);
|
|
45
|
-
const [runs, setRuns] = useState<any[]>([]);
|
|
46
|
-
const [loading, setLoading] = useState(true);
|
|
47
|
-
const [triggering, setTriggering] = useState(false);
|
|
48
|
-
const [inputJson, setInputJson] = useState("{}");
|
|
49
|
-
const [filter, setFilter] = useState<StatusFilter>("all");
|
|
50
|
-
|
|
51
|
-
useBreadcrumb(
|
|
52
|
-
[{ label: "Signals", href: "/signals" }, { label: decodedName }],
|
|
53
|
-
"signals",
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const loadRuns = useCallback(() => {
|
|
57
|
-
api.getSignalRuns(name).then((r) => setRuns(r.data)).catch((e) => console.error("Failed to refresh runs:", e));
|
|
58
|
-
}, [name]);
|
|
59
|
-
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
async function load() {
|
|
62
|
-
try {
|
|
63
|
-
const [signalRes, runsRes] = await Promise.all([
|
|
64
|
-
api.getSignal(name),
|
|
65
|
-
api.getSignalRuns(name),
|
|
66
|
-
]);
|
|
67
|
-
setSignal(signalRes.data);
|
|
68
|
-
setRuns(runsRes.data);
|
|
69
|
-
} catch (err: unknown) {
|
|
70
|
-
if (err instanceof Error) {
|
|
71
|
-
console.error("Failed to load signal:", err.message);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
setLoading(false);
|
|
75
|
-
}
|
|
76
|
-
load();
|
|
77
|
-
}, [name]);
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
if (events.length === 0) return;
|
|
81
|
-
const latest = events[0];
|
|
82
|
-
if (latest.type.startsWith("run:")) {
|
|
83
|
-
const eventSignal =
|
|
84
|
-
(latest.data.run as Record<string, unknown>)?.signalName ??
|
|
85
|
-
(latest.data as Record<string, unknown>).signalName;
|
|
86
|
-
if (eventSignal === decodedName) {
|
|
87
|
-
loadRuns();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}, [events.length, decodedName, loadRuns]);
|
|
91
|
-
|
|
92
|
-
async function handleTrigger() {
|
|
93
|
-
setTriggering(true);
|
|
94
|
-
try {
|
|
95
|
-
const input = JSON.parse(inputJson);
|
|
96
|
-
await api.triggerSignal(name, input);
|
|
97
|
-
setInputJson("{}");
|
|
98
|
-
setTimeout(loadRuns, 300);
|
|
99
|
-
} catch (err: unknown) {
|
|
100
|
-
if (err instanceof Error) {
|
|
101
|
-
console.error("Trigger failed:", err.message);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
setTriggering(false);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (loading) {
|
|
108
|
-
return (
|
|
109
|
-
<div>
|
|
110
|
-
<h1 className="page-title">{decodedName}</h1>
|
|
111
|
-
<div className="loading-bar"><div className="loading-bar-fill" /></div>
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!signal) {
|
|
117
|
-
return (
|
|
118
|
-
<div>
|
|
119
|
-
<h1 className="page-title">{decodedName}</h1>
|
|
120
|
-
<div className="empty-state">
|
|
121
|
-
<p className="empty-state-text">Signal not found.</p>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const hasSchema = signal.inputSchema !== null || signal.outputSchema !== null;
|
|
128
|
-
const filteredRuns = filter === "all" ? runs : runs.filter((r) => r.status === filter);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div>
|
|
132
|
-
<div className="page-header">
|
|
133
|
-
<h1 className="page-title" style={{ marginBottom: 0 }}>{decodedName}</h1>
|
|
134
|
-
<div className="page-header-actions">
|
|
135
|
-
<button className="btn btn--primary" onClick={handleTrigger} disabled={triggering}>
|
|
136
|
-
{triggering ? "Dispatching..." : "Trigger"}
|
|
137
|
-
</button>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div className="detail-section">
|
|
142
|
-
<div className="detail-section-label">Configuration</div>
|
|
143
|
-
<div className="config-grid">
|
|
144
|
-
<div className="config-item">
|
|
145
|
-
<span className="config-item-label">Schedule</span>
|
|
146
|
-
<span className="config-item-value">{signal.interval ?? "Manual trigger"}</span>
|
|
147
|
-
</div>
|
|
148
|
-
<div className="config-item">
|
|
149
|
-
<span className="config-item-label">Timeout</span>
|
|
150
|
-
<span className="config-item-value">{formatMs(signal.timeout)}</span>
|
|
151
|
-
</div>
|
|
152
|
-
<div className="config-item">
|
|
153
|
-
<span className="config-item-label">Max Attempts</span>
|
|
154
|
-
<span className="config-item-value">{signal.maxAttempts}</span>
|
|
155
|
-
</div>
|
|
156
|
-
<div className="config-item">
|
|
157
|
-
<span className="config-item-label">Max Concurrency</span>
|
|
158
|
-
<span className="config-item-value">{signal.maxConcurrency ?? "\u2014"}</span>
|
|
159
|
-
</div>
|
|
160
|
-
<div className="config-item">
|
|
161
|
-
<span className="config-item-label">Steps</span>
|
|
162
|
-
<span className="config-item-value">
|
|
163
|
-
{signal.hasSteps ? signal.stepNames.join(", ") : "Single handler"}
|
|
164
|
-
</span>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
{hasSchema && (
|
|
170
|
-
<div className="detail-section">
|
|
171
|
-
<div className="detail-section-label">Schema</div>
|
|
172
|
-
<div className="schema-pair">
|
|
173
|
-
<div>
|
|
174
|
-
<div style={{
|
|
175
|
-
fontFamily: "var(--font-mono)",
|
|
176
|
-
fontSize: "0.6875rem",
|
|
177
|
-
textTransform: "uppercase",
|
|
178
|
-
letterSpacing: "0.08em",
|
|
179
|
-
color: "var(--muted)",
|
|
180
|
-
marginBottom: "0.5rem",
|
|
181
|
-
}}>
|
|
182
|
-
Input
|
|
183
|
-
</div>
|
|
184
|
-
{signal.inputSchema ? (
|
|
185
|
-
<pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
|
|
186
|
-
{renderSchemaInline(signal.inputSchema)}
|
|
187
|
-
</pre>
|
|
188
|
-
) : (
|
|
189
|
-
<span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
|
|
190
|
-
)}
|
|
191
|
-
</div>
|
|
192
|
-
<div>
|
|
193
|
-
<div style={{
|
|
194
|
-
fontFamily: "var(--font-mono)",
|
|
195
|
-
fontSize: "0.6875rem",
|
|
196
|
-
textTransform: "uppercase",
|
|
197
|
-
letterSpacing: "0.08em",
|
|
198
|
-
color: "var(--muted)",
|
|
199
|
-
marginBottom: "0.5rem",
|
|
200
|
-
}}>
|
|
201
|
-
Output
|
|
202
|
-
</div>
|
|
203
|
-
{signal.outputSchema ? (
|
|
204
|
-
<pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
|
|
205
|
-
{renderSchemaInline(signal.outputSchema)}
|
|
206
|
-
</pre>
|
|
207
|
-
) : (
|
|
208
|
-
<span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
|
|
209
|
-
)}
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
)}
|
|
214
|
-
|
|
215
|
-
<div className="detail-section">
|
|
216
|
-
<div className="detail-section-label">Trigger</div>
|
|
217
|
-
<SchemaForm
|
|
218
|
-
schema={signal.inputSchema}
|
|
219
|
-
value={inputJson}
|
|
220
|
-
onChange={setInputJson}
|
|
221
|
-
/>
|
|
222
|
-
<div style={{ marginTop: "0.5rem" }}>
|
|
223
|
-
<button
|
|
224
|
-
className="btn btn--primary"
|
|
225
|
-
onClick={handleTrigger}
|
|
226
|
-
disabled={triggering}
|
|
227
|
-
>
|
|
228
|
-
{triggering ? "Dispatching..." : "Dispatch"}
|
|
229
|
-
</button>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div className="detail-section">
|
|
234
|
-
<div className="detail-section-label">Run History</div>
|
|
235
|
-
<div className="filter-bar">
|
|
236
|
-
{STATUS_FILTERS.map((f) => (
|
|
237
|
-
<button
|
|
238
|
-
key={f}
|
|
239
|
-
className={`filter-btn${filter === f ? " filter-btn--active" : ""}`}
|
|
240
|
-
onClick={() => setFilter(f)}
|
|
241
|
-
>
|
|
242
|
-
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
243
|
-
</button>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
<RunTable runs={filteredRuns} />
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
);
|
|
8
|
+
return <SignalDetail />;
|
|
250
9
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import { useParams } from "next/navigation";
|
|
5
|
+
import { useApi, type SignalMeta, type SchemaField } from "../../hooks/use-api";
|
|
6
|
+
import { useStation } from "../../hooks/use-station";
|
|
7
|
+
import { useBreadcrumb } from "../../hooks/use-breadcrumb";
|
|
8
|
+
import { RunTable } from "../../components/run-table";
|
|
9
|
+
import { SchemaForm } from "../../components/schema-form";
|
|
10
|
+
|
|
11
|
+
function formatMs(ms: number): string {
|
|
12
|
+
if (ms < 1000) return `${ms}ms`;
|
|
13
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(0)}s`;
|
|
14
|
+
if (ms < 3_600_000) return `${(ms / 60_000).toFixed(0)}m`;
|
|
15
|
+
return `${(ms / 3_600_000).toFixed(0)}h`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderSchemaInline(schema: SchemaField): string {
|
|
19
|
+
if (schema.type === "object" && schema.properties) {
|
|
20
|
+
const fields = Object.entries(schema.properties)
|
|
21
|
+
.map(([key, field]) => `${key}: ${field.type}${field.required ? "" : "?"}`)
|
|
22
|
+
.join(", ");
|
|
23
|
+
return `{ ${fields} }`;
|
|
24
|
+
}
|
|
25
|
+
if (schema.type === "array" && schema.items) {
|
|
26
|
+
return `${renderSchemaInline(schema.items)}[]`;
|
|
27
|
+
}
|
|
28
|
+
if (schema.type === "enum" && schema.values) {
|
|
29
|
+
return schema.values.map((v) => `"${v}"`).join(" | ");
|
|
30
|
+
}
|
|
31
|
+
return schema.type;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STATUS_FILTERS = ["all", "completed", "failed", "running", "pending"] as const;
|
|
35
|
+
type StatusFilter = (typeof STATUS_FILTERS)[number];
|
|
36
|
+
|
|
37
|
+
export function SignalDetail() {
|
|
38
|
+
const params = useParams();
|
|
39
|
+
const name = params.name as string;
|
|
40
|
+
const decodedName = decodeURIComponent(name);
|
|
41
|
+
const api = useApi();
|
|
42
|
+
const { events } = useStation();
|
|
43
|
+
|
|
44
|
+
const [signal, setSignal] = useState<SignalMeta | null>(null);
|
|
45
|
+
const [runs, setRuns] = useState<any[]>([]);
|
|
46
|
+
const [loading, setLoading] = useState(true);
|
|
47
|
+
const [triggering, setTriggering] = useState(false);
|
|
48
|
+
const [inputJson, setInputJson] = useState("{}");
|
|
49
|
+
const [filter, setFilter] = useState<StatusFilter>("all");
|
|
50
|
+
|
|
51
|
+
useBreadcrumb(
|
|
52
|
+
[{ label: "Signals", href: "/signals" }, { label: decodedName }],
|
|
53
|
+
"signals",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const loadRuns = useCallback(() => {
|
|
57
|
+
api.getSignalRuns(name).then((r) => setRuns(r.data)).catch((e) => console.error("Failed to refresh runs:", e));
|
|
58
|
+
}, [name]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
async function load() {
|
|
62
|
+
try {
|
|
63
|
+
const [signalRes, runsRes] = await Promise.all([
|
|
64
|
+
api.getSignal(name),
|
|
65
|
+
api.getSignalRuns(name),
|
|
66
|
+
]);
|
|
67
|
+
setSignal(signalRes.data);
|
|
68
|
+
setRuns(runsRes.data);
|
|
69
|
+
} catch (err: unknown) {
|
|
70
|
+
if (err instanceof Error) {
|
|
71
|
+
console.error("Failed to load signal:", err.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
load();
|
|
77
|
+
}, [name]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (events.length === 0) return;
|
|
81
|
+
const latest = events[0];
|
|
82
|
+
if (latest.type.startsWith("run:")) {
|
|
83
|
+
const eventSignal =
|
|
84
|
+
(latest.data.run as Record<string, unknown>)?.signalName ??
|
|
85
|
+
(latest.data as Record<string, unknown>).signalName;
|
|
86
|
+
if (eventSignal === decodedName) {
|
|
87
|
+
loadRuns();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [events.length, decodedName, loadRuns]);
|
|
91
|
+
|
|
92
|
+
async function handleTrigger() {
|
|
93
|
+
setTriggering(true);
|
|
94
|
+
try {
|
|
95
|
+
const input = JSON.parse(inputJson);
|
|
96
|
+
await api.triggerSignal(name, input);
|
|
97
|
+
setInputJson("{}");
|
|
98
|
+
setTimeout(loadRuns, 300);
|
|
99
|
+
} catch (err: unknown) {
|
|
100
|
+
if (err instanceof Error) {
|
|
101
|
+
console.error("Trigger failed:", err.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
setTriggering(false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (loading) {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<h1 className="page-title">{decodedName}</h1>
|
|
111
|
+
<div className="loading-bar"><div className="loading-bar-fill" /></div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!signal) {
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<h1 className="page-title">{decodedName}</h1>
|
|
120
|
+
<div className="empty-state">
|
|
121
|
+
<p className="empty-state-text">Signal not found.</p>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hasSchema = signal.inputSchema !== null || signal.outputSchema !== null;
|
|
128
|
+
const filteredRuns = filter === "all" ? runs : runs.filter((r) => r.status === filter);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<div className="page-header">
|
|
133
|
+
<h1 className="page-title" style={{ marginBottom: 0 }}>{decodedName}</h1>
|
|
134
|
+
<div className="page-header-actions">
|
|
135
|
+
<button className="btn btn--primary" onClick={handleTrigger} disabled={triggering}>
|
|
136
|
+
{triggering ? "Dispatching..." : "Trigger"}
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="detail-section">
|
|
142
|
+
<div className="detail-section-label">Configuration</div>
|
|
143
|
+
<div className="config-grid">
|
|
144
|
+
<div className="config-item">
|
|
145
|
+
<span className="config-item-label">Schedule</span>
|
|
146
|
+
<span className="config-item-value">{signal.interval ?? "Manual trigger"}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="config-item">
|
|
149
|
+
<span className="config-item-label">Timeout</span>
|
|
150
|
+
<span className="config-item-value">{formatMs(signal.timeout)}</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="config-item">
|
|
153
|
+
<span className="config-item-label">Max Attempts</span>
|
|
154
|
+
<span className="config-item-value">{signal.maxAttempts}</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="config-item">
|
|
157
|
+
<span className="config-item-label">Max Concurrency</span>
|
|
158
|
+
<span className="config-item-value">{signal.maxConcurrency ?? "\u2014"}</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="config-item">
|
|
161
|
+
<span className="config-item-label">Steps</span>
|
|
162
|
+
<span className="config-item-value">
|
|
163
|
+
{signal.hasSteps ? signal.stepNames.join(", ") : "Single handler"}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{hasSchema && (
|
|
170
|
+
<div className="detail-section">
|
|
171
|
+
<div className="detail-section-label">Schema</div>
|
|
172
|
+
<div className="schema-pair">
|
|
173
|
+
<div>
|
|
174
|
+
<div style={{
|
|
175
|
+
fontFamily: "var(--font-mono)",
|
|
176
|
+
fontSize: "0.6875rem",
|
|
177
|
+
textTransform: "uppercase",
|
|
178
|
+
letterSpacing: "0.08em",
|
|
179
|
+
color: "var(--muted)",
|
|
180
|
+
marginBottom: "0.5rem",
|
|
181
|
+
}}>
|
|
182
|
+
Input
|
|
183
|
+
</div>
|
|
184
|
+
{signal.inputSchema ? (
|
|
185
|
+
<pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
|
|
186
|
+
{renderSchemaInline(signal.inputSchema)}
|
|
187
|
+
</pre>
|
|
188
|
+
) : (
|
|
189
|
+
<span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<div style={{
|
|
194
|
+
fontFamily: "var(--font-mono)",
|
|
195
|
+
fontSize: "0.6875rem",
|
|
196
|
+
textTransform: "uppercase",
|
|
197
|
+
letterSpacing: "0.08em",
|
|
198
|
+
color: "var(--muted)",
|
|
199
|
+
marginBottom: "0.5rem",
|
|
200
|
+
}}>
|
|
201
|
+
Output
|
|
202
|
+
</div>
|
|
203
|
+
{signal.outputSchema ? (
|
|
204
|
+
<pre className="json-viewer" style={{ fontSize: "0.75rem" }}>
|
|
205
|
+
{renderSchemaInline(signal.outputSchema)}
|
|
206
|
+
</pre>
|
|
207
|
+
) : (
|
|
208
|
+
<span style={{ color: "var(--muted)", fontSize: "0.8125rem" }}>None</span>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
<div className="detail-section">
|
|
216
|
+
<div className="detail-section-label">Trigger</div>
|
|
217
|
+
<SchemaForm
|
|
218
|
+
schema={signal.inputSchema}
|
|
219
|
+
value={inputJson}
|
|
220
|
+
onChange={setInputJson}
|
|
221
|
+
/>
|
|
222
|
+
<div style={{ marginTop: "0.5rem" }}>
|
|
223
|
+
<button
|
|
224
|
+
className="btn btn--primary"
|
|
225
|
+
onClick={handleTrigger}
|
|
226
|
+
disabled={triggering}
|
|
227
|
+
>
|
|
228
|
+
{triggering ? "Dispatching..." : "Dispatch"}
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="detail-section">
|
|
234
|
+
<div className="detail-section-label">Run History</div>
|
|
235
|
+
<div className="filter-bar">
|
|
236
|
+
{STATUS_FILTERS.map((f) => (
|
|
237
|
+
<button
|
|
238
|
+
key={f}
|
|
239
|
+
className={`filter-btn${filter === f ? " filter-btn--active" : ""}`}
|
|
240
|
+
onClick={() => setFilter(f)}
|
|
241
|
+
>
|
|
242
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
243
|
+
</button>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
<RunTable runs={filteredRuns} />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
package/src/cli-main.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
1
|
import { loadConfig } from "./config/loader.js";
|
|
4
2
|
import { createStation } from "./server/index.js";
|
|
5
3
|
|
|
@@ -10,45 +8,15 @@ const config = await loadConfig(cwd);
|
|
|
10
8
|
const station = await createStation(config, cwd);
|
|
11
9
|
await station.start();
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
const nextPort = config.port + 1;
|
|
15
|
-
const stationRoot = resolve(import.meta.dirname, "..");
|
|
16
|
-
|
|
17
|
-
const nextProcess: ChildProcess = spawn(
|
|
18
|
-
"npx",
|
|
19
|
-
["next", "dev", "--port", String(nextPort), "--hostname", config.host],
|
|
20
|
-
{
|
|
21
|
-
cwd: stationRoot,
|
|
22
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
23
|
-
env: {
|
|
24
|
-
...process.env,
|
|
25
|
-
NEXT_PUBLIC_STATION_API: `http://${config.host}:${config.port}`,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
nextProcess.stdout?.on("data", (chunk: Buffer) => {
|
|
31
|
-
const msg = chunk.toString().trim();
|
|
32
|
-
if (msg) console.log(`[station:ui] ${msg}`);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
nextProcess.stderr?.on("data", (chunk: Buffer) => {
|
|
36
|
-
const msg = chunk.toString().trim();
|
|
37
|
-
// Filter out noisy Next.js dev warnings
|
|
38
|
-
if (msg && !msg.includes("ExperimentalWarning")) {
|
|
39
|
-
console.error(`[station:ui] ${msg}`);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
console.log(`[station] Dashboard on http://${config.host}:${nextPort}`);
|
|
11
|
+
console.log(`[station] Dashboard on http://${config.host}:${config.port}`);
|
|
44
12
|
|
|
45
13
|
// Open browser
|
|
46
14
|
if (config.open) {
|
|
47
|
-
const url = `http://${config.host}:${
|
|
15
|
+
const url = `http://${config.host}:${config.port}`;
|
|
48
16
|
const { execFile } = await import("node:child_process");
|
|
49
17
|
const platform = process.platform;
|
|
50
18
|
|
|
51
|
-
await new Promise((res) => setTimeout(() => res(true),
|
|
19
|
+
await new Promise((res) => setTimeout(() => res(true), 2000));
|
|
52
20
|
if (platform === "darwin") {
|
|
53
21
|
execFile("open", [url]);
|
|
54
22
|
} else if (platform === "linux") {
|
|
@@ -61,7 +29,6 @@ if (config.open) {
|
|
|
61
29
|
// Graceful shutdown
|
|
62
30
|
const shutdown = async () => {
|
|
63
31
|
console.log("\n[station] Shutting down...");
|
|
64
|
-
nextProcess.kill("SIGTERM");
|
|
65
32
|
await station.stop();
|
|
66
33
|
process.exit(0);
|
|
67
34
|
};
|
package/src/cli.ts
CHANGED
|
@@ -25,7 +25,7 @@ if (!process.env[MARKER]) {
|
|
|
25
25
|
const main = fileURLToPath(new URL("./cli-main.js", import.meta.url));
|
|
26
26
|
const child = spawn(execPath, ["--import", tsxSpecifier, main], {
|
|
27
27
|
stdio: "inherit",
|
|
28
|
-
env: { ...process.env, [MARKER]: "1" },
|
|
28
|
+
env: { ...process.env, [MARKER]: "1", __STATION_TSX: tsxSpecifier },
|
|
29
29
|
});
|
|
30
30
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
31
31
|
child.on("error", (err) => {
|