@symerian/symi 3.5.0 → 3.5.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.
Files changed (62) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/bundled/boot-md/handler.js +4 -4
  3. package/dist/bundled/session-memory/handler.js +4 -4
  4. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  5. package/dist/{chrome-C_I81hbq.js → chrome-B7-rO4i9.js} +4 -4
  6. package/dist/{chrome-BKUACyeO.js → chrome-DPjznJQ-.js} +4 -4
  7. package/dist/control-ui/css/revert-red-theme.md +141 -0
  8. package/dist/control-ui/css/style.css +5843 -0
  9. package/dist/control-ui/css/style.css.backup-2026-03-03-162525 +3546 -0
  10. package/dist/control-ui/css/style.css.backup-before-red-2026-03-03-162525 +3546 -0
  11. package/dist/control-ui/css/style.css.backup-before-red-theme-2026-03-03-162530 +3546 -0
  12. package/dist/control-ui/css/style.css.pre-2row +2165 -0
  13. package/dist/control-ui/css/style.css.pre-brand +1776 -0
  14. package/dist/control-ui/css/style.css.pre-history +1974 -0
  15. package/dist/control-ui/css/style.css.pre-nav +2264 -0
  16. package/dist/control-ui/css/style.css.pre-newsession +1898 -0
  17. package/dist/control-ui/css/style.css.pre-queue +2195 -0
  18. package/dist/control-ui/css/style.css.pre-red-prompt +2524 -0
  19. package/dist/control-ui/css/style.css.pre-stop +2239 -0
  20. package/dist/control-ui/css/style.css.pre-textarea +2184 -0
  21. package/dist/control-ui/css/style.css.pre-watchdog +1848 -0
  22. package/dist/control-ui/css/style.css.red-theme +2999 -0
  23. package/dist/control-ui/index.html +1049 -0
  24. package/dist/control-ui/js/app.js +1304 -0
  25. package/dist/control-ui/js/app.js.pre-2row +463 -0
  26. package/dist/control-ui/js/app.js.pre-heartbeat-filter +595 -0
  27. package/dist/control-ui/js/app.js.pre-newsession +408 -0
  28. package/dist/control-ui/js/app.js.pre-queue +476 -0
  29. package/dist/control-ui/js/app.js.pre-stop +564 -0
  30. package/dist/control-ui/js/app.js.pre-textarea +467 -0
  31. package/dist/control-ui/js/app.js.pre-watchdog +293 -0
  32. package/dist/control-ui/js/connections.js +438 -0
  33. package/dist/control-ui/js/gateway.js +233 -0
  34. package/dist/control-ui/js/gateway.js.pre-stop +110 -0
  35. package/dist/control-ui/js/history.js +732 -0
  36. package/dist/control-ui/js/logs.js +238 -0
  37. package/dist/control-ui/js/menu.js +232 -0
  38. package/dist/control-ui/js/menu.js.pre-nav +66 -0
  39. package/dist/control-ui/js/metrics.js +53 -0
  40. package/dist/control-ui/js/models.js +138 -0
  41. package/dist/control-ui/js/render.js +882 -0
  42. package/dist/control-ui/js/render.test.js +112 -0
  43. package/dist/control-ui/js/scheduling.js +461 -0
  44. package/dist/control-ui/js/settings.js +910 -0
  45. package/dist/control-ui/js/slash-autocomplete.js +168 -0
  46. package/dist/control-ui/js/subagents.js +560 -0
  47. package/dist/control-ui/js/utils.js +29 -0
  48. package/dist/control-ui/vendor/highlight.min.js +2518 -0
  49. package/dist/control-ui/vendor/marked.min.js +69 -0
  50. package/dist/{deliver-DyO3QD8O.js → deliver-DTRkeYm3.js} +4 -4
  51. package/dist/{deliver-Cjyb6h4g.js → deliver-oWGJwzFf.js} +4 -4
  52. package/dist/extensionAPI.js +4 -4
  53. package/dist/llm-slug-generator.js +4 -4
  54. package/dist/{manager-rvtFoeFT.js → manager-CFenq_aO.js} +1 -1
  55. package/dist/{manager-PTSjHNVq.js → manager-CsxTf96V.js} +1 -1
  56. package/dist/{pi-embedded-BPuUM-gD.js → pi-embedded-Cdub5Vs9.js} +10 -10
  57. package/dist/{pw-ai-BFS9ezWe.js → pw-ai-BOOB8qoi.js} +1 -1
  58. package/dist/{pw-ai-Cx-Ko_FL.js → pw-ai-D2pEVS5n.js} +1 -1
  59. package/dist/{synthesis-7UL3pCpj.js → synthesis-Be9nYyDd.js} +4 -4
  60. package/dist/{synthesis-fD8J2vag.js → synthesis-CBIT6Vnk.js} +4 -4
  61. package/dist/{unified-runner-BIiKFnNF.js → unified-runner-BVvvnjXW.js} +10 -10
  62. package/package.json +3 -3
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tests for render.js security and correctness
3
+ * Run with: node glass-ui/js/render.test.js
4
+ */
5
+
6
+ // Mock minimal browser environment
7
+ const escHtml = (s) =>
8
+ String(s ?? "")
9
+ .replace(/&/g, "&")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;")
13
+ .replace(/'/g, "&#39;");
14
+
15
+ // Test cases for XSS prevention
16
+ // Key insight: we check that < and > are escaped, which neutralizes ALL HTML injection
17
+ const xssTestCases = [
18
+ {
19
+ name: "Script tag in inline code - angle brackets escaped",
20
+ input: '<script>alert("XSS")</script>',
21
+ mustNotContain: "<script>", // Raw < must be escaped
22
+ mustContain: "&lt;script&gt;",
23
+ },
24
+ {
25
+ name: "Image tag XSS - angle brackets escaped",
26
+ input: '<img src=x onerror="alert(1)">',
27
+ mustNotContain: "<img", // Raw < must be escaped
28
+ mustContain: "&lt;img",
29
+ },
30
+ {
31
+ name: "Quotes escaped to prevent attribute injection",
32
+ input: '" onclick="alert(1)"',
33
+ mustNotContain: null, // All content is safe inside escaped code
34
+ mustContain: "&quot;", // Double quotes must be escaped
35
+ },
36
+ {
37
+ name: "HTML entities double-escaped",
38
+ input: "&amp; &lt; &gt;",
39
+ mustContain: "&amp;amp;",
40
+ },
41
+ {
42
+ name: "Single quotes escaped",
43
+ input: "it's a test",
44
+ mustContain: "&#39;",
45
+ },
46
+ {
47
+ name: "Normal code unchanged semantically",
48
+ input: "const x = 1;",
49
+ mustContain: "const x = 1;",
50
+ },
51
+ {
52
+ name: "SVG XSS vector blocked",
53
+ input: '<svg onload="alert(1)">',
54
+ mustNotContain: "<svg",
55
+ mustContain: "&lt;svg",
56
+ },
57
+ {
58
+ name: "Nested script blocked",
59
+ input: "<<script>script>alert(1)<</script>/script>",
60
+ mustNotContain: "<script",
61
+ mustContain: "&lt;",
62
+ },
63
+ ];
64
+
65
+ // Simulate renderer.codespan behavior (fixed version)
66
+ function renderCodespan(text) {
67
+ return `<code class="inline-code">${escHtml(text)}</code>`;
68
+ }
69
+
70
+ // Run tests
71
+ console.log("Glass UI render.js Security Tests\n" + "=".repeat(40) + "\n");
72
+
73
+ let passed = 0;
74
+ let failed = 0;
75
+
76
+ for (const tc of xssTestCases) {
77
+ const result = renderCodespan(tc.input);
78
+ let testPassed = true;
79
+ const errors = [];
80
+
81
+ if (tc.mustNotContain && result.includes(tc.mustNotContain)) {
82
+ testPassed = false;
83
+ errors.push(`Should NOT contain: "${tc.mustNotContain}"`);
84
+ }
85
+
86
+ if (tc.mustContain && !result.includes(tc.mustContain)) {
87
+ testPassed = false;
88
+ errors.push(`Should contain: "${tc.mustContain}"`);
89
+ }
90
+
91
+ if (testPassed) {
92
+ console.log(`✓ ${tc.name}`);
93
+ passed++;
94
+ } else {
95
+ console.log(`✗ ${tc.name}`);
96
+ console.log(` Input: ${tc.input}`);
97
+ console.log(` Output: ${result}`);
98
+ errors.forEach((e) => console.log(` Error: ${e}`));
99
+ failed++;
100
+ }
101
+ }
102
+
103
+ console.log("\n" + "=".repeat(40));
104
+ console.log(`Results: ${passed} passed, ${failed} failed`);
105
+
106
+ if (failed > 0) {
107
+ console.log("\n❌ XSS vulnerability detected!");
108
+ process.exit(1);
109
+ } else {
110
+ console.log("\n✅ All XSS tests passed - inline code is properly escaped");
111
+ process.exit(0);
112
+ }
@@ -0,0 +1,461 @@
1
+ // ── Scheduling Panel ─────────────────────────────────────────────────────────
2
+ // Manages scheduled tasks using the cron.* RPC methods.
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ "use strict";
6
+
7
+ (function () {
8
+ // ── State ──────────────────────────────────────────────────────────────────
9
+ let schedules = [];
10
+ let selectedInterval = null;
11
+ let editingScheduleId = null;
12
+ let _pollInterval = null;
13
+
14
+ // ── DOM Elements ───────────────────────────────────────────────────────────
15
+ const _panel = document.getElementById("scheduling-panel");
16
+ const list = document.getElementById("schedule-list");
17
+ const emptyState = document.getElementById("schedule-empty");
18
+ const countBadge = document.getElementById("schedule-count");
19
+ const addBtn = document.getElementById("schedule-add-btn");
20
+
21
+ // Modal elements
22
+ const modalOverlay = document.getElementById("schedule-modal-overlay");
23
+ const modalClose = document.getElementById("schedule-modal-close");
24
+ const modalCancel = document.getElementById("schedule-modal-cancel");
25
+ const modalSubmit = document.getElementById("schedule-modal-submit");
26
+ const nameInput = document.getElementById("schedule-name");
27
+ const taskInput = document.getElementById("schedule-task");
28
+ const intervalBtns = document.querySelectorAll(".schedule-interval-btn");
29
+ const customOptions = document.getElementById("schedule-custom-options");
30
+ const intervalValueInput = document.getElementById("schedule-interval-value");
31
+ const intervalUnitSelect = document.getElementById("schedule-interval-unit");
32
+ const timeInput = document.getElementById("schedule-time");
33
+
34
+ // ── Interval Helpers ───────────────────────────────────────────────────────
35
+ // Convert UI interval to milliseconds
36
+ const UNIT_TO_MS = {
37
+ minutes: 60 * 1000,
38
+ hours: 60 * 60 * 1000,
39
+ days: 24 * 60 * 60 * 1000,
40
+ weeks: 7 * 24 * 60 * 60 * 1000,
41
+ };
42
+
43
+ // Preset intervals
44
+ const PRESETS = {
45
+ hourly: { value: 1, unit: "hours" },
46
+ daily: { value: 1, unit: "days" },
47
+ weekly: { value: 1, unit: "weeks" },
48
+ };
49
+
50
+ function formatInterval(job) {
51
+ const schedule = job.schedule;
52
+ if (!schedule) {
53
+ return "Custom";
54
+ }
55
+
56
+ if (schedule.kind === "every") {
57
+ const ms = schedule.everyMs;
58
+ if (ms >= UNIT_TO_MS.weeks) {
59
+ const weeks = Math.round(ms / UNIT_TO_MS.weeks);
60
+ return weeks === 1 ? "WEEK" : `${weeks}W`;
61
+ }
62
+ if (ms >= UNIT_TO_MS.days) {
63
+ const days = Math.round(ms / UNIT_TO_MS.days);
64
+ return days === 1 ? "DAY" : `${days}D`;
65
+ }
66
+ if (ms >= UNIT_TO_MS.hours) {
67
+ const hours = Math.round(ms / UNIT_TO_MS.hours);
68
+ return hours === 1 ? "HOUR" : `${hours}H`;
69
+ }
70
+ const mins = Math.round(ms / UNIT_TO_MS.minutes);
71
+ return mins === 1 ? "MIN" : `${mins}M`;
72
+ }
73
+
74
+ if (schedule.kind === "cron") {
75
+ return "CRON";
76
+ }
77
+
78
+ if (schedule.kind === "at") {
79
+ return "ONCE";
80
+ }
81
+
82
+ return "CUSTOM";
83
+ }
84
+
85
+ function formatNextRun(job) {
86
+ const nextRunAtMs = job.state?.nextRunAtMs;
87
+ if (!nextRunAtMs) {
88
+ return "Not scheduled";
89
+ }
90
+
91
+ const next = new Date(nextRunAtMs);
92
+ const now = new Date();
93
+ const diffMs = next - now;
94
+
95
+ if (diffMs < 0) {
96
+ return "Overdue";
97
+ }
98
+
99
+ const diffMins = Math.floor(diffMs / 60000);
100
+ const diffHours = Math.floor(diffMins / 60);
101
+ const diffDays = Math.floor(diffHours / 24);
102
+
103
+ if (diffMins < 60) {
104
+ return `in ${diffMins}m`;
105
+ }
106
+ if (diffHours < 24) {
107
+ return `in ${diffHours}h`;
108
+ }
109
+ if (diffDays === 1) {
110
+ return "Tomorrow";
111
+ }
112
+ if (diffDays < 7) {
113
+ return `in ${diffDays} days`;
114
+ }
115
+
116
+ return next.toLocaleDateString("en-US", { month: "short", day: "numeric" });
117
+ }
118
+
119
+ // ── Render Schedules ───────────────────────────────────────────────────────
120
+ function render() {
121
+ const activeCount = schedules.filter((s) => s.enabled).length;
122
+ countBadge.textContent = activeCount > 0 ? activeCount : "";
123
+
124
+ // Clear existing items (except empty state)
125
+ const items = list.querySelectorAll(".schedule-item");
126
+ items.forEach((item) => item.remove());
127
+
128
+ if (schedules.length === 0) {
129
+ emptyState.style.display = "";
130
+ return;
131
+ }
132
+
133
+ emptyState.style.display = "none";
134
+
135
+ // Sort: enabled first, then by nextRunAtMs
136
+ const sorted = [...schedules].toSorted((a, b) => {
137
+ if (a.enabled !== b.enabled) {
138
+ return b.enabled ? 1 : -1;
139
+ }
140
+ const aNext = a.state?.nextRunAtMs || 0;
141
+ const bNext = b.state?.nextRunAtMs || 0;
142
+ return aNext - bNext;
143
+ });
144
+
145
+ for (const job of sorted) {
146
+ const item = document.createElement("div");
147
+ item.className = "schedule-item";
148
+ item.setAttribute("data-active", job.enabled ? "true" : "false");
149
+ item.setAttribute("data-schedule-id", job.id || "");
150
+
151
+ item.innerHTML = `
152
+ <div class="schedule-toggle" title="${job.enabled ? "Disable" : "Enable"}"></div>
153
+ <div class="schedule-info">
154
+ <div class="schedule-name">${escHtml(job.name || "Unnamed")}</div>
155
+ <div class="schedule-next">Next: ${formatNextRun(job)}</div>
156
+ </div>
157
+ <div class="schedule-interval">${formatInterval(job)}</div>
158
+ `;
159
+
160
+ // Toggle click
161
+ const toggle = item.querySelector(".schedule-toggle");
162
+ toggle.addEventListener("click", (e) => {
163
+ e.stopPropagation();
164
+ void toggleSchedule(job.id);
165
+ });
166
+
167
+ // Item click to edit
168
+ item.addEventListener("click", () => openModalForEdit(job));
169
+ list.appendChild(item);
170
+ }
171
+ }
172
+
173
+ // ── Toggle Schedule ────────────────────────────────────────────────────────
174
+ async function toggleSchedule(jobId) {
175
+ const job = schedules.find((s) => s.id === jobId);
176
+ if (!job) {
177
+ return;
178
+ }
179
+
180
+ const newEnabled = !job.enabled;
181
+
182
+ try {
183
+ if (window.gateway?.connected) {
184
+ await window.gateway.rpc("cron.update", {
185
+ id: jobId,
186
+ patch: { enabled: newEnabled },
187
+ });
188
+ }
189
+
190
+ job.enabled = newEnabled;
191
+ render();
192
+ } catch (err) {
193
+ console.error("[scheduling] Toggle failed:", err);
194
+ }
195
+ }
196
+
197
+ // ── Fetch Schedules ────────────────────────────────────────────────────────
198
+ async function fetchSchedules() {
199
+ if (!window.gateway?.connected) {
200
+ return;
201
+ }
202
+
203
+ try {
204
+ const result = await window.gateway.rpc("cron.list", {
205
+ includeDisabled: true,
206
+ });
207
+
208
+ if (result?.jobs) {
209
+ schedules = result.jobs;
210
+ render();
211
+ }
212
+ } catch (err) {
213
+ console.debug("[scheduling] Failed to fetch:", err.message);
214
+ }
215
+ }
216
+
217
+ // ── Modal Controls ─────────────────────────────────────────────────────────
218
+ function openModal() {
219
+ editingScheduleId = null;
220
+ selectedInterval = null;
221
+ resetForm();
222
+
223
+ modalOverlay.classList.add("open");
224
+ modalOverlay.setAttribute("aria-hidden", "false");
225
+ nameInput.focus();
226
+ document.addEventListener("keydown", onModalKey);
227
+ }
228
+
229
+ function openModalForEdit(job) {
230
+ editingScheduleId = job.id;
231
+
232
+ nameInput.value = job.name || "";
233
+ taskInput.value = job.payload?.message || "";
234
+
235
+ // Determine interval type from schedule
236
+ const schedule = job.schedule;
237
+ if (schedule?.kind === "every") {
238
+ const ms = schedule.everyMs;
239
+ if (ms === UNIT_TO_MS.hours) {
240
+ selectInterval("hourly");
241
+ } else if (ms === UNIT_TO_MS.days) {
242
+ selectInterval("daily");
243
+ } else if (ms === UNIT_TO_MS.weeks) {
244
+ selectInterval("weekly");
245
+ } else {
246
+ selectInterval("custom");
247
+ // Determine best unit
248
+ if (ms % UNIT_TO_MS.weeks === 0) {
249
+ intervalValueInput.value = ms / UNIT_TO_MS.weeks;
250
+ intervalUnitSelect.value = "weeks";
251
+ } else if (ms % UNIT_TO_MS.days === 0) {
252
+ intervalValueInput.value = ms / UNIT_TO_MS.days;
253
+ intervalUnitSelect.value = "days";
254
+ } else if (ms % UNIT_TO_MS.hours === 0) {
255
+ intervalValueInput.value = ms / UNIT_TO_MS.hours;
256
+ intervalUnitSelect.value = "hours";
257
+ } else {
258
+ intervalValueInput.value = ms / UNIT_TO_MS.minutes;
259
+ intervalUnitSelect.value = "minutes";
260
+ }
261
+ }
262
+ } else {
263
+ selectInterval("custom");
264
+ }
265
+
266
+ modalOverlay.classList.add("open");
267
+ modalOverlay.setAttribute("aria-hidden", "false");
268
+ nameInput.focus();
269
+ document.addEventListener("keydown", onModalKey);
270
+ }
271
+
272
+ function closeModal() {
273
+ modalOverlay.classList.remove("open");
274
+ modalOverlay.setAttribute("aria-hidden", "true");
275
+ document.removeEventListener("keydown", onModalKey);
276
+ resetForm();
277
+ editingScheduleId = null;
278
+ }
279
+
280
+ function resetForm() {
281
+ nameInput.value = "";
282
+ taskInput.value = "";
283
+ intervalValueInput.value = "1";
284
+ intervalUnitSelect.value = "days";
285
+ timeInput.value = "09:00";
286
+ selectedInterval = null;
287
+ customOptions.style.display = "none";
288
+ intervalBtns.forEach((btn) => btn.classList.remove("selected"));
289
+ }
290
+
291
+ function onModalKey(e) {
292
+ if (e.key === "Escape") {
293
+ closeModal();
294
+ }
295
+ }
296
+
297
+ // ── Interval Selection ─────────────────────────────────────────────────────
298
+ function selectInterval(interval) {
299
+ selectedInterval = interval;
300
+
301
+ intervalBtns.forEach((btn) => {
302
+ const btnInterval = btn.getAttribute("data-interval");
303
+ btn.classList.toggle("selected", btnInterval === interval);
304
+ });
305
+
306
+ if (interval === "custom") {
307
+ customOptions.style.display = "block";
308
+ } else {
309
+ customOptions.style.display = "none";
310
+ }
311
+ }
312
+
313
+ // ── Save Schedule ──────────────────────────────────────────────────────────
314
+ async function saveSchedule() {
315
+ const name = nameInput.value.trim();
316
+ const task = taskInput.value.trim();
317
+
318
+ if (!name) {
319
+ nameInput.focus();
320
+ return;
321
+ }
322
+
323
+ if (!task) {
324
+ taskInput.focus();
325
+ return;
326
+ }
327
+
328
+ if (!selectedInterval) {
329
+ // Default to daily if none selected
330
+ selectInterval("daily");
331
+ }
332
+
333
+ // Calculate everyMs based on interval selection
334
+ let everyMs;
335
+ if (selectedInterval === "custom") {
336
+ const value = parseInt(intervalValueInput.value) || 1;
337
+ const unit = intervalUnitSelect.value;
338
+ everyMs = value * UNIT_TO_MS[unit];
339
+ } else {
340
+ const preset = PRESETS[selectedInterval];
341
+ everyMs = preset.value * UNIT_TO_MS[preset.unit];
342
+ }
343
+
344
+ modalSubmit.disabled = true;
345
+ modalSubmit.innerHTML = `
346
+ <svg class="spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
347
+ <circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="20"/>
348
+ </svg>
349
+ Saving...
350
+ `;
351
+
352
+ try {
353
+ if (!window.gateway?.connected) {
354
+ throw new Error("Gateway not connected");
355
+ }
356
+
357
+ if (editingScheduleId) {
358
+ // Update existing job
359
+ await window.gateway.rpc("cron.update", {
360
+ id: editingScheduleId,
361
+ patch: {
362
+ name,
363
+ schedule: { kind: "every", everyMs },
364
+ payload: { kind: "agentTurn", message: task },
365
+ },
366
+ });
367
+ } else {
368
+ // Create new job
369
+ await window.gateway.rpc("cron.add", {
370
+ name,
371
+ enabled: true,
372
+ schedule: { kind: "every", everyMs },
373
+ sessionTarget: "isolated",
374
+ wakeMode: "now",
375
+ payload: { kind: "agentTurn", message: task },
376
+ });
377
+ }
378
+
379
+ closeModal();
380
+ setTimeout(fetchSchedules, 300);
381
+ } catch (err) {
382
+ console.error("[scheduling] Save failed:", err);
383
+ alert("Failed to save schedule: " + (err.message || "Unknown error"));
384
+ } finally {
385
+ modalSubmit.disabled = false;
386
+ modalSubmit.innerHTML = `
387
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
388
+ <circle cx="12" cy="12" r="10"/>
389
+ <polyline points="12 6 12 12 16 14"/>
390
+ </svg>
391
+ Schedule
392
+ `;
393
+ }
394
+ }
395
+
396
+ // ── Initialize ─────────────────────────────────────────────────────────────
397
+ function init() {
398
+ // Add button
399
+ if (addBtn) {
400
+ addBtn.addEventListener("click", openModal);
401
+ }
402
+
403
+ // Modal controls
404
+ if (modalClose) {
405
+ modalClose.addEventListener("click", closeModal);
406
+ }
407
+ if (modalCancel) {
408
+ modalCancel.addEventListener("click", closeModal);
409
+ }
410
+ if (modalSubmit) {
411
+ modalSubmit.addEventListener("click", saveSchedule);
412
+ }
413
+ if (modalOverlay) {
414
+ modalOverlay.addEventListener("click", (e) => {
415
+ if (e.target === modalOverlay) {
416
+ closeModal();
417
+ }
418
+ });
419
+ }
420
+
421
+ // Interval buttons
422
+ intervalBtns.forEach((btn) => {
423
+ btn.addEventListener("click", () => {
424
+ const interval = btn.getAttribute("data-interval");
425
+ selectInterval(interval);
426
+ });
427
+ });
428
+
429
+ // Submit on Enter in name field (with Ctrl/Cmd)
430
+ if (nameInput) {
431
+ nameInput.addEventListener("keydown", (e) => {
432
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
433
+ void saveSchedule();
434
+ }
435
+ });
436
+ }
437
+
438
+ // Initial fetch
439
+ void fetchSchedules();
440
+
441
+ // Poll every 30 seconds
442
+ _pollInterval = setInterval(fetchSchedules, 30000);
443
+
444
+ // Also fetch on gateway connect
445
+ window.addEventListener("gateway:connected", fetchSchedules);
446
+ window.addEventListener("gateway:hello", fetchSchedules);
447
+ }
448
+
449
+ // Start when DOM is ready
450
+ if (document.readyState === "loading") {
451
+ document.addEventListener("DOMContentLoaded", init);
452
+ } else {
453
+ init();
454
+ }
455
+
456
+ // Expose for external use
457
+ window.schedulingPanel = {
458
+ refresh: fetchSchedules,
459
+ open: openModal,
460
+ };
461
+ })();