groove-dev 0.16.3 → 0.17.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/README.md +18 -16
- package/node_modules/@groove-dev/daemon/integrations-registry.json +321 -0
- package/node_modules/@groove-dev/daemon/src/api.js +152 -0
- package/node_modules/@groove-dev/daemon/src/index.js +13 -1
- package/node_modules/@groove-dev/daemon/src/integrations.js +389 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +23 -0
- package/node_modules/@groove-dev/daemon/src/process.js +59 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
- package/node_modules/@groove-dev/daemon/src/scheduler.js +336 -0
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +119 -54
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C5k-qSwi.js +153 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +98 -7
- package/node_modules/@groove-dev/gui/src/components/Terminal.jsx +29 -12
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +954 -0
- package/node_modules/@groove-dev/gui/src/views/ScheduleManager.jsx +614 -0
- package/package.json +2 -2
- package/packages/daemon/integrations-registry.json +321 -0
- package/packages/daemon/src/api.js +152 -0
- package/packages/daemon/src/index.js +13 -1
- package/packages/daemon/src/integrations.js +389 -0
- package/packages/daemon/src/introducer.js +23 -0
- package/packages/daemon/src/process.js +59 -0
- package/packages/daemon/src/registry.js +2 -1
- package/packages/daemon/src/scheduler.js +336 -0
- package/packages/daemon/src/terminal-pty.js +119 -54
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/index-C5k-qSwi.js +153 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/App.jsx +6 -0
- package/packages/gui/src/components/SpawnPanel.jsx +98 -7
- package/packages/gui/src/components/Terminal.jsx +29 -12
- package/packages/gui/src/views/IntegrationsStore.jsx +954 -0
- package/packages/gui/src/views/ScheduleManager.jsx +614 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CFeltwTB.js +0 -153
- package/packages/gui/dist/assets/index-CFeltwTB.js +0 -153
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// GROOVE GUI — Schedule Manager
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
5
|
+
|
|
6
|
+
const CRON_PRESETS = [
|
|
7
|
+
{ label: 'Every 5 min', cron: '*/5 * * * *' },
|
|
8
|
+
{ label: 'Every 15 min', cron: '*/15 * * * *' },
|
|
9
|
+
{ label: 'Every hour', cron: '0 * * * *' },
|
|
10
|
+
{ label: 'Every 6 hours', cron: '0 */6 * * *' },
|
|
11
|
+
{ label: 'Daily 9 AM', cron: '0 9 * * *' },
|
|
12
|
+
{ label: 'Weekdays 9 AM', cron: '0 9 * * 1-5' },
|
|
13
|
+
{ label: 'Weekly (Mon)', cron: '0 0 * * 1' },
|
|
14
|
+
{ label: 'Monthly', cron: '0 0 1 * *' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const ROLE_PRESETS = [
|
|
18
|
+
{ id: 'backend', label: 'Backend' },
|
|
19
|
+
{ id: 'frontend', label: 'Frontend' },
|
|
20
|
+
{ id: 'fullstack', label: 'Fullstack' },
|
|
21
|
+
{ id: 'testing', label: 'Testing' },
|
|
22
|
+
{ id: 'devops', label: 'DevOps' },
|
|
23
|
+
{ id: 'docs', label: 'Docs' },
|
|
24
|
+
{ id: 'cmo', label: 'CMO' },
|
|
25
|
+
{ id: 'cfo', label: 'CFO' },
|
|
26
|
+
{ id: 'ea', label: 'EA' },
|
|
27
|
+
{ id: 'support', label: 'Support' },
|
|
28
|
+
{ id: 'analyst', label: 'Analyst' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function timeAgo(iso) {
|
|
32
|
+
if (!iso) return 'never';
|
|
33
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
34
|
+
if (ms < 60000) return 'just now';
|
|
35
|
+
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`;
|
|
36
|
+
if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`;
|
|
37
|
+
return `${Math.floor(ms / 86400000)}d ago`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function StatusDot({ status }) {
|
|
41
|
+
const colors = {
|
|
42
|
+
spawned: 'var(--green)',
|
|
43
|
+
skipped: 'var(--amber)',
|
|
44
|
+
error: 'var(--red)',
|
|
45
|
+
};
|
|
46
|
+
return (
|
|
47
|
+
<span style={{
|
|
48
|
+
width: 6, height: 6, borderRadius: '50%',
|
|
49
|
+
background: colors[status] || 'var(--text-muted)',
|
|
50
|
+
display: 'inline-block', marginRight: 6,
|
|
51
|
+
}} />
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// -- Create Schedule Form --
|
|
56
|
+
function CreateForm({ onSubmit, onCancel }) {
|
|
57
|
+
const [name, setName] = useState('');
|
|
58
|
+
const [cron, setCron] = useState('0 9 * * *');
|
|
59
|
+
const [role, setRole] = useState('fullstack');
|
|
60
|
+
const [prompt, setPrompt] = useState('');
|
|
61
|
+
const [submitting, setSubmitting] = useState(false);
|
|
62
|
+
const [error, setError] = useState('');
|
|
63
|
+
|
|
64
|
+
async function handleSubmit(e) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
if (!name.trim()) { setError('Name is required'); return; }
|
|
67
|
+
if (!prompt.trim()) { setError('Task prompt is required'); return; }
|
|
68
|
+
|
|
69
|
+
setSubmitting(true);
|
|
70
|
+
setError('');
|
|
71
|
+
try {
|
|
72
|
+
await onSubmit({
|
|
73
|
+
name: name.trim(),
|
|
74
|
+
cron,
|
|
75
|
+
agentConfig: { role, prompt: prompt.trim() },
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err.message);
|
|
79
|
+
}
|
|
80
|
+
setSubmitting(false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<form onSubmit={handleSubmit} style={styles.form}>
|
|
85
|
+
<div style={styles.formTitle}>New Schedule</div>
|
|
86
|
+
|
|
87
|
+
<label style={styles.label}>NAME</label>
|
|
88
|
+
<input
|
|
89
|
+
value={name}
|
|
90
|
+
onChange={(e) => setName(e.target.value)}
|
|
91
|
+
placeholder="e.g., Daily standup summary"
|
|
92
|
+
style={styles.input}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<label style={styles.label}>FREQUENCY</label>
|
|
96
|
+
<div style={styles.presetRow}>
|
|
97
|
+
{CRON_PRESETS.map((p) => (
|
|
98
|
+
<button
|
|
99
|
+
key={p.cron}
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={() => setCron(p.cron)}
|
|
102
|
+
style={{
|
|
103
|
+
...styles.presetBtn,
|
|
104
|
+
borderColor: cron === p.cron ? 'var(--accent)' : 'var(--border)',
|
|
105
|
+
color: cron === p.cron ? 'var(--accent)' : 'var(--text-dim)',
|
|
106
|
+
background: cron === p.cron ? 'rgba(51, 175, 188, 0.08)' : 'transparent',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{p.label}
|
|
110
|
+
</button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
<input
|
|
114
|
+
value={cron}
|
|
115
|
+
onChange={(e) => setCron(e.target.value)}
|
|
116
|
+
placeholder="* * * * * (min hr day mon wkday)"
|
|
117
|
+
style={{ ...styles.input, fontFamily: 'var(--font)', fontSize: 11 }}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<label style={styles.label}>AGENT ROLE</label>
|
|
121
|
+
<div style={styles.presetRow}>
|
|
122
|
+
{ROLE_PRESETS.map((r) => (
|
|
123
|
+
<button
|
|
124
|
+
key={r.id}
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => setRole(r.id)}
|
|
127
|
+
style={{
|
|
128
|
+
...styles.presetBtn,
|
|
129
|
+
borderColor: role === r.id ? 'var(--accent)' : 'var(--border)',
|
|
130
|
+
color: role === r.id ? 'var(--accent)' : 'var(--text-dim)',
|
|
131
|
+
background: role === r.id ? 'rgba(51, 175, 188, 0.08)' : 'transparent',
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{r.label}
|
|
135
|
+
</button>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<label style={styles.label}>TASK</label>
|
|
140
|
+
<textarea
|
|
141
|
+
value={prompt}
|
|
142
|
+
onChange={(e) => setPrompt(e.target.value)}
|
|
143
|
+
placeholder="What should this agent do each time it runs?"
|
|
144
|
+
rows={3}
|
|
145
|
+
style={styles.textarea}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
149
|
+
|
|
150
|
+
<div style={styles.formActions}>
|
|
151
|
+
<button type="button" onClick={onCancel} style={styles.cancelBtn}>Cancel</button>
|
|
152
|
+
<button type="submit" disabled={submitting} style={styles.submitBtn}>
|
|
153
|
+
{submitting ? 'Creating...' : 'Create Schedule'}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</form>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -- Schedule Row --
|
|
161
|
+
function ScheduleRow({ schedule, onToggle, onRun, onDelete, onSelect }) {
|
|
162
|
+
const [confirming, setConfirming] = useState(false);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
style={styles.scheduleRow}
|
|
167
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-hover)'; }}
|
|
168
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--bg-surface)'; }}
|
|
169
|
+
>
|
|
170
|
+
<div style={styles.scheduleMain} onClick={() => onSelect(schedule)}>
|
|
171
|
+
{/* Status indicator */}
|
|
172
|
+
<span style={{
|
|
173
|
+
width: 8, height: 8, borderRadius: '50%',
|
|
174
|
+
background: schedule.enabled
|
|
175
|
+
? schedule.isRunning ? 'var(--green)' : 'var(--accent)'
|
|
176
|
+
: 'var(--text-muted)',
|
|
177
|
+
flexShrink: 0,
|
|
178
|
+
animation: schedule.isRunning ? 'pulse 2s infinite' : 'none',
|
|
179
|
+
}} />
|
|
180
|
+
|
|
181
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
182
|
+
<div style={styles.scheduleName}>{schedule.name}</div>
|
|
183
|
+
<div style={styles.scheduleMeta}>
|
|
184
|
+
<span>{schedule.cronDescription || schedule.cron}</span>
|
|
185
|
+
<span style={{ color: 'var(--text-muted)' }}>
|
|
186
|
+
{schedule.agentConfig?.role}
|
|
187
|
+
</span>
|
|
188
|
+
{schedule.lastRun && (
|
|
189
|
+
<span style={{ color: 'var(--text-muted)' }}>
|
|
190
|
+
<StatusDot status={schedule.lastRun.status} />
|
|
191
|
+
{timeAgo(schedule.lastRun.timestamp)}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div style={styles.scheduleActions}>
|
|
199
|
+
{/* Toggle */}
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => onToggle(schedule.id, !schedule.enabled)}
|
|
202
|
+
title={schedule.enabled ? 'Disable' : 'Enable'}
|
|
203
|
+
style={{
|
|
204
|
+
...styles.actionBtn,
|
|
205
|
+
color: schedule.enabled ? 'var(--green)' : 'var(--text-muted)',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{schedule.enabled ? 'ON' : 'OFF'}
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
{/* Run now */}
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => onRun(schedule.id)}
|
|
214
|
+
title="Run now"
|
|
215
|
+
style={{ ...styles.actionBtn, color: 'var(--accent)' }}
|
|
216
|
+
>
|
|
217
|
+
{'\u25B6'}
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
{/* Delete */}
|
|
221
|
+
{confirming ? (
|
|
222
|
+
<button
|
|
223
|
+
onClick={() => { onDelete(schedule.id); setConfirming(false); }}
|
|
224
|
+
style={{ ...styles.actionBtn, color: 'var(--red)' }}
|
|
225
|
+
>
|
|
226
|
+
confirm
|
|
227
|
+
</button>
|
|
228
|
+
) : (
|
|
229
|
+
<button
|
|
230
|
+
onClick={() => setConfirming(true)}
|
|
231
|
+
style={{ ...styles.actionBtn, color: 'var(--text-muted)' }}
|
|
232
|
+
>
|
|
233
|
+
{'\u2715'}
|
|
234
|
+
</button>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// -- Schedule Detail --
|
|
242
|
+
function ScheduleDetail({ schedule, onClose }) {
|
|
243
|
+
if (!schedule) return null;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div style={styles.detailPanel}>
|
|
247
|
+
<div style={styles.detailHeader}>
|
|
248
|
+
<div style={styles.detailTitle}>{schedule.name}</div>
|
|
249
|
+
<button onClick={onClose} style={styles.detailClose}>×</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div style={styles.detailGrid}>
|
|
253
|
+
<div style={styles.detailLabel}>Cron</div>
|
|
254
|
+
<div style={styles.detailValue}>{schedule.cron}</div>
|
|
255
|
+
<div style={styles.detailLabel}>Description</div>
|
|
256
|
+
<div style={styles.detailValue}>{schedule.cronDescription}</div>
|
|
257
|
+
<div style={styles.detailLabel}>Role</div>
|
|
258
|
+
<div style={styles.detailValue}>{schedule.agentConfig?.role}</div>
|
|
259
|
+
<div style={styles.detailLabel}>Status</div>
|
|
260
|
+
<div style={styles.detailValue}>
|
|
261
|
+
<span style={{ color: schedule.enabled ? 'var(--green)' : 'var(--text-muted)' }}>
|
|
262
|
+
{schedule.enabled ? 'Enabled' : 'Disabled'}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div style={styles.detailLabel}>Created</div>
|
|
266
|
+
<div style={styles.detailValue}>{new Date(schedule.createdAt).toLocaleString()}</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{schedule.agentConfig?.prompt && (
|
|
270
|
+
<div style={{ marginTop: 16 }}>
|
|
271
|
+
<div style={styles.detailLabel}>Task Prompt</div>
|
|
272
|
+
<pre style={styles.promptPre}>{schedule.agentConfig.prompt}</pre>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{(schedule.history || []).length > 0 && (
|
|
277
|
+
<div style={{ marginTop: 16 }}>
|
|
278
|
+
<div style={{ ...styles.detailLabel, marginBottom: 8 }}>Execution History</div>
|
|
279
|
+
<div style={styles.historyList}>
|
|
280
|
+
{schedule.history.slice(0, 20).map((entry, i) => (
|
|
281
|
+
<div key={i} style={styles.historyEntry}>
|
|
282
|
+
<StatusDot status={entry.status} />
|
|
283
|
+
<span style={{ fontSize: 10, color: 'var(--text-dim)', flex: 1 }}>
|
|
284
|
+
{entry.status}
|
|
285
|
+
{entry.agentId && <span style={{ color: 'var(--text-muted)' }}> ({entry.agentId})</span>}
|
|
286
|
+
</span>
|
|
287
|
+
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>
|
|
288
|
+
{timeAgo(entry.timestamp)}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// -- Main --
|
|
300
|
+
export default function ScheduleManager() {
|
|
301
|
+
const [schedules, setSchedules] = useState([]);
|
|
302
|
+
const [loading, setLoading] = useState(true);
|
|
303
|
+
const [creating, setCreating] = useState(false);
|
|
304
|
+
const [selected, setSelected] = useState(null);
|
|
305
|
+
const [statusMsg, setStatusMsg] = useState('');
|
|
306
|
+
|
|
307
|
+
const fetchSchedules = useCallback(async () => {
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch('/api/schedules');
|
|
310
|
+
setSchedules(await res.json());
|
|
311
|
+
} catch { /* ignore */ }
|
|
312
|
+
setLoading(false);
|
|
313
|
+
}, []);
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
fetchSchedules();
|
|
317
|
+
const interval = setInterval(fetchSchedules, 10000);
|
|
318
|
+
return () => clearInterval(interval);
|
|
319
|
+
}, [fetchSchedules]);
|
|
320
|
+
|
|
321
|
+
async function handleCreate(config) {
|
|
322
|
+
const res = await fetch('/api/schedules', {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: { 'Content-Type': 'application/json' },
|
|
325
|
+
body: JSON.stringify(config),
|
|
326
|
+
});
|
|
327
|
+
if (!res.ok) {
|
|
328
|
+
const data = await res.json();
|
|
329
|
+
throw new Error(data.error || 'Failed to create schedule');
|
|
330
|
+
}
|
|
331
|
+
setCreating(false);
|
|
332
|
+
await fetchSchedules();
|
|
333
|
+
flash('Schedule created');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handleToggle(id, enabled) {
|
|
337
|
+
await fetch(`/api/schedules/${id}/${enabled ? 'enable' : 'disable'}`, { method: 'POST' });
|
|
338
|
+
await fetchSchedules();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function handleRun(id) {
|
|
342
|
+
try {
|
|
343
|
+
await fetch(`/api/schedules/${id}/run`, { method: 'POST' });
|
|
344
|
+
flash('Agent spawned');
|
|
345
|
+
await fetchSchedules();
|
|
346
|
+
} catch { flash('Failed to run'); }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function handleDelete(id) {
|
|
350
|
+
await fetch(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
351
|
+
if (selected?.id === id) setSelected(null);
|
|
352
|
+
await fetchSchedules();
|
|
353
|
+
flash('Schedule deleted');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function handleSelect(schedule) {
|
|
357
|
+
try {
|
|
358
|
+
const res = await fetch(`/api/schedules/${schedule.id}`);
|
|
359
|
+
setSelected(await res.json());
|
|
360
|
+
} catch { setSelected(schedule); }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function flash(msg) {
|
|
364
|
+
setStatusMsg(msg);
|
|
365
|
+
setTimeout(() => setStatusMsg(''), 3000);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div style={styles.root}>
|
|
370
|
+
{/* Header */}
|
|
371
|
+
<div style={styles.headerBar}>
|
|
372
|
+
<div style={styles.headerLeft}>
|
|
373
|
+
<div style={styles.title}>Schedules</div>
|
|
374
|
+
<span style={styles.headerCount}>
|
|
375
|
+
{schedules.length} schedule{schedules.length !== 1 ? 's' : ''}
|
|
376
|
+
{' \u2022 '}
|
|
377
|
+
{schedules.filter((s) => s.enabled).length} active
|
|
378
|
+
</span>
|
|
379
|
+
</div>
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => { setCreating(true); setSelected(null); }}
|
|
382
|
+
style={styles.createBtn}
|
|
383
|
+
>
|
|
384
|
+
+ New Schedule
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{statusMsg && (
|
|
389
|
+
<div style={styles.statusMsg}>{statusMsg}</div>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
<div style={styles.mainRow}>
|
|
393
|
+
{/* Schedule list */}
|
|
394
|
+
<div style={styles.listArea}>
|
|
395
|
+
{creating && (
|
|
396
|
+
<CreateForm
|
|
397
|
+
onSubmit={handleCreate}
|
|
398
|
+
onCancel={() => setCreating(false)}
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{loading && schedules.length === 0 && (
|
|
403
|
+
<div style={styles.empty}>Loading schedules...</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{!loading && schedules.length === 0 && !creating && (
|
|
407
|
+
<div style={styles.empty}>
|
|
408
|
+
<div style={{ fontSize: 13, marginBottom: 8 }}>No schedules yet</div>
|
|
409
|
+
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>
|
|
410
|
+
Create a schedule to run agents on a recurring basis — daily standups, weekly reports, monitoring checks.
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
{schedules.map((schedule) => (
|
|
416
|
+
<ScheduleRow
|
|
417
|
+
key={schedule.id}
|
|
418
|
+
schedule={schedule}
|
|
419
|
+
onToggle={handleToggle}
|
|
420
|
+
onRun={handleRun}
|
|
421
|
+
onDelete={handleDelete}
|
|
422
|
+
onSelect={handleSelect}
|
|
423
|
+
/>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
{/* Detail panel */}
|
|
428
|
+
{selected && (
|
|
429
|
+
<ScheduleDetail
|
|
430
|
+
schedule={selected}
|
|
431
|
+
onClose={() => setSelected(null)}
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// -- Styles --
|
|
440
|
+
const styles = {
|
|
441
|
+
root: {
|
|
442
|
+
height: '100%', display: 'flex', flexDirection: 'column',
|
|
443
|
+
overflow: 'hidden',
|
|
444
|
+
},
|
|
445
|
+
headerBar: {
|
|
446
|
+
padding: '12px 20px',
|
|
447
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
448
|
+
flexShrink: 0,
|
|
449
|
+
},
|
|
450
|
+
headerLeft: {
|
|
451
|
+
display: 'flex', alignItems: 'center', gap: 12,
|
|
452
|
+
},
|
|
453
|
+
title: {
|
|
454
|
+
fontSize: 15, fontWeight: 700, color: 'var(--text-bright)',
|
|
455
|
+
letterSpacing: 0.3,
|
|
456
|
+
},
|
|
457
|
+
headerCount: {
|
|
458
|
+
fontSize: 11, color: 'var(--text-muted)',
|
|
459
|
+
},
|
|
460
|
+
createBtn: {
|
|
461
|
+
padding: '6px 14px',
|
|
462
|
+
background: 'var(--accent)', color: 'var(--bg-base)',
|
|
463
|
+
border: 'none', borderRadius: 6,
|
|
464
|
+
fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
465
|
+
fontFamily: 'var(--font)',
|
|
466
|
+
},
|
|
467
|
+
statusMsg: {
|
|
468
|
+
padding: '4px 20px', fontSize: 10, color: 'var(--green)',
|
|
469
|
+
flexShrink: 0,
|
|
470
|
+
},
|
|
471
|
+
mainRow: {
|
|
472
|
+
flex: 1, display: 'flex', overflow: 'hidden',
|
|
473
|
+
},
|
|
474
|
+
listArea: {
|
|
475
|
+
flex: 1, overflowY: 'auto', padding: '0 20px 20px',
|
|
476
|
+
},
|
|
477
|
+
empty: {
|
|
478
|
+
padding: '60px 20px', textAlign: 'center',
|
|
479
|
+
color: 'var(--text-dim)', fontSize: 12,
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
// Schedule row
|
|
483
|
+
scheduleRow: {
|
|
484
|
+
display: 'flex', alignItems: 'center',
|
|
485
|
+
padding: '10px 12px',
|
|
486
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
487
|
+
borderRadius: 6, marginBottom: 4, cursor: 'pointer',
|
|
488
|
+
transition: 'background 0.1s',
|
|
489
|
+
},
|
|
490
|
+
scheduleMain: {
|
|
491
|
+
display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0,
|
|
492
|
+
},
|
|
493
|
+
scheduleName: {
|
|
494
|
+
fontSize: 12, fontWeight: 600, color: 'var(--text-bright)',
|
|
495
|
+
},
|
|
496
|
+
scheduleMeta: {
|
|
497
|
+
display: 'flex', gap: 10, fontSize: 10, color: 'var(--text-dim)',
|
|
498
|
+
marginTop: 2,
|
|
499
|
+
},
|
|
500
|
+
scheduleActions: {
|
|
501
|
+
display: 'flex', gap: 4, flexShrink: 0,
|
|
502
|
+
},
|
|
503
|
+
actionBtn: {
|
|
504
|
+
padding: '3px 8px',
|
|
505
|
+
background: 'transparent', border: '1px solid var(--border)',
|
|
506
|
+
borderRadius: 4, fontSize: 9, fontWeight: 600,
|
|
507
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
// Create form
|
|
511
|
+
form: {
|
|
512
|
+
padding: '16px',
|
|
513
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
514
|
+
borderRadius: 8, marginBottom: 12,
|
|
515
|
+
},
|
|
516
|
+
formTitle: {
|
|
517
|
+
fontSize: 13, fontWeight: 700, color: 'var(--text-bright)',
|
|
518
|
+
marginBottom: 12,
|
|
519
|
+
},
|
|
520
|
+
label: {
|
|
521
|
+
display: 'block', fontSize: 9, fontWeight: 700,
|
|
522
|
+
color: 'var(--text-muted)', letterSpacing: 0.5,
|
|
523
|
+
textTransform: 'uppercase', marginTop: 10, marginBottom: 4,
|
|
524
|
+
},
|
|
525
|
+
input: {
|
|
526
|
+
width: '100%', padding: '7px 10px',
|
|
527
|
+
background: 'var(--bg-base)', border: '1px solid var(--border)',
|
|
528
|
+
borderRadius: 5, color: 'var(--text-primary)', fontSize: 12,
|
|
529
|
+
fontFamily: 'var(--font)', outline: 'none',
|
|
530
|
+
boxSizing: 'border-box',
|
|
531
|
+
},
|
|
532
|
+
textarea: {
|
|
533
|
+
width: '100%', padding: '7px 10px',
|
|
534
|
+
background: 'var(--bg-base)', border: '1px solid var(--border)',
|
|
535
|
+
borderRadius: 5, color: 'var(--text-primary)', fontSize: 12,
|
|
536
|
+
fontFamily: 'var(--font)', outline: 'none', resize: 'vertical',
|
|
537
|
+
boxSizing: 'border-box',
|
|
538
|
+
},
|
|
539
|
+
presetRow: {
|
|
540
|
+
display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 6,
|
|
541
|
+
},
|
|
542
|
+
presetBtn: {
|
|
543
|
+
padding: '3px 10px',
|
|
544
|
+
background: 'transparent', border: '1px solid var(--border)',
|
|
545
|
+
borderRadius: 10, fontSize: 10, fontWeight: 500,
|
|
546
|
+
fontFamily: 'var(--font)', cursor: 'pointer',
|
|
547
|
+
transition: 'all 0.1s',
|
|
548
|
+
},
|
|
549
|
+
error: {
|
|
550
|
+
fontSize: 11, color: 'var(--red)', marginTop: 8,
|
|
551
|
+
},
|
|
552
|
+
formActions: {
|
|
553
|
+
display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 14,
|
|
554
|
+
},
|
|
555
|
+
cancelBtn: {
|
|
556
|
+
padding: '6px 14px',
|
|
557
|
+
background: 'transparent', color: 'var(--text-dim)',
|
|
558
|
+
border: '1px solid var(--border)', borderRadius: 5,
|
|
559
|
+
fontSize: 11, fontWeight: 500, cursor: 'pointer',
|
|
560
|
+
fontFamily: 'var(--font)',
|
|
561
|
+
},
|
|
562
|
+
submitBtn: {
|
|
563
|
+
padding: '6px 14px',
|
|
564
|
+
background: 'var(--accent)', color: 'var(--bg-base)',
|
|
565
|
+
border: 'none', borderRadius: 5,
|
|
566
|
+
fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
567
|
+
fontFamily: 'var(--font)',
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// Detail panel
|
|
571
|
+
detailPanel: {
|
|
572
|
+
width: 320, flexShrink: 0,
|
|
573
|
+
borderLeft: '1px solid var(--border)',
|
|
574
|
+
padding: '16px', overflowY: 'auto',
|
|
575
|
+
},
|
|
576
|
+
detailHeader: {
|
|
577
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
578
|
+
marginBottom: 16,
|
|
579
|
+
},
|
|
580
|
+
detailTitle: {
|
|
581
|
+
fontSize: 14, fontWeight: 700, color: 'var(--text-bright)',
|
|
582
|
+
},
|
|
583
|
+
detailClose: {
|
|
584
|
+
background: 'none', border: 'none',
|
|
585
|
+
color: 'var(--text-muted)', fontSize: 18,
|
|
586
|
+
cursor: 'pointer', fontFamily: 'var(--font)',
|
|
587
|
+
},
|
|
588
|
+
detailGrid: {
|
|
589
|
+
display: 'grid', gridTemplateColumns: '80px 1fr', gap: '6px 10px',
|
|
590
|
+
},
|
|
591
|
+
detailLabel: {
|
|
592
|
+
fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
|
593
|
+
textTransform: 'uppercase',
|
|
594
|
+
},
|
|
595
|
+
detailValue: {
|
|
596
|
+
fontSize: 11, color: 'var(--text-primary)',
|
|
597
|
+
},
|
|
598
|
+
promptPre: {
|
|
599
|
+
fontSize: 11, color: 'var(--text-primary)', lineHeight: 1.5,
|
|
600
|
+
padding: '8px 10px', background: 'var(--bg-base)',
|
|
601
|
+
border: '1px solid var(--border)', borderRadius: 4,
|
|
602
|
+
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
603
|
+
fontFamily: 'var(--font)',
|
|
604
|
+
maxHeight: 200, overflowY: 'auto', marginTop: 4,
|
|
605
|
+
},
|
|
606
|
+
historyList: {
|
|
607
|
+
display: 'flex', flexDirection: 'column', gap: 4,
|
|
608
|
+
},
|
|
609
|
+
historyEntry: {
|
|
610
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
611
|
+
padding: '4px 8px', borderRadius: 4,
|
|
612
|
+
background: 'var(--bg-base)',
|
|
613
|
+
},
|
|
614
|
+
};
|