station-kit 1.0.5 → 1.0.6

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 (68) hide show
  1. package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
  2. package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +15 -15
  3. package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +2 -2
  4. package/.next/standalone/packages/station-kit/.next/build-manifest.json +2 -2
  5. package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +10 -10
  6. package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
  8. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +7 -7
  9. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page.js +1 -1
  10. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
  13. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +8 -8
  14. package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
  15. package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +8 -8
  16. package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page.js +1 -1
  18. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
  21. package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +8 -8
  22. package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
  25. package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +8 -8
  26. package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +2 -2
  27. package/.next/standalone/packages/station-kit/.next/server/chunks/102.js +1 -1
  28. package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
  29. package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
  30. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +1 -0
  31. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +1 -0
  32. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +1 -0
  33. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +1 -0
  34. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +1 -0
  35. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +1 -0
  36. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +1 -0
  37. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +1 -0
  38. package/dist/server/index.js +2 -2
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/routes/broadcasts.d.ts.map +1 -1
  41. package/dist/server/routes/broadcasts.js +23 -0
  42. package/dist/server/routes/broadcasts.js.map +1 -1
  43. package/dist/server/routes/runs.d.ts +2 -0
  44. package/dist/server/routes/runs.d.ts.map +1 -1
  45. package/dist/server/routes/runs.js +66 -0
  46. package/dist/server/routes/runs.js.map +1 -1
  47. package/dist/server/routes/v1/trigger.d.ts +2 -1
  48. package/dist/server/routes/v1/trigger.d.ts.map +1 -1
  49. package/dist/server/routes/v1/trigger.js +88 -0
  50. package/dist/server/routes/v1/trigger.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/app/broadcasts/[id]/broadcast-detail.tsx +22 -1
  53. package/src/app/hooks/use-api.ts +3 -0
  54. package/src/app/runs/[id]/run-detail.tsx +45 -1
  55. package/src/server/index.ts +2 -2
  56. package/src/server/routes/broadcasts.ts +24 -0
  57. package/src/server/routes/runs.ts +76 -0
  58. package/src/server/routes/v1/trigger.ts +105 -1
  59. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a41371d946262e43.js +0 -1
  60. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-1b9fa68dcc219e1d.js +0 -1
  61. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-9d37f15ea4fdcabf.js +0 -1
  62. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-0c92bbc0acac1025.js +0 -1
  63. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-87e302dcd2f7516b.js +0 -1
  64. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-39f130df9a05bf2b.js +0 -1
  65. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-215ec395e232fb36.js +0 -1
  66. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-35b38b909a017693.js +0 -1
  67. /package/.next/standalone/packages/station-kit/.next/static/{_qcazCz3yLLsuU53Qtacw → kqMlNlRLox_FLRK-2GwrJ}/_buildManifest.js +0 -0
  68. /package/.next/standalone/packages/station-kit/.next/static/{_qcazCz3yLLsuU53Qtacw → kqMlNlRLox_FLRK-2GwrJ}/_ssgManifest.js +0 -0
@@ -40,6 +40,72 @@ export function v1TriggerRoutes(deps) {
40
40
  data: { id, signalName, status: "pending", createdAt: new Date().toISOString() },
41
41
  }, 201);
42
42
  });
43
+ app.post("/runs/:id/rerun", async (c) => {
44
+ const id = c.req.param("id");
45
+ if (!deps.signalRunner) {
46
+ return c.json({ error: "unavailable", message: "Station is in read-only mode." }, 503);
47
+ }
48
+ const run = await deps.signalAdapter.getRun(id);
49
+ if (!run) {
50
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
51
+ }
52
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
53
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled runs can be rerun." }, 400);
54
+ }
55
+ let maxAttempts = run.maxAttempts;
56
+ let timeout = run.timeout;
57
+ if (deps.signalSubscriber) {
58
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
59
+ if (meta) {
60
+ maxAttempts = meta.maxAttempts;
61
+ timeout = meta.timeout;
62
+ }
63
+ }
64
+ const newId = deps.signalAdapter.generateId();
65
+ await deps.signalAdapter.addRun({
66
+ id: newId,
67
+ signalName: run.signalName,
68
+ kind: "trigger",
69
+ input: run.input,
70
+ status: "pending",
71
+ attempts: 0,
72
+ maxAttempts,
73
+ timeout,
74
+ createdAt: new Date(),
75
+ });
76
+ return c.json({ data: { id: newId, signalName: run.signalName, status: "pending", createdAt: new Date().toISOString() } }, 201);
77
+ });
78
+ app.post("/runs/:id/retry", async (c) => {
79
+ const id = c.req.param("id");
80
+ if (!deps.signalRunner) {
81
+ return c.json({ error: "unavailable", message: "Station is in read-only mode." }, 503);
82
+ }
83
+ const run = await deps.signalAdapter.getRun(id);
84
+ if (!run) {
85
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
86
+ }
87
+ if (run.status !== "failed") {
88
+ return c.json({ error: "invalid_status", message: "Only failed runs can be retried." }, 400);
89
+ }
90
+ let maxAttempts = run.maxAttempts;
91
+ if (deps.signalSubscriber) {
92
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
93
+ if (meta) {
94
+ maxAttempts = meta.maxAttempts;
95
+ }
96
+ }
97
+ await deps.signalAdapter.updateRun(id, {
98
+ status: "pending",
99
+ attempts: 0,
100
+ maxAttempts,
101
+ error: undefined,
102
+ output: undefined,
103
+ startedAt: undefined,
104
+ completedAt: undefined,
105
+ lastRunAt: undefined,
106
+ });
107
+ return c.json({ data: { retried: true } });
108
+ });
43
109
  app.post("/trigger-broadcast", async (c) => {
44
110
  if (!deps.broadcastRunner) {
45
111
  return c.json({ error: "unavailable", message: "Broadcast runner not configured." }, 503);
@@ -68,6 +134,28 @@ export function v1TriggerRoutes(deps) {
68
134
  return c.json({ error: "trigger_failed", message }, 400);
69
135
  }
70
136
  });
137
+ app.post("/broadcast-runs/:id/rerun", async (c) => {
138
+ if (!deps.broadcastRunner || !deps.broadcastAdapter) {
139
+ return c.json({ error: "unavailable", message: "Broadcast runner not configured." }, 503);
140
+ }
141
+ const id = c.req.param("id");
142
+ const run = await deps.broadcastAdapter.getBroadcastRun(id);
143
+ if (!run) {
144
+ return c.json({ error: "not_found", message: "Broadcast run not found." }, 404);
145
+ }
146
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
147
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled broadcast runs can be rerun." }, 400);
148
+ }
149
+ try {
150
+ const input = typeof run.input === "string" ? JSON.parse(run.input) : run.input;
151
+ const newId = await deps.broadcastRunner.trigger(run.broadcastName, input);
152
+ return c.json({ data: { id: newId, broadcastName: run.broadcastName, status: "pending", createdAt: new Date().toISOString() } }, 201);
153
+ }
154
+ catch (err) {
155
+ const message = err instanceof Error ? err.message : String(err);
156
+ return c.json({ error: "rerun_failed", message }, 400);
157
+ }
158
+ });
71
159
  return app;
72
160
  }
73
161
  //# sourceMappingURL=trigger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../../../src/server/routes/v1/trigger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../../../src/server/routes/v1/trigger.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "station-kit",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Dashboard for station-signal — inspect and control signals and broadcasts",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState, useCallback } from "react";
4
- import { useParams } from "next/navigation";
4
+ import { useParams, useRouter } from "next/navigation";
5
5
  import Link from "next/link";
6
6
  import { useApi, type BroadcastMeta, type SchemaField } from "../../hooks/use-api";
7
7
  import { useStation } from "../../hooks/use-station";
@@ -248,6 +248,7 @@ function BroadcastNameView({ name }: { name: string }) {
248
248
 
249
249
  function BroadcastRunView({ id }: { id: string }) {
250
250
  const api = useApi();
251
+ const router = useRouter();
251
252
  const { events } = useStation();
252
253
  const [broadcastRun, setBroadcastRun] = useState<any>(null);
253
254
  const [nodeRuns, setNodeRuns] = useState<any[]>([]);
@@ -255,6 +256,7 @@ function BroadcastRunView({ id }: { id: string }) {
255
256
  const [logs, setLogs] = useState<BroadcastLogEntry[]>([]);
256
257
  const [loading, setLoading] = useState(true);
257
258
  const [cancelling, setCancelling] = useState(false);
259
+ const [rerunning, setRerunning] = useState(false);
258
260
  const [selectedNode, setSelectedNode] = useState<string | null>(null);
259
261
  const signalRunIdsRef = useRef<Set<string>>(new Set());
260
262
  const autoSelectedRef = useRef(false);
@@ -357,6 +359,19 @@ function BroadcastRunView({ id }: { id: string }) {
357
359
  setCancelling(false);
358
360
  }
359
361
 
362
+ async function handleRerun() {
363
+ setRerunning(true);
364
+ try {
365
+ const res = await api.rerunBroadcastRun(id);
366
+ router.push(`/broadcasts/${res.data.id}`);
367
+ } catch (err: unknown) {
368
+ if (err instanceof Error) {
369
+ console.error("Rerun failed:", err.message);
370
+ }
371
+ }
372
+ setRerunning(false);
373
+ }
374
+
360
375
  // Build deps map from broadcast metadata
361
376
  const depsMap = useMemo(() => {
362
377
  const map = new Map<string, string[]>();
@@ -439,6 +454,7 @@ function BroadcastRunView({ id }: { id: string }) {
439
454
  }
440
455
 
441
456
  const canCancel = broadcastRun.status === "pending" || broadcastRun.status === "running";
457
+ const canRerun = broadcastRun.status === "failed" || broadcastRun.status === "completed" || broadcastRun.status === "cancelled";
442
458
 
443
459
  return (
444
460
  <div>
@@ -458,6 +474,11 @@ function BroadcastRunView({ id }: { id: string }) {
458
474
  </span>
459
475
  </div>
460
476
  <div className="page-header-actions">
477
+ {canRerun && (
478
+ <button className="btn btn--primary" onClick={handleRerun} disabled={rerunning}>
479
+ {rerunning ? "Rerunning..." : "Rerun"}
480
+ </button>
481
+ )}
461
482
  {canCancel && (
462
483
  <button className="btn btn--danger" onClick={handleCancel} disabled={cancelling}>
463
484
  {cancelling ? "Cancelling..." : "Cancel"}
@@ -111,6 +111,8 @@ export function useApi() {
111
111
  getRunSteps: (id: string) => fetchApi<any[]>(`/runs/${id}/steps`),
112
112
  getRunLogs: (id: string) => fetchApi<Array<{ runId: string; signalName: string; level: string; message: string; timestamp: string }>>(`/runs/${id}/logs`),
113
113
  cancelRun: (id: string) => fetchApi<{ cancelled: boolean }>(`/runs/${id}/cancel`, { method: "POST" }),
114
+ rerunRun: (id: string) => fetchApi<{ id: string; signalName: string; status: string }>(`/runs/${id}/rerun`, { method: "POST" }),
115
+ retryRun: (id: string) => fetchApi<{ retried: boolean }>(`/runs/${id}/retry`, { method: "POST" }),
114
116
 
115
117
  // Broadcasts
116
118
  getBroadcasts: () => fetchApi<BroadcastMeta[]>("/broadcasts"),
@@ -125,6 +127,7 @@ export function useApi() {
125
127
  getBroadcastRunNodes: (id: string) => fetchApi<any[]>(`/broadcast-runs/${id}/nodes`),
126
128
  getBroadcastRunLogs: (id: string) => fetchApi<Array<{ runId: string; signalName: string; level: string; message: string; timestamp: string; nodeName: string }>>(`/broadcast-runs/${id}/logs`),
127
129
  cancelBroadcastRun: (id: string) => fetchApi<{ cancelled: boolean }>(`/broadcast-runs/${id}/cancel`, { method: "POST" }),
130
+ rerunBroadcastRun: (id: string) => fetchApi<{ id: string; broadcastName: string; status: string }>(`/broadcast-runs/${id}/rerun`, { method: "POST" }),
128
131
 
129
132
  // API Keys (v1 admin routes — session cookie provides admin scope)
130
133
  getApiKeys: () => fetchApi<Array<{ id: string; name: string; keyPrefix: string; scopes: string[]; createdAt: string; lastUsed: string | null; expiresAt: string | null; revoked: boolean }>>("/v1/keys"),
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useRef, useState } from "react";
4
- import { useParams } from "next/navigation";
4
+ import { useParams, useRouter } from "next/navigation";
5
5
  import Link from "next/link";
6
6
  import { useApi } from "../../hooks/use-api";
7
7
  import { useStation } from "../../hooks/use-station";
@@ -36,6 +36,7 @@ function computeDuration(startedAt?: string, completedAt?: string): string {
36
36
  export function RunDetail() {
37
37
  const params = useParams();
38
38
  const id = params.id as string;
39
+ const router = useRouter();
39
40
  const api = useApi();
40
41
  const { events } = useStation();
41
42
  const [run, setRun] = useState<any>(null);
@@ -43,6 +44,8 @@ export function RunDetail() {
43
44
  const [logs, setLogs] = useState<LogEntry[]>([]);
44
45
  const [loading, setLoading] = useState(true);
45
46
  const [cancelling, setCancelling] = useState(false);
47
+ const [rerunning, setRerunning] = useState(false);
48
+ const [retrying, setRetrying] = useState(false);
46
49
  const [copied, setCopied] = useState(false);
47
50
  const logEndRef = useRef<HTMLDivElement>(null);
48
51
 
@@ -116,6 +119,35 @@ export function RunDetail() {
116
119
  setCancelling(false);
117
120
  }
118
121
 
122
+ async function handleRerun() {
123
+ setRerunning(true);
124
+ try {
125
+ const res = await api.rerunRun(id);
126
+ router.push(`/runs/${res.data.id}`);
127
+ } catch (err: unknown) {
128
+ if (err instanceof Error) {
129
+ console.error("Rerun failed:", err.message);
130
+ }
131
+ }
132
+ setRerunning(false);
133
+ }
134
+
135
+ async function handleRetry() {
136
+ setRetrying(true);
137
+ try {
138
+ await api.retryRun(id);
139
+ const res = await api.getRun(id);
140
+ setRun(res.data);
141
+ const stepsRes = await api.getRunSteps(id);
142
+ setSteps(stepsRes.data);
143
+ } catch (err: unknown) {
144
+ if (err instanceof Error) {
145
+ console.error("Retry failed:", err.message);
146
+ }
147
+ }
148
+ setRetrying(false);
149
+ }
150
+
119
151
  function handleCopyId() {
120
152
  navigator.clipboard.writeText(run.id).then(() => {
121
153
  setCopied(true);
@@ -144,6 +176,8 @@ export function RunDetail() {
144
176
  }
145
177
 
146
178
  const canCancel = run.status === "pending" || run.status === "running";
179
+ const canRerun = run.status === "failed" || run.status === "completed" || run.status === "cancelled";
180
+ const canRetry = run.status === "failed";
147
181
 
148
182
  return (
149
183
  <div>
@@ -155,6 +189,16 @@ export function RunDetail() {
155
189
  </Link>
156
190
  </div>
157
191
  <div className="page-header-actions">
192
+ {canRetry && (
193
+ <button className="btn" onClick={handleRetry} disabled={retrying}>
194
+ {retrying ? "Retrying..." : "Retry"}
195
+ </button>
196
+ )}
197
+ {canRerun && (
198
+ <button className="btn btn--primary" onClick={handleRerun} disabled={rerunning}>
199
+ {rerunning ? "Rerunning..." : "Rerun"}
200
+ </button>
201
+ )}
158
202
  {canCancel && (
159
203
  <button className="btn btn--danger" onClick={handleCancel} disabled={cancelling}>
160
204
  {cancelling ? "Cancelling..." : "Cancel"}
@@ -169,7 +169,7 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
169
169
 
170
170
  app.route("/api", healthRoutes({ signalAdapter, broadcastAdapter }));
171
171
  app.route("/api", signalRoutes({ signalRunner, signalAdapter, signalSubscriber: stationSignalSub }));
172
- app.route("/api", runRoutes({ signalRunner, signalAdapter, logBuffer, logStore }));
172
+ app.route("/api", runRoutes({ signalRunner, signalAdapter, logBuffer, logStore, signalSubscriber: stationSignalSub }));
173
173
  app.route("/api", broadcastRoutes({ broadcastRunner, broadcastAdapter, broadcastSubscriber: stationBroadcastSub, logBuffer, logStore }));
174
174
 
175
175
  // ── v1 API routes (authenticated) ──────────────────────────────────
@@ -199,7 +199,7 @@ export async function createStation(config: StationConfig, cwd: string, nextPort
199
199
  // Trigger-scope routes
200
200
  const triggerRoutes = new Hono();
201
201
  triggerRoutes.use("/*", requireScope("trigger"));
202
- triggerRoutes.route("/", v1TriggerRoutes({ signalRunner, signalAdapter, broadcastRunner, signalSubscriber: stationSignalSub }));
202
+ triggerRoutes.route("/", v1TriggerRoutes({ signalRunner, signalAdapter, broadcastRunner, broadcastAdapter, signalSubscriber: stationSignalSub }));
203
203
  v1.route("/", triggerRoutes);
204
204
 
205
205
  // Cancel-scope routes — only the cancel endpoints
@@ -146,6 +146,30 @@ export function broadcastRoutes(deps: BroadcastDeps) {
146
146
  return c.json({ data: { cancelled: true } });
147
147
  });
148
148
 
149
+ // POST /broadcast-runs/:id/rerun
150
+ app.post("/broadcast-runs/:id/rerun", async (c) => {
151
+ const id = c.req.param("id");
152
+ if (!deps.broadcastRunner || !deps.broadcastAdapter) {
153
+ return c.json({ error: "read_only", message: "Station is in read-only mode." }, 403);
154
+ }
155
+ const run = await deps.broadcastAdapter.getBroadcastRun(id);
156
+ if (!run) {
157
+ return c.json({ error: "not_found", message: "Broadcast run not found." }, 404);
158
+ }
159
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
160
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled broadcast runs can be rerun." }, 400);
161
+ }
162
+
163
+ try {
164
+ const input = typeof run.input === "string" ? JSON.parse(run.input) : run.input;
165
+ const newId = await deps.broadcastRunner.trigger(run.broadcastName, input);
166
+ return c.json({ data: { id: newId, broadcastName: run.broadcastName, status: "pending" } });
167
+ } catch (err: unknown) {
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ return c.json({ error: "rerun_failed", message }, 400);
170
+ }
171
+ });
172
+
149
173
  return app;
150
174
  }
151
175
 
@@ -2,12 +2,14 @@ import { Hono } from "hono";
2
2
  import type { SignalRunner, SignalQueueAdapter } from "station-signal";
3
3
  import type { LogBuffer } from "../log-buffer.js";
4
4
  import type { LogStore } from "../log-store.js";
5
+ import type { StationSignalSubscriber } from "../subscriber.js";
5
6
 
6
7
  export interface RunDeps {
7
8
  signalRunner?: SignalRunner;
8
9
  signalAdapter: SignalQueueAdapter;
9
10
  logBuffer: LogBuffer;
10
11
  logStore?: LogStore;
12
+ signalSubscriber?: StationSignalSubscriber;
11
13
  }
12
14
 
13
15
  export function runRoutes(deps: RunDeps) {
@@ -134,6 +136,80 @@ export function runRoutes(deps: RunDeps) {
134
136
  return c.json({ data: { cancelled: true } });
135
137
  });
136
138
 
139
+ app.post("/runs/:id/rerun", async (c) => {
140
+ const id = c.req.param("id");
141
+ if (!deps.signalRunner) {
142
+ return c.json({ error: "read_only", message: "Station is in read-only mode." }, 403);
143
+ }
144
+ const run = await deps.signalAdapter.getRun(id);
145
+ if (!run) {
146
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
147
+ }
148
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
149
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled runs can be rerun." }, 400);
150
+ }
151
+
152
+ let maxAttempts = run.maxAttempts;
153
+ let timeout = run.timeout;
154
+ if (deps.signalSubscriber) {
155
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
156
+ if (meta) {
157
+ maxAttempts = meta.maxAttempts;
158
+ timeout = meta.timeout;
159
+ }
160
+ }
161
+
162
+ const newId = deps.signalAdapter.generateId();
163
+ await deps.signalAdapter.addRun({
164
+ id: newId,
165
+ signalName: run.signalName,
166
+ kind: "trigger",
167
+ input: run.input,
168
+ status: "pending",
169
+ attempts: 0,
170
+ maxAttempts,
171
+ timeout,
172
+ createdAt: new Date(),
173
+ });
174
+
175
+ return c.json({ data: { id: newId, signalName: run.signalName, status: "pending" } });
176
+ });
177
+
178
+ app.post("/runs/:id/retry", async (c) => {
179
+ const id = c.req.param("id");
180
+ if (!deps.signalRunner) {
181
+ return c.json({ error: "read_only", message: "Station is in read-only mode." }, 403);
182
+ }
183
+ const run = await deps.signalAdapter.getRun(id);
184
+ if (!run) {
185
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
186
+ }
187
+ if (run.status !== "failed") {
188
+ return c.json({ error: "invalid_status", message: "Only failed runs can be retried." }, 400);
189
+ }
190
+
191
+ let maxAttempts = run.maxAttempts;
192
+ if (deps.signalSubscriber) {
193
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
194
+ if (meta) {
195
+ maxAttempts = meta.maxAttempts;
196
+ }
197
+ }
198
+
199
+ await deps.signalAdapter.updateRun(id, {
200
+ status: "pending",
201
+ attempts: 0,
202
+ maxAttempts,
203
+ error: undefined,
204
+ output: undefined,
205
+ startedAt: undefined,
206
+ completedAt: undefined,
207
+ lastRunAt: undefined,
208
+ });
209
+
210
+ return c.json({ data: { retried: true } });
211
+ });
212
+
137
213
  return app;
138
214
  }
139
215
 
@@ -1,12 +1,13 @@
1
1
  import { Hono } from "hono";
2
2
  import type { SignalRunner, SignalQueueAdapter } from "station-signal";
3
- import type { BroadcastRunner } from "station-broadcast";
3
+ import type { BroadcastRunner, BroadcastQueueAdapter } from "station-broadcast";
4
4
  import type { StationSignalSubscriber } from "../../subscriber.js";
5
5
 
6
6
  export interface V1TriggerDeps {
7
7
  signalRunner?: SignalRunner;
8
8
  signalAdapter: SignalQueueAdapter;
9
9
  broadcastRunner?: BroadcastRunner;
10
+ broadcastAdapter?: BroadcastQueueAdapter;
10
11
  signalSubscriber?: StationSignalSubscriber;
11
12
  }
12
13
 
@@ -66,6 +67,83 @@ export function v1TriggerRoutes(deps: V1TriggerDeps) {
66
67
  );
67
68
  });
68
69
 
70
+ app.post("/runs/:id/rerun", async (c) => {
71
+ const id = c.req.param("id");
72
+ if (!deps.signalRunner) {
73
+ return c.json({ error: "unavailable", message: "Station is in read-only mode." }, 503);
74
+ }
75
+ const run = await deps.signalAdapter.getRun(id);
76
+ if (!run) {
77
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
78
+ }
79
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
80
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled runs can be rerun." }, 400);
81
+ }
82
+
83
+ let maxAttempts = run.maxAttempts;
84
+ let timeout = run.timeout;
85
+ if (deps.signalSubscriber) {
86
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
87
+ if (meta) {
88
+ maxAttempts = meta.maxAttempts;
89
+ timeout = meta.timeout;
90
+ }
91
+ }
92
+
93
+ const newId = deps.signalAdapter.generateId();
94
+ await deps.signalAdapter.addRun({
95
+ id: newId,
96
+ signalName: run.signalName,
97
+ kind: "trigger",
98
+ input: run.input,
99
+ status: "pending",
100
+ attempts: 0,
101
+ maxAttempts,
102
+ timeout,
103
+ createdAt: new Date(),
104
+ });
105
+
106
+ return c.json(
107
+ { data: { id: newId, signalName: run.signalName, status: "pending", createdAt: new Date().toISOString() } },
108
+ 201,
109
+ );
110
+ });
111
+
112
+ app.post("/runs/:id/retry", async (c) => {
113
+ const id = c.req.param("id");
114
+ if (!deps.signalRunner) {
115
+ return c.json({ error: "unavailable", message: "Station is in read-only mode." }, 503);
116
+ }
117
+ const run = await deps.signalAdapter.getRun(id);
118
+ if (!run) {
119
+ return c.json({ error: "not_found", message: "Run not found." }, 404);
120
+ }
121
+ if (run.status !== "failed") {
122
+ return c.json({ error: "invalid_status", message: "Only failed runs can be retried." }, 400);
123
+ }
124
+
125
+ let maxAttempts = run.maxAttempts;
126
+ if (deps.signalSubscriber) {
127
+ const meta = deps.signalSubscriber.getSignalMeta(run.signalName);
128
+ if (meta) {
129
+ maxAttempts = meta.maxAttempts;
130
+ }
131
+ }
132
+
133
+ await deps.signalAdapter.updateRun(id, {
134
+ status: "pending",
135
+ attempts: 0,
136
+ maxAttempts,
137
+ error: undefined,
138
+ output: undefined,
139
+ startedAt: undefined,
140
+ completedAt: undefined,
141
+ lastRunAt: undefined,
142
+ });
143
+
144
+ return c.json({ data: { retried: true } });
145
+ });
146
+
69
147
  app.post("/trigger-broadcast", async (c) => {
70
148
  if (!deps.broadcastRunner) {
71
149
  return c.json(
@@ -107,5 +185,31 @@ export function v1TriggerRoutes(deps: V1TriggerDeps) {
107
185
  }
108
186
  });
109
187
 
188
+ app.post("/broadcast-runs/:id/rerun", async (c) => {
189
+ if (!deps.broadcastRunner || !deps.broadcastAdapter) {
190
+ return c.json({ error: "unavailable", message: "Broadcast runner not configured." }, 503);
191
+ }
192
+ const id = c.req.param("id");
193
+ const run = await deps.broadcastAdapter.getBroadcastRun(id);
194
+ if (!run) {
195
+ return c.json({ error: "not_found", message: "Broadcast run not found." }, 404);
196
+ }
197
+ if (run.status !== "failed" && run.status !== "completed" && run.status !== "cancelled") {
198
+ return c.json({ error: "invalid_status", message: "Only failed, completed, or cancelled broadcast runs can be rerun." }, 400);
199
+ }
200
+
201
+ try {
202
+ const input = typeof run.input === "string" ? JSON.parse(run.input) : run.input;
203
+ const newId = await deps.broadcastRunner.trigger(run.broadcastName, input);
204
+ return c.json(
205
+ { data: { id: newId, broadcastName: run.broadcastName, status: "pending", createdAt: new Date().toISOString() } },
206
+ 201,
207
+ );
208
+ } catch (err: unknown) {
209
+ const message = err instanceof Error ? err.message : String(err);
210
+ return c.json({ error: "rerun_failed", message }, 400);
211
+ }
212
+ });
213
+
110
214
  return app;
111
215
  }