station-kit 1.0.8 → 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 (172) 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 +11 -4
  5. package/.next/standalone/packages/station-kit/.next/build-manifest.json +3 -3
  6. package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +105 -9
  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 +11 -4
  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 +5 -1
  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/server/auth/keys.d.ts +56 -8
  108. package/dist/server/auth/keys.d.ts.map +1 -1
  109. package/dist/server/auth/keys.js +155 -53
  110. package/dist/server/auth/keys.js.map +1 -1
  111. package/dist/server/index.d.ts +2 -2
  112. package/dist/server/index.d.ts.map +1 -1
  113. package/dist/server/index.js +53 -6
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/middleware/auth.js +1 -1
  116. package/dist/server/middleware/auth.js.map +1 -1
  117. package/dist/server/routes/v1/definitions.d.ts +21 -0
  118. package/dist/server/routes/v1/definitions.d.ts.map +1 -0
  119. package/dist/server/routes/v1/definitions.js +139 -0
  120. package/dist/server/routes/v1/definitions.js.map +1 -0
  121. package/dist/server/routes/v1/expressions.d.ts +3 -0
  122. package/dist/server/routes/v1/expressions.d.ts.map +1 -0
  123. package/dist/server/routes/v1/expressions.js +56 -0
  124. package/dist/server/routes/v1/expressions.js.map +1 -0
  125. package/dist/server/routes/v1/keys.js +3 -3
  126. package/dist/server/routes/v1/keys.js.map +1 -1
  127. package/dist/server/routes/v1/schedules.d.ts +10 -0
  128. package/dist/server/routes/v1/schedules.d.ts.map +1 -0
  129. package/dist/server/routes/v1/schedules.js +169 -0
  130. package/dist/server/routes/v1/schedules.js.map +1 -0
  131. package/dist/server/routes/v1/trigger.d.ts.map +1 -1
  132. package/dist/server/routes/v1/trigger.js +21 -0
  133. package/dist/server/routes/v1/trigger.js.map +1 -1
  134. package/package.json +11 -7
  135. package/src/app/broadcasts/components/broadcast-builder.tsx +535 -0
  136. package/src/app/broadcasts/components/dag-editor.tsx +510 -0
  137. package/src/app/broadcasts/dyn/[name]/dynamic-detail.tsx +243 -0
  138. package/src/app/broadcasts/dyn/[name]/page.tsx +10 -0
  139. package/src/app/broadcasts/dyn/[name]/v/[n]/page.tsx +10 -0
  140. package/src/app/broadcasts/dyn/[name]/v/[n]/version-view.tsx +285 -0
  141. package/src/app/broadcasts/new/page.tsx +102 -0
  142. package/src/app/broadcasts/page.tsx +176 -91
  143. package/src/app/components/api-panel.tsx +151 -0
  144. package/src/app/components/shell.tsx +23 -0
  145. package/src/app/hooks/use-api.ts +117 -0
  146. package/src/app/playground/expression/page.tsx +245 -0
  147. package/src/app/schedules/[id]/page.tsx +10 -0
  148. package/src/app/schedules/[id]/schedule-editor.tsx +195 -0
  149. package/src/app/schedules/components/schedule-form.tsx +140 -0
  150. package/src/app/schedules/new/page.tsx +166 -0
  151. package/src/app/schedules/page.tsx +126 -0
  152. package/src/config/schema.ts +14 -0
  153. package/src/server/auth/keys.ts +191 -56
  154. package/src/server/index.ts +72 -8
  155. package/src/server/middleware/auth.ts +1 -1
  156. package/src/server/routes/v1/definitions.ts +164 -0
  157. package/src/server/routes/v1/expressions.ts +76 -0
  158. package/src/server/routes/v1/keys.ts +3 -3
  159. package/src/server/routes/v1/schedules.ts +176 -0
  160. package/src/server/routes/v1/trigger.ts +27 -0
  161. package/.next/standalone/packages/station-kit/.next/static/chunks/580-f007f4d4c050db4e.js +0 -1
  162. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +0 -1
  163. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +0 -1
  164. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +0 -1
  165. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +0 -1
  166. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +0 -1
  167. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +0 -1
  168. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +0 -1
  169. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +0 -1
  170. package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +0 -1
  171. package/.next/standalone/packages/station-kit/.next/static/xYd6dn0Ox68DaamIrH_pB/_buildManifest.js +0 -1
  172. /package/.next/standalone/packages/station-kit/.next/static/{xYd6dn0Ox68DaamIrH_pB → 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
+ }