glad-web 1.0.46 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -61
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
package/lib/web/layout.js DELETED
@@ -1,69 +0,0 @@
1
- const GLAD_SPLIT_QUERY = window.gladLayout.splitQuery;
2
- const GLAD_SIDEBAR_KEY = window.gladLayout.sidebarStorageKey;
3
-
4
- function isSplitLayout() {
5
- return window.matchMedia(GLAD_SPLIT_QUERY).matches;
6
- }
7
-
8
- function clampSidebarWidth(value) {
9
- return window.gladLayout.clampSidebarWidth(value);
10
- }
11
-
12
- function applySidebarWidth(value) {
13
- return window.gladLayout.applySidebarWidth(value);
14
- }
15
-
16
- function initializeResponsiveLayout() {
17
- const storedWidth = Number(localStorage.getItem(GLAD_SIDEBAR_KEY));
18
- applySidebarWidth(storedWidth || 348);
19
-
20
- const handle = document.getElementById('sidebar-resizer');
21
- if (handle) {
22
- let dragging = false;
23
- const onPointerMove = event => {
24
- if (!dragging) return;
25
- applySidebarWidth(event.clientX);
26
- if (typeof syncLayout === 'function') syncLayout({ keepAtBottom: false });
27
- };
28
- const stopDragging = () => {
29
- if (!dragging) return;
30
- dragging = false;
31
- handle.classList.remove('dragging');
32
- document.body.style.removeProperty('cursor');
33
- document.body.style.removeProperty('user-select');
34
- const width = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w'));
35
- localStorage.setItem(GLAD_SIDEBAR_KEY, String(clampSidebarWidth(width)));
36
- };
37
- handle.addEventListener('pointerdown', event => {
38
- if (!isSplitLayout()) return;
39
- dragging = true;
40
- handle.classList.add('dragging');
41
- handle.setPointerCapture?.(event.pointerId);
42
- document.body.style.cursor = 'col-resize';
43
- document.body.style.userSelect = 'none';
44
- event.preventDefault();
45
- });
46
- window.addEventListener('pointermove', onPointerMove);
47
- window.addEventListener('pointerup', stopDragging);
48
- window.addEventListener('pointercancel', stopDragging);
49
- }
50
-
51
- const media = window.matchMedia(GLAD_SPLIT_QUERY);
52
- media.addEventListener('change', () => {
53
- applySidebarWidth(parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w')));
54
- if (isSplitLayout()) scheduleSessionPolling();
55
- if (typeof syncLayout === 'function') requestAnimationFrame(() => syncLayout({ keepAtBottom: false }));
56
- });
57
-
58
- const controls = document.getElementById('terminal-controls');
59
- if (controls && typeof ResizeObserver !== 'undefined') {
60
- new ResizeObserver(() => {
61
- if (typeof syncLayout === 'function') syncLayout();
62
- }).observe(controls);
63
- }
64
- }
65
-
66
- window.addEventListener('resize', () => {
67
- if (isSplitLayout()) applySidebarWidth(parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w')));
68
- });
69
- document.addEventListener('DOMContentLoaded', initializeResponsiveLayout);
@@ -1,164 +0,0 @@
1
- let serverChanSettings = null;
2
- let serverChanToastTimer = null;
3
-
4
- function serverChanBellSvg() {
5
- return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"></path><path d="M10 21h4"></path></svg>';
6
- }
7
-
8
- function renderServerChanSessionAction(session) {
9
- const enabled = Boolean(session.serverChanNotificationEnabled);
10
- const title = enabled ? 'Disable ServerChan notifications for this chat' : 'Enable ServerChan notifications for this chat';
11
- return `<button class="serverchan-toggle${enabled ? ' active' : ''}" type="button"
12
- data-serverchan-session="${escapeHtml(session.id)}"
13
- aria-label="${title}" title="${title}"
14
- onclick="toggleServerChanSession('${session.id}', ${enabled ? 'false' : 'true'}, event)">
15
- ${serverChanBellSvg()}
16
- </button>`;
17
- }
18
-
19
- function showAppToast(message) {
20
- const toast = document.getElementById('app-toast');
21
- toast.textContent = message;
22
- toast.classList.add('visible');
23
- clearTimeout(serverChanToastTimer);
24
- serverChanToastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
25
- }
26
-
27
- async function toggleServerChanSession(sessionId, enabled, event) {
28
- event?.stopPropagation();
29
- try {
30
- const response = await fetchWithTimeout(`/api/sessions/${sessionId}/notifications/serverchan`, {
31
- method: 'PUT',
32
- headers: { 'Content-Type': 'application/json' },
33
- body: JSON.stringify({ enabled })
34
- }, 10000);
35
- const data = await response.json();
36
- if (!response.ok) {
37
- if (data.code === 'SERVERCHAN_NOT_CONFIGURED') {
38
- showAppToast('Configure ServerChan in Settings first');
39
- return;
40
- }
41
- throw new Error(data.error || 'Could not update notifications');
42
- }
43
- await refreshSessionsNow();
44
- } catch (error) {
45
- showAppToast(error.message || 'Could not update notifications');
46
- }
47
- }
48
-
49
- async function openSettings(event = null) {
50
- event?.stopPropagation();
51
- document.getElementById('settings-modal-overlay').style.display = 'flex';
52
- if (typeof syncGladThemeControls === 'function') syncGladThemeControls();
53
- if (typeof loadSkillHubSettings === 'function') void loadSkillHubSettings();
54
- setServerChanStatus('Loading configuration…');
55
- try {
56
- const response = await fetchWithTimeout('/api/notifications/serverchan', {}, 10000);
57
- const data = await response.json();
58
- if (!response.ok) throw new Error(data.error || 'Could not load configuration');
59
- serverChanSettings = data;
60
- document.getElementById('serverchan-client-type').value = data.clientType || 'wechat';
61
- const keyInput = document.getElementById('serverchan-send-key');
62
- keyInput.value = '';
63
- keyInput.placeholder = data.configured ? data.maskedKey : 'SCT...';
64
- document.getElementById('serverchan-key-hint').textContent = data.configured
65
- ? `Saved: ${data.maskedKey}. Leave blank to keep the current SendKey.`
66
- : 'The SendKey is stored only on this Glad host.';
67
- document.getElementById('serverchan-remove-btn').style.display = data.configured ? 'inline-flex' : 'none';
68
- setServerChanStatus(data.configured ? 'Configuration saved.' : 'ServerChan is not configured.');
69
- } catch (error) {
70
- setServerChanStatus(error.message || 'Could not load configuration', 'error');
71
- }
72
- }
73
-
74
- function closeSettings(event = null) {
75
- if (event && event.target.id !== 'settings-modal-overlay') return;
76
- document.getElementById('settings-modal-overlay').style.display = 'none';
77
- }
78
-
79
- function currentServerChanForm() {
80
- return {
81
- sendKey: document.getElementById('serverchan-send-key').value.trim(),
82
- clientType: document.getElementById('serverchan-client-type').value
83
- };
84
- }
85
-
86
- function setServerChanStatus(message, type = '') {
87
- const status = document.getElementById('serverchan-settings-status');
88
- status.textContent = message;
89
- status.className = `serverchan-settings-status${type ? ` ${type}` : ''}`;
90
- }
91
-
92
- function setServerChanBusy(busy) {
93
- document.getElementById('serverchan-save-btn').disabled = busy;
94
- document.getElementById('serverchan-test-btn').disabled = busy;
95
- document.getElementById('serverchan-remove-btn').disabled = busy;
96
- }
97
-
98
- async function saveServerChanSettings() {
99
- setServerChanBusy(true);
100
- setServerChanStatus('Saving…');
101
- try {
102
- const response = await fetchWithTimeout('/api/notifications/serverchan', {
103
- method: 'PUT',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify(currentServerChanForm())
106
- }, 10000);
107
- const data = await response.json();
108
- if (!response.ok) throw new Error(data.error || 'Could not save configuration');
109
- serverChanSettings = data.settings;
110
- const keyInput = document.getElementById('serverchan-send-key');
111
- keyInput.value = '';
112
- keyInput.placeholder = data.settings.maskedKey;
113
- document.getElementById('serverchan-key-hint').textContent =
114
- `Saved: ${data.settings.maskedKey}. Leave blank to keep the current SendKey.`;
115
- document.getElementById('serverchan-remove-btn').style.display = 'inline-flex';
116
- setServerChanStatus('Configuration saved. Per-chat notification switches are unchanged.', 'success');
117
- } catch (error) {
118
- setServerChanStatus(error.message || 'Could not save configuration', 'error');
119
- } finally {
120
- setServerChanBusy(false);
121
- }
122
- }
123
-
124
- async function testServerChanSettings() {
125
- setServerChanBusy(true);
126
- setServerChanStatus('Sending test message…');
127
- try {
128
- const response = await fetchWithTimeout('/api/notifications/serverchan/test', {
129
- method: 'POST',
130
- headers: { 'Content-Type': 'application/json' },
131
- body: JSON.stringify(currentServerChanForm())
132
- }, 15000);
133
- const data = await response.json();
134
- if (!response.ok) throw new Error(data.error || 'Could not send test message');
135
- setServerChanStatus('Test message sent. The test did not save configuration.', 'success');
136
- } catch (error) {
137
- setServerChanStatus(error.message || 'Could not send test message', 'error');
138
- } finally {
139
- setServerChanBusy(false);
140
- }
141
- }
142
-
143
- async function removeServerChanSettings() {
144
- if (!confirm('Remove ServerChan configuration and disable notifications for every chat?')) return;
145
- setServerChanBusy(true);
146
- try {
147
- const response = await fetchWithTimeout('/api/notifications/serverchan', {
148
- method: 'DELETE'
149
- }, 10000);
150
- const data = await response.json();
151
- if (!response.ok) throw new Error(data.error || 'Could not remove configuration');
152
- serverChanSettings = data.settings;
153
- document.getElementById('serverchan-send-key').value = '';
154
- document.getElementById('serverchan-send-key').placeholder = 'SCT...';
155
- document.getElementById('serverchan-key-hint').textContent = 'The SendKey is stored only on this Glad host.';
156
- document.getElementById('serverchan-remove-btn').style.display = 'none';
157
- setServerChanStatus('Configuration removed. Notifications are disabled for every chat.', 'success');
158
- await refreshSessionsNow();
159
- } catch (error) {
160
- setServerChanStatus(error.message || 'Could not remove configuration', 'error');
161
- } finally {
162
- setServerChanBusy(false);
163
- }
164
- }
@@ -1,245 +0,0 @@
1
- async function ensureScheduleTools() {
2
- if (scheduleTools.length) return;
3
- const res = await fetchWithTimeout('/api/tools');
4
- scheduleTools = await res.json();
5
- }
6
-
7
- function defaultSchedule() {
8
- return {
9
- id: null,
10
- name: 'Scheduled Task',
11
- enabled: true,
12
- schedule: { time: '09:00', weekdays: [1, 2, 3, 4, 5] },
13
- target: { toolKey: (scheduleTools[0] && scheduleTools[0].key) || 'demo', workingDirectory: '' },
14
- steps: [
15
- { type: 'sleep', seconds: 60 },
16
- { type: 'sendText', text: 'hello' },
17
- { type: 'sleep', seconds: 1 },
18
- { type: 'sendKey', key: 'enter' },
19
- { type: 'sleep', seconds: 60 },
20
- { type: 'closeSession' }
21
- ]
22
- };
23
- }
24
-
25
- async function showScheduleModal(job = null) {
26
- try {
27
- await ensureScheduleTools();
28
- loadAppConfig();
29
- } catch (e) {
30
- alert('Failed to load tools');
31
- return;
32
- }
33
-
34
- const data = job || defaultSchedule();
35
- editingScheduleId = data.id || null;
36
- editingSteps = (data.steps || []).map(step => ({ ...step }));
37
- selectedWeekdays = [...(data.schedule.weekdays || [1, 2, 3, 4, 5])];
38
- document.getElementById('schedule-modal-title').textContent = editingScheduleId ? 'Edit Scheduled Task' : 'New Scheduled Task';
39
- document.getElementById('schedule-name').value = data.name || 'Scheduled Task';
40
- document.getElementById('schedule-cwd').value = data.target.workingDirectory || '';
41
- document.getElementById('schedule-time').value = data.schedule.time || '09:00';
42
-
43
- const toolSelect = document.getElementById('schedule-tool');
44
- toolSelect.innerHTML = scheduleTools.map(t => `<option value="${escapeHtml(t.key)}">${escapeHtml(t.displayName)}</option>`).join('');
45
- const requestedTool = data.target.toolKey || '';
46
- toolSelect.value = scheduleTools.some(t => t.key === requestedTool) ? requestedTool : ((scheduleTools[0] && scheduleTools[0].key) || 'demo');
47
- renderWeekdays();
48
- renderScheduleSteps();
49
- document.getElementById('schedule-modal-overlay').style.display = 'flex';
50
- }
51
-
52
- function closeScheduleModal(e) {
53
- if (!e || e.target.id === 'schedule-modal-overlay') {
54
- document.getElementById('schedule-modal-overlay').style.display = 'none';
55
- }
56
- }
57
-
58
- function renderWeekdays() {
59
- const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
60
- document.getElementById('schedule-weekdays').innerHTML = labels.map((label, day) => {
61
- const active = selectedWeekdays.includes(day);
62
- return `<button type="button" class="weekday-btn ${active ? 'active' : ''}" onclick="toggleWeekday(${day})">${label}</button>`;
63
- }).join('');
64
- }
65
-
66
- function toggleWeekday(day) {
67
- if (selectedWeekdays.includes(day)) {
68
- selectedWeekdays = selectedWeekdays.filter(value => value !== day);
69
- } else {
70
- selectedWeekdays.push(day);
71
- selectedWeekdays.sort((a, b) => a - b);
72
- }
73
- renderWeekdays();
74
- }
75
-
76
- function addScheduleStep(type) {
77
- if (!type) return;
78
- const defaults = {
79
- sleep: { type: 'sleep', seconds: 1 },
80
- sendText: { type: 'sendText', text: '' },
81
- sendKey: { type: 'sendKey', key: 'enter' },
82
- keyDown: { type: 'keyDown', key: 'ctrl' },
83
- keyUp: { type: 'keyUp', key: 'ctrl' },
84
- stop: { type: 'stop' },
85
- closeSession: { type: 'closeSession' }
86
- };
87
- editingSteps.push(defaults[type] || { type });
88
- renderScheduleSteps();
89
- }
90
-
91
- function updateStep(index, field, value) {
92
- if (!editingSteps[index]) return;
93
- if (field === 'seconds') editingSteps[index][field] = Math.max(0, Number(value) || 0);
94
- else editingSteps[index][field] = value;
95
- }
96
-
97
- function moveStep(index, delta) {
98
- const nextIndex = index + delta;
99
- if (nextIndex < 0 || nextIndex >= editingSteps.length) return;
100
- const [step] = editingSteps.splice(index, 1);
101
- editingSteps.splice(nextIndex, 0, step);
102
- renderScheduleSteps();
103
- }
104
-
105
- function copyStep(index) {
106
- editingSteps.splice(index + 1, 0, { ...editingSteps[index] });
107
- renderScheduleSteps();
108
- }
109
-
110
- function removeStep(index) {
111
- editingSteps.splice(index, 1);
112
- renderScheduleSteps();
113
- }
114
-
115
- function stepInputHtml(step, index) {
116
- if (step.type === 'sleep') {
117
- return `<input type="number" min="0" step="0.1" value="${escapeHtml(step.seconds || 0)}" onchange="updateStep(${index}, 'seconds', this.value)">`;
118
- }
119
- if (step.type === 'sendText') {
120
- return `<textarea rows="2" placeholder="Text to send" onchange="updateStep(${index}, 'text', this.value)" oninput="updateStep(${index}, 'text', this.value)">${escapeHtml(step.text || '')}</textarea>`;
121
- }
122
- if (step.type === 'sendKey') {
123
- return `<select onchange="updateStep(${index}, 'key', this.value)">
124
- ${['enter','tab','esc','up','down','left','right','backspace','delete','home','end','ctrl+c','ctrl+d','ctrl+l'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
125
- </select>`;
126
- }
127
- if (step.type === 'keyDown' || step.type === 'keyUp') {
128
- return `<select onchange="updateStep(${index}, 'key', this.value)">
129
- ${['ctrl','alt'].map(key => `<option value="${key}" ${step.key === key ? 'selected' : ''}>${key}</option>`).join('')}
130
- </select>`;
131
- }
132
- if (step.type === 'closeSession') {
133
- return '<span style="color:var(--text-dim); font-size:13px;">Close the session and end this run</span>';
134
- }
135
- return '<span style="color:var(--text-dim); font-size:13px;">End this run</span>';
136
- }
137
-
138
- function renderScheduleSteps() {
139
- const container = document.getElementById('schedule-steps');
140
- if (!editingSteps.length) {
141
- container.innerHTML = '<p style="color:var(--text-dim);">No steps yet.</p>';
142
- return;
143
- }
144
- container.innerHTML = editingSteps.map((step, index) => `
145
- <div class="step-card">
146
- <div class="step-grid">
147
- <select onchange="editingSteps[${index}] = { type: this.value }; addDefaultStepFields(${index}); renderScheduleSteps();">
148
- ${['sleep','sendText','sendKey','keyDown','keyUp','stop','closeSession'].map(type => `<option value="${type}" ${step.type === type ? 'selected' : ''}>${type}</option>`).join('')}
149
- </select>
150
- <div>${stepInputHtml(step, index)}</div>
151
- <div style="display:flex; gap:6px; justify-content:flex-end;">
152
- <button class="small-btn" onclick="moveStep(${index}, -1)">↑</button>
153
- <button class="small-btn" onclick="moveStep(${index}, 1)">↓</button>
154
- <button class="small-btn" onclick="copyStep(${index})">Copy</button>
155
- <button class="small-btn danger" onclick="removeStep(${index})">Del</button>
156
- </div>
157
- </div>
158
- </div>
159
- `).join('');
160
- }
161
-
162
- function addDefaultStepFields(index) {
163
- const type = editingSteps[index].type;
164
- if (type === 'sleep') editingSteps[index].seconds = 1;
165
- if (type === 'sendText') editingSteps[index].text = '';
166
- if (type === 'sendKey') editingSteps[index].key = 'enter';
167
- if (type === 'keyDown' || type === 'keyUp') editingSteps[index].key = 'ctrl';
168
- }
169
-
170
- function collectSchedulePayload() {
171
- if (!selectedWeekdays.length) throw new Error('Select at least one weekday');
172
- return {
173
- name: document.getElementById('schedule-name').value.trim() || 'Scheduled Task',
174
- enabled: true,
175
- schedule: {
176
- time: document.getElementById('schedule-time').value || '09:00',
177
- weekdays: selectedWeekdays
178
- },
179
- target: {
180
- toolKey: document.getElementById('schedule-tool').value,
181
- workingDirectory: document.getElementById('schedule-cwd').value.trim()
182
- },
183
- steps: editingSteps
184
- };
185
- }
186
-
187
- async function saveSchedule() {
188
- try {
189
- const payload = collectSchedulePayload();
190
- const url = editingScheduleId ? `/api/schedules/${editingScheduleId}` : '/api/schedules';
191
- const method = editingScheduleId ? 'PATCH' : 'POST';
192
- const res = await fetchWithTimeout(url, {
193
- method,
194
- headers: { 'Content-Type': 'application/json' },
195
- body: JSON.stringify(payload)
196
- });
197
- if (!res.ok) throw new Error((await res.json()).error || 'Save failed');
198
- closeScheduleModal();
199
- switchLobbyTab('schedules');
200
- } catch (e) {
201
- alert(e.message);
202
- }
203
- }
204
-
205
- async function editSchedule(id) {
206
- const res = await fetchWithTimeout(`/api/schedules/${id}`);
207
- if (!res.ok) return alert('Failed to load schedule');
208
- showScheduleModal(await res.json());
209
- }
210
-
211
- async function simulateSchedule(id) {
212
- try {
213
- const res = await fetchWithTimeout(`/api/schedules/${id}/simulate`, { method: 'POST' }, 30000);
214
- const data = await res.json();
215
- if (!res.ok) throw new Error(data.error || 'Test failed');
216
- await loadSchedules();
217
- if (data.sessionId) {
218
- const jobRes = await fetchWithTimeout(`/api/schedules/${id}`).catch(() => null);
219
- const job = jobRes && jobRes.ok ? await jobRes.json() : null;
220
- joinSession(data.sessionId, 'Schedule Test', job?.target?.toolKey || null);
221
- }
222
- } catch (e) {
223
- alert(e.message);
224
- }
225
- }
226
-
227
- async function duplicateSchedule(id) {
228
- await fetchWithTimeout(`/api/schedules/${id}/duplicate`, { method: 'POST' });
229
- loadSchedules();
230
- }
231
-
232
- async function toggleSchedule(id, enabled) {
233
- await fetchWithTimeout(`/api/schedules/${id}/enabled`, {
234
- method: 'PATCH',
235
- headers: { 'Content-Type': 'application/json' },
236
- body: JSON.stringify({ enabled })
237
- });
238
- loadSchedules();
239
- }
240
-
241
- async function deleteSchedule(id) {
242
- if (!confirm('Delete scheduled task?')) return;
243
- await fetchWithTimeout(`/api/schedules/${id}`, { method: 'DELETE' });
244
- loadSchedules();
245
- }