station-kit 1.0.7 → 1.0.9

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 (177) hide show
  1. package/.next/standalone/package.json +3 -1
  2. package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
  3. package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +76 -17
  4. package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +10 -3
  5. package/.next/standalone/packages/station-kit/.next/build-manifest.json +3 -3
  6. package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +112 -16
  7. package/.next/standalone/packages/station-kit/.next/routes-manifest.json +49 -0
  8. package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page.js +2 -2
  9. package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
  11. package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +7 -7
  12. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page.js +2 -2
  13. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js +2 -0
  15. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js.nft.json +1 -0
  16. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page_client-reference-manifest.js +1 -0
  17. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js +2 -0
  18. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js.nft.json +1 -0
  19. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page_client-reference-manifest.js +1 -0
  20. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js +2 -0
  21. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js.nft.json +1 -0
  22. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page_client-reference-manifest.js +1 -0
  23. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.html +1 -0
  24. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.meta +7 -0
  25. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.rsc +25 -0
  26. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page.js +2 -2
  27. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
  28. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
  29. package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +8 -8
  30. package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
  31. package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +8 -8
  32. package/.next/standalone/packages/station-kit/.next/server/app/page.js +2 -2
  33. package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
  34. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js +2 -0
  35. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js.nft.json +1 -0
  36. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page_client-reference-manifest.js +1 -0
  37. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.html +1 -0
  38. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.meta +7 -0
  39. package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.rsc +25 -0
  40. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page.js +2 -2
  41. package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js +2 -0
  43. package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js.nft.json +1 -0
  44. package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page_client-reference-manifest.js +1 -0
  45. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js +2 -0
  46. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js.nft.json +1 -0
  47. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page_client-reference-manifest.js +1 -0
  48. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.html +1 -0
  49. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.meta +7 -0
  50. package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.rsc +25 -0
  51. package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js +2 -0
  52. package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js.nft.json +1 -0
  53. package/.next/standalone/packages/station-kit/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
  54. package/.next/standalone/packages/station-kit/.next/server/app/schedules.html +1 -0
  55. package/.next/standalone/packages/station-kit/.next/server/app/schedules.meta +7 -0
  56. package/.next/standalone/packages/station-kit/.next/server/app/schedules.rsc +25 -0
  57. package/.next/standalone/packages/station-kit/.next/server/app/settings/page.js +2 -2
  58. package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
  60. package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +8 -8
  61. package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page.js +2 -2
  62. package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/packages/station-kit/.next/server/app/signals/page.js +2 -2
  64. package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
  66. package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +8 -8
  67. package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +10 -3
  68. package/.next/standalone/packages/station-kit/.next/server/chunks/102.js +1 -1
  69. package/.next/standalone/packages/station-kit/.next/server/chunks/535.js +2 -0
  70. package/.next/standalone/packages/station-kit/.next/server/chunks/606.js +14 -14
  71. package/.next/standalone/packages/station-kit/.next/server/chunks/783.js +3 -3
  72. package/.next/standalone/packages/station-kit/.next/server/middleware-build-manifest.js +1 -1
  73. package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
  74. package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
  75. package/.next/standalone/packages/station-kit/.next/server/pages/_app.js +1 -1
  76. package/.next/standalone/packages/station-kit/.next/server/pages/_document.js +1 -1
  77. package/.next/standalone/packages/station-kit/.next/server/pages/_error.js +9 -9
  78. package/.next/standalone/packages/station-kit/.next/server/pages-manifest.json +1 -1
  79. package/.next/standalone/packages/station-kit/.next/server/server-reference-manifest.json +1 -1
  80. package/.next/standalone/packages/station-kit/.next/static/chunks/145-9e370afd2e5aba39.js +1 -0
  81. package/.next/standalone/packages/station-kit/.next/static/chunks/285-ff198f0a909c4fdd.js +1 -0
  82. package/.next/standalone/packages/station-kit/.next/static/chunks/561-33d912169940283e.js +1 -0
  83. package/.next/standalone/packages/station-kit/.next/static/chunks/935-dff12960528de017.js +1 -0
  84. package/.next/standalone/packages/station-kit/.next/static/chunks/app/_not-found/{page-ce21b4ba9038a5a7.js → page-67ef312aee40cfeb.js} +1 -1
  85. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-fe2f5467a0c68fef.js +1 -0
  86. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/page-0d2505242014f51e.js +1 -0
  87. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-5eac0507f49a00ec.js +1 -0
  88. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/new/page-3d02707043d24dc7.js +1 -0
  89. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-dee500ccc01f0821.js +1 -0
  90. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e14e14f3e5b0b8a9.js +1 -0
  91. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-aac41ef7a470daab.js +1 -0
  92. package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-dc9d91f3f50f4716.js +1 -0
  93. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-9e4c4f751a4bea72.js +1 -0
  94. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/[id]/page-435f67be180b8e4f.js +1 -0
  95. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/new/page-f697c289c813496a.js +1 -0
  96. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-738d98dc0b63166e.js +1 -0
  97. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-fc5654b31f57ac21.js +1 -0
  98. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4b1c09a539a1ebcd.js +1 -0
  99. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-d2f2403dfede87cc.js +1 -0
  100. package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-a3774a320f58a018.js +1 -0
  101. package/.next/standalone/packages/station-kit/.next/static/demLiQWDy62JuUkBw-ILG/_buildManifest.js +1 -0
  102. package/.next/standalone/packages/station-kit/package.json +10 -2
  103. package/dist/config/schema.d.ts +13 -0
  104. package/dist/config/schema.d.ts.map +1 -1
  105. package/dist/config/schema.js +1 -0
  106. package/dist/config/schema.js.map +1 -1
  107. package/dist/index.d.ts +2 -0
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +2 -0
  110. package/dist/index.js.map +1 -1
  111. package/dist/server/auth/keys.d.ts +56 -8
  112. package/dist/server/auth/keys.d.ts.map +1 -1
  113. package/dist/server/auth/keys.js +155 -53
  114. package/dist/server/auth/keys.js.map +1 -1
  115. package/dist/server/index.d.ts +7 -0
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +55 -5
  118. package/dist/server/index.js.map +1 -1
  119. package/dist/server/middleware/auth.js +1 -1
  120. package/dist/server/middleware/auth.js.map +1 -1
  121. package/dist/server/routes/v1/definitions.d.ts +21 -0
  122. package/dist/server/routes/v1/definitions.d.ts.map +1 -0
  123. package/dist/server/routes/v1/definitions.js +139 -0
  124. package/dist/server/routes/v1/definitions.js.map +1 -0
  125. package/dist/server/routes/v1/expressions.d.ts +3 -0
  126. package/dist/server/routes/v1/expressions.d.ts.map +1 -0
  127. package/dist/server/routes/v1/expressions.js +56 -0
  128. package/dist/server/routes/v1/expressions.js.map +1 -0
  129. package/dist/server/routes/v1/keys.js +3 -3
  130. package/dist/server/routes/v1/keys.js.map +1 -1
  131. package/dist/server/routes/v1/schedules.d.ts +10 -0
  132. package/dist/server/routes/v1/schedules.d.ts.map +1 -0
  133. package/dist/server/routes/v1/schedules.js +169 -0
  134. package/dist/server/routes/v1/schedules.js.map +1 -0
  135. package/dist/server/routes/v1/trigger.d.ts.map +1 -1
  136. package/dist/server/routes/v1/trigger.js +21 -0
  137. package/dist/server/routes/v1/trigger.js.map +1 -1
  138. package/package.json +15 -7
  139. package/src/app/broadcasts/components/broadcast-builder.tsx +535 -0
  140. package/src/app/broadcasts/components/dag-editor.tsx +510 -0
  141. package/src/app/broadcasts/dyn/[name]/dynamic-detail.tsx +243 -0
  142. package/src/app/broadcasts/dyn/[name]/page.tsx +10 -0
  143. package/src/app/broadcasts/dyn/[name]/v/[n]/page.tsx +10 -0
  144. package/src/app/broadcasts/dyn/[name]/v/[n]/version-view.tsx +285 -0
  145. package/src/app/broadcasts/new/page.tsx +102 -0
  146. package/src/app/broadcasts/page.tsx +176 -91
  147. package/src/app/components/api-panel.tsx +151 -0
  148. package/src/app/components/shell.tsx +23 -0
  149. package/src/app/hooks/use-api.ts +117 -0
  150. package/src/app/playground/expression/page.tsx +245 -0
  151. package/src/app/schedules/[id]/page.tsx +10 -0
  152. package/src/app/schedules/[id]/schedule-editor.tsx +195 -0
  153. package/src/app/schedules/components/schedule-form.tsx +140 -0
  154. package/src/app/schedules/new/page.tsx +166 -0
  155. package/src/app/schedules/page.tsx +126 -0
  156. package/src/config/schema.ts +14 -0
  157. package/src/index.ts +2 -0
  158. package/src/server/auth/keys.ts +191 -56
  159. package/src/server/index.ts +78 -5
  160. package/src/server/middleware/auth.ts +1 -1
  161. package/src/server/routes/v1/definitions.ts +164 -0
  162. package/src/server/routes/v1/expressions.ts +76 -0
  163. package/src/server/routes/v1/keys.ts +3 -3
  164. package/src/server/routes/v1/schedules.ts +176 -0
  165. package/src/server/routes/v1/trigger.ts +27 -0
  166. package/.next/standalone/packages/station-kit/.next/static/chunks/580-f007f4d4c050db4e.js +0 -1
  167. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +0 -1
  168. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +0 -1
  169. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +0 -1
  170. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +0 -1
  171. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +0 -1
  172. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +0 -1
  173. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +0 -1
  174. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +0 -1
  175. package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +0 -1
  176. package/.next/standalone/packages/station-kit/.next/static/pHHaxeGaet0VW1dhcIcuY/_buildManifest.js +0 -1
  177. /package/.next/standalone/packages/station-kit/.next/static/{pHHaxeGaet0VW1dhcIcuY → demLiQWDy62JuUkBw-ILG}/_ssgManifest.js +0 -0
@@ -0,0 +1,243 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import {
6
+ useApi,
7
+ type DynamicBroadcastSpec,
8
+ type DynamicValidationResult,
9
+ type SignalMeta,
10
+ } from "../../../hooks/use-api";
11
+ import { useBreadcrumb } from "../../../hooks/use-breadcrumb";
12
+ import { BroadcastBuilder } from "../../components/broadcast-builder";
13
+ import { SchemaForm } from "../../../components/schema-form";
14
+
15
+ export function DynamicBroadcastDetail({ name }: { name: string }) {
16
+ const api = useApi();
17
+ const router = useRouter();
18
+ const [latest, setLatest] = useState<DynamicBroadcastSpec | null>(null);
19
+ const [versions, setVersions] = useState<DynamicBroadcastSpec[]>([]);
20
+ const [json, setJson] = useState("");
21
+ const [validation, setValidation] = useState<DynamicValidationResult | null>(null);
22
+ const [signals, setSignals] = useState<SignalMeta[]>([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [busy, setBusy] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [triggerOpen, setTriggerOpen] = useState(false);
27
+ const [triggerInput, setTriggerInput] = useState("{}");
28
+ const [lastRunId, setLastRunId] = useState<string | null>(null);
29
+
30
+ useBreadcrumb(
31
+ [
32
+ { label: "Broadcasts", href: "/broadcasts" },
33
+ { label: name },
34
+ ],
35
+ "broadcasts",
36
+ );
37
+
38
+ useEffect(() => {
39
+ Promise.all([
40
+ api.getBroadcastDefinition(name),
41
+ api.getBroadcastDefinitionVersions(name).catch(() => ({ data: [] as DynamicBroadcastSpec[] })),
42
+ api.getSignals().catch(() => ({ data: [] as SignalMeta[] })),
43
+ ])
44
+ .then(([latestRes, versionsRes, signalsRes]) => {
45
+ setLatest(latestRes.data);
46
+ setJson(JSON.stringify(stripMeta(latestRes.data), null, 2));
47
+ setVersions(versionsRes.data);
48
+ setSignals(signalsRes.data);
49
+ })
50
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)))
51
+ .finally(() => setLoading(false));
52
+ }, [name]);
53
+
54
+ async function handleValidate() {
55
+ setError(null);
56
+ let parsed: unknown;
57
+ try {
58
+ parsed = JSON.parse(json);
59
+ } catch (err) {
60
+ setError(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
61
+ return;
62
+ }
63
+ try {
64
+ const res = await api.validateBroadcastDefinition(parsed as never);
65
+ setValidation(res.data);
66
+ } catch (err) {
67
+ setError(err instanceof Error ? err.message : String(err));
68
+ }
69
+ }
70
+
71
+ async function handleSave() {
72
+ setError(null);
73
+ setBusy(true);
74
+ let parsed: unknown;
75
+ try {
76
+ parsed = JSON.parse(json);
77
+ } catch (err) {
78
+ setError(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
79
+ setBusy(false);
80
+ return;
81
+ }
82
+ try {
83
+ const res = await api.saveBroadcastDefinition(parsed as never);
84
+ setLatest(res.data);
85
+ const versionsRes = await api.getBroadcastDefinitionVersions(name);
86
+ setVersions(versionsRes.data);
87
+ setValidation({ ok: true, errors: [] });
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : String(err));
90
+ } finally {
91
+ setBusy(false);
92
+ }
93
+ }
94
+
95
+ async function handleDelete() {
96
+ if (!confirm(`Delete dynamic broadcast "${name}"? Run history is retained.`)) return;
97
+ try {
98
+ await api.deleteBroadcastDefinition(name);
99
+ router.push("/broadcasts");
100
+ } catch (err) {
101
+ setError(err instanceof Error ? err.message : String(err));
102
+ }
103
+ }
104
+
105
+ async function handleTrigger() {
106
+ setError(null);
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(triggerInput);
110
+ } catch (err) {
111
+ setError(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
112
+ return;
113
+ }
114
+ try {
115
+ const res = await api.triggerDynamicBroadcast(name, parsed);
116
+ setLastRunId(res.data.id);
117
+ setTriggerOpen(false);
118
+ } catch (err) {
119
+ setError(err instanceof Error ? err.message : String(err));
120
+ }
121
+ }
122
+
123
+ if (loading) {
124
+ return (
125
+ <div>
126
+ <h1 className="page-title">{name}</h1>
127
+ <div className="loading-bar"><div className="loading-bar-fill" /></div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ if (!latest) {
133
+ return (
134
+ <div>
135
+ <h1 className="page-title">{name}</h1>
136
+ <div className="empty-state">
137
+ <p className="empty-state-text">{error ?? "Not found."}</p>
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <div>
145
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.5rem" }}>
146
+ <h1 className="page-title" style={{ margin: 0 }}>
147
+ <span className="mono">{name}</span>
148
+ <span style={{ marginLeft: "0.5rem", color: "var(--muted)", fontSize: "0.875rem" }}>
149
+ v{latest.version}
150
+ </span>
151
+ </h1>
152
+ <div style={{ display: "flex", gap: "0.5rem" }}>
153
+ <button className="btn" onClick={() => setTriggerOpen(!triggerOpen)}>
154
+ {triggerOpen ? "Cancel" : "Trigger"}
155
+ </button>
156
+ <button className="btn btn--danger" onClick={handleDelete}>Delete</button>
157
+ </div>
158
+ </div>
159
+
160
+ {lastRunId && (
161
+ <div style={{
162
+ marginBottom: "0.75rem",
163
+ padding: "0.5rem 0.75rem",
164
+ background: "var(--success-bg, #efe)",
165
+ color: "var(--success, #060)",
166
+ borderRadius: "4px",
167
+ fontSize: "0.8125rem",
168
+ }}>
169
+ Triggered run <span className="mono">{lastRunId}</span>
170
+ </div>
171
+ )}
172
+
173
+ {triggerOpen && (
174
+ <div style={{ marginBottom: "1rem", padding: "0.75rem", border: "1px solid var(--border)", borderRadius: "4px" }}>
175
+ <div className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", marginBottom: "0.5rem" }}>
176
+ Trigger input
177
+ </div>
178
+ <SchemaForm schema={null} value={triggerInput} onChange={setTriggerInput} />
179
+ <div style={{ marginTop: "0.5rem" }}>
180
+ <button className="btn btn--primary" onClick={handleTrigger}>Dispatch</button>
181
+ </div>
182
+ </div>
183
+ )}
184
+
185
+ <BroadcastBuilder
186
+ json={json}
187
+ onChange={setJson}
188
+ validation={validation}
189
+ onValidationStale={() => setValidation(null)}
190
+ signals={signals}
191
+ onValidate={handleValidate}
192
+ onSave={handleSave}
193
+ saveLabel={busy ? "Saving..." : `Save (creates v${latest.version + 1})`}
194
+ saving={busy}
195
+ error={error}
196
+ />
197
+
198
+ {versions.length > 0 && (
199
+ <section style={{ marginTop: "2rem" }}>
200
+ <h2 style={{ fontSize: "0.875rem", textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--muted)", marginBottom: "0.5rem" }}>
201
+ Version history
202
+ </h2>
203
+ <table className="station-table">
204
+ <thead>
205
+ <tr>
206
+ <th>Version</th>
207
+ <th>Updated</th>
208
+ <th>Author</th>
209
+ <th>Status</th>
210
+ </tr>
211
+ </thead>
212
+ <tbody>
213
+ {versions.map((v) => (
214
+ <tr
215
+ key={v.version}
216
+ className="clickable-row"
217
+ onClick={() => router.push(`/broadcasts/dyn/${encodeURIComponent(name)}/v/${v.version}`)}
218
+ >
219
+ <td className="mono">v{v.version}</td>
220
+ <td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
221
+ {new Date(v.updatedAt).toLocaleString()}
222
+ </td>
223
+ <td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
224
+ {v.createdBy ?? "—"}
225
+ </td>
226
+ <td className="mono" style={{ fontSize: "0.75rem" }}>
227
+ {v.deletedAt ? <span style={{ color: "var(--error, #b00)" }}>deleted</span> : v.version === latest.version ? "current" : "archived"}
228
+ </td>
229
+ </tr>
230
+ ))}
231
+ </tbody>
232
+ </table>
233
+ </section>
234
+ )}
235
+ </div>
236
+ );
237
+ }
238
+
239
+ function stripMeta(spec: DynamicBroadcastSpec): Record<string, unknown> {
240
+ // The user only edits the durable shape; version/createdAt/etc. are server-managed.
241
+ const { version: _v, createdAt: _c, updatedAt: _u, deletedAt: _d, createdBy: _b, ...rest } = spec;
242
+ return rest;
243
+ }
@@ -0,0 +1,10 @@
1
+ import { DynamicBroadcastDetail } from "./dynamic-detail";
2
+
3
+ export default async function DynamicBroadcastPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ name: string }>;
7
+ }) {
8
+ const { name } = await params;
9
+ return <DynamicBroadcastDetail name={decodeURIComponent(name)} />;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { VersionView } from "./version-view";
2
+
3
+ export default async function VersionPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ name: string; n: string }>;
7
+ }) {
8
+ const { name, n } = await params;
9
+ return <VersionView name={decodeURIComponent(name)} version={Number(n)} />;
10
+ }
@@ -0,0 +1,285 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import Link from "next/link";
5
+ import { useApi, type DynamicBroadcastSpec } from "../../../../../hooks/use-api";
6
+ import { useBreadcrumb } from "../../../../../hooks/use-breadcrumb";
7
+ import { ApiPanel } from "../../../../../components/api-panel";
8
+
9
+ export function VersionView({ name, version }: { name: string; version: number }) {
10
+ const api = useApi();
11
+ const [versions, setVersions] = useState<DynamicBroadcastSpec[]>([]);
12
+ const [spec, setSpec] = useState<DynamicBroadcastSpec | null>(null);
13
+ const [compareTo, setCompareTo] = useState<number | null>(null);
14
+ const [loading, setLoading] = useState(true);
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ useBreadcrumb(
18
+ [
19
+ { label: "Broadcasts", href: "/broadcasts" },
20
+ { label: name, href: `/broadcasts/dyn/${encodeURIComponent(name)}` },
21
+ { label: `v${version}` },
22
+ ],
23
+ "broadcasts",
24
+ );
25
+
26
+ useEffect(() => {
27
+ Promise.all([
28
+ api.getBroadcastDefinitionVersions(name),
29
+ api.getBroadcastDefinition(name).then((r) => r.data, () => null),
30
+ ])
31
+ .then(([versionsRes]) => {
32
+ setVersions(versionsRes.data);
33
+ const found = versionsRes.data.find((v) => v.version === version);
34
+ setSpec(found ?? null);
35
+ // Default compare target = the version immediately before this one
36
+ const prev = versionsRes.data.find((v) => v.version === version - 1);
37
+ setCompareTo(prev?.version ?? null);
38
+ })
39
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)))
40
+ .finally(() => setLoading(false));
41
+ }, [name, version]);
42
+
43
+ const compareSpec = useMemo(
44
+ () => versions.find((v) => v.version === compareTo) ?? null,
45
+ [versions, compareTo],
46
+ );
47
+
48
+ const diff = useMemo(() => {
49
+ if (!spec || !compareSpec) return null;
50
+ return computeDiff(stripVolatile(compareSpec), stripVolatile(spec));
51
+ }, [spec, compareSpec]);
52
+
53
+ if (loading) {
54
+ return (
55
+ <div>
56
+ <h1 className="page-title">{name} v{version}</h1>
57
+ <div className="loading-bar"><div className="loading-bar-fill" /></div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ if (!spec) {
63
+ return (
64
+ <div>
65
+ <h1 className="page-title">{name} v{version}</h1>
66
+ <div className="empty-state"><p className="empty-state-text">{error ?? "Version not found."}</p></div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <div>
73
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem" }}>
74
+ <h1 className="page-title" style={{ margin: 0 }}>
75
+ <span className="mono">{name}</span>
76
+ <span style={{ marginLeft: "0.5rem", color: "var(--muted)", fontSize: "0.875rem" }}>v{version}</span>
77
+ {spec.deletedAt && (
78
+ <span style={{
79
+ marginLeft: "0.75rem",
80
+ fontSize: "0.6875rem",
81
+ padding: "0.125rem 0.375rem",
82
+ borderRadius: "3px",
83
+ background: "var(--error-bg, #fee)",
84
+ color: "var(--error, #b00)",
85
+ }}>deleted</span>
86
+ )}
87
+ </h1>
88
+ <Link href={`/broadcasts/dyn/${encodeURIComponent(name)}`} className="btn">Back to current</Link>
89
+ </div>
90
+
91
+ <div style={{
92
+ display: "grid",
93
+ gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
94
+ gap: "1rem",
95
+ marginBottom: "1rem",
96
+ }}>
97
+ <div>
98
+ <div className="mono" style={labelStyle}>Compare to</div>
99
+ <select
100
+ className="mono"
101
+ value={compareTo ?? ""}
102
+ onChange={(e) => setCompareTo(e.target.value ? Number(e.target.value) : null)}
103
+ style={{
104
+ width: "100%",
105
+ padding: "0.5rem 0.625rem",
106
+ border: "1px solid var(--border)",
107
+ borderRadius: "4px",
108
+ background: "var(--surface)",
109
+ color: "var(--text)",
110
+ fontSize: "0.875rem",
111
+ }}
112
+ >
113
+ <option value="">— No comparison —</option>
114
+ {versions
115
+ .filter((v) => v.version !== version)
116
+ .map((v) => (
117
+ <option key={v.version} value={v.version}>
118
+ v{v.version}
119
+ {v.deletedAt ? " (deleted)" : ""}
120
+ {" — "}
121
+ {new Date(v.updatedAt).toLocaleString()}
122
+ </option>
123
+ ))}
124
+ </select>
125
+ </div>
126
+ <div className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", alignSelf: "end" }}>
127
+ {compareSpec
128
+ ? `Diffing v${compareSpec.version} → v${spec.version}`
129
+ : "Showing the spec as-saved (no diff)."}
130
+ </div>
131
+ </div>
132
+
133
+ <div style={{
134
+ display: "grid",
135
+ gridTemplateColumns: compareSpec ? "minmax(0, 1fr) minmax(0, 1fr)" : "1fr",
136
+ gap: "1rem",
137
+ }}>
138
+ {compareSpec && (
139
+ <DiffPane title={`v${compareSpec.version}`} spec={compareSpec} highlight={diff?.removedLines ?? new Set()} side="left" />
140
+ )}
141
+ <DiffPane title={`v${spec.version}`} spec={spec} highlight={diff?.addedLines ?? new Set()} side="right" />
142
+ </div>
143
+
144
+ <ApiPanel
145
+ title="Inspect this version"
146
+ snippets={[
147
+ {
148
+ label: "Get this version",
149
+ method: "GET",
150
+ path: `/api/v1/broadcast-definitions/${encodeURIComponent(name)}/versions/${version}`,
151
+ },
152
+ {
153
+ label: "List all versions",
154
+ method: "GET",
155
+ path: `/api/v1/broadcast-definitions/${encodeURIComponent(name)}/versions`,
156
+ },
157
+ ]}
158
+ />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function DiffPane({
164
+ title,
165
+ spec,
166
+ highlight,
167
+ side,
168
+ }: {
169
+ title: string;
170
+ spec: DynamicBroadcastSpec;
171
+ highlight: Set<number>;
172
+ side: "left" | "right";
173
+ }) {
174
+ const lines = JSON.stringify(stripVolatile(spec), null, 2).split("\n");
175
+ return (
176
+ <div>
177
+ <div className="mono" style={labelStyle}>{title}</div>
178
+ <pre className="mono" style={{
179
+ margin: 0,
180
+ padding: "0.5rem",
181
+ background: "var(--surface)",
182
+ border: "1px solid var(--border)",
183
+ borderRadius: "4px",
184
+ fontSize: "0.75rem",
185
+ lineHeight: 1.5,
186
+ overflowX: "auto",
187
+ maxHeight: "640px",
188
+ overflowY: "auto",
189
+ }}>
190
+ {lines.map((line, i) => (
191
+ <div
192
+ key={i}
193
+ style={{
194
+ padding: "0 0.25rem",
195
+ background: highlight.has(i)
196
+ ? side === "left"
197
+ ? "rgba(196, 131, 74, 0.18)"
198
+ : "rgba(107, 153, 98, 0.18)"
199
+ : "transparent",
200
+ whiteSpace: "pre",
201
+ }}
202
+ >
203
+ {line || " "}
204
+ </div>
205
+ ))}
206
+ </pre>
207
+ </div>
208
+ );
209
+ }
210
+
211
+ interface DiffResult {
212
+ addedLines: Set<number>;
213
+ removedLines: Set<number>;
214
+ }
215
+
216
+ /**
217
+ * Line-level naive diff: marks lines unique to each side. Works well for the
218
+ * small JSON specs typical of broadcast definitions; for larger specs a real
219
+ * Myers diff would be better but is overkill here.
220
+ */
221
+ function computeDiff(left: unknown, right: unknown): DiffResult {
222
+ const leftLines = JSON.stringify(left, null, 2).split("\n");
223
+ const rightLines = JSON.stringify(right, null, 2).split("\n");
224
+ const leftSet = new Map<string, number[]>();
225
+ for (let i = 0; i < leftLines.length; i++) {
226
+ const list = leftSet.get(leftLines[i]);
227
+ if (list) list.push(i);
228
+ else leftSet.set(leftLines[i], [i]);
229
+ }
230
+ const rightSet = new Map<string, number[]>();
231
+ for (let i = 0; i < rightLines.length; i++) {
232
+ const list = rightSet.get(rightLines[i]);
233
+ if (list) list.push(i);
234
+ else rightSet.set(rightLines[i], [i]);
235
+ }
236
+ const addedLines = new Set<number>();
237
+ const removedLines = new Set<number>();
238
+ for (let i = 0; i < rightLines.length; i++) {
239
+ const counterpart = leftSet.get(rightLines[i]);
240
+ if (!counterpart || counterpart.length === 0) {
241
+ addedLines.add(i);
242
+ } else {
243
+ counterpart.shift();
244
+ }
245
+ }
246
+ for (let i = 0; i < leftLines.length; i++) {
247
+ const counterpart = rightSet.get(leftLines[i]);
248
+ if (!counterpart || counterpart.length === 0) {
249
+ removedLines.add(i);
250
+ } else {
251
+ counterpart.shift();
252
+ }
253
+ }
254
+ return { addedLines, removedLines };
255
+ }
256
+
257
+ function stripVolatile(spec: DynamicBroadcastSpec): Omit<DynamicBroadcastSpec, "createdAt" | "updatedAt" | "deletedAt" | "createdBy" | "version"> & { version: number } {
258
+ const { createdAt: _c, updatedAt: _u, deletedAt: _d, createdBy: _b, ...rest } = spec;
259
+ // Deep-sort keys so the diff doesn't fire on insertion-order changes
260
+ // (e.g. spread-then-patch reorders `name` after spread).
261
+ return sortKeysDeep(rest) as ReturnType<typeof stripVolatile>;
262
+ }
263
+
264
+ function sortKeysDeep<T>(value: T): T {
265
+ if (Array.isArray(value)) {
266
+ return value.map((v) => sortKeysDeep(v)) as unknown as T;
267
+ }
268
+ if (value && typeof value === "object") {
269
+ const sorted: Record<string, unknown> = {};
270
+ for (const k of Object.keys(value as Record<string, unknown>).sort()) {
271
+ sorted[k] = sortKeysDeep((value as Record<string, unknown>)[k]);
272
+ }
273
+ return sorted as T;
274
+ }
275
+ return value;
276
+ }
277
+
278
+ const labelStyle: React.CSSProperties = {
279
+ display: "block",
280
+ fontSize: "0.75rem",
281
+ color: "var(--muted)",
282
+ textTransform: "uppercase",
283
+ letterSpacing: "0.05em",
284
+ marginBottom: "0.375rem",
285
+ };
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useApi, type DynamicValidationResult, type SignalMeta } from "../../hooks/use-api";
6
+ import { useBreadcrumb } from "../../hooks/use-breadcrumb";
7
+ import { BroadcastBuilder } from "../components/broadcast-builder";
8
+
9
+ const STARTER_SPEC = JSON.stringify(
10
+ {
11
+ name: "myBroadcast",
12
+ failurePolicy: "fail-fast",
13
+ timeout: 300000,
14
+ nodes: [
15
+ {
16
+ name: "first",
17
+ signalName: "<existing-signal-name>",
18
+ dependsOn: [],
19
+ },
20
+ ],
21
+ },
22
+ null,
23
+ 2,
24
+ );
25
+
26
+ export default function NewDynamicBroadcastPage() {
27
+ const api = useApi();
28
+ const router = useRouter();
29
+ const [json, setJson] = useState(STARTER_SPEC);
30
+ const [validation, setValidation] = useState<DynamicValidationResult | null>(null);
31
+ const [signals, setSignals] = useState<SignalMeta[]>([]);
32
+ const [busy, setBusy] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ useBreadcrumb(
36
+ [
37
+ { label: "Broadcasts", href: "/broadcasts" },
38
+ { label: "New dynamic" },
39
+ ],
40
+ "broadcasts",
41
+ );
42
+
43
+ useEffect(() => {
44
+ api.getSignals().then((res) => setSignals(res.data)).catch(() => {});
45
+ }, []);
46
+
47
+ async function handleValidate() {
48
+ setError(null);
49
+ let parsed: unknown;
50
+ try {
51
+ parsed = JSON.parse(json);
52
+ } catch (err) {
53
+ setError(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
54
+ return;
55
+ }
56
+ try {
57
+ const res = await api.validateBroadcastDefinition(parsed as never);
58
+ setValidation(res.data);
59
+ } catch (err) {
60
+ setError(err instanceof Error ? err.message : String(err));
61
+ }
62
+ }
63
+
64
+ async function handleSave() {
65
+ setError(null);
66
+ setBusy(true);
67
+ let parsed: unknown;
68
+ try {
69
+ parsed = JSON.parse(json);
70
+ } catch (err) {
71
+ setError(`JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
72
+ setBusy(false);
73
+ return;
74
+ }
75
+ try {
76
+ const res = await api.saveBroadcastDefinition(parsed as never);
77
+ router.push(`/broadcasts/dyn/${encodeURIComponent(res.data.name)}`);
78
+ } catch (err) {
79
+ setError(err instanceof Error ? err.message : String(err));
80
+ } finally {
81
+ setBusy(false);
82
+ }
83
+ }
84
+
85
+ return (
86
+ <div>
87
+ <h1 className="page-title">New dynamic broadcast</h1>
88
+ <BroadcastBuilder
89
+ json={json}
90
+ onChange={setJson}
91
+ validation={validation}
92
+ onValidationStale={() => setValidation(null)}
93
+ signals={signals}
94
+ onValidate={handleValidate}
95
+ onSave={handleSave}
96
+ saveLabel={busy ? "Saving..." : "Save (creates v1)"}
97
+ saving={busy}
98
+ error={error}
99
+ />
100
+ </div>
101
+ );
102
+ }