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.
Files changed (58) hide show
  1. package/dist/cli-main.js +3 -29
  2. package/dist/cli-main.js.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server/index.d.ts.map +1 -1
  6. package/dist/server/index.js +89 -9
  7. package/dist/server/index.js.map +1 -1
  8. package/next.config.ts +1 -1
  9. package/out/404.html +1 -0
  10. package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_buildManifest.js +1 -0
  11. package/out/_next/static/7q5_eGqbkbdP_jAqzl6RE/_ssgManifest.js +1 -0
  12. package/out/_next/static/chunks/580-f007f4d4c050db4e.js +1 -0
  13. package/out/_next/static/chunks/743-5bb03adbb0e4ddec.js +1 -0
  14. package/out/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +1 -0
  15. package/out/_next/static/chunks/app/_not-found/page-ce21b4ba9038a5a7.js +1 -0
  16. package/out/_next/static/chunks/app/broadcasts/[id]/page-057eeaa51d28cbfd.js +1 -0
  17. package/out/_next/static/chunks/app/broadcasts/page-ac768ee4bcf3086f.js +1 -0
  18. package/out/_next/static/chunks/app/layout-a5f4d2f2e87939b2.js +1 -0
  19. package/out/_next/static/chunks/app/page-62d1dbcfdc93b566.js +1 -0
  20. package/out/_next/static/chunks/app/runs/[id]/page-7f726f2f4ea8f616.js +1 -0
  21. package/out/_next/static/chunks/app/signals/[name]/page-8f9f032eb0171ded.js +1 -0
  22. package/out/_next/static/chunks/app/signals/page-fb42d9c0368bbc58.js +1 -0
  23. package/out/_next/static/chunks/framework-077b27ad7787463c.js +1 -0
  24. package/out/_next/static/chunks/main-app-a9f19d5831b41b19.js +1 -0
  25. package/out/_next/static/chunks/main-f1c74cefd4965abf.js +1 -0
  26. package/out/_next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +1 -0
  27. package/out/_next/static/chunks/pages/_error-273a093c18b5ed0f.js +1 -0
  28. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  29. package/out/_next/static/chunks/webpack-e40b05d6bdcb3589.js +1 -0
  30. package/out/_next/static/css/0be0e65a5f561f37.css +1 -0
  31. package/out/broadcasts/_.html +1 -0
  32. package/out/broadcasts/_.txt +22 -0
  33. package/out/broadcasts.html +1 -0
  34. package/out/broadcasts.txt +25 -0
  35. package/out/index.html +1 -0
  36. package/out/index.txt +25 -0
  37. package/out/runs/_.html +1 -0
  38. package/out/runs/_.txt +22 -0
  39. package/out/signals/_.html +1 -0
  40. package/out/signals/_.txt +22 -0
  41. package/out/signals.html +1 -0
  42. package/out/signals.txt +25 -0
  43. package/package.json +16 -9
  44. package/src/app/broadcasts/[id]/broadcast-detail.tsx +511 -0
  45. package/src/app/broadcasts/[id]/page.tsx +4 -506
  46. package/src/app/hooks/use-api.ts +1 -1
  47. package/src/app/hooks/use-realtime.ts +2 -3
  48. package/src/app/runs/[id]/page.tsx +4 -272
  49. package/src/app/runs/[id]/run-detail.tsx +277 -0
  50. package/src/app/signals/[name]/page.tsx +4 -245
  51. package/src/app/signals/[name]/signal-detail.tsx +250 -0
  52. package/src/cli-main.ts +3 -36
  53. package/src/cli.ts +1 -1
  54. package/src/server/index.ts +94 -10
  55. package/next-env.d.ts +0 -6
  56. package/station.config.example.ts +0 -16
  57. package/tsconfig.json +0 -12
  58. package/tsconfig.tsbuildinfo +0 -1
@@ -1,277 +1,9 @@
1
- "use client";
1
+ import { RunDetail } from "./run-detail";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
4
- import { useParams } from "next/navigation";
5
- import Link from "next/link";
6
- import { useApi } from "../../hooks/use-api";
7
- import { useStation } from "../../hooks/use-station";
8
- import { useBreadcrumb } from "../../hooks/use-breadcrumb";
9
- import { StatusBadge } from "../../components/status-badge";
10
- import { StepTimeline } from "../../components/step-timeline";
11
- import { JsonViewer } from "../../components/json-viewer";
12
-
13
- interface LogEntry {
14
- runId: string;
15
- signalName: string;
16
- level: string;
17
- message: string;
18
- timestamp: string;
19
- }
20
-
21
- function formatMs(ms: number): string {
22
- if (ms < 1000) return `${ms}ms`;
23
- if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
24
- if (ms < 3_600_000) return `${(ms / 60_000).toFixed(1)}m`;
25
- return `${(ms / 3_600_000).toFixed(1)}h`;
26
- }
27
-
28
- function computeDuration(startedAt?: string, completedAt?: string): string {
29
- if (!startedAt) return "\u2014";
30
- const start = new Date(startedAt).getTime();
31
- const end = completedAt ? new Date(completedAt).getTime() : Date.now();
32
- const ms = end - start;
33
- return formatMs(ms);
3
+ export function generateStaticParams() {
4
+ return [{ id: "_" }];
34
5
  }
35
6
 
36
7
  export default function RunDetailPage() {
37
- const params = useParams();
38
- const id = params.id as string;
39
- const api = useApi();
40
- const { events } = useStation();
41
- const [run, setRun] = useState<any>(null);
42
- const [steps, setSteps] = useState<any[]>([]);
43
- const [logs, setLogs] = useState<LogEntry[]>([]);
44
- const [loading, setLoading] = useState(true);
45
- const [cancelling, setCancelling] = useState(false);
46
- const [copied, setCopied] = useState(false);
47
- const logEndRef = useRef<HTMLDivElement>(null);
48
-
49
- useBreadcrumb(
50
- run
51
- ? [
52
- { label: "Signals", href: "/signals" },
53
- { label: run.signalName, href: `/signals/${encodeURIComponent(run.signalName)}` },
54
- { label: `Run ${id.slice(0, 8)}` },
55
- ]
56
- : [{ label: "Signals", href: "/signals" }, { label: `Run ${id.slice(0, 8)}` }],
57
- "signals",
58
- );
59
-
60
- useEffect(() => {
61
- async function load() {
62
- try {
63
- const [runRes, stepsRes, logsRes] = await Promise.all([
64
- api.getRun(id),
65
- api.getRunSteps(id),
66
- api.getRunLogs(id),
67
- ]);
68
- setRun(runRes.data);
69
- setSteps(stepsRes.data);
70
- setLogs(logsRes.data);
71
- } catch (err: unknown) {
72
- if (err instanceof Error) {
73
- console.error("Failed to load run:", err.message);
74
- }
75
- }
76
- setLoading(false);
77
- }
78
- load();
79
- }, [id]);
80
-
81
- useEffect(() => {
82
- if (events.length === 0) return;
83
- const latest = events[0];
84
- const eventRunId = (latest.data.run as Record<string, unknown>)?.id ?? (latest.data as Record<string, unknown>)?.runId;
85
- if (eventRunId === id) {
86
- if (latest.type === "log:output") {
87
- setLogs((prev) => [...prev, {
88
- runId: latest.data.runId as string,
89
- signalName: latest.data.signalName as string,
90
- level: latest.data.level as string,
91
- message: latest.data.message as string,
92
- timestamp: (latest.data.timestamp as string) ?? latest.timestamp,
93
- }]);
94
- } else {
95
- api.getRun(id).then((r) => setRun(r.data)).catch((e) => console.error("Failed to refresh run:", e));
96
- api.getRunSteps(id).then((r) => setSteps(r.data)).catch((e) => console.error("Failed to refresh steps:", e));
97
- }
98
- }
99
- }, [events.length, id]);
100
-
101
- useEffect(() => {
102
- logEndRef.current?.scrollIntoView({ behavior: "smooth" });
103
- }, [logs.length]);
104
-
105
- async function handleCancel() {
106
- setCancelling(true);
107
- try {
108
- await api.cancelRun(id);
109
- const res = await api.getRun(id);
110
- setRun(res.data);
111
- } catch (err: unknown) {
112
- if (err instanceof Error) {
113
- console.error("Cancel failed:", err.message);
114
- }
115
- }
116
- setCancelling(false);
117
- }
118
-
119
- function handleCopyId() {
120
- navigator.clipboard.writeText(run.id).then(() => {
121
- setCopied(true);
122
- setTimeout(() => setCopied(false), 1500);
123
- }).catch(() => {});
124
- }
125
-
126
- if (loading) {
127
- return (
128
- <div>
129
- <h1 className="page-title">Run</h1>
130
- <div className="loading-bar"><div className="loading-bar-fill" /></div>
131
- </div>
132
- );
133
- }
134
-
135
- if (!run) {
136
- return (
137
- <div>
138
- <h1 className="page-title">Run</h1>
139
- <div className="empty-state">
140
- <p className="empty-state-text">Run not found.</p>
141
- </div>
142
- </div>
143
- );
144
- }
145
-
146
- const canCancel = run.status === "pending" || run.status === "running";
147
-
148
- return (
149
- <div>
150
- <div className="page-header">
151
- <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
152
- <StatusBadge status={run.status} />
153
- <Link href={`/signals/${run.signalName}`} className="page-title" style={{ marginBottom: 0, textDecoration: "none" }}>
154
- {run.signalName}
155
- </Link>
156
- </div>
157
- <div className="page-header-actions">
158
- {canCancel && (
159
- <button className="btn btn--danger" onClick={handleCancel} disabled={cancelling}>
160
- {cancelling ? "Cancelling..." : "Cancel"}
161
- </button>
162
- )}
163
- </div>
164
- </div>
165
-
166
- <div className="detail-section">
167
- <div className="detail-section-label">Metadata</div>
168
- <div className="detail-grid">
169
- <span className="detail-label">ID</span>
170
- <span className="detail-value mono" style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
171
- {run.id}
172
- <button
173
- onClick={handleCopyId}
174
- className="btn btn--sm"
175
- style={{
176
- fontSize: "0.625rem",
177
- padding: "0.125rem 0.375rem",
178
- lineHeight: 1.2,
179
- }}
180
- >
181
- {copied ? "copied" : "copy"}
182
- </button>
183
- </span>
184
-
185
- <span className="detail-label">Signal</span>
186
- <span className="detail-value">
187
- <Link href={`/signals/${run.signalName}`} className="mono meta-value--link">
188
- {run.signalName}
189
- </Link>
190
- </span>
191
-
192
- <span className="detail-label">Kind</span>
193
- <span className="detail-value">{run.kind}</span>
194
-
195
- <span className="detail-label">Attempts</span>
196
- <span className="detail-value">{run.attempts} of {run.maxAttempts}</span>
197
-
198
- <span className="detail-label">Timeout</span>
199
- <span className="detail-value mono">{run.timeout ? formatMs(run.timeout) : "\u2014"}</span>
200
-
201
- <span className="detail-label">Duration</span>
202
- <span className="detail-value mono">{computeDuration(run.startedAt, run.completedAt)}</span>
203
-
204
- <span className="detail-label">Created</span>
205
- <span className="detail-value mono">{run.createdAt}</span>
206
-
207
- <span className="detail-label">Started</span>
208
- <span className="detail-value mono">{run.startedAt ?? "\u2014"}</span>
209
-
210
- <span className="detail-label">Completed</span>
211
- <span className="detail-value mono">{run.completedAt ?? "\u2014"}</span>
212
- </div>
213
- </div>
214
-
215
- {run.error && (
216
- <div className="detail-section">
217
- <div className="detail-section-label">Error</div>
218
- <div className="error-block">{run.error}</div>
219
- </div>
220
- )}
221
-
222
- <div className="detail-section">
223
- <div className="detail-section-label">Input</div>
224
- <JsonViewer data={run.input} />
225
- </div>
226
-
227
- {run.output && (
228
- <div className="detail-section">
229
- <div className="detail-section-label">Output</div>
230
- <JsonViewer data={run.output} />
231
- </div>
232
- )}
233
-
234
- {steps.length > 0 && (
235
- <div className="detail-section">
236
- <div className="detail-section-label">Steps</div>
237
- <StepTimeline steps={steps} />
238
- </div>
239
- )}
240
-
241
- <div className="detail-section">
242
- <div className="detail-section-label">Logs</div>
243
- <div className="log-container">
244
- {logs.length === 0 ? (
245
- <div style={{
246
- padding: "1rem",
247
- color: "var(--muted)",
248
- fontFamily: "var(--font-mono)",
249
- fontSize: "0.75rem",
250
- }}>
251
- {run.status === "pending" ? "Waiting for execution..." : "No log output captured."}
252
- </div>
253
- ) : (
254
- logs.map((log, i) => (
255
- <div
256
- key={i}
257
- className="log-line"
258
- style={{
259
- color: log.level === "stderr" ? "var(--rust)" : "var(--charcoal)",
260
- }}
261
- >
262
- <span className="log-timestamp">
263
- {new Date(log.timestamp).toLocaleTimeString()}
264
- </span>
265
- <span className="log-level" data-level={log.level}>
266
- {log.level === "stderr" ? "ERR" : "OUT"}
267
- </span>
268
- <span className="log-message">{log.message}</span>
269
- </div>
270
- ))
271
- )}
272
- <div ref={logEndRef} />
273
- </div>
274
- </div>
275
- </div>
276
- );
8
+ return <RunDetail />;
277
9
  }
@@ -0,0 +1,277 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { useParams } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { useApi } from "../../hooks/use-api";
7
+ import { useStation } from "../../hooks/use-station";
8
+ import { useBreadcrumb } from "../../hooks/use-breadcrumb";
9
+ import { StatusBadge } from "../../components/status-badge";
10
+ import { StepTimeline } from "../../components/step-timeline";
11
+ import { JsonViewer } from "../../components/json-viewer";
12
+
13
+ interface LogEntry {
14
+ runId: string;
15
+ signalName: string;
16
+ level: string;
17
+ message: string;
18
+ timestamp: string;
19
+ }
20
+
21
+ function formatMs(ms: number): string {
22
+ if (ms < 1000) return `${ms}ms`;
23
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
24
+ if (ms < 3_600_000) return `${(ms / 60_000).toFixed(1)}m`;
25
+ return `${(ms / 3_600_000).toFixed(1)}h`;
26
+ }
27
+
28
+ function computeDuration(startedAt?: string, completedAt?: string): string {
29
+ if (!startedAt) return "\u2014";
30
+ const start = new Date(startedAt).getTime();
31
+ const end = completedAt ? new Date(completedAt).getTime() : Date.now();
32
+ const ms = end - start;
33
+ return formatMs(ms);
34
+ }
35
+
36
+ export function RunDetail() {
37
+ const params = useParams();
38
+ const id = params.id as string;
39
+ const api = useApi();
40
+ const { events } = useStation();
41
+ const [run, setRun] = useState<any>(null);
42
+ const [steps, setSteps] = useState<any[]>([]);
43
+ const [logs, setLogs] = useState<LogEntry[]>([]);
44
+ const [loading, setLoading] = useState(true);
45
+ const [cancelling, setCancelling] = useState(false);
46
+ const [copied, setCopied] = useState(false);
47
+ const logEndRef = useRef<HTMLDivElement>(null);
48
+
49
+ useBreadcrumb(
50
+ run
51
+ ? [
52
+ { label: "Signals", href: "/signals" },
53
+ { label: run.signalName, href: `/signals/${encodeURIComponent(run.signalName)}` },
54
+ { label: `Run ${id.slice(0, 8)}` },
55
+ ]
56
+ : [{ label: "Signals", href: "/signals" }, { label: `Run ${id.slice(0, 8)}` }],
57
+ "signals",
58
+ );
59
+
60
+ useEffect(() => {
61
+ async function load() {
62
+ try {
63
+ const [runRes, stepsRes, logsRes] = await Promise.all([
64
+ api.getRun(id),
65
+ api.getRunSteps(id),
66
+ api.getRunLogs(id),
67
+ ]);
68
+ setRun(runRes.data);
69
+ setSteps(stepsRes.data);
70
+ setLogs(logsRes.data);
71
+ } catch (err: unknown) {
72
+ if (err instanceof Error) {
73
+ console.error("Failed to load run:", err.message);
74
+ }
75
+ }
76
+ setLoading(false);
77
+ }
78
+ load();
79
+ }, [id]);
80
+
81
+ useEffect(() => {
82
+ if (events.length === 0) return;
83
+ const latest = events[0];
84
+ const eventRunId = (latest.data.run as Record<string, unknown>)?.id ?? (latest.data as Record<string, unknown>)?.runId;
85
+ if (eventRunId === id) {
86
+ if (latest.type === "log:output") {
87
+ setLogs((prev) => [...prev, {
88
+ runId: latest.data.runId as string,
89
+ signalName: latest.data.signalName as string,
90
+ level: latest.data.level as string,
91
+ message: latest.data.message as string,
92
+ timestamp: (latest.data.timestamp as string) ?? latest.timestamp,
93
+ }]);
94
+ } else {
95
+ api.getRun(id).then((r) => setRun(r.data)).catch((e) => console.error("Failed to refresh run:", e));
96
+ api.getRunSteps(id).then((r) => setSteps(r.data)).catch((e) => console.error("Failed to refresh steps:", e));
97
+ }
98
+ }
99
+ }, [events.length, id]);
100
+
101
+ useEffect(() => {
102
+ logEndRef.current?.scrollIntoView({ behavior: "smooth" });
103
+ }, [logs.length]);
104
+
105
+ async function handleCancel() {
106
+ setCancelling(true);
107
+ try {
108
+ await api.cancelRun(id);
109
+ const res = await api.getRun(id);
110
+ setRun(res.data);
111
+ } catch (err: unknown) {
112
+ if (err instanceof Error) {
113
+ console.error("Cancel failed:", err.message);
114
+ }
115
+ }
116
+ setCancelling(false);
117
+ }
118
+
119
+ function handleCopyId() {
120
+ navigator.clipboard.writeText(run.id).then(() => {
121
+ setCopied(true);
122
+ setTimeout(() => setCopied(false), 1500);
123
+ }).catch(() => {});
124
+ }
125
+
126
+ if (loading) {
127
+ return (
128
+ <div>
129
+ <h1 className="page-title">Run</h1>
130
+ <div className="loading-bar"><div className="loading-bar-fill" /></div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ if (!run) {
136
+ return (
137
+ <div>
138
+ <h1 className="page-title">Run</h1>
139
+ <div className="empty-state">
140
+ <p className="empty-state-text">Run not found.</p>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ const canCancel = run.status === "pending" || run.status === "running";
147
+
148
+ return (
149
+ <div>
150
+ <div className="page-header">
151
+ <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
152
+ <StatusBadge status={run.status} />
153
+ <Link href={`/signals/${run.signalName}`} className="page-title" style={{ marginBottom: 0, textDecoration: "none" }}>
154
+ {run.signalName}
155
+ </Link>
156
+ </div>
157
+ <div className="page-header-actions">
158
+ {canCancel && (
159
+ <button className="btn btn--danger" onClick={handleCancel} disabled={cancelling}>
160
+ {cancelling ? "Cancelling..." : "Cancel"}
161
+ </button>
162
+ )}
163
+ </div>
164
+ </div>
165
+
166
+ <div className="detail-section">
167
+ <div className="detail-section-label">Metadata</div>
168
+ <div className="detail-grid">
169
+ <span className="detail-label">ID</span>
170
+ <span className="detail-value mono" style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
171
+ {run.id}
172
+ <button
173
+ onClick={handleCopyId}
174
+ className="btn btn--sm"
175
+ style={{
176
+ fontSize: "0.625rem",
177
+ padding: "0.125rem 0.375rem",
178
+ lineHeight: 1.2,
179
+ }}
180
+ >
181
+ {copied ? "copied" : "copy"}
182
+ </button>
183
+ </span>
184
+
185
+ <span className="detail-label">Signal</span>
186
+ <span className="detail-value">
187
+ <Link href={`/signals/${run.signalName}`} className="mono meta-value--link">
188
+ {run.signalName}
189
+ </Link>
190
+ </span>
191
+
192
+ <span className="detail-label">Kind</span>
193
+ <span className="detail-value">{run.kind}</span>
194
+
195
+ <span className="detail-label">Attempts</span>
196
+ <span className="detail-value">{run.attempts} of {run.maxAttempts}</span>
197
+
198
+ <span className="detail-label">Timeout</span>
199
+ <span className="detail-value mono">{run.timeout ? formatMs(run.timeout) : "\u2014"}</span>
200
+
201
+ <span className="detail-label">Duration</span>
202
+ <span className="detail-value mono">{computeDuration(run.startedAt, run.completedAt)}</span>
203
+
204
+ <span className="detail-label">Created</span>
205
+ <span className="detail-value mono">{run.createdAt}</span>
206
+
207
+ <span className="detail-label">Started</span>
208
+ <span className="detail-value mono">{run.startedAt ?? "\u2014"}</span>
209
+
210
+ <span className="detail-label">Completed</span>
211
+ <span className="detail-value mono">{run.completedAt ?? "\u2014"}</span>
212
+ </div>
213
+ </div>
214
+
215
+ {run.error && (
216
+ <div className="detail-section">
217
+ <div className="detail-section-label">Error</div>
218
+ <div className="error-block">{run.error}</div>
219
+ </div>
220
+ )}
221
+
222
+ <div className="detail-section">
223
+ <div className="detail-section-label">Input</div>
224
+ <JsonViewer data={run.input} />
225
+ </div>
226
+
227
+ {run.output && (
228
+ <div className="detail-section">
229
+ <div className="detail-section-label">Output</div>
230
+ <JsonViewer data={run.output} />
231
+ </div>
232
+ )}
233
+
234
+ {steps.length > 0 && (
235
+ <div className="detail-section">
236
+ <div className="detail-section-label">Steps</div>
237
+ <StepTimeline steps={steps} />
238
+ </div>
239
+ )}
240
+
241
+ <div className="detail-section">
242
+ <div className="detail-section-label">Logs</div>
243
+ <div className="log-container">
244
+ {logs.length === 0 ? (
245
+ <div style={{
246
+ padding: "1rem",
247
+ color: "var(--muted)",
248
+ fontFamily: "var(--font-mono)",
249
+ fontSize: "0.75rem",
250
+ }}>
251
+ {run.status === "pending" ? "Waiting for execution..." : "No log output captured."}
252
+ </div>
253
+ ) : (
254
+ logs.map((log, i) => (
255
+ <div
256
+ key={i}
257
+ className="log-line"
258
+ style={{
259
+ color: log.level === "stderr" ? "var(--rust)" : "var(--charcoal)",
260
+ }}
261
+ >
262
+ <span className="log-timestamp">
263
+ {new Date(log.timestamp).toLocaleTimeString()}
264
+ </span>
265
+ <span className="log-level" data-level={log.level}>
266
+ {log.level === "stderr" ? "ERR" : "OUT"}
267
+ </span>
268
+ <span className="log-message">{log.message}</span>
269
+ </div>
270
+ ))
271
+ )}
272
+ <div ref={logEndRef} />
273
+ </div>
274
+ </div>
275
+ </div>
276
+ );
277
+ }