synabun 2026.4.21 → 2026.4.25-beta.2
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/CHANGELOG.md +175 -881
- package/README.md +1 -1
- package/hooks/claude-code/prompt-submit.mjs +39 -18
- package/hooks/claude-code/session-start.mjs +10 -3
- package/hooks/claude-code/shared.mjs +39 -2
- package/hooks/claude-code/stop.mjs +33 -6
- package/mcp-server/src/index.ts +9 -1
- package/mcp-server/src/services/neural-interface.ts +42 -14
- package/mcp-server/src/services/profiles.ts +8 -7
- package/mcp-server/src/tools/browser-advanced.ts +12 -10
- package/mcp-server/src/tools/browser-interact.ts +39 -38
- package/mcp-server/src/tools/browser-navigate.ts +12 -4
- package/mcp-server/src/tools/loop.ts +7 -3
- package/mcp-server/src/tools/profile.ts +19 -1
- package/neural-interface/lib/loop-logger.js +103 -0
- package/neural-interface/lib/mcp-installer.js +109 -0
- package/neural-interface/lib/plugin-installer.js +264 -0
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/api.js +66 -17
- package/neural-interface/public/shared/cdx/cdx-icons.js +5 -4
- package/neural-interface/public/shared/cdx/cdx-panel.js +117 -14
- package/neural-interface/public/shared/cdx/cdx-render.js +170 -43
- package/neural-interface/public/shared/cdx/cdx-styles.js +55 -10
- package/neural-interface/public/shared/cdx/cdx-tabs.js +394 -72
- package/neural-interface/public/shared/cli-status.js +157 -0
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/html-shell.js +32 -11
- package/neural-interface/public/shared/ocp/ocp-panel.js +127 -28
- package/neural-interface/public/shared/ocp/ocp-render.js +52 -44
- package/neural-interface/public/shared/ocp/ocp-styles.js +33 -0
- package/neural-interface/public/shared/ocp/ocp-tabs.js +693 -82
- package/neural-interface/public/shared/styles.css +635 -0
- package/neural-interface/public/shared/ui-automation-studio.js +38 -12
- package/neural-interface/public/shared/ui-claude-panel.js +322 -15
- package/neural-interface/public/shared/ui-file-explorer.js +38 -18
- package/neural-interface/public/shared/ui-menubar.js +34 -10
- package/neural-interface/public/shared/ui-navbar.js +8 -6
- package/neural-interface/public/shared/ui-schedules-studio.js +1464 -0
- package/neural-interface/public/shared/ui-settings.js +274 -50
- package/neural-interface/public/shared/ui-sidepanel-layout.js +18 -7
- package/neural-interface/public/shared/ui-sidepanel-tray.js +287 -0
- package/neural-interface/public/shared/ui-sync.js +9 -5
- package/neural-interface/public/shared/ui-terminal.js +76 -27
- package/neural-interface/public/shared/ui-tool-updates.js +49 -15
- package/neural-interface/public/shared/ui-tutorial-steps.js +4 -2
- package/neural-interface/public/shared/ui-tutorial.js +24 -18
- package/neural-interface/public/shared/ui-whiteboard.js +79 -31
- package/neural-interface/public/shared/vterm-buffer.js +9 -8
- package/neural-interface/public/variant/2d/main.js +9 -7
- package/neural-interface/public/variant/3d/main.js +10 -8
- package/neural-interface/server.js +1292 -255
- package/package.json +1 -1
- package/skills/leonardo/SKILL.md +5 -1
- package/skills/leonardo/modules/image-prompter.md +5 -1
- package/skills/leonardo/modules/video-prompter.md +5 -1
- package/skills/synabun/SKILL.md +44 -26
- package/skills/synabun/modules/audit.md +40 -36
- package/skills/synabun/modules/changelog.md +22 -18
- package/skills/synabun/modules/memorize.md +19 -15
- package/skills/synabun/modules/schedule.md +285 -0
- package/CLAUDE.md +0 -217
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
// SynaBun Neural Interface - Dedicated Schedules Studio
|
|
2
|
+
|
|
3
|
+
import { emit, on } from './state.js';
|
|
4
|
+
import { storage } from './storage.js';
|
|
5
|
+
import { isGuest, hasPermission, showGuestToast } from './ui-sync.js';
|
|
6
|
+
import { sendToPanel } from './ui-claude-panel.js';
|
|
7
|
+
import { getProviderMeta } from './provider-icons.js';
|
|
8
|
+
import {
|
|
9
|
+
fetchLoopTemplates,
|
|
10
|
+
fetchSchedules,
|
|
11
|
+
createSchedule,
|
|
12
|
+
updateSchedule,
|
|
13
|
+
deleteSchedule,
|
|
14
|
+
testSchedule,
|
|
15
|
+
startScheduleTimer,
|
|
16
|
+
cancelScheduleTimer,
|
|
17
|
+
fetchScheduleTimers,
|
|
18
|
+
fetchScheduleGroups,
|
|
19
|
+
createScheduleGroup,
|
|
20
|
+
updateScheduleGroup,
|
|
21
|
+
deleteScheduleGroup,
|
|
22
|
+
reorderScheduleGroups,
|
|
23
|
+
createQuickTimer,
|
|
24
|
+
fetchQuickTimers,
|
|
25
|
+
cancelQuickTimer,
|
|
26
|
+
triggerQuickTimerNow,
|
|
27
|
+
} from './api.js';
|
|
28
|
+
|
|
29
|
+
const $ = (id) => document.getElementById(id);
|
|
30
|
+
const PANEL_ID = 'schedules-studio-panel';
|
|
31
|
+
const POLL_INTERVAL = 1000;
|
|
32
|
+
|
|
33
|
+
let _panel = null;
|
|
34
|
+
let _backdrop = null;
|
|
35
|
+
let _docListeners = [];
|
|
36
|
+
let _templates = [];
|
|
37
|
+
let _schedules = [];
|
|
38
|
+
let _groups = [];
|
|
39
|
+
let _scheduleTimerData = {};
|
|
40
|
+
let _quickTimers = [];
|
|
41
|
+
let _tab = 'cron';
|
|
42
|
+
let _selectedGroup = 'all';
|
|
43
|
+
let _scheduleEditor = null;
|
|
44
|
+
let _draggingScheduleId = null;
|
|
45
|
+
let _pollTimer = null;
|
|
46
|
+
let _selectedQtMinutes = null;
|
|
47
|
+
let _qtUsesBrowser = null;
|
|
48
|
+
let _syncReady = false;
|
|
49
|
+
let _promptModal = null;
|
|
50
|
+
let _promptBackdrop = null;
|
|
51
|
+
let _promptKeyHandler = null;
|
|
52
|
+
|
|
53
|
+
let _launchProfile = storage.getItem('as-launch-profile') || 'claude-code';
|
|
54
|
+
let _launchModel = storage.getItem('as-launch-model') || null;
|
|
55
|
+
let _launchEffort = storage.getItem('as-launch-effort') || 'off';
|
|
56
|
+
|
|
57
|
+
let _opencodeModelsCache = null;
|
|
58
|
+
let _opencodeModelsLoading = null;
|
|
59
|
+
|
|
60
|
+
const _s = (d) => `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`;
|
|
61
|
+
|
|
62
|
+
const ICONS = {
|
|
63
|
+
clock: _s('<circle cx="8" cy="8" r="6"/><path d="M8 4.5V8l2.5 2.5"/>'),
|
|
64
|
+
plus: _s('<path d="M8 3v10M3 8h10"/>'),
|
|
65
|
+
bolt: _s('<path d="M9 2L4 9h4l-1 5 5-7H8l1-5z"/>'),
|
|
66
|
+
edit: _s('<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z"/>'),
|
|
67
|
+
script: _s('<path d="M3.5 2h6.5l3 3v8.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5z"/><path d="M5.5 7h5M5.5 9.5h5M5.5 12h3"/>'),
|
|
68
|
+
close: _s('<path d="M4 4l8 8M12 4l-8 8"/>'),
|
|
69
|
+
pause: _s('<path d="M5 3v10M11 3v10"/>'),
|
|
70
|
+
play: _s('<path d="M5 3l8 5-8 5V3z"/>'),
|
|
71
|
+
back: _s('<path d="M10 3L5 8l5 5"/>'),
|
|
72
|
+
down: _s('<path d="M3 6l5 5 5-5"/>'),
|
|
73
|
+
folder: _s('<path d="M2.5 5h4l1 1.5h6v6a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/>'),
|
|
74
|
+
list: _s('<path d="M5 4h8M5 8h8M5 12h8"/><path d="M2.5 4h.01M2.5 8h.01M2.5 12h.01"/>'),
|
|
75
|
+
eye: _s('<path d="M1.5 8s2.5-4 6.5-4 6.5 4 6.5 4-2.5 4-6.5 4-6.5-4-6.5-4z"/><circle cx="8" cy="8" r="2"/>'),
|
|
76
|
+
sparkle: _s('<path d="M8 2L9.2 6.8L14 8L9.2 9.2L8 14L6.8 9.2L2 8L6.8 6.8L8 2z"/>'),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const CLI_PROFILES = [
|
|
80
|
+
{ id: 'claude-code', label: 'Claude Code', desc: 'Anthropic' },
|
|
81
|
+
{ id: 'codex', label: 'Codex CLI', desc: 'OpenAI' },
|
|
82
|
+
{ id: 'gemini', label: 'Gemini CLI', desc: 'Google' },
|
|
83
|
+
{ id: 'opencode', label: 'OpenCode CLI', desc: 'Multi-provider' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const CLI_MODELS = {
|
|
87
|
+
'claude-code': [
|
|
88
|
+
{ id: 'claude-opus-4-7', label: 'Opus 4.7', desc: 'Most capable' },
|
|
89
|
+
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: 'Balanced', tier: 'default' },
|
|
90
|
+
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: 'Fastest' },
|
|
91
|
+
],
|
|
92
|
+
codex: [
|
|
93
|
+
{ id: 'gpt-5.5', label: 'GPT-5.5', desc: 'Most capable' },
|
|
94
|
+
{ id: 'gpt-5.4', label: 'GPT-5.4', desc: 'Strong general' },
|
|
95
|
+
{ id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', desc: 'Fast and cheap', tier: 'default' },
|
|
96
|
+
{ id: 'gpt-5.3-codex', label: 'GPT-5.3 Codex', desc: 'Codex-tuned' },
|
|
97
|
+
],
|
|
98
|
+
gemini: [
|
|
99
|
+
{ id: 'gemini-2.5-pro', label: '2.5 Pro', desc: 'Most capable', tier: 'default' },
|
|
100
|
+
{ id: 'gemini-2.5-flash', label: '2.5 Flash', desc: 'Lightweight' },
|
|
101
|
+
],
|
|
102
|
+
opencode: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const EFFORT_LEVELS = [
|
|
106
|
+
{ id: 'off', label: 'Off' },
|
|
107
|
+
{ id: 'low', label: 'Low' },
|
|
108
|
+
{ id: 'medium', label: 'Med' },
|
|
109
|
+
{ id: 'high', label: 'High' },
|
|
110
|
+
{ id: 'max', label: 'Max' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const CRON_PRESET_GROUPS = [
|
|
114
|
+
{ group: 'Frequency', presets: [
|
|
115
|
+
{ label: 'Every 15 Min', cron: '*/15 * * * *', desc: 'Every 15 minutes' },
|
|
116
|
+
{ label: 'Every 30 Min', cron: '*/30 * * * *', desc: 'Every 30 minutes' },
|
|
117
|
+
{ label: 'Every Hour', cron: '0 * * * *', desc: 'Every hour at :00' },
|
|
118
|
+
{ label: 'Every 2 Hours', cron: '0 */2 * * *', desc: 'Every 2 hours at :00' },
|
|
119
|
+
{ label: 'Every 6 Hours', cron: '0 */6 * * *', desc: 'Every 6 hours at :00' },
|
|
120
|
+
] },
|
|
121
|
+
{ group: 'Daily', presets: [
|
|
122
|
+
{ label: 'Morning 9am', cron: '0 9 * * *', desc: 'Every day at 09:00' },
|
|
123
|
+
{ label: 'Midday 12pm', cron: '0 12 * * *', desc: 'Every day at 12:00' },
|
|
124
|
+
{ label: 'Evening 6pm', cron: '0 18 * * *', desc: 'Every day at 18:00' },
|
|
125
|
+
{ label: '2x Daily', cron: '0 10,18 * * *', desc: 'Every day at 10:00 and 18:00' },
|
|
126
|
+
{ label: '3x Daily', cron: '0 9,14,19 * * *', desc: 'Every day at 09:00, 14:00, 19:00' },
|
|
127
|
+
] },
|
|
128
|
+
{ group: 'Weekly', presets: [
|
|
129
|
+
{ label: 'Weekdays 9am', cron: '0 9 * * 1-5', desc: 'Mon-Fri at 09:00' },
|
|
130
|
+
{ label: 'Weekdays 2x', cron: '0 10,18 * * 1-5', desc: 'Mon-Fri at 10:00 and 18:00' },
|
|
131
|
+
{ label: 'Weekends 10am', cron: '0 10 * * 0,6', desc: 'Sat-Sun at 10:00' },
|
|
132
|
+
{ label: 'Mon/Wed/Fri', cron: '0 9 * * 1,3,5', desc: 'Mon, Wed, Fri at 09:00' },
|
|
133
|
+
{ label: 'Tue/Thu', cron: '0 9 * * 2,4', desc: 'Tue, Thu at 09:00' },
|
|
134
|
+
] },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
138
|
+
|
|
139
|
+
function esc(value) {
|
|
140
|
+
return String(value ?? '').replace(/[&<>"']/g, (c) => ({
|
|
141
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
142
|
+
}[c]));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function showToast(msg) {
|
|
146
|
+
const existing = document.querySelector('.as-toast');
|
|
147
|
+
if (existing) existing.remove();
|
|
148
|
+
const toast = document.createElement('div');
|
|
149
|
+
toast.className = 'as-toast';
|
|
150
|
+
toast.textContent = msg;
|
|
151
|
+
document.body.appendChild(toast);
|
|
152
|
+
requestAnimationFrame(() => toast.classList.add('show'));
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
toast.classList.remove('show');
|
|
155
|
+
setTimeout(() => toast.remove(), 180);
|
|
156
|
+
}, 2500);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getTemplate(id) {
|
|
160
|
+
return _templates.find(t => t.id === id) || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getGroup(id) {
|
|
164
|
+
return _groups.find(g => g.id === id) || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getModelsForProfile(profile) {
|
|
168
|
+
if (profile === 'opencode') return _opencodeModelsCache || [];
|
|
169
|
+
return CLI_MODELS[profile] || [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function fetchOpencodeModels(force = false) {
|
|
173
|
+
if (!force && _opencodeModelsCache) return _opencodeModelsCache;
|
|
174
|
+
if (_opencodeModelsLoading) return _opencodeModelsLoading;
|
|
175
|
+
_opencodeModelsLoading = (async () => {
|
|
176
|
+
try {
|
|
177
|
+
const resp = await fetch('/api/opencode/providers/full');
|
|
178
|
+
const data = await resp.json().catch(() => ({}));
|
|
179
|
+
if (!data || data.ok === false) return [];
|
|
180
|
+
const raw = data.data || {};
|
|
181
|
+
const all = Array.isArray(raw.all) ? raw.all : (Array.isArray(raw) ? raw : []);
|
|
182
|
+
_opencodeModelsCache = all.map(m => ({
|
|
183
|
+
id: m.id || m.model || m.name,
|
|
184
|
+
label: m.name || m.label || m.id || m.model,
|
|
185
|
+
desc: m.provider || m.desc || '',
|
|
186
|
+
})).filter(m => m.id);
|
|
187
|
+
return _opencodeModelsCache;
|
|
188
|
+
} catch {
|
|
189
|
+
_opencodeModelsCache = [];
|
|
190
|
+
return [];
|
|
191
|
+
} finally {
|
|
192
|
+
_opencodeModelsLoading = null;
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
return _opencodeModelsLoading;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function describeCronClient(cronStr) {
|
|
199
|
+
const fields = String(cronStr || '').trim().split(/\s+/);
|
|
200
|
+
if (fields.length !== 5) return cronStr || '';
|
|
201
|
+
const [minute, hour, , , dayOfWeek] = fields;
|
|
202
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
203
|
+
let dayPart = '';
|
|
204
|
+
if (dayOfWeek === '*') dayPart = 'Every day';
|
|
205
|
+
else if (dayOfWeek === '1-5') dayPart = 'Weekdays';
|
|
206
|
+
else if (dayOfWeek === '0,6') dayPart = 'Weekends';
|
|
207
|
+
else dayPart = dayOfWeek.split(',').map(d => DAY_NAMES[+d] || d).join(', ');
|
|
208
|
+
|
|
209
|
+
const minStep = minute.match(/^\*\/(\d+)$/);
|
|
210
|
+
if (minStep && hour === '*') return `${dayPart}, every ${minStep[1]} minutes`;
|
|
211
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
212
|
+
if (hourStep) return `${dayPart}, every ${hourStep[1]} hours at :${pad(+minute)}`;
|
|
213
|
+
if (hour.includes(',')) return `${dayPart} at ${hour.split(',').map(h => `${pad(+h)}:${pad(+minute)}`).join(', ')}`;
|
|
214
|
+
if (hour === '*') return `${dayPart}, every hour at :${pad(+minute)}`;
|
|
215
|
+
return `${dayPart} at ${pad(+hour)}:${pad(+minute)}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatNextRun(nextRun) {
|
|
219
|
+
if (!nextRun) return 'N/A';
|
|
220
|
+
const diffMs = new Date(nextRun) - Date.now();
|
|
221
|
+
if (diffMs <= 0) return 'Overdue';
|
|
222
|
+
const totalSec = Math.floor(diffMs / 1000);
|
|
223
|
+
const days = Math.floor(totalSec / 86400);
|
|
224
|
+
const hours = Math.floor((totalSec % 86400) / 3600);
|
|
225
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
226
|
+
const seconds = totalSec % 60;
|
|
227
|
+
if (days > 0) return hours ? `${days}d ${hours}h` : `${days}d`;
|
|
228
|
+
if (hours > 0) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
229
|
+
if (minutes > 0) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
230
|
+
return `${seconds}s`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatDateTime(value) {
|
|
234
|
+
if (!value) return 'Never';
|
|
235
|
+
try { return new Date(value).toLocaleString(); } catch { return value; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatTimerCountdown(firesAtISO) {
|
|
239
|
+
const diff = new Date(firesAtISO) - Date.now();
|
|
240
|
+
if (diff <= 0) return 'firing';
|
|
241
|
+
const mins = Math.ceil(diff / 60_000);
|
|
242
|
+
if (mins >= 60) {
|
|
243
|
+
const h = Math.floor(mins / 60);
|
|
244
|
+
const m = mins % 60;
|
|
245
|
+
return m ? `${h}h ${m}m` : `${h}h`;
|
|
246
|
+
}
|
|
247
|
+
return `${mins}m`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scheduleStatusBadge(schedule) {
|
|
251
|
+
if (!schedule.enabled) return '<span class="sched-badge sched-badge--paused">Paused</span>';
|
|
252
|
+
if (schedule.lastRunResult === 'template_missing') return '<span class="sched-badge sched-badge--error">Missing Template</span>';
|
|
253
|
+
if (schedule.lastRunResult?.startsWith('error:')) return '<span class="sched-badge sched-badge--error">Error</span>';
|
|
254
|
+
return '<span class="sched-badge sched-badge--ok">Active</span>';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function initSchedulesStudio() {
|
|
258
|
+
on('automations:open-schedules', (opts) => openPanel(opts || {}));
|
|
259
|
+
on('schedules:open', (opts) => openPanel(opts || {}));
|
|
260
|
+
setupSync();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function openPanel(opts = {}) {
|
|
264
|
+
if (isGuest() && !hasPermission('automations')) {
|
|
265
|
+
showGuestToast('Schedules are disabled by the host');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (_panel) {
|
|
269
|
+
applyOpenOptions(opts);
|
|
270
|
+
_panel.focus();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_backdrop = document.createElement('div');
|
|
275
|
+
_backdrop.className = 'studio-backdrop';
|
|
276
|
+
document.body.appendChild(_backdrop);
|
|
277
|
+
|
|
278
|
+
_panel = document.createElement('div');
|
|
279
|
+
_panel.className = 'schedules-studio-panel glass resizable';
|
|
280
|
+
_panel.id = PANEL_ID;
|
|
281
|
+
_panel.innerHTML = buildPanelHTML();
|
|
282
|
+
document.body.appendChild(_panel);
|
|
283
|
+
_panel.style.left = Math.max(20, (window.innerWidth - 980) / 2) + 'px';
|
|
284
|
+
_panel.style.top = Math.max(48, (window.innerHeight - 620) / 2) + 'px';
|
|
285
|
+
|
|
286
|
+
wirePanel();
|
|
287
|
+
await loadData();
|
|
288
|
+
applyOpenOptions(opts);
|
|
289
|
+
render();
|
|
290
|
+
requestAnimationFrame(() => {
|
|
291
|
+
_backdrop?.classList.add('open');
|
|
292
|
+
_panel?.classList.add('open');
|
|
293
|
+
});
|
|
294
|
+
startPolling();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function applyOpenOptions(opts = {}) {
|
|
298
|
+
if (opts.tab) _tab = opts.tab;
|
|
299
|
+
if (opts.groupId) _selectedGroup = opts.groupId;
|
|
300
|
+
if (opts.action === 'new-schedule') {
|
|
301
|
+
_scheduleEditor = {};
|
|
302
|
+
_tab = 'cron';
|
|
303
|
+
} else if (opts.action === 'new-group') {
|
|
304
|
+
_tab = 'groups';
|
|
305
|
+
_scheduleEditor = null;
|
|
306
|
+
} else if (opts.action === 'quick-timer') {
|
|
307
|
+
_tab = 'timers';
|
|
308
|
+
_scheduleEditor = null;
|
|
309
|
+
}
|
|
310
|
+
if (_panel) render();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function closePanel() {
|
|
314
|
+
if (!_panel) return;
|
|
315
|
+
stopPolling();
|
|
316
|
+
closePromptScheduleModal();
|
|
317
|
+
for (const [evt, fn] of _docListeners) document.removeEventListener(evt, fn);
|
|
318
|
+
_docListeners = [];
|
|
319
|
+
_backdrop?.remove();
|
|
320
|
+
_backdrop = null;
|
|
321
|
+
_panel.remove();
|
|
322
|
+
_panel = null;
|
|
323
|
+
_scheduleEditor = null;
|
|
324
|
+
_draggingScheduleId = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildPanelHTML() {
|
|
328
|
+
return `
|
|
329
|
+
<div class="resize-handle resize-handle-t" data-resize="t"></div>
|
|
330
|
+
<div class="resize-handle resize-handle-r" data-resize="r"></div>
|
|
331
|
+
<div class="resize-handle resize-handle-b" data-resize="b"></div>
|
|
332
|
+
<div class="resize-handle resize-handle-l" data-resize="l"></div>
|
|
333
|
+
<div class="resize-handle resize-handle-tl" data-resize="tl"></div>
|
|
334
|
+
<div class="resize-handle resize-handle-tr" data-resize="tr"></div>
|
|
335
|
+
<div class="resize-handle resize-handle-bl" data-resize="bl"></div>
|
|
336
|
+
<div class="resize-handle resize-handle-br" data-resize="br"></div>
|
|
337
|
+
|
|
338
|
+
<div class="sched-header drag-handle" data-drag="${PANEL_ID}">
|
|
339
|
+
<div class="sched-header-left">
|
|
340
|
+
<span class="sched-header-icon">${ICONS.clock}</span>
|
|
341
|
+
<h3>Schedules</h3>
|
|
342
|
+
<span class="sched-count" id="sched-total-count"></span>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="sched-header-actions">
|
|
345
|
+
<button class="sched-header-btn" data-action="schedule-new">${ICONS.plus} New Schedule</button>
|
|
346
|
+
<button class="sched-header-btn" data-action="group-new">${ICONS.folder} New Group</button>
|
|
347
|
+
</div>
|
|
348
|
+
<button class="backdrop-toggle-btn" id="sched-backdrop-toggle" data-tooltip="Toggle backdrop">${ICONS.eye}</button>
|
|
349
|
+
<button class="sched-close" id="sched-close">×</button>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="sched-body">
|
|
353
|
+
<aside class="sched-sidebar" id="sched-sidebar"></aside>
|
|
354
|
+
<main class="sched-main" id="sched-main"></main>
|
|
355
|
+
</div>
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function wirePanel() {
|
|
360
|
+
$('sched-close')?.addEventListener('click', closePanel);
|
|
361
|
+
$('sched-backdrop-toggle')?.addEventListener('click', () => {
|
|
362
|
+
_backdrop?.classList.toggle('backdrop-hidden');
|
|
363
|
+
$('sched-backdrop-toggle')?.classList.toggle('active', _backdrop?.classList.contains('backdrop-hidden'));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
_panel.addEventListener('click', handleClick);
|
|
367
|
+
_panel.addEventListener('change', handleChange);
|
|
368
|
+
_panel.addEventListener('dragstart', handleDragStart);
|
|
369
|
+
_panel.addEventListener('dragend', handleDragEnd);
|
|
370
|
+
_panel.addEventListener('dragover', handleDragOver);
|
|
371
|
+
_panel.addEventListener('drop', handleDrop);
|
|
372
|
+
|
|
373
|
+
const onEsc = (e) => {
|
|
374
|
+
if (e.key !== 'Escape' || !_panel) return;
|
|
375
|
+
if (_promptModal) return;
|
|
376
|
+
if (_scheduleEditor) {
|
|
377
|
+
_scheduleEditor = null;
|
|
378
|
+
render();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
closePanel();
|
|
382
|
+
};
|
|
383
|
+
document.addEventListener('keydown', onEsc);
|
|
384
|
+
_docListeners.push(['keydown', onEsc]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function loadData() {
|
|
388
|
+
try { _templates = await fetchLoopTemplates(); } catch { _templates = []; }
|
|
389
|
+
try { _schedules = await fetchSchedules(); } catch { _schedules = []; }
|
|
390
|
+
try { _groups = await fetchScheduleGroups(); } catch { _groups = []; }
|
|
391
|
+
try { _scheduleTimerData = await fetchScheduleTimers(); } catch { _scheduleTimerData = {}; }
|
|
392
|
+
try { _quickTimers = await fetchQuickTimers(); } catch { _quickTimers = []; }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function startPolling() {
|
|
396
|
+
stopPolling();
|
|
397
|
+
_pollTimer = setInterval(() => {
|
|
398
|
+
if (!_panel) { stopPolling(); return; }
|
|
399
|
+
updateTimerCountdowns();
|
|
400
|
+
}, POLL_INTERVAL);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function stopPolling() {
|
|
404
|
+
if (_pollTimer) clearInterval(_pollTimer);
|
|
405
|
+
_pollTimer = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function render() {
|
|
409
|
+
if (!_panel) return;
|
|
410
|
+
const count = $('sched-total-count');
|
|
411
|
+
if (count) count.textContent = String(_schedules.length);
|
|
412
|
+
renderSidebar();
|
|
413
|
+
if (_scheduleEditor) renderScheduleEditor();
|
|
414
|
+
else renderMain();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderSidebar() {
|
|
418
|
+
const sidebar = $('sched-sidebar');
|
|
419
|
+
if (!sidebar) return;
|
|
420
|
+
|
|
421
|
+
const ungroupedCount = _schedules.filter(s => !s.groupId || !getGroup(s.groupId)).length;
|
|
422
|
+
const activeCount = _schedules.filter(s => s.enabled).length;
|
|
423
|
+
const groupRows = _groups.map(group => {
|
|
424
|
+
const count = _schedules.filter(s => s.groupId === group.id).length;
|
|
425
|
+
return `
|
|
426
|
+
<button class="sched-group-row${_selectedGroup === group.id ? ' active' : ''}" data-action="select-group" data-group-id="${esc(group.id)}" data-drop-group="${esc(group.id)}">
|
|
427
|
+
<span class="sched-group-color" style="background:${esc(group.color || 'rgba(255,255,255,0.22)')}"></span>
|
|
428
|
+
<span class="sched-group-name">${esc(group.name)}</span>
|
|
429
|
+
<span class="sched-group-count">${count}</span>
|
|
430
|
+
</button>`;
|
|
431
|
+
}).join('');
|
|
432
|
+
|
|
433
|
+
sidebar.innerHTML = `
|
|
434
|
+
<div class="sched-sidebar-summary">
|
|
435
|
+
<div><span>${activeCount}</span><label>Active</label></div>
|
|
436
|
+
<div><span>${_groups.length}</span><label>Groups</label></div>
|
|
437
|
+
</div>
|
|
438
|
+
<button class="sched-group-row${_selectedGroup === 'all' ? ' active' : ''}" data-action="select-group" data-group-id="all" data-drop-group="">
|
|
439
|
+
<span class="sched-group-icon">${ICONS.list}</span>
|
|
440
|
+
<span class="sched-group-name">All Schedules</span>
|
|
441
|
+
<span class="sched-group-count">${_schedules.length}</span>
|
|
442
|
+
</button>
|
|
443
|
+
<button class="sched-group-row${_selectedGroup === 'ungrouped' ? ' active' : ''}" data-action="select-group" data-group-id="ungrouped" data-drop-group="">
|
|
444
|
+
<span class="sched-group-icon">${ICONS.folder}</span>
|
|
445
|
+
<span class="sched-group-name">Ungrouped</span>
|
|
446
|
+
<span class="sched-group-count">${ungroupedCount}</span>
|
|
447
|
+
</button>
|
|
448
|
+
<div class="sched-sidebar-label">Groups</div>
|
|
449
|
+
<div class="sched-group-list">${groupRows || '<div class="sched-sidebar-empty">No schedule groups yet.</div>'}</div>
|
|
450
|
+
<button class="sched-sidebar-prompt" data-action="schedule-prompt" data-tooltip="Let AI build a schedule for you">${ICONS.sparkle} Prompt a Schedule</button>
|
|
451
|
+
<button class="sched-sidebar-new" data-action="group-new">${ICONS.plus} Create Group</button>
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderMain() {
|
|
456
|
+
const main = $('sched-main');
|
|
457
|
+
if (!main) return;
|
|
458
|
+
const tabs = [
|
|
459
|
+
['cron', 'Cron'],
|
|
460
|
+
['timers', 'Timers'],
|
|
461
|
+
['groups', 'Groups'],
|
|
462
|
+
['activity', 'Activity'],
|
|
463
|
+
].map(([id, label]) => `<button class="sched-tab${_tab === id ? ' active' : ''}" data-action="set-tab" data-tab="${id}">${label}</button>`).join('');
|
|
464
|
+
|
|
465
|
+
main.innerHTML = `
|
|
466
|
+
<div class="sched-tabs">${tabs}</div>
|
|
467
|
+
<div class="sched-content" id="sched-content"></div>
|
|
468
|
+
`;
|
|
469
|
+
|
|
470
|
+
if (_tab === 'cron') renderCronTab();
|
|
471
|
+
else if (_tab === 'timers') renderTimersTab();
|
|
472
|
+
else if (_tab === 'groups') renderGroupsTab();
|
|
473
|
+
else renderActivityTab();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function schedulesForSelectedGroup() {
|
|
477
|
+
if (_selectedGroup === 'all') return [..._schedules];
|
|
478
|
+
if (_selectedGroup === 'ungrouped') return _schedules.filter(s => !s.groupId || !getGroup(s.groupId));
|
|
479
|
+
return _schedules.filter(s => s.groupId === _selectedGroup);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function sortSchedules(list) {
|
|
483
|
+
return [...list].sort((a, b) => {
|
|
484
|
+
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
485
|
+
const an = a.nextRun ? new Date(a.nextRun).getTime() : Infinity;
|
|
486
|
+
const bn = b.nextRun ? new Date(b.nextRun).getTime() : Infinity;
|
|
487
|
+
if (an !== bn) return an - bn;
|
|
488
|
+
return String(a.name || '').localeCompare(String(b.name || ''));
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function renderCronTab() {
|
|
493
|
+
const content = $('sched-content');
|
|
494
|
+
if (!content) return;
|
|
495
|
+
|
|
496
|
+
const active = _schedules.filter(s => s.enabled).length;
|
|
497
|
+
const paused = _schedules.length - active;
|
|
498
|
+
const overdue = _schedules.filter(s => s.enabled && s.nextRun && new Date(s.nextRun) < new Date()).length;
|
|
499
|
+
const title = _selectedGroup === 'all' ? 'All Schedules'
|
|
500
|
+
: _selectedGroup === 'ungrouped' ? 'Ungrouped'
|
|
501
|
+
: getGroup(_selectedGroup)?.name || 'Group';
|
|
502
|
+
|
|
503
|
+
content.innerHTML = `
|
|
504
|
+
<div class="sched-stat-strip">
|
|
505
|
+
<div><span>${active}</span><label>Active</label></div>
|
|
506
|
+
<div><span>${paused}</span><label>Paused</label></div>
|
|
507
|
+
<div><span>${overdue}</span><label>Overdue</label></div>
|
|
508
|
+
<button class="sched-primary" data-action="schedule-new">${ICONS.plus} New Cron</button>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="sched-section-head">
|
|
511
|
+
<h4>${esc(title)}</h4>
|
|
512
|
+
<span>${schedulesForSelectedGroup().length} schedules</span>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="sched-card-list" id="sched-card-list"></div>
|
|
515
|
+
`;
|
|
516
|
+
renderScheduleCards($('sched-card-list'), schedulesForSelectedGroup());
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderScheduleCards(container, list) {
|
|
520
|
+
if (!container) return;
|
|
521
|
+
const schedules = sortSchedules(list);
|
|
522
|
+
if (!schedules.length) {
|
|
523
|
+
container.innerHTML = `
|
|
524
|
+
<div class="sched-empty">
|
|
525
|
+
<div>No schedules here.</div>
|
|
526
|
+
<button class="sched-primary" data-action="schedule-new">${ICONS.plus} Create Schedule</button>
|
|
527
|
+
</div>`;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const groupOptions = [
|
|
532
|
+
'<option value="">Ungrouped</option>',
|
|
533
|
+
..._groups.map(g => `<option value="${esc(g.id)}">%NAME%</option>`.replace('%NAME%', esc(g.name))),
|
|
534
|
+
].join('');
|
|
535
|
+
|
|
536
|
+
container.innerHTML = schedules.map(s => {
|
|
537
|
+
const tpl = getTemplate(s.templateId);
|
|
538
|
+
const group = getGroup(s.groupId);
|
|
539
|
+
const model = s.model ? ` · ${esc(s.model)}` : '';
|
|
540
|
+
const profile = s.profile ? CLI_PROFILES.find(p => p.id === s.profile)?.label || s.profile : 'Template default';
|
|
541
|
+
return `
|
|
542
|
+
<article class="sched-card${s.enabled ? '' : ' is-paused'}" draggable="true" data-schedule-id="${esc(s.id)}">
|
|
543
|
+
<div class="sched-card-main">
|
|
544
|
+
<div class="sched-card-title-row">
|
|
545
|
+
<h4>${esc(s.name)}</h4>
|
|
546
|
+
${scheduleStatusBadge(s)}
|
|
547
|
+
</div>
|
|
548
|
+
<div class="sched-card-meta">${esc(tpl?.name || 'Missing template')} · ${esc(describeCronClient(s.cron))}</div>
|
|
549
|
+
<div class="sched-card-sub">
|
|
550
|
+
<span>Next: <span class="sched-countdown" data-countdown="${esc(s.nextRun || '')}">${esc(formatNextRun(s.nextRun))}</span></span>
|
|
551
|
+
<span>Runs: ${s.runCount || 0}</span>
|
|
552
|
+
<span>${esc(profile)}${model}</span>
|
|
553
|
+
${group ? `<span>${esc(group.name)}</span>` : '<span>Ungrouped</span>'}
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
<div class="sched-card-controls">
|
|
557
|
+
<select class="sched-card-group-select" data-schedule-id="${esc(s.id)}" data-value="${esc(s.groupId || '')}">
|
|
558
|
+
${groupOptions}
|
|
559
|
+
</select>
|
|
560
|
+
<button class="sched-icon-btn" data-action="schedule-toggle" data-id="${esc(s.id)}" data-tooltip="${s.enabled ? 'Pause' : 'Enable'}">${s.enabled ? ICONS.pause : ICONS.play}</button>
|
|
561
|
+
<button class="sched-icon-btn" data-action="schedule-test" data-id="${esc(s.id)}" data-tooltip="Run now">${ICONS.bolt}</button>
|
|
562
|
+
<button class="sched-icon-btn" data-action="schedule-edit-prompt" data-id="${esc(s.id)}" data-template-id="${esc(s.templateId || '')}" data-tooltip="Edit prompt">${ICONS.script}</button>
|
|
563
|
+
<button class="sched-icon-btn" data-action="schedule-edit" data-id="${esc(s.id)}" data-tooltip="Edit schedule">${ICONS.edit}</button>
|
|
564
|
+
<button class="sched-icon-btn danger" data-action="schedule-delete" data-id="${esc(s.id)}" data-tooltip="Delete">${ICONS.close}</button>
|
|
565
|
+
</div>
|
|
566
|
+
</article>`;
|
|
567
|
+
}).join('');
|
|
568
|
+
|
|
569
|
+
container.querySelectorAll('.sched-card-group-select').forEach(select => {
|
|
570
|
+
select.value = select.dataset.value || '';
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderTimersTab() {
|
|
575
|
+
const content = $('sched-content');
|
|
576
|
+
if (!content) return;
|
|
577
|
+
const templateOptions = _templates.map(t => `<option value="${esc(t.id)}">${esc(t.name)}</option>`).join('');
|
|
578
|
+
const scheduleOptions = _schedules.map(s => `<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('');
|
|
579
|
+
const profileOptions = CLI_PROFILES.map(p => `<option value="${p.id}"${p.id === _launchProfile ? ' selected' : ''}>${p.label}</option>`).join('');
|
|
580
|
+
const modelOptions = renderModelOptions(_launchProfile, _launchModel);
|
|
581
|
+
const effortOptions = EFFORT_LEVELS.map(e => `<option value="${e.id}"${e.id === _launchEffort ? ' selected' : ''}>${e.label}</option>`).join('');
|
|
582
|
+
const qtPresetButtons = [5, 15, 30, 60, 120, 240]
|
|
583
|
+
.map(min => `<button class="sched-time-chip${_selectedQtMinutes === min ? ' active' : ''}" data-action="qt-select" data-minutes="${min}">${min >= 60 ? `${min / 60}h` : `${min}m`}</button>`)
|
|
584
|
+
.join('');
|
|
585
|
+
|
|
586
|
+
if (_launchProfile === 'opencode' && !_opencodeModelsCache) {
|
|
587
|
+
fetchOpencodeModels().then(() => { if (_panel && _tab === 'timers') renderTimersTab(); });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
content.innerHTML = `
|
|
591
|
+
${renderActiveTimers()}
|
|
592
|
+
<section class="sched-tool-section">
|
|
593
|
+
<div class="sched-section-head"><h4>Quick Timer</h4><span>Launch a template once after a delay</span></div>
|
|
594
|
+
<div class="sched-form-grid">
|
|
595
|
+
<label class="sched-field wide"><span>Automation</span><select id="sched-qt-template"><option value="">Select automation...</option>${templateOptions}</select></label>
|
|
596
|
+
<label class="sched-field"><span>CLI</span><select id="sched-qt-profile">${profileOptions}</select></label>
|
|
597
|
+
<label class="sched-field"><span>Model</span><select id="sched-qt-model">${modelOptions}</select></label>
|
|
598
|
+
<label class="sched-field"><span>Think</span><select id="sched-qt-effort">${effortOptions}</select></label>
|
|
599
|
+
<label class="sched-toggle"><input type="checkbox" id="sched-qt-browser"><span>Uses browser</span></label>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="sched-time-row">
|
|
602
|
+
${qtPresetButtons}
|
|
603
|
+
<input class="sched-min-input" id="sched-qt-custom-min" type="number" min="1" max="1440" placeholder="min">
|
|
604
|
+
<button class="sched-primary" data-action="qt-go">${ICONS.play} Go</button>
|
|
605
|
+
<button class="sched-secondary" data-action="qt-now">${ICONS.bolt} Run Now</button>
|
|
606
|
+
</div>
|
|
607
|
+
</section>
|
|
608
|
+
<section class="sched-tool-section">
|
|
609
|
+
<div class="sched-section-head"><h4>Saved Schedule Timer</h4><span>Fire an existing cron schedule once after a delay</span></div>
|
|
610
|
+
<div class="sched-form-grid compact">
|
|
611
|
+
<label class="sched-field wide"><span>Schedule</span><select id="sched-existing-timer-schedule"><option value="">Select schedule...</option>${scheduleOptions}</select></label>
|
|
612
|
+
<label class="sched-field"><span>Minutes</span><input id="sched-existing-timer-min" type="number" min="1" max="1440" placeholder="15"></label>
|
|
613
|
+
<button class="sched-primary align-end" data-action="schedule-timer-create">Set Timer</button>
|
|
614
|
+
</div>
|
|
615
|
+
</section>
|
|
616
|
+
`;
|
|
617
|
+
|
|
618
|
+
const qtProfile = $('sched-qt-profile');
|
|
619
|
+
qtProfile?.addEventListener('change', () => {
|
|
620
|
+
_launchProfile = qtProfile.value;
|
|
621
|
+
storage.setItem('as-launch-profile', _launchProfile);
|
|
622
|
+
const models = getModelsForProfile(_launchProfile);
|
|
623
|
+
_launchModel = (models.find(m => m.tier === 'default') || models[0])?.id || '';
|
|
624
|
+
storage.setItem('as-launch-model', _launchModel);
|
|
625
|
+
renderTimersTab();
|
|
626
|
+
});
|
|
627
|
+
$('sched-qt-model')?.addEventListener('change', (e) => {
|
|
628
|
+
_launchModel = e.target.value || null;
|
|
629
|
+
storage.setItem('as-launch-model', _launchModel);
|
|
630
|
+
});
|
|
631
|
+
$('sched-qt-effort')?.addEventListener('change', (e) => {
|
|
632
|
+
_launchEffort = e.target.value || 'off';
|
|
633
|
+
storage.setItem('as-launch-effort', _launchEffort);
|
|
634
|
+
});
|
|
635
|
+
$('sched-qt-browser')?.addEventListener('change', (e) => { _qtUsesBrowser = e.target.checked; });
|
|
636
|
+
$('sched-qt-template')?.addEventListener('change', (e) => {
|
|
637
|
+
const template = getTemplate(e.target.value);
|
|
638
|
+
const browser = $('sched-qt-browser');
|
|
639
|
+
if (browser && template) {
|
|
640
|
+
browser.checked = !!template.usesBrowser;
|
|
641
|
+
_qtUsesBrowser = null;
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function renderModelOptions(profile, selected) {
|
|
647
|
+
const models = getModelsForProfile(profile);
|
|
648
|
+
if (!models.length) return '<option value="" selected>Default</option>';
|
|
649
|
+
const current = selected && models.some(m => m.id === selected) ? selected : '';
|
|
650
|
+
return [
|
|
651
|
+
`<option value=""${!current ? ' selected' : ''}>Template/default</option>`,
|
|
652
|
+
...models.map(m => `<option value="${esc(m.id)}"${m.id === current ? ' selected' : ''}>${esc(m.label || m.id)}</option>`),
|
|
653
|
+
].join('');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function renderActiveTimers() {
|
|
657
|
+
const scheduleTimers = Object.entries(_scheduleTimerData).map(([scheduleId, timer]) => {
|
|
658
|
+
const schedule = _schedules.find(s => s.id === scheduleId);
|
|
659
|
+
return `
|
|
660
|
+
<div class="sched-active-timer">
|
|
661
|
+
<span>${esc(schedule?.name || 'Schedule')}</span>
|
|
662
|
+
<strong>${formatTimerCountdown(timer.firesAt)}</strong>
|
|
663
|
+
<button class="sched-icon-btn" data-action="schedule-timer-cancel" data-id="${esc(scheduleId)}">${ICONS.close}</button>
|
|
664
|
+
</div>`;
|
|
665
|
+
}).join('');
|
|
666
|
+
|
|
667
|
+
const quickTimers = _quickTimers.map(timer => `
|
|
668
|
+
<div class="sched-active-timer">
|
|
669
|
+
<span>${esc(timer.templateName)}</span>
|
|
670
|
+
<strong>${formatTimerCountdown(timer.firesAt)}</strong>
|
|
671
|
+
<button class="sched-icon-btn" data-action="qt-cancel" data-id="${esc(timer.id)}">${ICONS.close}</button>
|
|
672
|
+
</div>
|
|
673
|
+
`).join('');
|
|
674
|
+
|
|
675
|
+
if (!scheduleTimers && !quickTimers) return '';
|
|
676
|
+
return `<section class="sched-active-timers">${quickTimers}${scheduleTimers}</section>`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function renderGroupsTab() {
|
|
680
|
+
const content = $('sched-content');
|
|
681
|
+
if (!content) return;
|
|
682
|
+
const rows = _groups.map((group, idx) => {
|
|
683
|
+
const count = _schedules.filter(s => s.groupId === group.id).length;
|
|
684
|
+
return `
|
|
685
|
+
<article class="sched-group-card" data-group-id="${esc(group.id)}">
|
|
686
|
+
<div class="sched-group-card-main">
|
|
687
|
+
<span class="sched-group-color large" style="background:${esc(group.color || 'rgba(255,255,255,0.22)')}"></span>
|
|
688
|
+
<div>
|
|
689
|
+
<h4>${esc(group.name)}</h4>
|
|
690
|
+
<span>${count} schedules</span>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
<div class="sched-group-card-actions">
|
|
694
|
+
<button class="sched-icon-btn" data-action="group-up" data-id="${esc(group.id)}" ${idx === 0 ? 'disabled' : ''}>Up</button>
|
|
695
|
+
<button class="sched-icon-btn" data-action="group-down" data-id="${esc(group.id)}" ${idx === _groups.length - 1 ? 'disabled' : ''}>Down</button>
|
|
696
|
+
<button class="sched-icon-btn" data-action="group-edit" data-id="${esc(group.id)}">${ICONS.edit}</button>
|
|
697
|
+
<button class="sched-icon-btn danger" data-action="group-delete" data-id="${esc(group.id)}">${ICONS.close}</button>
|
|
698
|
+
</div>
|
|
699
|
+
</article>`;
|
|
700
|
+
}).join('');
|
|
701
|
+
|
|
702
|
+
content.innerHTML = `
|
|
703
|
+
<section class="sched-tool-section">
|
|
704
|
+
<div class="sched-section-head"><h4>Create Group</h4><span>Use groups to categorize schedules freely</span></div>
|
|
705
|
+
<div class="sched-form-grid compact">
|
|
706
|
+
<label class="sched-field wide"><span>Name</span><input id="sched-group-name" type="text" placeholder="e.g. Twitter cadence"></label>
|
|
707
|
+
<label class="sched-field"><span>Color</span><input id="sched-group-color" type="color" value="#4fc3f7"></label>
|
|
708
|
+
<button class="sched-primary align-end" data-action="group-save-new">${ICONS.plus} Create</button>
|
|
709
|
+
</div>
|
|
710
|
+
</section>
|
|
711
|
+
<div class="sched-group-card-list">${rows || '<div class="sched-empty">No groups yet.</div>'}</div>
|
|
712
|
+
`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function renderActivityTab() {
|
|
716
|
+
const content = $('sched-content');
|
|
717
|
+
if (!content) return;
|
|
718
|
+
const rows = [..._schedules]
|
|
719
|
+
.sort((a, b) => new Date(b.lastRun || 0) - new Date(a.lastRun || 0))
|
|
720
|
+
.map(s => `
|
|
721
|
+
<div class="sched-activity-row">
|
|
722
|
+
<div>
|
|
723
|
+
<strong>${esc(s.name)}</strong>
|
|
724
|
+
<span>${esc(getTemplate(s.templateId)?.name || 'Missing template')}</span>
|
|
725
|
+
</div>
|
|
726
|
+
${scheduleStatusBadge(s)}
|
|
727
|
+
<span>${esc(s.lastRunResult || 'not run')}</span>
|
|
728
|
+
<span>${esc(formatDateTime(s.lastRun))}</span>
|
|
729
|
+
<span>Next ${esc(formatNextRun(s.nextRun))}</span>
|
|
730
|
+
</div>
|
|
731
|
+
`).join('');
|
|
732
|
+
|
|
733
|
+
content.innerHTML = `
|
|
734
|
+
<div class="sched-section-head"><h4>Schedule Activity</h4><span>Last run state and upcoming runs</span></div>
|
|
735
|
+
<div class="sched-activity-list">${rows || '<div class="sched-empty">No activity yet.</div>'}</div>
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function renderScheduleEditor() {
|
|
740
|
+
const main = $('sched-main');
|
|
741
|
+
if (!main) return;
|
|
742
|
+
const s = _scheduleEditor || {};
|
|
743
|
+
const isEdit = !!s.id;
|
|
744
|
+
const templateOptions = _templates.map(t => `<option value="${esc(t.id)}"${s.templateId === t.id ? ' selected' : ''}>${esc(t.name)} (${esc(t.category || 'custom')})</option>`).join('');
|
|
745
|
+
const groupOptions = [
|
|
746
|
+
`<option value=""${!s.groupId ? ' selected' : ''}>Ungrouped</option>`,
|
|
747
|
+
..._groups.map(g => `<option value="${esc(g.id)}"${s.groupId === g.id ? ' selected' : ''}>${esc(g.name)}</option>`),
|
|
748
|
+
].join('');
|
|
749
|
+
const tz = s.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
750
|
+
const profileOptions = [
|
|
751
|
+
`<option value=""${!s.profile ? ' selected' : ''}>Template default</option>`,
|
|
752
|
+
...CLI_PROFILES.map(p => `<option value="${p.id}"${s.profile === p.id ? ' selected' : ''}>${p.label}</option>`),
|
|
753
|
+
].join('');
|
|
754
|
+
const profileForModels = s.profile || 'claude-code';
|
|
755
|
+
const modelOptions = renderModelOptions(profileForModels, s.model || '');
|
|
756
|
+
const effortOptions = [
|
|
757
|
+
`<option value=""${!s.effort ? ' selected' : ''}>Template/default</option>`,
|
|
758
|
+
...EFFORT_LEVELS.map(e => `<option value="${e.id}"${s.effort === e.id ? ' selected' : ''}>${e.label}</option>`),
|
|
759
|
+
].join('');
|
|
760
|
+
const browserValue = s.usesBrowser === true ? 'true' : s.usesBrowser === false ? 'false' : '';
|
|
761
|
+
const presetsHTML = CRON_PRESET_GROUPS.map(group => `
|
|
762
|
+
<div class="sched-preset-group">
|
|
763
|
+
<span>${esc(group.group)}</span>
|
|
764
|
+
<div>${group.presets.map(p => `<button class="sched-preset" data-action="cron-preset" data-cron="${esc(p.cron)}" data-tooltip="${esc(p.desc)}">${esc(p.label)}</button>`).join('')}</div>
|
|
765
|
+
</div>`).join('');
|
|
766
|
+
const dayOrder = [1, 2, 3, 4, 5, 6, 0];
|
|
767
|
+
const dayThemes = dayOrder.map(idx => `
|
|
768
|
+
<label class="sched-day-field">
|
|
769
|
+
<span>${DAY_NAMES[idx]}</span>
|
|
770
|
+
<input type="text" data-day="${idx}" value="${esc(s.dayThemes?.[String(idx)]?.contextOverride || '')}" placeholder="Optional context override">
|
|
771
|
+
</label>`).join('');
|
|
772
|
+
|
|
773
|
+
if (profileForModels === 'opencode' && !_opencodeModelsCache) {
|
|
774
|
+
fetchOpencodeModels().then(() => { if (_panel && _scheduleEditor) renderScheduleEditor(); });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
main.innerHTML = `
|
|
778
|
+
<div class="sched-editor">
|
|
779
|
+
<div class="sched-editor-head">
|
|
780
|
+
<button class="sched-icon-btn" data-action="schedule-editor-close">${ICONS.back}</button>
|
|
781
|
+
<div>
|
|
782
|
+
<h3>${isEdit ? 'Edit Schedule' : 'New Schedule'}</h3>
|
|
783
|
+
<span>${isEdit ? esc(s.name) : 'Create a recurring cron launch'}</span>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
<div class="sched-editor-body">
|
|
787
|
+
<div class="sched-form-grid">
|
|
788
|
+
<label class="sched-field"><span>Name</span><input id="sched-ed-name" type="text" value="${esc(s.name || '')}" placeholder="Morning post"></label>
|
|
789
|
+
<label class="sched-field"><span>Automation</span><select id="sched-ed-template"><option value="">Select automation...</option>${templateOptions}</select></label>
|
|
790
|
+
<label class="sched-field"><span>Group</span><select id="sched-ed-group">${groupOptions}</select></label>
|
|
791
|
+
<label class="sched-field"><span>Timezone</span><input id="sched-ed-tz" type="text" value="${esc(tz)}"></label>
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
<section class="sched-editor-section">
|
|
795
|
+
<div class="sched-section-head"><h4>Cron</h4><span id="sched-ed-cron-desc">${esc(describeCronClient(s.cron || '0 9 * * *'))}</span></div>
|
|
796
|
+
<div class="sched-presets">${presetsHTML}</div>
|
|
797
|
+
<input class="sched-cron-input" id="sched-ed-cron" type="text" value="${esc(s.cron || '0 9 * * *')}" placeholder="0 9 * * *">
|
|
798
|
+
</section>
|
|
799
|
+
|
|
800
|
+
<section class="sched-editor-section">
|
|
801
|
+
<div class="sched-section-head"><h4>Launch Overrides</h4><span>Leave blank to use automation defaults</span></div>
|
|
802
|
+
<div class="sched-form-grid">
|
|
803
|
+
<label class="sched-field"><span>CLI</span><select id="sched-ed-profile">${profileOptions}</select></label>
|
|
804
|
+
<label class="sched-field"><span>Model</span><select id="sched-ed-model">${modelOptions}</select></label>
|
|
805
|
+
<label class="sched-field"><span>Think</span><select id="sched-ed-effort">${effortOptions}</select></label>
|
|
806
|
+
<label class="sched-field"><span>Browser</span><select id="sched-ed-browser">
|
|
807
|
+
<option value=""${browserValue === '' ? ' selected' : ''}>Template default</option>
|
|
808
|
+
<option value="true"${browserValue === 'true' ? ' selected' : ''}>Force on</option>
|
|
809
|
+
<option value="false"${browserValue === 'false' ? ' selected' : ''}>Force off</option>
|
|
810
|
+
</select></label>
|
|
811
|
+
</div>
|
|
812
|
+
</section>
|
|
813
|
+
|
|
814
|
+
<section class="sched-editor-section">
|
|
815
|
+
<div class="sched-section-head"><h4>Day Themes</h4><span>Optional context per weekday</span></div>
|
|
816
|
+
<div class="sched-day-grid" id="sched-ed-daythemes">${dayThemes}</div>
|
|
817
|
+
</section>
|
|
818
|
+
|
|
819
|
+
<label class="sched-toggle"><input id="sched-ed-enabled" type="checkbox" ${s.enabled !== false ? 'checked' : ''}><span>Enabled</span></label>
|
|
820
|
+
</div>
|
|
821
|
+
<div class="sched-editor-actions">
|
|
822
|
+
<button class="sched-secondary" data-action="schedule-editor-close">Cancel</button>
|
|
823
|
+
<button class="sched-primary" data-action="schedule-editor-save">${isEdit ? 'Save Changes' : 'Create Schedule'}</button>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
`;
|
|
827
|
+
|
|
828
|
+
$('sched-ed-cron')?.addEventListener('input', updateCronDesc);
|
|
829
|
+
$('sched-ed-profile')?.addEventListener('change', (e) => {
|
|
830
|
+
collectScheduleEditorDraft();
|
|
831
|
+
_scheduleEditor.profile = e.target.value || null;
|
|
832
|
+
_scheduleEditor.model = null;
|
|
833
|
+
renderScheduleEditor();
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function updateCronDesc() {
|
|
838
|
+
const input = $('sched-ed-cron');
|
|
839
|
+
const desc = $('sched-ed-cron-desc');
|
|
840
|
+
if (input && desc) desc.textContent = describeCronClient(input.value);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function collectScheduleEditorDraft() {
|
|
844
|
+
if (!_scheduleEditor) return;
|
|
845
|
+
const dayThemes = {};
|
|
846
|
+
_panel?.querySelectorAll('#sched-ed-daythemes input[data-day]').forEach(input => {
|
|
847
|
+
const val = input.value.trim();
|
|
848
|
+
if (val) dayThemes[input.dataset.day] = { contextOverride: val };
|
|
849
|
+
});
|
|
850
|
+
const browserRaw = $('sched-ed-browser')?.value || '';
|
|
851
|
+
Object.assign(_scheduleEditor, {
|
|
852
|
+
name: $('sched-ed-name')?.value ?? _scheduleEditor.name,
|
|
853
|
+
templateId: $('sched-ed-template')?.value || _scheduleEditor.templateId,
|
|
854
|
+
groupId: $('sched-ed-group')?.value || null,
|
|
855
|
+
timezone: $('sched-ed-tz')?.value || _scheduleEditor.timezone,
|
|
856
|
+
cron: $('sched-ed-cron')?.value || _scheduleEditor.cron,
|
|
857
|
+
profile: $('sched-ed-profile')?.value || null,
|
|
858
|
+
model: $('sched-ed-model')?.value || null,
|
|
859
|
+
effort: $('sched-ed-effort')?.value || null,
|
|
860
|
+
usesBrowser: browserRaw === '' ? undefined : browserRaw === 'true',
|
|
861
|
+
enabled: $('sched-ed-enabled')?.checked ?? _scheduleEditor.enabled,
|
|
862
|
+
dayThemes,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function saveScheduleFromEditor() {
|
|
867
|
+
const name = $('sched-ed-name')?.value?.trim();
|
|
868
|
+
const templateId = $('sched-ed-template')?.value;
|
|
869
|
+
const cron = $('sched-ed-cron')?.value?.trim();
|
|
870
|
+
const timezone = $('sched-ed-tz')?.value?.trim();
|
|
871
|
+
const groupId = $('sched-ed-group')?.value || null;
|
|
872
|
+
const profile = $('sched-ed-profile')?.value || null;
|
|
873
|
+
const model = $('sched-ed-model')?.value || null;
|
|
874
|
+
const effort = $('sched-ed-effort')?.value || null;
|
|
875
|
+
const browserRaw = $('sched-ed-browser')?.value || '';
|
|
876
|
+
const enabled = $('sched-ed-enabled')?.checked ?? true;
|
|
877
|
+
|
|
878
|
+
if (!name) { showToast('Name is required'); return; }
|
|
879
|
+
if (!templateId) { showToast('Select an automation'); return; }
|
|
880
|
+
if (!cron || cron.split(/\s+/).length !== 5) { showToast('Valid 5-field cron expression required'); return; }
|
|
881
|
+
|
|
882
|
+
const dayThemes = {};
|
|
883
|
+
_panel.querySelectorAll('#sched-ed-daythemes input[data-day]').forEach(input => {
|
|
884
|
+
const val = input.value.trim();
|
|
885
|
+
if (val) dayThemes[input.dataset.day] = { contextOverride: val };
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const payload = {
|
|
889
|
+
name, templateId, cron, timezone, groupId, enabled, dayThemes,
|
|
890
|
+
profile, model, effort,
|
|
891
|
+
usesBrowser: browserRaw === '' ? null : browserRaw === 'true',
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
if (_scheduleEditor?.id) {
|
|
896
|
+
const updated = await updateSchedule(_scheduleEditor.id, payload);
|
|
897
|
+
const idx = _schedules.findIndex(s => s.id === updated.id);
|
|
898
|
+
if (idx >= 0) _schedules[idx] = updated;
|
|
899
|
+
showToast('Schedule updated');
|
|
900
|
+
} else {
|
|
901
|
+
const created = await createSchedule(payload);
|
|
902
|
+
if (!_schedules.some(s => s.id === created.id)) _schedules.push(created);
|
|
903
|
+
showToast('Schedule created');
|
|
904
|
+
}
|
|
905
|
+
_scheduleEditor = null;
|
|
906
|
+
render();
|
|
907
|
+
} catch (err) {
|
|
908
|
+
showToast('Save failed: ' + err.message);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function handleClick(e) {
|
|
913
|
+
const el = e.target.closest('[data-action]');
|
|
914
|
+
if (!el || !_panel?.contains(el)) return;
|
|
915
|
+
const action = el.dataset.action;
|
|
916
|
+
const id = el.dataset.id;
|
|
917
|
+
|
|
918
|
+
switch (action) {
|
|
919
|
+
case 'set-tab':
|
|
920
|
+
_tab = el.dataset.tab || 'cron';
|
|
921
|
+
_scheduleEditor = null;
|
|
922
|
+
render();
|
|
923
|
+
break;
|
|
924
|
+
case 'select-group':
|
|
925
|
+
_selectedGroup = el.dataset.groupId || 'all';
|
|
926
|
+
_tab = 'cron';
|
|
927
|
+
render();
|
|
928
|
+
break;
|
|
929
|
+
case 'schedule-new':
|
|
930
|
+
_scheduleEditor = { groupId: _selectedGroup !== 'all' && _selectedGroup !== 'ungrouped' ? _selectedGroup : null };
|
|
931
|
+
render();
|
|
932
|
+
break;
|
|
933
|
+
case 'schedule-edit': {
|
|
934
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
935
|
+
if (schedule) {
|
|
936
|
+
_scheduleEditor = { ...schedule, dayThemes: { ...(schedule.dayThemes || {}) } };
|
|
937
|
+
render();
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
case 'schedule-edit-prompt': {
|
|
942
|
+
const templateId = el.dataset.templateId;
|
|
943
|
+
if (!templateId) { showToast('No automation linked to this schedule'); break; }
|
|
944
|
+
closePanel();
|
|
945
|
+
emit('automations:open', { templateId });
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
case 'schedule-editor-close':
|
|
949
|
+
_scheduleEditor = null;
|
|
950
|
+
render();
|
|
951
|
+
break;
|
|
952
|
+
case 'schedule-editor-save':
|
|
953
|
+
await saveScheduleFromEditor();
|
|
954
|
+
break;
|
|
955
|
+
case 'cron-preset': {
|
|
956
|
+
const cronInput = $('sched-ed-cron');
|
|
957
|
+
if (cronInput) {
|
|
958
|
+
cronInput.value = el.dataset.cron || cronInput.value;
|
|
959
|
+
updateCronDesc();
|
|
960
|
+
}
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
case 'schedule-toggle':
|
|
964
|
+
await toggleSchedule(id);
|
|
965
|
+
break;
|
|
966
|
+
case 'schedule-test':
|
|
967
|
+
await fireSchedule(id);
|
|
968
|
+
break;
|
|
969
|
+
case 'schedule-delete':
|
|
970
|
+
await removeSchedule(id);
|
|
971
|
+
break;
|
|
972
|
+
case 'group-new':
|
|
973
|
+
_tab = 'groups';
|
|
974
|
+
_scheduleEditor = null;
|
|
975
|
+
render();
|
|
976
|
+
setTimeout(() => $('sched-group-name')?.focus(), 0);
|
|
977
|
+
break;
|
|
978
|
+
case 'schedule-prompt':
|
|
979
|
+
openPromptScheduleModal();
|
|
980
|
+
break;
|
|
981
|
+
case 'prompt-modal-close':
|
|
982
|
+
closePromptScheduleModal();
|
|
983
|
+
break;
|
|
984
|
+
case 'prompt-modal-pick':
|
|
985
|
+
handlePromptPick(el.dataset.runtime);
|
|
986
|
+
break;
|
|
987
|
+
case 'group-save-new':
|
|
988
|
+
await saveNewGroup();
|
|
989
|
+
break;
|
|
990
|
+
case 'group-edit':
|
|
991
|
+
await editGroup(id);
|
|
992
|
+
break;
|
|
993
|
+
case 'group-delete':
|
|
994
|
+
await removeGroup(id);
|
|
995
|
+
break;
|
|
996
|
+
case 'group-up':
|
|
997
|
+
await moveGroup(id, -1);
|
|
998
|
+
break;
|
|
999
|
+
case 'group-down':
|
|
1000
|
+
await moveGroup(id, 1);
|
|
1001
|
+
break;
|
|
1002
|
+
case 'qt-select':
|
|
1003
|
+
_selectedQtMinutes = Number(el.dataset.minutes);
|
|
1004
|
+
renderTimersTab();
|
|
1005
|
+
break;
|
|
1006
|
+
case 'qt-go':
|
|
1007
|
+
await createQuickTimerFromForm(false);
|
|
1008
|
+
break;
|
|
1009
|
+
case 'qt-now':
|
|
1010
|
+
await createQuickTimerFromForm(true);
|
|
1011
|
+
break;
|
|
1012
|
+
case 'qt-cancel':
|
|
1013
|
+
await cancelQuickTimerById(id);
|
|
1014
|
+
break;
|
|
1015
|
+
case 'schedule-timer-create':
|
|
1016
|
+
await createExistingScheduleTimer();
|
|
1017
|
+
break;
|
|
1018
|
+
case 'schedule-timer-cancel':
|
|
1019
|
+
await cancelExistingScheduleTimer(id);
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function handleChange(e) {
|
|
1025
|
+
const select = e.target.closest('.sched-card-group-select');
|
|
1026
|
+
if (!select) return;
|
|
1027
|
+
const scheduleId = select.dataset.scheduleId;
|
|
1028
|
+
try {
|
|
1029
|
+
const updated = await updateSchedule(scheduleId, { groupId: select.value || null });
|
|
1030
|
+
const idx = _schedules.findIndex(s => s.id === updated.id);
|
|
1031
|
+
if (idx >= 0) _schedules[idx] = updated;
|
|
1032
|
+
showToast(select.value ? `Moved to ${getGroup(select.value)?.name || 'group'}` : 'Moved to Ungrouped');
|
|
1033
|
+
render();
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
showToast('Move failed: ' + err.message);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function handleDragStart(e) {
|
|
1040
|
+
const card = e.target.closest('.sched-card');
|
|
1041
|
+
if (!card) return;
|
|
1042
|
+
_draggingScheduleId = card.dataset.scheduleId;
|
|
1043
|
+
card.classList.add('dragging');
|
|
1044
|
+
try { e.dataTransfer.setData('text/plain', _draggingScheduleId); } catch {}
|
|
1045
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function handleDragEnd() {
|
|
1049
|
+
_draggingScheduleId = null;
|
|
1050
|
+
_panel?.querySelectorAll('.sched-card.dragging').forEach(el => el.classList.remove('dragging'));
|
|
1051
|
+
_panel?.querySelectorAll('.sched-group-row.drop-target').forEach(el => el.classList.remove('drop-target'));
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function handleDragOver(e) {
|
|
1055
|
+
if (!_draggingScheduleId) return;
|
|
1056
|
+
const target = e.target.closest('[data-drop-group]');
|
|
1057
|
+
if (!target) return;
|
|
1058
|
+
e.preventDefault();
|
|
1059
|
+
target.classList.add('drop-target');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function handleDrop(e) {
|
|
1063
|
+
if (!_draggingScheduleId) return;
|
|
1064
|
+
const target = e.target.closest('[data-drop-group]');
|
|
1065
|
+
if (!target) return;
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
const groupId = target.dataset.dropGroup || null;
|
|
1068
|
+
try {
|
|
1069
|
+
const updated = await updateSchedule(_draggingScheduleId, { groupId });
|
|
1070
|
+
const idx = _schedules.findIndex(s => s.id === updated.id);
|
|
1071
|
+
if (idx >= 0) _schedules[idx] = updated;
|
|
1072
|
+
_selectedGroup = groupId || 'ungrouped';
|
|
1073
|
+
showToast(groupId ? `Moved to ${getGroup(groupId)?.name || 'group'}` : 'Moved to Ungrouped');
|
|
1074
|
+
render();
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
showToast('Move failed: ' + err.message);
|
|
1077
|
+
} finally {
|
|
1078
|
+
handleDragEnd();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function toggleSchedule(id) {
|
|
1083
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
1084
|
+
if (!schedule) return;
|
|
1085
|
+
try {
|
|
1086
|
+
const updated = await updateSchedule(id, { enabled: !schedule.enabled });
|
|
1087
|
+
const idx = _schedules.findIndex(s => s.id === id);
|
|
1088
|
+
if (idx >= 0) _schedules[idx] = updated;
|
|
1089
|
+
showToast(updated.enabled ? 'Schedule enabled' : 'Schedule paused');
|
|
1090
|
+
render();
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
showToast('Toggle failed: ' + err.message);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function fireSchedule(id) {
|
|
1097
|
+
try {
|
|
1098
|
+
await testSchedule(id);
|
|
1099
|
+
showToast('Schedule queued');
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
showToast('Run failed: ' + err.message);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async function removeSchedule(id) {
|
|
1106
|
+
if (!confirm('Delete this schedule?')) return;
|
|
1107
|
+
try {
|
|
1108
|
+
await deleteSchedule(id);
|
|
1109
|
+
_schedules = _schedules.filter(s => s.id !== id);
|
|
1110
|
+
render();
|
|
1111
|
+
showToast('Schedule deleted');
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
showToast('Delete failed: ' + err.message);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function saveNewGroup() {
|
|
1118
|
+
const name = $('sched-group-name')?.value?.trim();
|
|
1119
|
+
const color = $('sched-group-color')?.value || null;
|
|
1120
|
+
if (!name) { showToast('Group name is required'); return; }
|
|
1121
|
+
try {
|
|
1122
|
+
const group = await createScheduleGroup({ name, color });
|
|
1123
|
+
if (!_groups.some(g => g.id === group.id)) _groups.push(group);
|
|
1124
|
+
_selectedGroup = group.id;
|
|
1125
|
+
_tab = 'cron';
|
|
1126
|
+
render();
|
|
1127
|
+
showToast('Group created');
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
showToast('Group failed: ' + err.message);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function editGroup(id) {
|
|
1134
|
+
const group = getGroup(id);
|
|
1135
|
+
if (!group) return;
|
|
1136
|
+
const name = prompt('Group name', group.name);
|
|
1137
|
+
if (name === null) return;
|
|
1138
|
+
const trimmed = name.trim();
|
|
1139
|
+
if (!trimmed) { showToast('Group name is required'); return; }
|
|
1140
|
+
try {
|
|
1141
|
+
const updated = await updateScheduleGroup(id, { name: trimmed });
|
|
1142
|
+
const idx = _groups.findIndex(g => g.id === id);
|
|
1143
|
+
if (idx >= 0) _groups[idx] = updated;
|
|
1144
|
+
render();
|
|
1145
|
+
showToast('Group updated');
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
showToast('Group update failed: ' + err.message);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async function removeGroup(id) {
|
|
1152
|
+
const count = _schedules.filter(s => s.groupId === id).length;
|
|
1153
|
+
if (!confirm(`Delete this group? ${count} schedules will move to Ungrouped.`)) return;
|
|
1154
|
+
try {
|
|
1155
|
+
await deleteScheduleGroup(id);
|
|
1156
|
+
_groups = _groups.filter(g => g.id !== id);
|
|
1157
|
+
for (const schedule of _schedules) if (schedule.groupId === id) schedule.groupId = null;
|
|
1158
|
+
if (_selectedGroup === id) _selectedGroup = 'ungrouped';
|
|
1159
|
+
render();
|
|
1160
|
+
showToast('Group deleted');
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
showToast('Group delete failed: ' + err.message);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function openPromptScheduleModal() {
|
|
1167
|
+
if (_promptModal) return;
|
|
1168
|
+
|
|
1169
|
+
_promptBackdrop = document.createElement('div');
|
|
1170
|
+
_promptBackdrop.className = 'sched-prompt-backdrop';
|
|
1171
|
+
_promptBackdrop.addEventListener('click', closePromptScheduleModal);
|
|
1172
|
+
document.body.appendChild(_promptBackdrop);
|
|
1173
|
+
|
|
1174
|
+
const claudeMeta = getProviderMeta('claude-code');
|
|
1175
|
+
const opencodeMeta = getProviderMeta('opencode');
|
|
1176
|
+
const codexMeta = getProviderMeta('codex');
|
|
1177
|
+
_promptModal = document.createElement('div');
|
|
1178
|
+
_promptModal.className = 'sched-prompt-modal glass';
|
|
1179
|
+
_promptModal.innerHTML = `
|
|
1180
|
+
<header class="sched-prompt-head">
|
|
1181
|
+
<div class="sched-prompt-head-text">
|
|
1182
|
+
<span class="sched-prompt-icon">${ICONS.sparkle}</span>
|
|
1183
|
+
<div>
|
|
1184
|
+
<h3>Prompt a Schedule</h3>
|
|
1185
|
+
<p>Start the schedule draft in a sidepanel.</p>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
<button class="sched-prompt-close" data-action="prompt-modal-close" aria-label="Close">${ICONS.close}</button>
|
|
1189
|
+
</header>
|
|
1190
|
+
<div class="sched-prompt-tiles">
|
|
1191
|
+
<button class="sched-prompt-tile" style="--provider-color:${claudeMeta.color};" data-action="prompt-modal-pick" data-runtime="claude-code">
|
|
1192
|
+
<span class="sched-prompt-tile-logo" aria-hidden="true">${claudeMeta.icon}</span>
|
|
1193
|
+
<span class="sched-prompt-tile-body">
|
|
1194
|
+
<span class="sched-prompt-tile-title">${claudeMeta.label}</span>
|
|
1195
|
+
<span class="sched-prompt-tile-sub">Anthropic</span>
|
|
1196
|
+
</span>
|
|
1197
|
+
<span class="sched-prompt-pill sched-prompt-pill--active">Ready</span>
|
|
1198
|
+
</button>
|
|
1199
|
+
<button class="sched-prompt-tile disabled" style="--provider-color:${opencodeMeta.color};" disabled aria-disabled="true">
|
|
1200
|
+
<span class="sched-prompt-tile-logo" aria-hidden="true">${opencodeMeta.icon}</span>
|
|
1201
|
+
<span class="sched-prompt-tile-body">
|
|
1202
|
+
<span class="sched-prompt-tile-title">${opencodeMeta.label}</span>
|
|
1203
|
+
<span class="sched-prompt-tile-sub">Multi-provider</span>
|
|
1204
|
+
</span>
|
|
1205
|
+
<span class="sched-prompt-pill">Planned</span>
|
|
1206
|
+
</button>
|
|
1207
|
+
<button class="sched-prompt-tile disabled" style="--provider-color:${codexMeta.color};" disabled aria-disabled="true">
|
|
1208
|
+
<span class="sched-prompt-tile-logo" aria-hidden="true">${codexMeta.icon}</span>
|
|
1209
|
+
<span class="sched-prompt-tile-body">
|
|
1210
|
+
<span class="sched-prompt-tile-title">Codex CLI</span>
|
|
1211
|
+
<span class="sched-prompt-tile-sub">OpenAI</span>
|
|
1212
|
+
</span>
|
|
1213
|
+
<span class="sched-prompt-pill">Planned</span>
|
|
1214
|
+
</button>
|
|
1215
|
+
</div>
|
|
1216
|
+
`;
|
|
1217
|
+
_promptModal.addEventListener('click', handlePromptModalClick);
|
|
1218
|
+
document.body.appendChild(_promptModal);
|
|
1219
|
+
|
|
1220
|
+
_promptKeyHandler = (e) => { if (e.key === 'Escape') closePromptScheduleModal(); };
|
|
1221
|
+
document.addEventListener('keydown', _promptKeyHandler);
|
|
1222
|
+
|
|
1223
|
+
requestAnimationFrame(() => {
|
|
1224
|
+
_promptBackdrop?.classList.add('open');
|
|
1225
|
+
_promptModal?.classList.add('open');
|
|
1226
|
+
_promptModal?.querySelector('.sched-prompt-tile:not(.disabled)')?.focus();
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function handlePromptModalClick(e) {
|
|
1231
|
+
const el = e.target.closest('[data-action]');
|
|
1232
|
+
if (!el || !_promptModal?.contains(el)) return;
|
|
1233
|
+
const action = el.dataset.action;
|
|
1234
|
+
if (action === 'prompt-modal-close') {
|
|
1235
|
+
closePromptScheduleModal();
|
|
1236
|
+
} else if (action === 'prompt-modal-pick') {
|
|
1237
|
+
handlePromptPick(el.dataset.runtime, el);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function closePromptScheduleModal() {
|
|
1242
|
+
if (_promptKeyHandler) {
|
|
1243
|
+
document.removeEventListener('keydown', _promptKeyHandler);
|
|
1244
|
+
_promptKeyHandler = null;
|
|
1245
|
+
}
|
|
1246
|
+
_promptBackdrop?.remove();
|
|
1247
|
+
_promptModal?.remove();
|
|
1248
|
+
_promptBackdrop = null;
|
|
1249
|
+
_promptModal = null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
async function handlePromptPick(runtime, triggerEl = null) {
|
|
1253
|
+
if (runtime !== 'claude-code') {
|
|
1254
|
+
showToast('Coming soon — Claude Code only for now');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (triggerEl) {
|
|
1258
|
+
triggerEl.disabled = true;
|
|
1259
|
+
triggerEl.setAttribute('aria-busy', 'true');
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
await sendToPanel('/synabun schedule', { newTab: true, tabLabel: 'Schedule Wizard', autoSubmit: true });
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
showToast('Failed to open Claude panel: ' + err.message);
|
|
1265
|
+
} finally {
|
|
1266
|
+
closePromptScheduleModal();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function moveGroup(id, direction) {
|
|
1271
|
+
const idx = _groups.findIndex(g => g.id === id);
|
|
1272
|
+
const next = idx + direction;
|
|
1273
|
+
if (idx < 0 || next < 0 || next >= _groups.length) return;
|
|
1274
|
+
const copy = [..._groups];
|
|
1275
|
+
const [moved] = copy.splice(idx, 1);
|
|
1276
|
+
copy.splice(next, 0, moved);
|
|
1277
|
+
try {
|
|
1278
|
+
await reorderScheduleGroups(copy.map(g => g.id));
|
|
1279
|
+
_groups = copy.map((g, i) => ({ ...g, order: i }));
|
|
1280
|
+
render();
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
showToast('Reorder failed: ' + err.message);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function createQuickTimerFromForm(runNow) {
|
|
1287
|
+
const templateId = $('sched-qt-template')?.value;
|
|
1288
|
+
if (!templateId) { showToast('Select an automation first'); return; }
|
|
1289
|
+
const customVal = Number($('sched-qt-custom-min')?.value);
|
|
1290
|
+
const minutes = customVal >= 1 ? customVal : _selectedQtMinutes;
|
|
1291
|
+
if (!runNow && !minutes) { showToast('Select a time or enter minutes'); return; }
|
|
1292
|
+
const template = getTemplate(templateId);
|
|
1293
|
+
const profile = $('sched-qt-profile')?.value || 'claude-code';
|
|
1294
|
+
const model = $('sched-qt-model')?.value || null;
|
|
1295
|
+
const effortRaw = $('sched-qt-effort')?.value || null;
|
|
1296
|
+
const effort = profile === 'claude-code' && effortRaw && effortRaw !== 'off' ? effortRaw : null;
|
|
1297
|
+
const usesBrowser = _qtUsesBrowser !== null ? _qtUsesBrowser : !!template?.usesBrowser;
|
|
1298
|
+
try {
|
|
1299
|
+
if (runNow) {
|
|
1300
|
+
const result = await triggerQuickTimerNow(templateId, { profile, model, effort, usesBrowser });
|
|
1301
|
+
showToast(`Running now: ${result.templateName}`);
|
|
1302
|
+
} else {
|
|
1303
|
+
const result = await createQuickTimer(templateId, minutes, { profile, model, effort, usesBrowser });
|
|
1304
|
+
if (!_quickTimers.some(t => t.id === result.timerId)) {
|
|
1305
|
+
_quickTimers.push({ id: result.timerId, templateId, templateName: result.templateName, firesAt: result.firesAt, minutes: result.minutes, profile: result.profile, model: result.model, usesBrowser: result.usesBrowser });
|
|
1306
|
+
}
|
|
1307
|
+
_selectedQtMinutes = null;
|
|
1308
|
+
showToast(`Timer set: ${result.templateName}`);
|
|
1309
|
+
}
|
|
1310
|
+
renderTimersTab();
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
showToast((runNow ? 'Run now' : 'Timer') + ' failed: ' + err.message);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function cancelQuickTimerById(id) {
|
|
1317
|
+
try {
|
|
1318
|
+
await cancelQuickTimer(id);
|
|
1319
|
+
_quickTimers = _quickTimers.filter(t => t.id !== id);
|
|
1320
|
+
renderTimersTab();
|
|
1321
|
+
showToast('Timer cancelled');
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
showToast('Cancel failed: ' + err.message);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function createExistingScheduleTimer() {
|
|
1328
|
+
const scheduleId = $('sched-existing-timer-schedule')?.value;
|
|
1329
|
+
const minutes = Number($('sched-existing-timer-min')?.value);
|
|
1330
|
+
if (!scheduleId) { showToast('Select a schedule'); return; }
|
|
1331
|
+
if (!minutes || minutes < 1) { showToast('Enter minutes'); return; }
|
|
1332
|
+
try {
|
|
1333
|
+
const result = await startScheduleTimer(scheduleId, minutes);
|
|
1334
|
+
_scheduleTimerData[scheduleId] = { firesAt: result.firesAt, minutes: result.minutes };
|
|
1335
|
+
renderTimersTab();
|
|
1336
|
+
showToast('Schedule timer set');
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
showToast('Timer failed: ' + err.message);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
async function cancelExistingScheduleTimer(id) {
|
|
1343
|
+
try {
|
|
1344
|
+
await cancelScheduleTimer(id);
|
|
1345
|
+
delete _scheduleTimerData[id];
|
|
1346
|
+
renderTimersTab();
|
|
1347
|
+
showToast('Timer cancelled');
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
showToast('Cancel failed: ' + err.message);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function updateTimerCountdowns() {
|
|
1354
|
+
_panel?.querySelectorAll('.sched-active-timer').forEach(row => {
|
|
1355
|
+
const button = row.querySelector('[data-action="qt-cancel"], [data-action="schedule-timer-cancel"]');
|
|
1356
|
+
const id = button?.dataset.id;
|
|
1357
|
+
let timer = _quickTimers.find(t => t.id === id);
|
|
1358
|
+
if (!timer) timer = _scheduleTimerData[id];
|
|
1359
|
+
const strong = row.querySelector('strong');
|
|
1360
|
+
if (timer && strong) strong.textContent = formatTimerCountdown(timer.firesAt);
|
|
1361
|
+
});
|
|
1362
|
+
_panel?.querySelectorAll('[data-countdown]').forEach(el => {
|
|
1363
|
+
const iso = el.dataset.countdown;
|
|
1364
|
+
el.textContent = formatNextRun(iso || null);
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function setupSync() {
|
|
1369
|
+
if (_syncReady) return;
|
|
1370
|
+
_syncReady = true;
|
|
1371
|
+
|
|
1372
|
+
on('sync:schedule:created', (data) => {
|
|
1373
|
+
if (!data?.schedule) return;
|
|
1374
|
+
if (!_schedules.some(s => s.id === data.schedule.id)) _schedules.push(data.schedule);
|
|
1375
|
+
if (_panel) render();
|
|
1376
|
+
});
|
|
1377
|
+
on('sync:schedule:updated', (data) => {
|
|
1378
|
+
if (!data?.schedule) return;
|
|
1379
|
+
const idx = _schedules.findIndex(s => s.id === data.schedule.id);
|
|
1380
|
+
if (idx >= 0) _schedules[idx] = data.schedule;
|
|
1381
|
+
else _schedules.push(data.schedule);
|
|
1382
|
+
if (_panel) render();
|
|
1383
|
+
});
|
|
1384
|
+
on('sync:schedule:deleted', (data) => {
|
|
1385
|
+
if (!data?.scheduleId) return;
|
|
1386
|
+
_schedules = _schedules.filter(s => s.id !== data.scheduleId);
|
|
1387
|
+
if (_panel) render();
|
|
1388
|
+
});
|
|
1389
|
+
on('sync:schedule:fired', (data) => {
|
|
1390
|
+
if (data?.scheduleName) showToast(`Schedule fired: ${data.scheduleName}`);
|
|
1391
|
+
});
|
|
1392
|
+
on('sync:schedule:completed', (data) => {
|
|
1393
|
+
loadData().then(() => { if (_panel) render(); });
|
|
1394
|
+
if (data?.terminalSessionId) {
|
|
1395
|
+
emit('terminal:attach-floating', {
|
|
1396
|
+
terminalSessionId: data.terminalSessionId,
|
|
1397
|
+
profile: data.profile || 'claude-code',
|
|
1398
|
+
snapToPanel: false,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
on('sync:schedule:failed', (data) => {
|
|
1403
|
+
if (data?.reason) showToast(`Schedule failed: ${data.reason}`);
|
|
1404
|
+
loadData().then(() => { if (_panel) render(); });
|
|
1405
|
+
});
|
|
1406
|
+
on('sync:schedule:timer-set', (data) => {
|
|
1407
|
+
if (data?.scheduleId && data?.firesAt) {
|
|
1408
|
+
_scheduleTimerData[data.scheduleId] = { firesAt: data.firesAt, minutes: data.minutes };
|
|
1409
|
+
if (_panel && _tab === 'timers') renderTimersTab();
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
on('sync:schedule:timer-fired', (data) => {
|
|
1413
|
+
if (data?.scheduleName) showToast(`Timer fired: ${data.scheduleName}`);
|
|
1414
|
+
if (data?.scheduleId) delete _scheduleTimerData[data.scheduleId];
|
|
1415
|
+
if (_panel) render();
|
|
1416
|
+
});
|
|
1417
|
+
on('sync:schedule:timer-cancelled', (data) => {
|
|
1418
|
+
if (data?.scheduleId) delete _scheduleTimerData[data.scheduleId];
|
|
1419
|
+
if (_panel && _tab === 'timers') renderTimersTab();
|
|
1420
|
+
});
|
|
1421
|
+
on('sync:schedule-group:created', (data) => {
|
|
1422
|
+
if (data?.group && !_groups.some(g => g.id === data.group.id)) _groups.push(data.group);
|
|
1423
|
+
if (_panel) render();
|
|
1424
|
+
});
|
|
1425
|
+
on('sync:schedule-group:updated', (data) => {
|
|
1426
|
+
if (!data?.group) return;
|
|
1427
|
+
const idx = _groups.findIndex(g => g.id === data.group.id);
|
|
1428
|
+
if (idx >= 0) _groups[idx] = data.group;
|
|
1429
|
+
else _groups.push(data.group);
|
|
1430
|
+
if (_panel) render();
|
|
1431
|
+
});
|
|
1432
|
+
on('sync:schedule-group:deleted', (data) => {
|
|
1433
|
+
if (!data?.groupId) return;
|
|
1434
|
+
_groups = _groups.filter(g => g.id !== data.groupId);
|
|
1435
|
+
for (const schedule of _schedules) if (schedule.groupId === data.groupId) schedule.groupId = null;
|
|
1436
|
+
if (_selectedGroup === data.groupId) _selectedGroup = 'ungrouped';
|
|
1437
|
+
if (_panel) render();
|
|
1438
|
+
});
|
|
1439
|
+
on('sync:schedule-group:reordered', (data) => {
|
|
1440
|
+
if (Array.isArray(data?.groups)) _groups = data.groups;
|
|
1441
|
+
if (_panel) render();
|
|
1442
|
+
});
|
|
1443
|
+
on('sync:quick-timer:set', (data) => {
|
|
1444
|
+
if (data?.timerId && !_quickTimers.some(t => t.id === data.timerId)) {
|
|
1445
|
+
_quickTimers.push({ id: data.timerId, templateName: data.templateName, firesAt: data.firesAt, minutes: data.minutes, profile: data.profile, model: data.model, usesBrowser: data.usesBrowser });
|
|
1446
|
+
if (_panel && _tab === 'timers') renderTimersTab();
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
on('sync:quick-timer:fired', (data) => {
|
|
1450
|
+
if (data?.templateName) showToast(`Timer fired: ${data.templateName}`);
|
|
1451
|
+
if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
|
|
1452
|
+
if (_panel && _tab === 'timers') renderTimersTab();
|
|
1453
|
+
});
|
|
1454
|
+
on('sync:quick-timer:fired-now', (data) => {
|
|
1455
|
+
if (data?.templateName) showToast(`Running now: ${data.templateName}`);
|
|
1456
|
+
});
|
|
1457
|
+
on('sync:quick-timer:cancelled', (data) => {
|
|
1458
|
+
if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
|
|
1459
|
+
if (_panel && _tab === 'timers') renderTimersTab();
|
|
1460
|
+
});
|
|
1461
|
+
on('sync:quick-timer:failed', (data) => {
|
|
1462
|
+
if (data?.reason) showToast(`Timer failed: ${data.reason}`);
|
|
1463
|
+
});
|
|
1464
|
+
}
|