@polderlabs/bizar-dash 3.0.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 (59) hide show
  1. package/dist/assets/index-B5X9g8B4.css +1 -0
  2. package/dist/assets/index-LqQuSp9d.js +388 -0
  3. package/dist/assets/index-LqQuSp9d.js.map +1 -0
  4. package/dist/index.html +18 -0
  5. package/package.json +67 -0
  6. package/src/cli.mjs +228 -0
  7. package/src/server/agents-store.mjs +190 -0
  8. package/src/server/api.mjs +913 -0
  9. package/src/server/browser.mjs +40 -0
  10. package/src/server/diagnostics-store.mjs +138 -0
  11. package/src/server/mods-loader.mjs +361 -0
  12. package/src/server/projects-store.mjs +198 -0
  13. package/src/server/providers-store.mjs +183 -0
  14. package/src/server/schedules-runner.mjs +150 -0
  15. package/src/server/schedules-store.mjs +233 -0
  16. package/src/server/search-store.mjs +120 -0
  17. package/src/server/server.mjs +388 -0
  18. package/src/server/state.mjs +357 -0
  19. package/src/server/tailscale-store.mjs +113 -0
  20. package/src/server/tasks-store.mjs +275 -0
  21. package/src/server/tui.mjs +844 -0
  22. package/src/server/watcher.mjs +81 -0
  23. package/src/web/App.tsx +316 -0
  24. package/src/web/components/Button.tsx +55 -0
  25. package/src/web/components/Card.tsx +40 -0
  26. package/src/web/components/EmptyState.tsx +30 -0
  27. package/src/web/components/Modal.tsx +137 -0
  28. package/src/web/components/SearchModal.tsx +185 -0
  29. package/src/web/components/Spinner.tsx +19 -0
  30. package/src/web/components/StatusBadge.tsx +25 -0
  31. package/src/web/components/Tag.tsx +28 -0
  32. package/src/web/components/Toast.tsx +142 -0
  33. package/src/web/components/Topbar.tsx +203 -0
  34. package/src/web/index.html +17 -0
  35. package/src/web/lib/api.ts +71 -0
  36. package/src/web/lib/markdown.tsx +59 -0
  37. package/src/web/lib/types.ts +388 -0
  38. package/src/web/lib/utils.ts +79 -0
  39. package/src/web/lib/ws.ts +132 -0
  40. package/src/web/main.tsx +12 -0
  41. package/src/web/styles/main.css +3148 -0
  42. package/src/web/views/Agents.tsx +406 -0
  43. package/src/web/views/Chat.tsx +527 -0
  44. package/src/web/views/Config.tsx +683 -0
  45. package/src/web/views/Mods.tsx +350 -0
  46. package/src/web/views/Overview.tsx +350 -0
  47. package/src/web/views/Plans.tsx +667 -0
  48. package/src/web/views/Schedules.tsx +299 -0
  49. package/src/web/views/Settings.tsx +571 -0
  50. package/src/web/views/Tasks.tsx +761 -0
  51. package/templates/mod/FORMAT.md +76 -0
  52. package/templates/mod/hello-mod/README.md +19 -0
  53. package/templates/mod/hello-mod/agents/greeter.md +8 -0
  54. package/templates/mod/hello-mod/commands/hello.md +6 -0
  55. package/templates/mod/hello-mod/mod.json +20 -0
  56. package/templates/mod/hello-mod/routes/ping.mjs +9 -0
  57. package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  58. package/tsconfig.json +23 -0
  59. package/vite.config.ts +24 -0
@@ -0,0 +1,299 @@
1
+ // src/views/Schedules.tsx — list, create, run, delete schedules for the active project.
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ Clock,
5
+ Plus,
6
+ PlayCircle,
7
+ Trash2,
8
+ RefreshCw,
9
+ History,
10
+ X,
11
+ } from 'lucide-react';
12
+ import { Card, CardTitle, CardMeta } from '../components/Card';
13
+ import { Button } from '../components/Button';
14
+ import { EmptyState } from '../components/EmptyState';
15
+ import { Spinner } from '../components/Spinner';
16
+ import { useToast } from '../components/Toast';
17
+ import { useModal } from '../components/Modal';
18
+ import { api } from '../lib/api';
19
+ import { formatRelative } from '../lib/utils';
20
+ import type { Schedule, Settings, Snapshot } from '../lib/types';
21
+
22
+ type Props = {
23
+ snapshot: Snapshot;
24
+ settings: Settings;
25
+ activeTab: string;
26
+ setActiveTab: (id: string) => void;
27
+ refreshSnapshot: () => Promise<void>;
28
+ };
29
+
30
+ const TYPES = ['interval', 'cron', 'once'] as const;
31
+ const ACTIONS = ['command', 'agent', 'webhook'] as const;
32
+
33
+ export function Schedules({ snapshot, refreshSnapshot }: Props) {
34
+ const toast = useToast();
35
+ const modal = useModal();
36
+ const [schedules, setSchedules] = useState<Schedule[]>(snapshot.schedules || []);
37
+ const [loading, setLoading] = useState(!snapshot.schedules);
38
+
39
+ const reload = async () => {
40
+ try {
41
+ const r = await api.get<{ schedules: Schedule[]; projectId: string | null }>(
42
+ '/projects/active/schedules',
43
+ );
44
+ setSchedules(r.schedules || []);
45
+ } catch (err) {
46
+ toast.error(`Schedules load failed: ${(err as Error).message}`);
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ };
51
+
52
+ useEffect(() => {
53
+ if (snapshot.schedules?.length || snapshot.schedules) {
54
+ setSchedules(snapshot.schedules || []);
55
+ setLoading(false);
56
+ return;
57
+ }
58
+ reload();
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [snapshot.schedules]);
61
+
62
+ const onCreate = () => {
63
+ let nameEl: HTMLInputElement | null = null;
64
+ let typeEl: HTMLSelectElement | null = null;
65
+ let scheduleEl: HTMLInputElement | null = null;
66
+ let actionTypeEl: HTMLSelectElement | null = null;
67
+ let actionTargetEl: HTMLInputElement | null = null;
68
+ let enabledEl: HTMLInputElement | null = null;
69
+
70
+ modal.open({
71
+ title: 'New schedule',
72
+ children: (
73
+ <div className="schedule-form">
74
+ <label className="field-label">Name</label>
75
+ <input
76
+ ref={(el) => (nameEl = el)}
77
+ className="input"
78
+ type="text"
79
+ placeholder="Daily backup"
80
+ autoFocus
81
+ />
82
+ <div className="task-form-row">
83
+ <div className="task-form-field">
84
+ <label className="field-label">Type</label>
85
+ <select ref={(el) => (typeEl = el)} className="select" defaultValue="interval">
86
+ {TYPES.map((t) => (
87
+ <option key={t} value={t}>{t}</option>
88
+ ))}
89
+ </select>
90
+ </div>
91
+ <div className="task-form-field">
92
+ <label className="field-label">Schedule</label>
93
+ <input
94
+ ref={(el) => (scheduleEl = el)}
95
+ className="input"
96
+ type="text"
97
+ placeholder="30m | 0 0 * * * | 2026-12-31T00:00:00Z"
98
+ />
99
+ </div>
100
+ </div>
101
+ <div className="task-form-row">
102
+ <div className="task-form-field">
103
+ <label className="field-label">Action</label>
104
+ <select ref={(el) => (actionTypeEl = el)} className="select" defaultValue="command">
105
+ {ACTIONS.map((t) => (
106
+ <option key={t} value={t}>{t}</option>
107
+ ))}
108
+ </select>
109
+ </div>
110
+ <div className="task-form-field" style={{ flex: 2 }}>
111
+ <label className="field-label">Target</label>
112
+ <input
113
+ ref={(el) => (actionTargetEl = el)}
114
+ className="input"
115
+ type="text"
116
+ placeholder='echo hi | thor | https://...'
117
+ />
118
+ </div>
119
+ </div>
120
+ <label className="checkbox-row">
121
+ <input
122
+ ref={(el) => (enabledEl = el)}
123
+ type="checkbox"
124
+ defaultChecked
125
+ />
126
+ <span>Enabled</span>
127
+ </label>
128
+ </div>
129
+ ),
130
+ footer: (
131
+ <div className="modal-footer-actions">
132
+ <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
133
+ <Button
134
+ variant="primary"
135
+ onClick={async () => {
136
+ const name = (nameEl?.value || '').trim();
137
+ const type = typeEl?.value || 'interval';
138
+ const schedule = (scheduleEl?.value || '').trim();
139
+ const actionType = actionTypeEl?.value || 'command';
140
+ const actionTarget = (actionTargetEl?.value || '').trim();
141
+ if (!name) return toast.warning('Name is required.');
142
+ if (!schedule) return toast.warning('Schedule is required.');
143
+ if (!actionTarget) return toast.warning('Action target is required.');
144
+ try {
145
+ const s = await api.post<Schedule>('/schedules', {
146
+ name, type, schedule,
147
+ enabled: enabledEl?.checked !== false,
148
+ action: { type: actionType, target: actionTarget },
149
+ });
150
+ setSchedules((cur) => [...cur, s]);
151
+ toast.success('Schedule created.');
152
+ modal.close();
153
+ await refreshSnapshot();
154
+ } catch (err) {
155
+ toast.error(`Create failed: ${(err as Error).message}`);
156
+ }
157
+ }}
158
+ >
159
+ Create
160
+ </Button>
161
+ </div>
162
+ ),
163
+ });
164
+ };
165
+
166
+ const onRun = async (s: Schedule) => {
167
+ try {
168
+ const r = await api.post<{ ok: boolean; schedule: Schedule; runResult: { error?: string; stdout?: string } }>(
169
+ `/schedules/${encodeURIComponent(s.id)}/run`,
170
+ );
171
+ toast.success(r.ok ? 'Schedule ran.' : `Run failed: ${r.runResult?.error || 'unknown'}`);
172
+ await reload();
173
+ } catch (err) {
174
+ toast.error(`Run failed: ${(err as Error).message}`);
175
+ }
176
+ };
177
+
178
+ const onDelete = async (s: Schedule) => {
179
+ if (!confirm(`Delete schedule "${s.name}"?`)) return;
180
+ try {
181
+ await api.del(`/schedules/${encodeURIComponent(s.id)}`);
182
+ setSchedules((cur) => cur.filter((x) => x.id !== s.id));
183
+ toast.success('Schedule deleted.');
184
+ } catch (err) {
185
+ toast.error(`Delete failed: ${(err as Error).message}`);
186
+ }
187
+ };
188
+
189
+ if (loading) {
190
+ return (
191
+ <div className="view-loading"><Spinner size="lg" /></div>
192
+ );
193
+ }
194
+
195
+ return (
196
+ <div className="view view-schedules">
197
+ <header className="view-header">
198
+ <div className="view-header-text">
199
+ <h2 className="view-title">
200
+ <Clock size={18} /> Schedules ({schedules.length})
201
+ </h2>
202
+ <p className="view-subtitle">
203
+ Recurring tasks for the active project: <strong>{snapshot.activeProject?.name || '(none)'}</strong>.
204
+ Service daemon runs them at the right time.
205
+ </p>
206
+ </div>
207
+ <div className="view-actions">
208
+ <Button variant="secondary" size="sm" onClick={reload}>
209
+ <RefreshCw size={14} /> Refresh
210
+ </Button>
211
+ <Button variant="primary" size="sm" onClick={onCreate}>
212
+ <Plus size={14} /> New schedule
213
+ </Button>
214
+ </div>
215
+ </header>
216
+
217
+ {schedules.length === 0 ? (
218
+ <EmptyState
219
+ icon={<Clock size={32} />}
220
+ title="No schedules"
221
+ message={
222
+ snapshot.activeProject
223
+ ? 'Add a schedule to run commands, webhooks, or agent tasks on a cron / interval / one-shot basis.'
224
+ : 'Activate a project first to scope schedules.'
225
+ }
226
+ />
227
+ ) : (
228
+ <div className="schedule-grid">
229
+ {schedules.map((s) => (
230
+ <Card key={s.id} className="schedule-card">
231
+ <div className="schedule-card-head">
232
+ <div>
233
+ <div className="schedule-card-name">{s.name}</div>
234
+ <div className="schedule-card-meta">
235
+ <code>{s.type}</code> · <code>{s.schedule}</code> ·{' '}
236
+ <span className={s.enabled ? 'status-on' : 'status-neutral'}>
237
+ {s.enabled ? 'enabled' : 'disabled'}
238
+ </span>
239
+ </div>
240
+ </div>
241
+ <div className="schedule-card-actions">
242
+ <Button variant="secondary" size="sm" onClick={() => onRun(s)}>
243
+ <PlayCircle size={12} /> Run now
244
+ </Button>
245
+ <button
246
+ type="button"
247
+ className="icon-btn icon-btn-danger"
248
+ aria-label="Delete"
249
+ title="Delete"
250
+ onClick={() => onDelete(s)}
251
+ >
252
+ <Trash2 size={12} />
253
+ </button>
254
+ </div>
255
+ </div>
256
+ <div className="schedule-card-action">
257
+ <span className="muted">action:</span>{' '}
258
+ <code>{s.action.type} {s.action.target}</code>
259
+ </div>
260
+ <div className="schedule-card-times">
261
+ <div>
262
+ <span className="muted">last</span>{' '}
263
+ {s.lastRun ? formatRelative(s.lastRun) : '—'}
264
+ {s.lastResult && (
265
+ <span className={`tag ${s.lastResult === 'success' ? 'tag-success' : 'tag-error'}`}>
266
+ {s.lastResult}
267
+ </span>
268
+ )}
269
+ </div>
270
+ <div>
271
+ <span className="muted">next</span>{' '}
272
+ {s.nextRun ? formatRelative(s.nextRun) : '—'}
273
+ </div>
274
+ </div>
275
+ {s.history && s.history.length > 0 && (
276
+ <details className="schedule-card-history">
277
+ <summary>
278
+ <History size={12} /> {s.history.length} run{s.history.length === 1 ? '' : 's'}
279
+ </summary>
280
+ <ul>
281
+ {s.history.slice(-10).reverse().map((h, i) => (
282
+ <li key={i}>
283
+ <span className="tabular-nums muted">{formatRelative(h.ts)}</span>{' '}
284
+ <span className={`tag ${h.result === 'success' ? 'tag-success' : 'tag-error'}`}>
285
+ {h.result}
286
+ </span>
287
+ {h.error && <span className="muted"> — {h.error}</span>}
288
+ </li>
289
+ ))}
290
+ </ul>
291
+ </details>
292
+ )}
293
+ </Card>
294
+ ))}
295
+ </div>
296
+ )}
297
+ </div>
298
+ );
299
+ }