@symerian/symi 3.5.0 → 3.5.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.
- package/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +4 -4
- package/dist/bundled/session-memory/handler.js +4 -4
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-C_I81hbq.js → chrome-B7-rO4i9.js} +4 -4
- package/dist/{chrome-BKUACyeO.js → chrome-DPjznJQ-.js} +4 -4
- package/dist/control-ui/css/revert-red-theme.md +141 -0
- package/dist/control-ui/css/style.css +5843 -0
- package/dist/control-ui/css/style.css.backup-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-theme-2026-03-03-162530 +3546 -0
- package/dist/control-ui/css/style.css.pre-2row +2165 -0
- package/dist/control-ui/css/style.css.pre-brand +1776 -0
- package/dist/control-ui/css/style.css.pre-history +1974 -0
- package/dist/control-ui/css/style.css.pre-nav +2264 -0
- package/dist/control-ui/css/style.css.pre-newsession +1898 -0
- package/dist/control-ui/css/style.css.pre-queue +2195 -0
- package/dist/control-ui/css/style.css.pre-red-prompt +2524 -0
- package/dist/control-ui/css/style.css.pre-stop +2239 -0
- package/dist/control-ui/css/style.css.pre-textarea +2184 -0
- package/dist/control-ui/css/style.css.pre-watchdog +1848 -0
- package/dist/control-ui/css/style.css.red-theme +2999 -0
- package/dist/control-ui/index.html +1049 -0
- package/dist/control-ui/js/app.js +1304 -0
- package/dist/control-ui/js/app.js.pre-2row +463 -0
- package/dist/control-ui/js/app.js.pre-heartbeat-filter +595 -0
- package/dist/control-ui/js/app.js.pre-newsession +408 -0
- package/dist/control-ui/js/app.js.pre-queue +476 -0
- package/dist/control-ui/js/app.js.pre-stop +564 -0
- package/dist/control-ui/js/app.js.pre-textarea +467 -0
- package/dist/control-ui/js/app.js.pre-watchdog +293 -0
- package/dist/control-ui/js/connections.js +438 -0
- package/dist/control-ui/js/gateway.js +233 -0
- package/dist/control-ui/js/gateway.js.pre-stop +110 -0
- package/dist/control-ui/js/history.js +732 -0
- package/dist/control-ui/js/logs.js +238 -0
- package/dist/control-ui/js/menu.js +232 -0
- package/dist/control-ui/js/menu.js.pre-nav +66 -0
- package/dist/control-ui/js/metrics.js +53 -0
- package/dist/control-ui/js/models.js +138 -0
- package/dist/control-ui/js/render.js +882 -0
- package/dist/control-ui/js/render.test.js +112 -0
- package/dist/control-ui/js/scheduling.js +461 -0
- package/dist/control-ui/js/settings.js +910 -0
- package/dist/control-ui/js/slash-autocomplete.js +168 -0
- package/dist/control-ui/js/subagents.js +560 -0
- package/dist/control-ui/js/utils.js +29 -0
- package/dist/control-ui/vendor/highlight.min.js +2518 -0
- package/dist/control-ui/vendor/marked.min.js +69 -0
- package/dist/{deliver-DyO3QD8O.js → deliver-DTRkeYm3.js} +4 -4
- package/dist/{deliver-Cjyb6h4g.js → deliver-oWGJwzFf.js} +4 -4
- package/dist/extensionAPI.js +4 -4
- package/dist/llm-slug-generator.js +4 -4
- package/dist/{manager-rvtFoeFT.js → manager-CFenq_aO.js} +1 -1
- package/dist/{manager-PTSjHNVq.js → manager-CsxTf96V.js} +1 -1
- package/dist/{pi-embedded-BPuUM-gD.js → pi-embedded-Cdub5Vs9.js} +10 -10
- package/dist/{pw-ai-BFS9ezWe.js → pw-ai-BOOB8qoi.js} +1 -1
- package/dist/{pw-ai-Cx-Ko_FL.js → pw-ai-D2pEVS5n.js} +1 -1
- package/dist/{synthesis-7UL3pCpj.js → synthesis-Be9nYyDd.js} +4 -4
- package/dist/{synthesis-fD8J2vag.js → synthesis-CBIT6Vnk.js} +4 -4
- package/dist/{unified-runner-BIiKFnNF.js → unified-runner-BVvvnjXW.js} +10 -10
- package/package.json +1 -1
|
@@ -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, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """)
|
|
13
|
+
.replace(/'/g, "'");
|
|
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: "<script>",
|
|
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: "<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: """, // Double quotes must be escaped
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "HTML entities double-escaped",
|
|
38
|
+
input: "& < >",
|
|
39
|
+
mustContain: "&amp;",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Single quotes escaped",
|
|
43
|
+
input: "it's a test",
|
|
44
|
+
mustContain: "'",
|
|
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: "<svg",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Nested script blocked",
|
|
59
|
+
input: "<<script>script>alert(1)<</script>/script>",
|
|
60
|
+
mustNotContain: "<script",
|
|
61
|
+
mustContain: "<",
|
|
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
|
+
})();
|