@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.
- package/dist/assets/index-B5X9g8B4.css +1 -0
- package/dist/assets/index-LqQuSp9d.js +388 -0
- package/dist/assets/index-LqQuSp9d.js.map +1 -0
- package/dist/index.html +18 -0
- package/package.json +67 -0
- package/src/cli.mjs +228 -0
- package/src/server/agents-store.mjs +190 -0
- package/src/server/api.mjs +913 -0
- package/src/server/browser.mjs +40 -0
- package/src/server/diagnostics-store.mjs +138 -0
- package/src/server/mods-loader.mjs +361 -0
- package/src/server/projects-store.mjs +198 -0
- package/src/server/providers-store.mjs +183 -0
- package/src/server/schedules-runner.mjs +150 -0
- package/src/server/schedules-store.mjs +233 -0
- package/src/server/search-store.mjs +120 -0
- package/src/server/server.mjs +388 -0
- package/src/server/state.mjs +357 -0
- package/src/server/tailscale-store.mjs +113 -0
- package/src/server/tasks-store.mjs +275 -0
- package/src/server/tui.mjs +844 -0
- package/src/server/watcher.mjs +81 -0
- package/src/web/App.tsx +316 -0
- package/src/web/components/Button.tsx +55 -0
- package/src/web/components/Card.tsx +40 -0
- package/src/web/components/EmptyState.tsx +30 -0
- package/src/web/components/Modal.tsx +137 -0
- package/src/web/components/SearchModal.tsx +185 -0
- package/src/web/components/Spinner.tsx +19 -0
- package/src/web/components/StatusBadge.tsx +25 -0
- package/src/web/components/Tag.tsx +28 -0
- package/src/web/components/Toast.tsx +142 -0
- package/src/web/components/Topbar.tsx +203 -0
- package/src/web/index.html +17 -0
- package/src/web/lib/api.ts +71 -0
- package/src/web/lib/markdown.tsx +59 -0
- package/src/web/lib/types.ts +388 -0
- package/src/web/lib/utils.ts +79 -0
- package/src/web/lib/ws.ts +132 -0
- package/src/web/main.tsx +12 -0
- package/src/web/styles/main.css +3148 -0
- package/src/web/views/Agents.tsx +406 -0
- package/src/web/views/Chat.tsx +527 -0
- package/src/web/views/Config.tsx +683 -0
- package/src/web/views/Mods.tsx +350 -0
- package/src/web/views/Overview.tsx +350 -0
- package/src/web/views/Plans.tsx +667 -0
- package/src/web/views/Schedules.tsx +299 -0
- package/src/web/views/Settings.tsx +571 -0
- package/src/web/views/Tasks.tsx +761 -0
- package/templates/mod/FORMAT.md +76 -0
- package/templates/mod/hello-mod/README.md +19 -0
- package/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/templates/mod/hello-mod/commands/hello.md +6 -0
- package/templates/mod/hello-mod/mod.json +20 -0
- package/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/tsconfig.json +23 -0
- 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
|
+
}
|