station-kit 1.0.8 → 1.1.0

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 (187) 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 +75 -16
  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 +108 -12
  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/THKSkCipW_pj0F6DRXYEG/_buildManifest.js +1 -0
  81. package/.next/standalone/packages/station-kit/.next/static/chunks/145-9e370afd2e5aba39.js +1 -0
  82. package/.next/standalone/packages/station-kit/.next/static/chunks/285-ff198f0a909c4fdd.js +1 -0
  83. package/.next/standalone/packages/station-kit/.next/static/chunks/561-33d912169940283e.js +1 -0
  84. package/.next/standalone/packages/station-kit/.next/static/chunks/935-dff12960528de017.js +1 -0
  85. package/.next/standalone/packages/station-kit/.next/static/chunks/app/_not-found/{page-ce21b4ba9038a5a7.js → page-67ef312aee40cfeb.js} +1 -1
  86. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-fe2f5467a0c68fef.js +1 -0
  87. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/page-0d2505242014f51e.js +1 -0
  88. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-5eac0507f49a00ec.js +1 -0
  89. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/new/page-3d02707043d24dc7.js +1 -0
  90. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-dee500ccc01f0821.js +1 -0
  91. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e14e14f3e5b0b8a9.js +1 -0
  92. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-aac41ef7a470daab.js +1 -0
  93. package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-dc9d91f3f50f4716.js +1 -0
  94. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-9e4c4f751a4bea72.js +1 -0
  95. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/[id]/page-435f67be180b8e4f.js +1 -0
  96. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/new/page-f697c289c813496a.js +1 -0
  97. package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-738d98dc0b63166e.js +1 -0
  98. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-fc5654b31f57ac21.js +1 -0
  99. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4b1c09a539a1ebcd.js +1 -0
  100. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-d2f2403dfede87cc.js +1 -0
  101. package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-a3774a320f58a018.js +1 -0
  102. package/.next/standalone/packages/station-kit/package.json +7 -4
  103. package/dist/config/schema.d.ts +23 -0
  104. package/dist/config/schema.d.ts.map +1 -1
  105. package/dist/config/schema.js +2 -0
  106. package/dist/config/schema.js.map +1 -1
  107. package/dist/server/auth/keys.d.ts +91 -8
  108. package/dist/server/auth/keys.d.ts.map +1 -1
  109. package/dist/server/auth/keys.js +289 -54
  110. package/dist/server/auth/keys.js.map +1 -1
  111. package/dist/server/index.d.ts +5 -2
  112. package/dist/server/index.d.ts.map +1 -1
  113. package/dist/server/index.js +84 -9
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/log-store.d.ts +102 -6
  116. package/dist/server/log-store.d.ts.map +1 -1
  117. package/dist/server/log-store.js +140 -32
  118. package/dist/server/log-store.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/broadcasts.d.ts.map +1 -1
  122. package/dist/server/routes/broadcasts.js +3 -1
  123. package/dist/server/routes/broadcasts.js.map +1 -1
  124. package/dist/server/routes/runs.js +1 -1
  125. package/dist/server/routes/runs.js.map +1 -1
  126. package/dist/server/routes/v1/definitions.d.ts +21 -0
  127. package/dist/server/routes/v1/definitions.d.ts.map +1 -0
  128. package/dist/server/routes/v1/definitions.js +139 -0
  129. package/dist/server/routes/v1/definitions.js.map +1 -0
  130. package/dist/server/routes/v1/expressions.d.ts +3 -0
  131. package/dist/server/routes/v1/expressions.d.ts.map +1 -0
  132. package/dist/server/routes/v1/expressions.js +56 -0
  133. package/dist/server/routes/v1/expressions.js.map +1 -0
  134. package/dist/server/routes/v1/keys.js +3 -3
  135. package/dist/server/routes/v1/keys.js.map +1 -1
  136. package/dist/server/routes/v1/runs.js +1 -1
  137. package/dist/server/routes/v1/runs.js.map +1 -1
  138. package/dist/server/routes/v1/schedules.d.ts +10 -0
  139. package/dist/server/routes/v1/schedules.d.ts.map +1 -0
  140. package/dist/server/routes/v1/schedules.js +169 -0
  141. package/dist/server/routes/v1/schedules.js.map +1 -0
  142. package/dist/server/routes/v1/trigger.d.ts.map +1 -1
  143. package/dist/server/routes/v1/trigger.js +21 -0
  144. package/dist/server/routes/v1/trigger.js.map +1 -1
  145. package/package.json +12 -9
  146. package/src/app/broadcasts/components/broadcast-builder.tsx +535 -0
  147. package/src/app/broadcasts/components/dag-editor.tsx +510 -0
  148. package/src/app/broadcasts/dyn/[name]/dynamic-detail.tsx +243 -0
  149. package/src/app/broadcasts/dyn/[name]/page.tsx +10 -0
  150. package/src/app/broadcasts/dyn/[name]/v/[n]/page.tsx +10 -0
  151. package/src/app/broadcasts/dyn/[name]/v/[n]/version-view.tsx +285 -0
  152. package/src/app/broadcasts/new/page.tsx +102 -0
  153. package/src/app/broadcasts/page.tsx +176 -91
  154. package/src/app/components/api-panel.tsx +151 -0
  155. package/src/app/components/shell.tsx +23 -0
  156. package/src/app/hooks/use-api.ts +117 -0
  157. package/src/app/playground/expression/page.tsx +245 -0
  158. package/src/app/schedules/[id]/page.tsx +10 -0
  159. package/src/app/schedules/[id]/schedule-editor.tsx +195 -0
  160. package/src/app/schedules/components/schedule-form.tsx +140 -0
  161. package/src/app/schedules/new/page.tsx +166 -0
  162. package/src/app/schedules/page.tsx +126 -0
  163. package/src/config/schema.ts +25 -0
  164. package/src/server/auth/keys.ts +348 -58
  165. package/src/server/index.ts +118 -11
  166. package/src/server/log-store.ts +196 -45
  167. package/src/server/middleware/auth.ts +1 -1
  168. package/src/server/routes/broadcasts.ts +3 -1
  169. package/src/server/routes/runs.ts +1 -1
  170. package/src/server/routes/v1/definitions.ts +164 -0
  171. package/src/server/routes/v1/expressions.ts +76 -0
  172. package/src/server/routes/v1/keys.ts +3 -3
  173. package/src/server/routes/v1/runs.ts +1 -1
  174. package/src/server/routes/v1/schedules.ts +176 -0
  175. package/src/server/routes/v1/trigger.ts +27 -0
  176. package/.next/standalone/packages/station-kit/.next/static/chunks/580-f007f4d4c050db4e.js +0 -1
  177. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +0 -1
  178. package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +0 -1
  179. package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +0 -1
  180. package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +0 -1
  181. package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +0 -1
  182. package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +0 -1
  183. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +0 -1
  184. package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +0 -1
  185. package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +0 -1
  186. package/.next/standalone/packages/station-kit/.next/static/xYd6dn0Ox68DaamIrH_pB/_buildManifest.js +0 -1
  187. /package/.next/standalone/packages/station-kit/.next/static/{xYd6dn0Ox68DaamIrH_pB → THKSkCipW_pj0F6DRXYEG}/_ssgManifest.js +0 -0
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useApi } from "../../hooks/use-api";
6
+ import { useBreadcrumb } from "../../hooks/use-breadcrumb";
7
+ import { ScheduleForm, type ScheduleFormValue } from "../components/schedule-form";
8
+ import { ApiPanel } from "../../components/api-panel";
9
+
10
+ const INITIAL: ScheduleFormValue = {
11
+ kind: "signal",
12
+ target: "",
13
+ interval: "5m",
14
+ input: "{}",
15
+ enabled: true,
16
+ };
17
+
18
+ const INTERVAL_REGEX = /^(\d+)(ms|s|m|h|d|w)$/i;
19
+ const UNIT_MS: Record<string, number> = {
20
+ ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
21
+ };
22
+
23
+ function parseIntervalLocal(s: string): number | null {
24
+ const m = INTERVAL_REGEX.exec(s.trim());
25
+ if (!m) return null;
26
+ return Number(m[1]) * UNIT_MS[m[2].toLowerCase()];
27
+ }
28
+
29
+ export default function NewSchedulePage() {
30
+ const api = useApi();
31
+ const router = useRouter();
32
+ const [value, setValue] = useState<ScheduleFormValue>(INITIAL);
33
+ const [busy, setBusy] = useState(false);
34
+ const [error, setError] = useState<string | null>(null);
35
+
36
+ useBreadcrumb(
37
+ [
38
+ { label: "Schedules", href: "/schedules" },
39
+ { label: "New" },
40
+ ],
41
+ "schedules",
42
+ );
43
+
44
+ // Compute the next 5 fire times locally so users can see the cadence
45
+ // before saving.
46
+ const previewFires = useMemo(() => {
47
+ const ms = parseIntervalLocal(value.interval);
48
+ if (ms === null || ms <= 0) return [];
49
+ const now = Date.now();
50
+ const out: string[] = [];
51
+ for (let i = 1; i <= 5; i++) {
52
+ out.push(new Date(now + ms * i).toISOString());
53
+ }
54
+ return out;
55
+ }, [value.interval]);
56
+
57
+ async function handleSave() {
58
+ setError(null);
59
+ if (!value.target) {
60
+ setError("Target is required.");
61
+ return;
62
+ }
63
+ let inputParsed: unknown = undefined;
64
+ if (value.input.trim()) {
65
+ try {
66
+ inputParsed = JSON.parse(value.input);
67
+ } catch (err) {
68
+ setError(`Input JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
69
+ return;
70
+ }
71
+ }
72
+ setBusy(true);
73
+ try {
74
+ const res = await api.createSchedule({
75
+ kind: value.kind,
76
+ target: value.target,
77
+ interval: value.interval,
78
+ input: inputParsed,
79
+ enabled: value.enabled,
80
+ });
81
+ router.push(`/schedules/${res.data.id}`);
82
+ } catch (err) {
83
+ setError(err instanceof Error ? err.message : String(err));
84
+ } finally {
85
+ setBusy(false);
86
+ }
87
+ }
88
+
89
+ return (
90
+ <div>
91
+ <h1 className="page-title">New schedule</h1>
92
+
93
+ <div style={{ display: "grid", gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr)", gap: "2rem" }}>
94
+ <div>
95
+ <ScheduleForm value={value} onChange={setValue} />
96
+
97
+ {error && (
98
+ <div style={{
99
+ marginTop: "1rem",
100
+ padding: "0.625rem 0.75rem",
101
+ background: "var(--error-bg, #fee)",
102
+ color: "var(--error, #b00)",
103
+ border: "1px solid var(--error, #b00)",
104
+ borderRadius: "4px",
105
+ fontSize: "0.8125rem",
106
+ maxWidth: "640px",
107
+ }}>{error}</div>
108
+ )}
109
+
110
+ <div style={{ marginTop: "1.5rem", display: "flex", gap: "0.5rem" }}>
111
+ <button className="btn btn--primary" onClick={handleSave} disabled={busy}>
112
+ {busy ? "Creating..." : "Create schedule"}
113
+ </button>
114
+ <button className="btn" onClick={() => router.push("/schedules")}>Cancel</button>
115
+ </div>
116
+ </div>
117
+
118
+ <aside>
119
+ <h3 className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "0.5rem" }}>
120
+ Next fire times (preview)
121
+ </h3>
122
+ {previewFires.length === 0 ? (
123
+ <div className="mono" style={{ fontSize: "0.75rem", color: "var(--error, #b00)" }}>
124
+ Invalid interval. Use formats like &quot;30s&quot;, &quot;5m&quot;, &quot;1h&quot;, &quot;1d&quot;, &quot;1w&quot;.
125
+ </div>
126
+ ) : (
127
+ <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
128
+ {previewFires.map((iso, i) => (
129
+ <li key={i} className="mono" style={{
130
+ fontSize: "0.8125rem",
131
+ padding: "0.375rem 0",
132
+ borderBottom: "1px dashed var(--border)",
133
+ color: i === 0 ? "var(--text)" : "var(--muted)",
134
+ }}>
135
+ {new Date(iso).toLocaleString()}
136
+ </li>
137
+ ))}
138
+ </ul>
139
+ )}
140
+ </aside>
141
+ </div>
142
+
143
+ <ApiPanel
144
+ title="Create this schedule"
145
+ snippets={[
146
+ {
147
+ label: "POST /api/v1/schedules",
148
+ method: "POST",
149
+ path: "/api/v1/schedules",
150
+ body: {
151
+ kind: value.kind,
152
+ target: value.target,
153
+ interval: value.interval,
154
+ enabled: value.enabled,
155
+ input: tryParse(value.input),
156
+ },
157
+ },
158
+ ]}
159
+ />
160
+ </div>
161
+ );
162
+ }
163
+
164
+ function tryParse(s: string): unknown {
165
+ try { return JSON.parse(s); } catch { return s; }
166
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { useApi, type Schedule } from "../hooks/use-api";
7
+ import { useBreadcrumb } from "../hooks/use-breadcrumb";
8
+ import { ApiPanel } from "../components/api-panel";
9
+
10
+ const KIND_LABEL: Record<Schedule["kind"], string> = {
11
+ signal: "Signal",
12
+ "broadcast-static": "Broadcast (file)",
13
+ "broadcast-dynamic": "Broadcast (dynamic)",
14
+ };
15
+
16
+ export default function SchedulesPage() {
17
+ const api = useApi();
18
+ const router = useRouter();
19
+ const [schedules, setSchedules] = useState<Schedule[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ useBreadcrumb([{ label: "Schedules" }], "schedules");
24
+
25
+ useEffect(() => {
26
+ api.getSchedules()
27
+ .then((res) => setSchedules(res.data))
28
+ .catch((err) => setError(err instanceof Error ? err.message : String(err)))
29
+ .finally(() => setLoading(false));
30
+ }, []);
31
+
32
+ if (loading) {
33
+ return (
34
+ <div>
35
+ <h1 className="page-title">Schedules</h1>
36
+ <div className="loading-bar"><div className="loading-bar-fill" /></div>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem" }}>
44
+ <h1 className="page-title" style={{ margin: 0 }}>Schedules</h1>
45
+ <Link href="/schedules/new" className="btn btn--primary">+ New schedule</Link>
46
+ </div>
47
+
48
+ {error && (
49
+ <div style={{
50
+ marginBottom: "1rem",
51
+ padding: "0.625rem 0.75rem",
52
+ background: "var(--error-bg, #fee)",
53
+ color: "var(--error, #b00)",
54
+ borderRadius: "4px",
55
+ fontSize: "0.8125rem",
56
+ }}>{error}</div>
57
+ )}
58
+
59
+ {schedules.length === 0 ? (
60
+ <div className="empty-state">
61
+ <p className="empty-state-text">
62
+ No schedules yet. <Link href="/schedules/new">Create one</Link> to fire signals or broadcasts on intervals.
63
+ </p>
64
+ </div>
65
+ ) : (
66
+ <table className="station-table">
67
+ <thead>
68
+ <tr>
69
+ <th>Target</th>
70
+ <th>Kind</th>
71
+ <th>Interval</th>
72
+ <th>Next run</th>
73
+ <th>Last run</th>
74
+ <th>Status</th>
75
+ <th></th>
76
+ </tr>
77
+ </thead>
78
+ <tbody>
79
+ {schedules.map((s) => (
80
+ <tr
81
+ key={s.id}
82
+ className="clickable-row"
83
+ onClick={() => router.push(`/schedules/${s.id}`)}
84
+ >
85
+ <td className="mono">{s.target}</td>
86
+ <td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>{KIND_LABEL[s.kind]}</td>
87
+ <td className="mono">{s.interval}</td>
88
+ <td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
89
+ {new Date(s.nextRunAt).toLocaleString()}
90
+ </td>
91
+ <td className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>
92
+ {s.lastRunAt ? new Date(s.lastRunAt).toLocaleString() : "—"}
93
+ </td>
94
+ <td className="mono" style={{ fontSize: "0.75rem" }}>
95
+ <span style={{
96
+ padding: "0.125rem 0.375rem",
97
+ borderRadius: "3px",
98
+ background: s.enabled ? "var(--success-bg, #efe)" : "var(--surface)",
99
+ color: s.enabled ? "var(--success, #060)" : "var(--muted)",
100
+ }}>
101
+ {s.enabled ? "enabled" : "disabled"}
102
+ </span>
103
+ </td>
104
+ <td></td>
105
+ </tr>
106
+ ))}
107
+ </tbody>
108
+ </table>
109
+ )}
110
+
111
+ <ApiPanel
112
+ title="List & manage schedules"
113
+ snippets={[
114
+ { label: "List all", method: "GET", path: "/api/v1/schedules" },
115
+ { label: "List by kind", method: "GET", path: "/api/v1/schedules", query: { kind: "signal" } },
116
+ {
117
+ label: "Create",
118
+ method: "POST",
119
+ path: "/api/v1/schedules",
120
+ body: { kind: "signal", target: "<signalName>", interval: "5m", enabled: true, input: {} },
121
+ },
122
+ ]}
123
+ />
124
+ </div>
125
+ );
126
+ }
@@ -1,10 +1,20 @@
1
1
  import type { SignalQueueAdapter } from "station-signal";
2
2
  import type { BroadcastQueueAdapter } from "station-broadcast";
3
+ import type { ScheduleAdapter } from "station-schedules";
4
+ import type { ApiKeyStorageAdapter } from "../server/auth/keys.js";
5
+ import type { LogStorageAdapter } from "../server/log-store.js";
3
6
 
4
7
  export interface AuthConfig {
5
8
  username: string;
6
9
  password: string;
7
10
  sessionTtlMs?: number;
11
+ /**
12
+ * Pluggable storage backend for API keys. Defaults to a JSON file at
13
+ * `<dataDir>/station-keys.json` (no native dependencies required).
14
+ * Provide a custom adapter to host keys in SQLite, Postgres, MySQL,
15
+ * Redis, etc.
16
+ */
17
+ keyStorage?: ApiKeyStorageAdapter;
8
18
  }
9
19
 
10
20
  export interface RunnerConfig {
@@ -27,6 +37,19 @@ export interface StationConfig {
27
37
  host: string;
28
38
  adapter?: SignalQueueAdapter;
29
39
  broadcastAdapter?: BroadcastQueueAdapter;
40
+ /**
41
+ * Optional schedule storage adapter. When provided, runtime-editable
42
+ * schedules are persisted here and reconciled by both runners.
43
+ */
44
+ scheduleAdapter?: ScheduleAdapter;
45
+ /**
46
+ * Pluggable storage backend for run logs. Defaults to a `FileLogStorage`
47
+ * (append-only JSONL file at `<dataDir>/station-logs.jsonl`, no native
48
+ * dependencies). The default is single-process only — for multi-process
49
+ * deployments or guaranteed durability, implement `LogStorageAdapter`
50
+ * against Postgres, MySQL, Redis, S3, etc., and pass it here.
51
+ */
52
+ logStorage?: LogStorageAdapter;
30
53
  signalsDir?: string;
31
54
  broadcastsDir?: string;
32
55
  stationDir: string;
@@ -85,6 +108,8 @@ export function resolveConfig(input: StationUserConfig): StationConfig {
85
108
  host: input.host ?? envHost ?? DEFAULTS.host,
86
109
  adapter: input.adapter,
87
110
  broadcastAdapter: input.broadcastAdapter,
111
+ scheduleAdapter: input.scheduleAdapter,
112
+ logStorage: input.logStorage,
88
113
  signalsDir: input.signalsDir,
89
114
  broadcastsDir: input.broadcastsDir,
90
115
  stationDir: input.stationDir ?? DEFAULTS.stationDir,