clay-server 2.7.2 → 2.8.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/bin/cli.js +31 -17
- package/lib/config.js +7 -4
- package/lib/project.js +343 -15
- package/lib/public/app.js +1039 -134
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/base.css +18 -1
- package/lib/public/css/filebrowser.css +1 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +6 -5
- package/lib/public/css/loop.css +141 -23
- package/lib/public/css/messages.css +2 -0
- package/lib/public/css/mobile-nav.css +38 -12
- package/lib/public/css/overlays.css +205 -169
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1305 -0
- package/lib/public/css/sidebar.css +305 -11
- package/lib/public/css/sticky-notes.css +23 -19
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +14 -6
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/index.html +336 -44
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/markdown.js +18 -0
- package/lib/public/modules/notifications.js +50 -63
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +357 -0
- package/lib/public/modules/project-settings.js +1 -9
- package/lib/public/modules/scheduler.js +2826 -0
- package/lib/public/modules/server-settings.js +1 -1
- package/lib/public/modules/sidebar.js +376 -32
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +32 -0
- package/lib/public/modules/theme.js +3 -10
- package/lib/public/style.css +6 -0
- package/lib/public/sw.js +82 -3
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +3 -2
- package/lib/server.js +124 -3
- package/lib/sessions.js +35 -2
- package/package.json +1 -1
|
@@ -0,0 +1,2826 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler module — Split-panel layout: sidebar (task list) + content area.
|
|
3
|
+
*
|
|
4
|
+
* Modes: calendar (month/week grid), detail (single task view), crafting (reparented chat).
|
|
5
|
+
* Edit modal: change cron/name/enabled for existing records.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderMarkdown } from './markdown.js';
|
|
9
|
+
import { iconHtml } from './icons.js';
|
|
10
|
+
|
|
11
|
+
var ctx = null;
|
|
12
|
+
var records = []; // all loop registry records
|
|
13
|
+
|
|
14
|
+
// Calendar state
|
|
15
|
+
var currentView = "month";
|
|
16
|
+
var viewDate = new Date();
|
|
17
|
+
|
|
18
|
+
// Mode state
|
|
19
|
+
var currentMode = "calendar"; // "calendar" | "detail" | "crafting"
|
|
20
|
+
var selectedTaskId = null;
|
|
21
|
+
var showRalphTasks = false; // toggle: show ralph-source tasks in sidebar
|
|
22
|
+
var showAllProjects = false; // toggle: show tasks from all projects (default: current only)
|
|
23
|
+
var currentProjectSlug = null; // derived from basePath on init
|
|
24
|
+
var draggedTaskId = null; // drag-and-drop: task ID being dragged
|
|
25
|
+
var draggedTaskName = null; // drag-and-drop: task name being dragged
|
|
26
|
+
var previewEl = null; // temporary preview event element on calendar
|
|
27
|
+
var craftingTaskId = null; // task ID currently being crafted
|
|
28
|
+
var craftingSessionId = null; // session ID used for crafting
|
|
29
|
+
var logPreviousSessionId = null; // session to restore when leaving log mode
|
|
30
|
+
|
|
31
|
+
// DOM refs
|
|
32
|
+
var panel = null; // #scheduler-panel
|
|
33
|
+
var bodyEl = null;
|
|
34
|
+
var monthLabel = null;
|
|
35
|
+
var calHeader = null;
|
|
36
|
+
var editModal = null;
|
|
37
|
+
var popoverEl = null;
|
|
38
|
+
var panelOpen = false;
|
|
39
|
+
|
|
40
|
+
// Split-panel DOM refs
|
|
41
|
+
var sidebarListEl = null;
|
|
42
|
+
var contentCalEl = null;
|
|
43
|
+
var contentDetailEl = null;
|
|
44
|
+
var contentCraftEl = null;
|
|
45
|
+
var messagesOrigParent = null; // for reparenting
|
|
46
|
+
var inputOrigNextSibling = null; // anchor for restoring input-area position
|
|
47
|
+
|
|
48
|
+
// Edit state
|
|
49
|
+
var editingId = null;
|
|
50
|
+
|
|
51
|
+
// Create popover state
|
|
52
|
+
var createEditingRecId = null; // non-null when editing existing schedule
|
|
53
|
+
var createPopover = null;
|
|
54
|
+
var createSelectedDate = null; // Date object for clicked calendar date
|
|
55
|
+
var createRecurrence = "none"; // current recurrence selection
|
|
56
|
+
var createCustomConfirmed = false; // whether custom repeat was confirmed via OK
|
|
57
|
+
var createColor = "#ffb86c"; // selected event color (default: accent)
|
|
58
|
+
var createEndType = "never"; // "never" | "until" | "after"
|
|
59
|
+
var createEndDate = null; // Date for "until" end type
|
|
60
|
+
var createEndCalMonth = null; // Date tracking displayed month in end calendar
|
|
61
|
+
var createEndAfter = 10; // occurrence count for "after" end type
|
|
62
|
+
var weekTzAbbr = ""; // cached timezone abbreviation for week view
|
|
63
|
+
var nowLineTimer = null; // interval timer for updating current-time indicator
|
|
64
|
+
|
|
65
|
+
// Day names
|
|
66
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
67
|
+
var DAY_SHORT = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
68
|
+
var MONTH_NAMES = [
|
|
69
|
+
"January", "February", "March", "April", "May", "June",
|
|
70
|
+
"July", "August", "September", "October", "November", "December"
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// --- Init ---
|
|
74
|
+
|
|
75
|
+
export function initScheduler(_ctx) {
|
|
76
|
+
ctx = _ctx;
|
|
77
|
+
currentProjectSlug = ctx.currentSlug || null;
|
|
78
|
+
editModal = document.getElementById("schedule-edit-modal");
|
|
79
|
+
createPopover = document.getElementById("schedule-create-popover");
|
|
80
|
+
popoverEl = document.getElementById("schedule-popover");
|
|
81
|
+
|
|
82
|
+
// Sidebar button
|
|
83
|
+
var btn = document.getElementById("scheduler-btn");
|
|
84
|
+
if (btn) {
|
|
85
|
+
btn.addEventListener("click", function () {
|
|
86
|
+
if (panelOpen) {
|
|
87
|
+
closeScheduler();
|
|
88
|
+
} else {
|
|
89
|
+
openScheduler();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Edit modal
|
|
95
|
+
setupEditModal();
|
|
96
|
+
|
|
97
|
+
// Create modal
|
|
98
|
+
setupCreateModal();
|
|
99
|
+
|
|
100
|
+
// Close popover on outside click
|
|
101
|
+
document.addEventListener("click", function (e) {
|
|
102
|
+
if (popoverEl && !popoverEl.classList.contains("hidden") && !popoverEl.contains(e.target)) {
|
|
103
|
+
popoverEl.classList.add("hidden");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ensurePanel() {
|
|
109
|
+
if (panel) return;
|
|
110
|
+
|
|
111
|
+
var appEl = document.getElementById("app");
|
|
112
|
+
if (!appEl) return;
|
|
113
|
+
|
|
114
|
+
panel = document.createElement("div");
|
|
115
|
+
panel.id = "scheduler-panel";
|
|
116
|
+
panel.className = "hidden";
|
|
117
|
+
|
|
118
|
+
// --- Top header bar ---
|
|
119
|
+
var topBar = document.createElement("div");
|
|
120
|
+
topBar.className = "scheduler-top-bar";
|
|
121
|
+
topBar.innerHTML =
|
|
122
|
+
'<span class="scheduler-top-title"><i data-lucide="calendar-clock"></i>Scheduled Tasks</span>' +
|
|
123
|
+
'<label class="scheduler-scope-toggle" id="scheduler-scope-toggle">' +
|
|
124
|
+
'<span class="scheduler-scope-label" data-side="off">This project</span>' +
|
|
125
|
+
'<span class="scheduler-scope-switch"><span class="scheduler-scope-thumb"></span></span>' +
|
|
126
|
+
'<span class="scheduler-scope-label" data-side="on">All projects</span>' +
|
|
127
|
+
'</label>' +
|
|
128
|
+
'<button class="scheduler-close-btn" id="scheduler-panel-close" title="Close"><i data-lucide="x"></i></button>';
|
|
129
|
+
panel.appendChild(topBar);
|
|
130
|
+
|
|
131
|
+
// Scope toggle handler (in top bar)
|
|
132
|
+
var scopeToggle = topBar.querySelector("#scheduler-scope-toggle");
|
|
133
|
+
if (scopeToggle) {
|
|
134
|
+
scopeToggle.addEventListener("click", function (e) {
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
showAllProjects = !showAllProjects;
|
|
137
|
+
scopeToggle.classList.toggle("active", showAllProjects);
|
|
138
|
+
renderSidebar();
|
|
139
|
+
if (currentMode === "calendar") render();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Body row (sidebar + content) ---
|
|
144
|
+
var bodyRow = document.createElement("div");
|
|
145
|
+
bodyRow.className = "scheduler-body-row";
|
|
146
|
+
|
|
147
|
+
// --- Sidebar ---
|
|
148
|
+
var sidebar = document.createElement("div");
|
|
149
|
+
sidebar.className = "scheduler-sidebar";
|
|
150
|
+
|
|
151
|
+
// Sidebar header
|
|
152
|
+
var sidebarHeader = document.createElement("div");
|
|
153
|
+
sidebarHeader.className = "scheduler-sidebar-header";
|
|
154
|
+
sidebarHeader.innerHTML =
|
|
155
|
+
'<span class="scheduler-sidebar-title">Tasks</span>' +
|
|
156
|
+
'<span class="scheduler-sidebar-count">0</span>' +
|
|
157
|
+
'<button class="scheduler-ralph-toggle" id="scheduler-ralph-toggle" title="Show Ralph Loops">' +
|
|
158
|
+
'<i data-lucide="repeat"></i> <span>Show Ralph</span>' +
|
|
159
|
+
'</button>';
|
|
160
|
+
sidebar.appendChild(sidebarHeader);
|
|
161
|
+
|
|
162
|
+
// Ralph toggle handler
|
|
163
|
+
var ralphToggleBtn = sidebarHeader.querySelector("#scheduler-ralph-toggle");
|
|
164
|
+
if (ralphToggleBtn) {
|
|
165
|
+
ralphToggleBtn.addEventListener("click", function (e) {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
showRalphTasks = !showRalphTasks;
|
|
168
|
+
ralphToggleBtn.classList.toggle("active", showRalphTasks);
|
|
169
|
+
renderSidebar();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Inline add task
|
|
174
|
+
var addRow = document.createElement("div");
|
|
175
|
+
addRow.className = "scheduler-add-row";
|
|
176
|
+
addRow.innerHTML =
|
|
177
|
+
'<div class="scheduler-add-trigger" id="scheduler-add-trigger">' +
|
|
178
|
+
'<i data-lucide="plus-circle"></i> <span>Add new task</span>' +
|
|
179
|
+
'</div>' +
|
|
180
|
+
'<div class="scheduler-add-form hidden" id="scheduler-add-form">' +
|
|
181
|
+
'<textarea id="scheduler-add-input" rows="2" placeholder="Describe what to build..."></textarea>' +
|
|
182
|
+
'<div class="scheduler-add-actions">' +
|
|
183
|
+
'<button type="button" class="scheduler-add-submit" id="scheduler-add-submit">Add</button>' +
|
|
184
|
+
'<button type="button" class="scheduler-add-cancel" id="scheduler-add-cancel">Cancel</button>' +
|
|
185
|
+
'</div>' +
|
|
186
|
+
'</div>';
|
|
187
|
+
sidebar.appendChild(addRow);
|
|
188
|
+
|
|
189
|
+
// Sidebar list
|
|
190
|
+
var sidebarList = document.createElement("div");
|
|
191
|
+
sidebarList.className = "scheduler-sidebar-list";
|
|
192
|
+
sidebar.appendChild(sidebarList);
|
|
193
|
+
sidebarListEl = sidebarList;
|
|
194
|
+
|
|
195
|
+
bodyRow.appendChild(sidebar);
|
|
196
|
+
|
|
197
|
+
// --- Content ---
|
|
198
|
+
var content = document.createElement("div");
|
|
199
|
+
content.className = "scheduler-content";
|
|
200
|
+
|
|
201
|
+
// Content: calendar
|
|
202
|
+
var contentCal = document.createElement("div");
|
|
203
|
+
contentCal.className = "scheduler-content-calendar";
|
|
204
|
+
|
|
205
|
+
// Calendar header (nav, month label, view toggle)
|
|
206
|
+
var calHdr = document.createElement("div");
|
|
207
|
+
calHdr.className = "scheduler-header";
|
|
208
|
+
calHdr.id = "scheduler-cal-header";
|
|
209
|
+
calHdr.innerHTML =
|
|
210
|
+
'<div class="scheduler-nav">' +
|
|
211
|
+
'<button class="scheduler-nav-btn" id="scheduler-prev"><i data-lucide="chevron-left"></i></button>' +
|
|
212
|
+
'<button class="scheduler-nav-btn" id="scheduler-next"><i data-lucide="chevron-right"></i></button>' +
|
|
213
|
+
'</div>' +
|
|
214
|
+
'<span class="scheduler-month-label" id="scheduler-month-label"></span>' +
|
|
215
|
+
'<button class="scheduler-today-btn" id="scheduler-today">Today</button>' +
|
|
216
|
+
'<div class="scheduler-view-toggle">' +
|
|
217
|
+
'<button class="scheduler-view-btn active" data-view="month">Month</button>' +
|
|
218
|
+
'<button class="scheduler-view-btn" data-view="week">Week</button>' +
|
|
219
|
+
'</div>';
|
|
220
|
+
contentCal.appendChild(calHdr);
|
|
221
|
+
calHeader = calHdr;
|
|
222
|
+
monthLabel = calHdr.querySelector("#scheduler-month-label");
|
|
223
|
+
|
|
224
|
+
// Calendar body
|
|
225
|
+
var body = document.createElement("div");
|
|
226
|
+
body.className = "scheduler-body";
|
|
227
|
+
body.id = "scheduler-body";
|
|
228
|
+
contentCal.appendChild(body);
|
|
229
|
+
bodyEl = body;
|
|
230
|
+
|
|
231
|
+
content.appendChild(contentCal);
|
|
232
|
+
contentCalEl = contentCal;
|
|
233
|
+
|
|
234
|
+
// Content: detail
|
|
235
|
+
var contentDetail = document.createElement("div");
|
|
236
|
+
contentDetail.className = "scheduler-content-detail hidden";
|
|
237
|
+
content.appendChild(contentDetail);
|
|
238
|
+
contentDetailEl = contentDetail;
|
|
239
|
+
|
|
240
|
+
// Content: crafting
|
|
241
|
+
var contentCraft = document.createElement("div");
|
|
242
|
+
contentCraft.className = "scheduler-content-crafting hidden";
|
|
243
|
+
content.appendChild(contentCraft);
|
|
244
|
+
contentCraftEl = contentCraft;
|
|
245
|
+
|
|
246
|
+
bodyRow.appendChild(content);
|
|
247
|
+
panel.appendChild(bodyRow);
|
|
248
|
+
|
|
249
|
+
appEl.appendChild(panel);
|
|
250
|
+
|
|
251
|
+
// --- Close button (in top bar) ---
|
|
252
|
+
panel.querySelector("#scheduler-panel-close").addEventListener("click", function () {
|
|
253
|
+
closeScheduler();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Inline add task
|
|
257
|
+
var addTrigger = addRow.querySelector("#scheduler-add-trigger");
|
|
258
|
+
var addForm = addRow.querySelector("#scheduler-add-form");
|
|
259
|
+
var addInput = addRow.querySelector("#scheduler-add-input");
|
|
260
|
+
var addSubmitBtn = addRow.querySelector("#scheduler-add-submit");
|
|
261
|
+
var addCancelBtn = addRow.querySelector("#scheduler-add-cancel");
|
|
262
|
+
|
|
263
|
+
addTrigger.addEventListener("click", function () {
|
|
264
|
+
addTrigger.classList.add("hidden");
|
|
265
|
+
addForm.classList.remove("hidden");
|
|
266
|
+
addInput.value = "";
|
|
267
|
+
addInput.focus();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
addCancelBtn.addEventListener("click", function () {
|
|
271
|
+
addForm.classList.add("hidden");
|
|
272
|
+
addTrigger.classList.remove("hidden");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
addInput.addEventListener("keydown", function (e) {
|
|
276
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
submitInlineTask();
|
|
279
|
+
}
|
|
280
|
+
if (e.key === "Escape") {
|
|
281
|
+
addForm.classList.add("hidden");
|
|
282
|
+
addTrigger.classList.remove("hidden");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
addSubmitBtn.addEventListener("click", function () {
|
|
287
|
+
submitInlineTask();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
var addSubmitting = false;
|
|
291
|
+
function submitInlineTask() {
|
|
292
|
+
if (addSubmitting) return;
|
|
293
|
+
var task = addInput.value.trim();
|
|
294
|
+
if (!task) { addInput.focus(); return; }
|
|
295
|
+
addSubmitting = true;
|
|
296
|
+
addInput.value = "";
|
|
297
|
+
addForm.classList.add("hidden");
|
|
298
|
+
addTrigger.classList.remove("hidden");
|
|
299
|
+
// Send wizard complete directly (skip modal)
|
|
300
|
+
send({
|
|
301
|
+
type: "ralph_wizard_complete",
|
|
302
|
+
data: { name: task, task: task, maxIterations: 3, cron: null, source: "task" }
|
|
303
|
+
});
|
|
304
|
+
setTimeout(function () { addSubmitting = false; }, 1000);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Calendar controls
|
|
308
|
+
calHdr.querySelector("#scheduler-prev").addEventListener("click", function () { navigate(-1); });
|
|
309
|
+
calHdr.querySelector("#scheduler-next").addEventListener("click", function () { navigate(1); });
|
|
310
|
+
calHdr.querySelector("#scheduler-today").addEventListener("click", function () { viewDate = new Date(); render(); });
|
|
311
|
+
|
|
312
|
+
// View toggle
|
|
313
|
+
var viewBtns = calHdr.querySelectorAll(".scheduler-view-btn");
|
|
314
|
+
for (var i = 0; i < viewBtns.length; i++) {
|
|
315
|
+
(function (vbtn) {
|
|
316
|
+
vbtn.addEventListener("click", function () {
|
|
317
|
+
currentView = vbtn.dataset.view;
|
|
318
|
+
for (var j = 0; j < viewBtns.length; j++) {
|
|
319
|
+
viewBtns[j].classList.toggle("active", viewBtns[j] === vbtn);
|
|
320
|
+
}
|
|
321
|
+
render();
|
|
322
|
+
});
|
|
323
|
+
})(viewBtns[i]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try { lucide.createIcons({ node: panel }); } catch (e) {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- Mode switching ---
|
|
330
|
+
|
|
331
|
+
function switchMode(mode) {
|
|
332
|
+
currentMode = mode;
|
|
333
|
+
if (contentCalEl) contentCalEl.classList.toggle("hidden", mode !== "calendar");
|
|
334
|
+
if (contentDetailEl) contentDetailEl.classList.toggle("hidden", mode !== "detail");
|
|
335
|
+
if (contentCraftEl) contentCraftEl.classList.toggle("hidden", mode !== "crafting");
|
|
336
|
+
|
|
337
|
+
if (mode === "calendar") {
|
|
338
|
+
selectedTaskId = null;
|
|
339
|
+
updateSidebarSelection();
|
|
340
|
+
unparentChat();
|
|
341
|
+
if (contentDetailEl) contentDetailEl.innerHTML = "";
|
|
342
|
+
render();
|
|
343
|
+
} else if (mode === "detail") {
|
|
344
|
+
unparentChat();
|
|
345
|
+
renderDetail();
|
|
346
|
+
} else if (mode === "crafting") {
|
|
347
|
+
reparentChat();
|
|
348
|
+
updateCraftingHeader();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function updateCraftingHeader() {
|
|
353
|
+
if (!contentCraftEl) return;
|
|
354
|
+
var existing = contentCraftEl.querySelector(".scheduler-crafting-header");
|
|
355
|
+
if (existing) existing.remove();
|
|
356
|
+
|
|
357
|
+
var isLog = !!logPreviousSessionId;
|
|
358
|
+
var hdr = document.createElement("div");
|
|
359
|
+
hdr.className = "scheduler-crafting-header";
|
|
360
|
+
|
|
361
|
+
var backBtn = document.createElement("button");
|
|
362
|
+
backBtn.className = "scheduler-crafting-back";
|
|
363
|
+
backBtn.innerHTML = '<i data-lucide="arrow-left"></i> <span>' + (isLog ? "Back to task" : "Back to tasks") + '</span>';
|
|
364
|
+
backBtn.addEventListener("click", function () {
|
|
365
|
+
if (isLog) {
|
|
366
|
+
switchMode("detail");
|
|
367
|
+
} else {
|
|
368
|
+
switchMode("calendar");
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
hdr.appendChild(backBtn);
|
|
372
|
+
|
|
373
|
+
var label = document.createElement("span");
|
|
374
|
+
label.className = "scheduler-crafting-label";
|
|
375
|
+
if (isLog) {
|
|
376
|
+
label.innerHTML = '<i data-lucide="message-square"></i> Session Log';
|
|
377
|
+
} else {
|
|
378
|
+
label.innerHTML = '<i data-lucide="radio"></i> Crafting in progress';
|
|
379
|
+
}
|
|
380
|
+
hdr.appendChild(label);
|
|
381
|
+
|
|
382
|
+
contentCraftEl.insertBefore(hdr, contentCraftEl.firstChild);
|
|
383
|
+
try { lucide.createIcons({ node: hdr }); } catch (e) {}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// --- Open/Close ---
|
|
387
|
+
|
|
388
|
+
function openScheduler() {
|
|
389
|
+
if (panelOpen) return;
|
|
390
|
+
panelOpen = true;
|
|
391
|
+
ensurePanel();
|
|
392
|
+
if (!panel) return;
|
|
393
|
+
|
|
394
|
+
var messagesEl = document.getElementById("messages");
|
|
395
|
+
var inputArea = document.getElementById("input-area");
|
|
396
|
+
var titleBar = document.querySelector("#main-column > .title-bar-content");
|
|
397
|
+
var notesContainer = document.getElementById("sticky-notes-container");
|
|
398
|
+
var notesArchive = document.getElementById("notes-archive");
|
|
399
|
+
|
|
400
|
+
if (messagesEl) messagesEl.classList.add("hidden");
|
|
401
|
+
if (inputArea) inputArea.classList.add("hidden");
|
|
402
|
+
if (titleBar) titleBar.classList.add("hidden");
|
|
403
|
+
if (notesContainer) notesContainer.classList.add("hidden");
|
|
404
|
+
if (notesArchive) notesArchive.classList.add("hidden");
|
|
405
|
+
|
|
406
|
+
// Un-mark sticky notes sidebar button when scheduler takes over
|
|
407
|
+
var notesSidebarBtn = document.getElementById("sticky-notes-sidebar-btn");
|
|
408
|
+
if (notesSidebarBtn) notesSidebarBtn.classList.remove("active");
|
|
409
|
+
|
|
410
|
+
panel.classList.remove("hidden");
|
|
411
|
+
viewDate = new Date();
|
|
412
|
+
currentMode = "calendar";
|
|
413
|
+
selectedTaskId = null;
|
|
414
|
+
send({ type: "loop_registry_list" });
|
|
415
|
+
switchMode("calendar");
|
|
416
|
+
renderSidebar();
|
|
417
|
+
try { lucide.createIcons({ node: panel }); } catch (e) {}
|
|
418
|
+
|
|
419
|
+
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
420
|
+
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function closeScheduler() {
|
|
424
|
+
if (!panelOpen) return;
|
|
425
|
+
panelOpen = false;
|
|
426
|
+
stopNowLineTimer();
|
|
427
|
+
if (currentMode === "crafting") unparentChat();
|
|
428
|
+
|
|
429
|
+
if (panel) panel.classList.add("hidden");
|
|
430
|
+
if (popoverEl) popoverEl.classList.add("hidden");
|
|
431
|
+
closeCreateModal();
|
|
432
|
+
|
|
433
|
+
var messagesEl = document.getElementById("messages");
|
|
434
|
+
var inputArea = document.getElementById("input-area");
|
|
435
|
+
var titleBar = document.querySelector("#main-column > .title-bar-content");
|
|
436
|
+
|
|
437
|
+
if (messagesEl) messagesEl.classList.remove("hidden");
|
|
438
|
+
if (inputArea) inputArea.classList.remove("hidden");
|
|
439
|
+
if (titleBar) titleBar.classList.remove("hidden");
|
|
440
|
+
|
|
441
|
+
currentMode = "calendar";
|
|
442
|
+
selectedTaskId = null;
|
|
443
|
+
|
|
444
|
+
// Un-mark sidebar button
|
|
445
|
+
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
446
|
+
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Reset state on project switch (SPA navigation, no full reload)
|
|
450
|
+
export function resetScheduler(newSlug) {
|
|
451
|
+
records = [];
|
|
452
|
+
currentProjectSlug = newSlug || null;
|
|
453
|
+
selectedTaskId = null;
|
|
454
|
+
craftingTaskId = null;
|
|
455
|
+
craftingSessionId = null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function send(msg) {
|
|
459
|
+
if (ctx && ctx.ws && ctx.ws.readyState === 1) {
|
|
460
|
+
ctx.ws.send(JSON.stringify(msg));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// --- Project filtering ---
|
|
465
|
+
|
|
466
|
+
function filterByProject(recs) {
|
|
467
|
+
if (showAllProjects || !currentProjectSlug) return recs;
|
|
468
|
+
return recs.filter(function (r) { return !r.projectSlug || r.projectSlug === currentProjectSlug; });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isOwnRecord(rec) {
|
|
472
|
+
if (!currentProjectSlug) return true;
|
|
473
|
+
return !rec.projectSlug || rec.projectSlug === currentProjectSlug;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// --- Sidebar ---
|
|
477
|
+
|
|
478
|
+
function renderSidebar() {
|
|
479
|
+
if (!sidebarListEl) return;
|
|
480
|
+
|
|
481
|
+
// Apply project filter first
|
|
482
|
+
var projectFiltered = filterByProject(records);
|
|
483
|
+
|
|
484
|
+
// Update count badge (exclude ralph and schedule items from count)
|
|
485
|
+
var taskRecords = projectFiltered.filter(function (r) { return r.source !== "ralph" && r.source !== "schedule"; });
|
|
486
|
+
var ralphRecords = projectFiltered.filter(function (r) { return r.source === "ralph"; });
|
|
487
|
+
var countEl = panel ? panel.querySelector(".scheduler-sidebar-count") : null;
|
|
488
|
+
if (countEl) countEl.textContent = showRalphTasks ? (taskRecords.length + ralphRecords.length) : taskRecords.length;
|
|
489
|
+
|
|
490
|
+
// Update toggle badges
|
|
491
|
+
var toggleBtn = panel ? panel.querySelector("#scheduler-ralph-toggle") : null;
|
|
492
|
+
if (toggleBtn) {
|
|
493
|
+
toggleBtn.classList.toggle("has-items", ralphRecords.length > 0);
|
|
494
|
+
toggleBtn.classList.toggle("active", showRalphTasks);
|
|
495
|
+
}
|
|
496
|
+
var scopeEl = panel ? panel.querySelector("#scheduler-scope-toggle") : null;
|
|
497
|
+
if (scopeEl) {
|
|
498
|
+
scopeEl.classList.toggle("active", showAllProjects);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
var filtered = showRalphTasks
|
|
502
|
+
? projectFiltered.filter(function (r) { return r.source !== "schedule"; })
|
|
503
|
+
: taskRecords;
|
|
504
|
+
|
|
505
|
+
if (filtered.length === 0) {
|
|
506
|
+
sidebarListEl.innerHTML = '<div class="scheduler-empty">' + (showRalphTasks ? "No tasks" : "No tasks yet") + '</div>';
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
var sorted = filtered.sort(function (a, b) { return (b.createdAt || 0) - (a.createdAt || 0); });
|
|
511
|
+
var html = "";
|
|
512
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
513
|
+
var rec = sorted[i];
|
|
514
|
+
var isRalph = rec.source === "ralph";
|
|
515
|
+
var isScheduled = !!rec.cron;
|
|
516
|
+
var selected = rec.id === selectedTaskId ? " selected" : "";
|
|
517
|
+
var isCrafting = craftingTaskId === rec.id;
|
|
518
|
+
var isOwn = isOwnRecord(rec);
|
|
519
|
+
|
|
520
|
+
html += '<div class="scheduler-task-item' + selected + (isOwn ? "" : " foreign") + '" data-rec-id="' + rec.id + '" data-rec-name="' + esc(rec.name || rec.id) + '"' + (isOwn ? ' draggable="true"' : '') + '>';
|
|
521
|
+
html += '<div class="scheduler-task-name-row">';
|
|
522
|
+
if (isOwn) {
|
|
523
|
+
html += '<span class="scheduler-task-drag-handle" title="Drag to calendar">' + iconHtml("grip-vertical") + '</span>';
|
|
524
|
+
}
|
|
525
|
+
html += '<div class="scheduler-task-name">' + esc(rec.name || rec.id) + '</div>';
|
|
526
|
+
if (!isCrafting && isOwn) {
|
|
527
|
+
html += '<button class="scheduler-task-edit-btn" data-edit-id="' + rec.id + '" type="button" title="Rename">' + iconHtml("pencil") + '</button>';
|
|
528
|
+
}
|
|
529
|
+
html += '</div>';
|
|
530
|
+
// Badges row
|
|
531
|
+
var badges = [];
|
|
532
|
+
if (showAllProjects && rec.projectTitle) {
|
|
533
|
+
badges.push('<span class="scheduler-task-badge project">' + esc(rec.projectTitle) + '</span>');
|
|
534
|
+
}
|
|
535
|
+
if (isRalph) badges.push('<span class="scheduler-task-badge ralph">Ralph</span>');
|
|
536
|
+
if (isCrafting) badges.push('<span class="scheduler-task-badge crafting">Crafting</span>');
|
|
537
|
+
else if (isScheduled && rec.enabled) badges.push('<span class="scheduler-task-badge scheduled">Scheduled</span>');
|
|
538
|
+
if (badges.length > 0) {
|
|
539
|
+
html += '<div class="scheduler-task-row">' + badges.join("") + '</div>';
|
|
540
|
+
}
|
|
541
|
+
html += '</div>';
|
|
542
|
+
}
|
|
543
|
+
if (sorted.length > 0) {
|
|
544
|
+
html += '<div class="scheduler-drag-hint">' + iconHtml("arrow-right-to-line") + ' Drag task to calendar to schedule</div>';
|
|
545
|
+
}
|
|
546
|
+
sidebarListEl.innerHTML = html;
|
|
547
|
+
|
|
548
|
+
// Attach click handlers
|
|
549
|
+
var items = sidebarListEl.querySelectorAll(".scheduler-task-item");
|
|
550
|
+
for (var i = 0; i < items.length; i++) {
|
|
551
|
+
(function (item) {
|
|
552
|
+
item.addEventListener("click", function () {
|
|
553
|
+
var clickedId = item.dataset.recId;
|
|
554
|
+
if (selectedTaskId === clickedId) {
|
|
555
|
+
if (currentMode === "detail") {
|
|
556
|
+
// Toggle: detail → crafting (if this task is being crafted) or calendar
|
|
557
|
+
if (craftingTaskId === clickedId) {
|
|
558
|
+
switchMode("crafting");
|
|
559
|
+
} else {
|
|
560
|
+
switchMode("calendar");
|
|
561
|
+
renderSidebar();
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
} else if (currentMode === "crafting") {
|
|
565
|
+
// Toggle: crafting → detail
|
|
566
|
+
switchMode("detail");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
selectedTaskId = clickedId;
|
|
571
|
+
updateSidebarSelection();
|
|
572
|
+
switchMode("detail");
|
|
573
|
+
});
|
|
574
|
+
})(items[i]);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Attach drag handlers for drag-to-calendar
|
|
578
|
+
for (var i = 0; i < items.length; i++) {
|
|
579
|
+
(function (item) {
|
|
580
|
+
item.addEventListener("dragstart", function (e) {
|
|
581
|
+
draggedTaskId = item.dataset.recId;
|
|
582
|
+
draggedTaskName = item.dataset.recName;
|
|
583
|
+
e.dataTransfer.setData("text/plain", draggedTaskId);
|
|
584
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
585
|
+
item.classList.add("dragging");
|
|
586
|
+
});
|
|
587
|
+
item.addEventListener("dragend", function () {
|
|
588
|
+
draggedTaskId = null;
|
|
589
|
+
draggedTaskName = null;
|
|
590
|
+
item.classList.remove("dragging");
|
|
591
|
+
// Clean up any lingering drag-over highlights
|
|
592
|
+
var overs = document.querySelectorAll(".drag-over");
|
|
593
|
+
for (var j = 0; j < overs.length; j++) overs[j].classList.remove("drag-over");
|
|
594
|
+
});
|
|
595
|
+
})(items[i]);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Attach pencil edit handlers
|
|
599
|
+
var editBtns = sidebarListEl.querySelectorAll(".scheduler-task-edit-btn");
|
|
600
|
+
for (var i = 0; i < editBtns.length; i++) {
|
|
601
|
+
(function (btn) {
|
|
602
|
+
btn.addEventListener("click", function (e) {
|
|
603
|
+
e.stopPropagation();
|
|
604
|
+
var editId = btn.dataset.editId;
|
|
605
|
+
var rec = null;
|
|
606
|
+
for (var j = 0; j < records.length; j++) {
|
|
607
|
+
if (records[j].id === editId) { rec = records[j]; break; }
|
|
608
|
+
}
|
|
609
|
+
if (!rec) return;
|
|
610
|
+
var nameEl = btn.parentElement.querySelector(".scheduler-task-name");
|
|
611
|
+
var original = rec.name || rec.id;
|
|
612
|
+
var input = document.createElement("input");
|
|
613
|
+
input.type = "text";
|
|
614
|
+
input.className = "scheduler-task-name-input";
|
|
615
|
+
input.value = original;
|
|
616
|
+
nameEl.replaceWith(input);
|
|
617
|
+
btn.classList.add("hidden");
|
|
618
|
+
input.focus();
|
|
619
|
+
input.select();
|
|
620
|
+
|
|
621
|
+
function finishEdit() {
|
|
622
|
+
var newName = input.value.trim();
|
|
623
|
+
if (newName && newName !== original) {
|
|
624
|
+
send({ type: "loop_registry_update", id: editId, data: { name: newName } });
|
|
625
|
+
}
|
|
626
|
+
renderSidebar();
|
|
627
|
+
}
|
|
628
|
+
input.addEventListener("keydown", function (ev) {
|
|
629
|
+
if (ev.key === "Enter") { ev.preventDefault(); finishEdit(); }
|
|
630
|
+
if (ev.key === "Escape") { ev.preventDefault(); renderSidebar(); }
|
|
631
|
+
});
|
|
632
|
+
input.addEventListener("blur", finishEdit);
|
|
633
|
+
});
|
|
634
|
+
})(editBtns[i]);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try { lucide.createIcons({ node: sidebarListEl }); } catch (e) {}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function updateSidebarSelection() {
|
|
641
|
+
if (!sidebarListEl) return;
|
|
642
|
+
var items = sidebarListEl.querySelectorAll(".scheduler-task-item");
|
|
643
|
+
for (var i = 0; i < items.length; i++) {
|
|
644
|
+
items[i].classList.toggle("selected", items[i].dataset.recId === selectedTaskId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// --- Detail view ---
|
|
649
|
+
|
|
650
|
+
function renderDetail() {
|
|
651
|
+
if (!contentDetailEl || !selectedTaskId) return;
|
|
652
|
+
var rec = null;
|
|
653
|
+
for (var i = 0; i < records.length; i++) {
|
|
654
|
+
if (records[i].id === selectedTaskId) { rec = records[i]; break; }
|
|
655
|
+
}
|
|
656
|
+
if (!rec) {
|
|
657
|
+
// Task not found — fall back to calendar view
|
|
658
|
+
selectedTaskId = null;
|
|
659
|
+
switchMode("calendar");
|
|
660
|
+
renderSidebar();
|
|
661
|
+
render();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
var isScheduled = !!rec.cron;
|
|
666
|
+
var lastRun = rec.runs && rec.runs.length > 0 ? rec.runs[rec.runs.length - 1] : null;
|
|
667
|
+
|
|
668
|
+
var isCraftingThis = craftingTaskId === rec.id;
|
|
669
|
+
var hasSession = rec.craftingSessionId || null;
|
|
670
|
+
|
|
671
|
+
var html = '<div class="scheduler-detail-header">';
|
|
672
|
+
html += '<button class="scheduler-crafting-back" data-action="close" title="Back to tasks"><i data-lucide="arrow-left"></i></button>';
|
|
673
|
+
html += '<span class="scheduler-detail-name">' + esc(rec.name || rec.id) + '</span>';
|
|
674
|
+
html += '<div class="scheduler-detail-actions">';
|
|
675
|
+
if (isCraftingThis || hasSession) {
|
|
676
|
+
html += '<button class="scheduler-detail-btn" data-action="session">';
|
|
677
|
+
html += '<i data-lucide="' + (isCraftingThis ? "radio" : "message-square") + '"></i> ';
|
|
678
|
+
html += isCraftingThis ? "Live session" : "Session log";
|
|
679
|
+
html += '</button>';
|
|
680
|
+
}
|
|
681
|
+
if (rec.source === "ralph") {
|
|
682
|
+
html += '<button class="scheduler-detail-btn" data-action="convert" title="Convert to regular task"><i data-lucide="arrow-right-left"></i> To Task</button>';
|
|
683
|
+
}
|
|
684
|
+
html += '<button class="scheduler-detail-btn primary" data-action="run">Run now</button>';
|
|
685
|
+
html += '<button class="scheduler-detail-icon-btn" data-action="delete" title="Delete task"><i data-lucide="trash-2"></i></button>';
|
|
686
|
+
html += '</div>';
|
|
687
|
+
html += '</div>';
|
|
688
|
+
|
|
689
|
+
html += '<div class="scheduler-detail-tabs">';
|
|
690
|
+
html += '<button class="scheduler-detail-tab active" data-tab="prompt">PROMPT.md</button>';
|
|
691
|
+
html += '<button class="scheduler-detail-tab" data-tab="judge">JUDGE.md</button>';
|
|
692
|
+
html += '<button class="scheduler-detail-tab" data-tab="meta">Info</button>';
|
|
693
|
+
html += '</div>';
|
|
694
|
+
|
|
695
|
+
html += '<div class="scheduler-detail-body" id="scheduler-detail-body">';
|
|
696
|
+
html += '<div class="scheduler-detail-loading">Loading...</div>';
|
|
697
|
+
html += '</div>';
|
|
698
|
+
|
|
699
|
+
contentDetailEl.innerHTML = html;
|
|
700
|
+
|
|
701
|
+
// Bind action handlers
|
|
702
|
+
var actionBtns = contentDetailEl.querySelectorAll("[data-action]");
|
|
703
|
+
for (var i = 0; i < actionBtns.length; i++) {
|
|
704
|
+
(function (btn) {
|
|
705
|
+
btn.addEventListener("click", function (e) {
|
|
706
|
+
e.stopPropagation();
|
|
707
|
+
var action = btn.dataset.action;
|
|
708
|
+
if (action === "run") {
|
|
709
|
+
send({ type: "loop_registry_rerun", id: selectedTaskId });
|
|
710
|
+
} else if (action === "delete") {
|
|
711
|
+
if (confirm("Delete this task?")) {
|
|
712
|
+
send({ type: "loop_registry_remove", id: selectedTaskId });
|
|
713
|
+
}
|
|
714
|
+
} else if (action === "close") {
|
|
715
|
+
switchMode("calendar");
|
|
716
|
+
renderSidebar();
|
|
717
|
+
} else if (action === "convert") {
|
|
718
|
+
send({ type: "loop_registry_convert", id: selectedTaskId });
|
|
719
|
+
} else if (action === "session") {
|
|
720
|
+
if (craftingTaskId === rec.id) {
|
|
721
|
+
switchMode("crafting");
|
|
722
|
+
} else if (rec.craftingSessionId) {
|
|
723
|
+
logPreviousSessionId = ctx.activeSessionId || null;
|
|
724
|
+
send({ type: "switch_session", id: rec.craftingSessionId });
|
|
725
|
+
switchMode("crafting");
|
|
726
|
+
var inputArea = document.getElementById("input-area");
|
|
727
|
+
if (inputArea && contentCraftEl && contentCraftEl.contains(inputArea)) {
|
|
728
|
+
inputArea.classList.add("hidden");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
})(actionBtns[i]);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Bind tab switching
|
|
737
|
+
var tabBtns = contentDetailEl.querySelectorAll(".scheduler-detail-tab");
|
|
738
|
+
for (var i = 0; i < tabBtns.length; i++) {
|
|
739
|
+
(function (tabBtn) {
|
|
740
|
+
tabBtn.addEventListener("click", function () {
|
|
741
|
+
for (var j = 0; j < tabBtns.length; j++) {
|
|
742
|
+
tabBtns[j].classList.toggle("active", tabBtns[j] === tabBtn);
|
|
743
|
+
}
|
|
744
|
+
renderDetailBody(tabBtn.dataset.tab, rec);
|
|
745
|
+
});
|
|
746
|
+
})(tabBtns[i]);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Request files for prompt tab (default)
|
|
750
|
+
send({ type: "loop_registry_files", id: selectedTaskId });
|
|
751
|
+
|
|
752
|
+
try { lucide.createIcons({ node: contentDetailEl }); } catch (e) {}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function renderDetailBody(tab, rec) {
|
|
756
|
+
var bodyEl2 = document.getElementById("scheduler-detail-body");
|
|
757
|
+
if (!bodyEl2) return;
|
|
758
|
+
|
|
759
|
+
if (tab === "meta") {
|
|
760
|
+
var isScheduled = !!rec.cron;
|
|
761
|
+
var lastRun = rec.runs && rec.runs.length > 0 ? rec.runs[rec.runs.length - 1] : null;
|
|
762
|
+
var scheduleStr = isScheduled ? cronToHuman(rec.cron) : "One-off";
|
|
763
|
+
var statusStr = isScheduled ? (rec.enabled ? "Enabled" : "Paused") : "One-off";
|
|
764
|
+
var createdStr = rec.createdAt ? formatDateTime(new Date(rec.createdAt)) : "—";
|
|
765
|
+
var lastRunStr = "Never";
|
|
766
|
+
if (lastRun) {
|
|
767
|
+
var resultStr = lastRun.result || "?";
|
|
768
|
+
var iterStr = (lastRun.iterations || 0) + " iter";
|
|
769
|
+
lastRunStr = formatDateTime(new Date(lastRun.finishedAt || lastRun.startedAt)) + " — " + resultStr + " (" + iterStr + ")";
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
var html = '<div class="scheduler-detail-meta">';
|
|
773
|
+
html += '<span class="scheduler-detail-meta-label">Schedule</span>';
|
|
774
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(scheduleStr) + '</span>';
|
|
775
|
+
html += '<span class="scheduler-detail-meta-label">Status</span>';
|
|
776
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(statusStr) + '</span>';
|
|
777
|
+
html += '<span class="scheduler-detail-meta-label">Max Iterations</span>';
|
|
778
|
+
html += '<span class="scheduler-detail-meta-value">' + (rec.maxIterations || "—") + '</span>';
|
|
779
|
+
html += '<span class="scheduler-detail-meta-label">Created</span>';
|
|
780
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(createdStr) + '</span>';
|
|
781
|
+
html += '<span class="scheduler-detail-meta-label">Last Run</span>';
|
|
782
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(lastRunStr) + '</span>';
|
|
783
|
+
html += '</div>';
|
|
784
|
+
bodyEl2.innerHTML = html;
|
|
785
|
+
} else {
|
|
786
|
+
// prompt or judge — request files from server
|
|
787
|
+
bodyEl2.innerHTML = '<div class="scheduler-detail-loading">Loading...</div>';
|
|
788
|
+
send({ type: "loop_registry_files", id: selectedTaskId });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// --- Chat reparenting ---
|
|
793
|
+
|
|
794
|
+
function reparentChat() {
|
|
795
|
+
var messagesEl = document.getElementById("messages");
|
|
796
|
+
var inputArea = document.getElementById("input-area");
|
|
797
|
+
if (!messagesEl || !inputArea || !contentCraftEl) return;
|
|
798
|
+
if (messagesOrigParent) return; // already reparented
|
|
799
|
+
messagesOrigParent = messagesEl.parentNode;
|
|
800
|
+
inputOrigNextSibling = inputArea.nextSibling;
|
|
801
|
+
contentCraftEl.appendChild(messagesEl);
|
|
802
|
+
contentCraftEl.appendChild(inputArea);
|
|
803
|
+
messagesEl.classList.remove("hidden");
|
|
804
|
+
inputArea.classList.remove("hidden");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function unparentChat() {
|
|
808
|
+
var messagesEl = document.getElementById("messages");
|
|
809
|
+
var inputArea = document.getElementById("input-area");
|
|
810
|
+
if (!messagesOrigParent) return;
|
|
811
|
+
var infoPanels = messagesOrigParent.querySelector("#info-panels");
|
|
812
|
+
if (infoPanels) {
|
|
813
|
+
messagesOrigParent.insertBefore(messagesEl, infoPanels);
|
|
814
|
+
} else {
|
|
815
|
+
messagesOrigParent.appendChild(messagesEl);
|
|
816
|
+
}
|
|
817
|
+
if (inputOrigNextSibling) {
|
|
818
|
+
messagesOrigParent.insertBefore(inputArea, inputOrigNextSibling);
|
|
819
|
+
} else {
|
|
820
|
+
messagesOrigParent.appendChild(inputArea);
|
|
821
|
+
}
|
|
822
|
+
messagesOrigParent = null;
|
|
823
|
+
inputOrigNextSibling = null;
|
|
824
|
+
|
|
825
|
+
// Restore input-area visibility (may have been hidden in log mode)
|
|
826
|
+
if (inputArea) inputArea.classList.remove("hidden");
|
|
827
|
+
|
|
828
|
+
// Remove crafting header
|
|
829
|
+
if (contentCraftEl) {
|
|
830
|
+
var craftHdr = contentCraftEl.querySelector(".scheduler-crafting-header");
|
|
831
|
+
if (craftHdr) craftHdr.remove();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// If we were in log mode, switch back to the original session
|
|
835
|
+
if (logPreviousSessionId) {
|
|
836
|
+
send({ type: "switch_session", id: logPreviousSessionId });
|
|
837
|
+
logPreviousSessionId = null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// --- Navigation ---
|
|
842
|
+
|
|
843
|
+
function navigate(dir) {
|
|
844
|
+
if (currentView === "month") {
|
|
845
|
+
viewDate.setMonth(viewDate.getMonth() + dir);
|
|
846
|
+
} else {
|
|
847
|
+
viewDate.setDate(viewDate.getDate() + dir * 7);
|
|
848
|
+
}
|
|
849
|
+
render();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// --- Render ---
|
|
853
|
+
|
|
854
|
+
function render() {
|
|
855
|
+
if (!bodyEl) return;
|
|
856
|
+
updateMonthLabel();
|
|
857
|
+
if (currentView === "month") {
|
|
858
|
+
renderMonthView();
|
|
859
|
+
} else {
|
|
860
|
+
renderWeekView();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function updateMonthLabel() {
|
|
865
|
+
if (!monthLabel) return;
|
|
866
|
+
if (currentView === "month") {
|
|
867
|
+
monthLabel.textContent = MONTH_NAMES[viewDate.getMonth()] + " " + viewDate.getFullYear();
|
|
868
|
+
} else {
|
|
869
|
+
var weekStart = getWeekStart(viewDate);
|
|
870
|
+
var weekEnd = new Date(weekStart);
|
|
871
|
+
weekEnd.setDate(weekEnd.getDate() + 6);
|
|
872
|
+
monthLabel.textContent = MONTH_NAMES[weekStart.getMonth()].substring(0, 3) + " " + weekStart.getDate() + " – " + MONTH_NAMES[weekEnd.getMonth()].substring(0, 3) + " " + weekEnd.getDate() + ", " + weekEnd.getFullYear();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// --- Month View ---
|
|
877
|
+
|
|
878
|
+
function renderMonthView() {
|
|
879
|
+
stopNowLineTimer();
|
|
880
|
+
var year = viewDate.getFullYear();
|
|
881
|
+
var month = viewDate.getMonth();
|
|
882
|
+
var today = new Date();
|
|
883
|
+
var todayStr = today.getFullYear() + "-" + pad(today.getMonth() + 1) + "-" + pad(today.getDate());
|
|
884
|
+
|
|
885
|
+
var firstDay = new Date(year, month, 1);
|
|
886
|
+
var startDay = new Date(firstDay);
|
|
887
|
+
startDay.setDate(startDay.getDate() - firstDay.getDay());
|
|
888
|
+
|
|
889
|
+
var html = '<div class="scheduler-weekdays">';
|
|
890
|
+
html += '<div class="scheduler-weekday scheduler-week-num-hdr"></div>';
|
|
891
|
+
for (var d = 0; d < 7; d++) {
|
|
892
|
+
var wkdCls = "scheduler-weekday" + (d === 0 || d === 6 ? " weekend" : "");
|
|
893
|
+
html += '<div class="' + wkdCls + '">' + DAY_NAMES[d] + '</div>';
|
|
894
|
+
}
|
|
895
|
+
html += '</div><div class="scheduler-grid">';
|
|
896
|
+
|
|
897
|
+
var cursor = new Date(startDay);
|
|
898
|
+
for (var w = 0; w < 6; w++) {
|
|
899
|
+
// Week number label
|
|
900
|
+
var wn = getISOWeekNumber(cursor);
|
|
901
|
+
html += '<div class="scheduler-week-num">W' + wn + '</div>';
|
|
902
|
+
for (var d = 0; d < 7; d++) {
|
|
903
|
+
var dateStr = cursor.getFullYear() + "-" + pad(cursor.getMonth() + 1) + "-" + pad(cursor.getDate());
|
|
904
|
+
var isOther = cursor.getMonth() !== month;
|
|
905
|
+
var isToday = dateStr === todayStr;
|
|
906
|
+
var isWeekend = d === 0 || d === 6;
|
|
907
|
+
var cls = "scheduler-cell" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (isWeekend ? " weekend" : "");
|
|
908
|
+
html += '<div class="' + cls + '" data-date="' + dateStr + '">';
|
|
909
|
+
var dayLabel = cursor.getDate() === 1
|
|
910
|
+
? MONTH_NAMES[cursor.getMonth()].substring(0, 3) + ", " + cursor.getDate()
|
|
911
|
+
: String(cursor.getDate());
|
|
912
|
+
html += '<div class="scheduler-day-num">' + dayLabel + '</div>';
|
|
913
|
+
var events = getEventsForDate(cursor);
|
|
914
|
+
for (var e = 0; e < events.length && e < 3; e++) {
|
|
915
|
+
var ev = events[e];
|
|
916
|
+
html += '<div class="scheduler-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '">';
|
|
917
|
+
html += '<span class="scheduler-event-time">' + ev.timeStr + '</span> ' + esc(ev.name);
|
|
918
|
+
html += '</div>';
|
|
919
|
+
}
|
|
920
|
+
if (events.length > 3) {
|
|
921
|
+
html += '<div class="scheduler-event" style="opacity:0.6;font-size:10px">+' + (events.length - 3) + ' more</div>';
|
|
922
|
+
}
|
|
923
|
+
html += '</div>';
|
|
924
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
html += '</div>';
|
|
928
|
+
bodyEl.innerHTML = html;
|
|
929
|
+
attachEventClicks(bodyEl, ".scheduler-event[data-rec-id]");
|
|
930
|
+
attachCellClicks(bodyEl);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// --- Week View ---
|
|
934
|
+
|
|
935
|
+
function renderWeekView() {
|
|
936
|
+
var weekStart = getWeekStart(viewDate);
|
|
937
|
+
var today = new Date();
|
|
938
|
+
var todayStr = today.getFullYear() + "-" + pad(today.getMonth() + 1) + "-" + pad(today.getDate());
|
|
939
|
+
|
|
940
|
+
// Detect timezone abbreviation (prefer named like NZDT/KST/PDT, fallback to short)
|
|
941
|
+
weekTzAbbr = "";
|
|
942
|
+
try {
|
|
943
|
+
// Try longGeneric first to extract abbreviation from toString()
|
|
944
|
+
var tzStr = today.toLocaleTimeString("en", { timeZoneName: "short" });
|
|
945
|
+
var tzMatch = tzStr.match(/[A-Z]{2,5}$/);
|
|
946
|
+
if (tzMatch) {
|
|
947
|
+
weekTzAbbr = tzMatch[0];
|
|
948
|
+
} else {
|
|
949
|
+
// Fallback: extract from Date.toString() which usually has e.g. "(New Zealand Daylight Time)"
|
|
950
|
+
var dStr = today.toString();
|
|
951
|
+
var parenMatch = dStr.match(/\((.+)\)/);
|
|
952
|
+
if (parenMatch) {
|
|
953
|
+
// Build abbreviation from first letters of each word
|
|
954
|
+
var words = parenMatch[1].split(/\s+/);
|
|
955
|
+
var abbr = "";
|
|
956
|
+
for (var w = 0; w < words.length; w++) abbr += words[w].charAt(0);
|
|
957
|
+
weekTzAbbr = abbr;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (e) {}
|
|
961
|
+
|
|
962
|
+
// Header: timezone label + day columns
|
|
963
|
+
var html = '<div class="scheduler-week-header">';
|
|
964
|
+
html += '<div class="scheduler-week-tz-label">' + esc(weekTzAbbr) + '</div>';
|
|
965
|
+
for (var d = 0; d < 7; d++) {
|
|
966
|
+
var day = new Date(weekStart);
|
|
967
|
+
day.setDate(day.getDate() + d);
|
|
968
|
+
var dateStr = day.getFullYear() + "-" + pad(day.getMonth() + 1) + "-" + pad(day.getDate());
|
|
969
|
+
var dayShort = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day.getDay()];
|
|
970
|
+
html += '<div class="scheduler-week-header-cell' + (dateStr === todayStr ? ' today' : '') + '">';
|
|
971
|
+
html += '<span class="wday">' + dayShort + '</span> ';
|
|
972
|
+
html += '<span class="wdate">' + day.getDate() + '</span></div>';
|
|
973
|
+
}
|
|
974
|
+
html += '</div>';
|
|
975
|
+
|
|
976
|
+
// Week body wrapper (for relative positioning of current-time indicator)
|
|
977
|
+
html += '<div class="scheduler-week-body">';
|
|
978
|
+
html += '<div class="scheduler-week-view">';
|
|
979
|
+
|
|
980
|
+
// Time column
|
|
981
|
+
html += '<div class="scheduler-week-time-col">';
|
|
982
|
+
for (var h = 0; h < 24; h++) {
|
|
983
|
+
html += '<div class="scheduler-week-time-label">' + (h === 0 ? "" : pad(h) + ":00") + '</div>';
|
|
984
|
+
}
|
|
985
|
+
html += '</div>';
|
|
986
|
+
|
|
987
|
+
// Day columns with 4 sub-slots per hour (15-min)
|
|
988
|
+
for (var d = 0; d < 7; d++) {
|
|
989
|
+
var day = new Date(weekStart);
|
|
990
|
+
day.setDate(day.getDate() + d);
|
|
991
|
+
var dayDateStr = day.getFullYear() + "-" + pad(day.getMonth() + 1) + "-" + pad(day.getDate());
|
|
992
|
+
html += '<div class="scheduler-week-day-col" data-date="' + dayDateStr + '">';
|
|
993
|
+
for (var h = 0; h < 24; h++) {
|
|
994
|
+
html += '<div class="scheduler-week-hour" data-date="' + dayDateStr + '" data-hour="' + h + '">';
|
|
995
|
+
for (var q = 0; q < 4; q++) {
|
|
996
|
+
html += '<div class="scheduler-week-slot" data-date="' + dayDateStr + '" data-hour="' + h + '" data-quarter="' + q + '"></div>';
|
|
997
|
+
}
|
|
998
|
+
html += '</div>';
|
|
999
|
+
}
|
|
1000
|
+
// Events — detect overlaps and lay out side-by-side
|
|
1001
|
+
var events = getEventsForDate(day);
|
|
1002
|
+
var evDuration = 30; // assumed event duration in minutes for overlap detection
|
|
1003
|
+
// Assign overlap columns: greedy left-to-right
|
|
1004
|
+
// Sort by start time
|
|
1005
|
+
var sorted = events.slice().sort(function (a, b) {
|
|
1006
|
+
return (a.hour * 60 + a.minute) - (b.hour * 60 + b.minute);
|
|
1007
|
+
});
|
|
1008
|
+
// Build overlap groups
|
|
1009
|
+
var colAssign = {}; // ev.id -> { col, totalCols }
|
|
1010
|
+
var groups = []; // array of arrays of event indices sharing overlap
|
|
1011
|
+
for (var e = 0; e < sorted.length; e++) {
|
|
1012
|
+
var ev = sorted[e];
|
|
1013
|
+
var evStart = ev.hour * 60 + ev.minute;
|
|
1014
|
+
var evEnd = evStart + evDuration;
|
|
1015
|
+
// Find which group this event overlaps with
|
|
1016
|
+
var placed = false;
|
|
1017
|
+
for (var g = 0; g < groups.length; g++) {
|
|
1018
|
+
var grp = groups[g];
|
|
1019
|
+
var overlaps = false;
|
|
1020
|
+
for (var gi = 0; gi < grp.length; gi++) {
|
|
1021
|
+
var other = grp[gi];
|
|
1022
|
+
var oStart = other.hour * 60 + other.minute;
|
|
1023
|
+
var oEnd = oStart + evDuration;
|
|
1024
|
+
if (evStart < oEnd && evEnd > oStart) { overlaps = true; break; }
|
|
1025
|
+
}
|
|
1026
|
+
if (overlaps) { grp.push(ev); placed = true; break; }
|
|
1027
|
+
}
|
|
1028
|
+
if (!placed) groups.push([ev]);
|
|
1029
|
+
}
|
|
1030
|
+
// Assign columns within each group
|
|
1031
|
+
for (var g = 0; g < groups.length; g++) {
|
|
1032
|
+
var grp = groups[g];
|
|
1033
|
+
for (var gi = 0; gi < grp.length; gi++) {
|
|
1034
|
+
colAssign[grp[gi].id] = { col: gi, totalCols: grp.length };
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (var e = 0; e < events.length; e++) {
|
|
1038
|
+
var ev = events[e];
|
|
1039
|
+
var topPct = ((ev.hour * 60 + ev.minute) / 1440) * 100;
|
|
1040
|
+
var evColor = ev.color || "";
|
|
1041
|
+
var assign = colAssign[ev.id] || { col: 0, totalCols: 1 };
|
|
1042
|
+
var rightMargin = 15; // percentage reserved for "add new" click area
|
|
1043
|
+
var usableWidth = 100 - rightMargin;
|
|
1044
|
+
var colWidth = usableWidth / assign.totalCols;
|
|
1045
|
+
var leftPct = assign.col * colWidth;
|
|
1046
|
+
var evStyle = "top:" + topPct + "%;height:calc(160vh / 48)";
|
|
1047
|
+
evStyle += ";left:" + leftPct + "%;width:" + (colWidth - 1) + "%";
|
|
1048
|
+
if (evColor) evStyle += ";background:" + evColor;
|
|
1049
|
+
html += '<div class="scheduler-week-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" style="' + evStyle + '">';
|
|
1050
|
+
html += '<span class="scheduler-week-event-title">' + esc(ev.name) + '</span>';
|
|
1051
|
+
html += '<span class="scheduler-week-event-time">' + ev.timeStr + '</span>';
|
|
1052
|
+
html += '</div>';
|
|
1053
|
+
}
|
|
1054
|
+
html += '</div>';
|
|
1055
|
+
}
|
|
1056
|
+
// Current time indicator — per-column segments (past=dim, today=bright, future=hidden)
|
|
1057
|
+
var nowMinutes = today.getHours() * 60 + today.getMinutes();
|
|
1058
|
+
var nowPct = (nowMinutes / 1440) * 100;
|
|
1059
|
+
var todayDayIdx = -1;
|
|
1060
|
+
for (var d = 0; d < 7; d++) {
|
|
1061
|
+
var chk = new Date(weekStart);
|
|
1062
|
+
chk.setDate(chk.getDate() + d);
|
|
1063
|
+
var chkStr = chk.getFullYear() + "-" + pad(chk.getMonth() + 1) + "-" + pad(chk.getDate());
|
|
1064
|
+
if (chkStr === todayStr) { todayDayIdx = d; break; }
|
|
1065
|
+
}
|
|
1066
|
+
html += '<div class="scheduler-week-now-line" style="top:' + nowPct + '%">';
|
|
1067
|
+
html += '<span class="scheduler-week-now-label">' + pad(today.getHours()) + ':' + pad(today.getMinutes()) + '</span>';
|
|
1068
|
+
for (var d = 0; d < 7; d++) {
|
|
1069
|
+
var segCls = "now-seg";
|
|
1070
|
+
if (d < todayDayIdx) segCls += " past";
|
|
1071
|
+
else if (d === todayDayIdx) segCls += " today";
|
|
1072
|
+
else segCls += " future";
|
|
1073
|
+
html += '<div class="' + segCls + '"></div>';
|
|
1074
|
+
}
|
|
1075
|
+
html += '</div>';
|
|
1076
|
+
|
|
1077
|
+
html += '</div>'; // .scheduler-week-view
|
|
1078
|
+
html += '</div>'; // .scheduler-week-body
|
|
1079
|
+
|
|
1080
|
+
// Task count footer
|
|
1081
|
+
html += '<div class="scheduler-week-footer">';
|
|
1082
|
+
html += '<div class="scheduler-week-footer-tz"></div>';
|
|
1083
|
+
for (var d = 0; d < 7; d++) {
|
|
1084
|
+
var day = new Date(weekStart);
|
|
1085
|
+
day.setDate(day.getDate() + d);
|
|
1086
|
+
var dayEvents = getEventsForDate(day);
|
|
1087
|
+
var taskCount = dayEvents.length;
|
|
1088
|
+
html += '<div class="scheduler-week-footer-cell">';
|
|
1089
|
+
if (taskCount > 0) html += '<span class="scheduler-week-task-badge">' + taskCount + (taskCount === 1 ? ' Task' : ' Tasks') + '</span>';
|
|
1090
|
+
html += '</div>';
|
|
1091
|
+
}
|
|
1092
|
+
html += '</div>';
|
|
1093
|
+
|
|
1094
|
+
bodyEl.innerHTML = html;
|
|
1095
|
+
|
|
1096
|
+
// Scroll to current time area
|
|
1097
|
+
var weekBody = bodyEl.querySelector(".scheduler-week-body");
|
|
1098
|
+
if (weekBody) {
|
|
1099
|
+
var hourH = weekBody.scrollHeight / 24;
|
|
1100
|
+
weekBody.scrollTop = Math.max(0, today.getHours() - 2) * hourH;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
attachEventClicks(bodyEl, ".scheduler-week-event[data-rec-id]");
|
|
1104
|
+
attachWeekSlotClicks(bodyEl);
|
|
1105
|
+
attachWeekHoverTooltip(bodyEl);
|
|
1106
|
+
startNowLineTimer();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function startNowLineTimer() {
|
|
1110
|
+
if (nowLineTimer) clearInterval(nowLineTimer);
|
|
1111
|
+
nowLineTimer = setInterval(updateNowLine, 30000); // every 30s
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function stopNowLineTimer() {
|
|
1115
|
+
if (nowLineTimer) { clearInterval(nowLineTimer); nowLineTimer = null; }
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function updateNowLine() {
|
|
1119
|
+
if (!bodyEl) return;
|
|
1120
|
+
var line = bodyEl.querySelector(".scheduler-week-now-line");
|
|
1121
|
+
if (!line) return;
|
|
1122
|
+
var now = new Date();
|
|
1123
|
+
var mins = now.getHours() * 60 + now.getMinutes();
|
|
1124
|
+
var pct = (mins / 1440) * 100;
|
|
1125
|
+
line.style.top = pct + "%";
|
|
1126
|
+
var label = line.querySelector(".scheduler-week-now-label");
|
|
1127
|
+
if (label) label.textContent = pad(now.getHours()) + ":" + pad(now.getMinutes());
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function attachWeekHoverTooltip(container) {
|
|
1131
|
+
var tooltip = document.createElement("div");
|
|
1132
|
+
tooltip.className = "scheduler-week-tooltip hidden";
|
|
1133
|
+
container.appendChild(tooltip);
|
|
1134
|
+
|
|
1135
|
+
var dayCols = container.querySelectorAll(".scheduler-week-day-col");
|
|
1136
|
+
for (var i = 0; i < dayCols.length; i++) {
|
|
1137
|
+
(function (col) {
|
|
1138
|
+
col.addEventListener("mousemove", function (e) {
|
|
1139
|
+
var rect = col.getBoundingClientRect();
|
|
1140
|
+
// e.clientY - rect.top gives position within the full column (rect reflects scroll offset)
|
|
1141
|
+
var relY = e.clientY - rect.top;
|
|
1142
|
+
var colH = rect.height;
|
|
1143
|
+
var totalMin = (relY / colH) * 1440;
|
|
1144
|
+
var snapped = Math.floor(totalMin / 15) * 15;
|
|
1145
|
+
if (snapped < 0) snapped = 0;
|
|
1146
|
+
if (snapped >= 1440) snapped = 1425;
|
|
1147
|
+
var hh = Math.floor(snapped / 60);
|
|
1148
|
+
var mm = snapped % 60;
|
|
1149
|
+
tooltip.textContent = pad(hh) + ":" + pad(mm) + " " + weekTzAbbr;
|
|
1150
|
+
// Position tooltip near cursor
|
|
1151
|
+
var bodyRect = container.querySelector(".scheduler-week-body").getBoundingClientRect();
|
|
1152
|
+
tooltip.style.left = (e.clientX - bodyRect.left + 12) + "px";
|
|
1153
|
+
tooltip.style.top = (e.clientY - bodyRect.top - 14) + "px";
|
|
1154
|
+
tooltip.classList.remove("hidden");
|
|
1155
|
+
});
|
|
1156
|
+
col.addEventListener("mouseleave", function () {
|
|
1157
|
+
tooltip.classList.add("hidden");
|
|
1158
|
+
});
|
|
1159
|
+
})(dayCols[i]);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// --- Events for calendar ---
|
|
1164
|
+
|
|
1165
|
+
function getEventsForDate(date) {
|
|
1166
|
+
var results = [];
|
|
1167
|
+
var dow = date.getDay();
|
|
1168
|
+
var dom = date.getDate();
|
|
1169
|
+
var month = date.getMonth() + 1;
|
|
1170
|
+
var dateStr = date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
|
|
1171
|
+
|
|
1172
|
+
var visibleRecords = filterByProject(records);
|
|
1173
|
+
for (var i = 0; i < visibleRecords.length; i++) {
|
|
1174
|
+
var r = visibleRecords[i];
|
|
1175
|
+
|
|
1176
|
+
// One-off schedule (no cron) with a specific date
|
|
1177
|
+
if (!r.cron && r.date) {
|
|
1178
|
+
if (r.date === dateStr) {
|
|
1179
|
+
var evHour = 0;
|
|
1180
|
+
var evMinute = 0;
|
|
1181
|
+
if (r.time) {
|
|
1182
|
+
var tp = r.time.split(":");
|
|
1183
|
+
evHour = parseInt(tp[0], 10) || 0;
|
|
1184
|
+
evMinute = parseInt(tp[1], 10) || 0;
|
|
1185
|
+
}
|
|
1186
|
+
results.push({
|
|
1187
|
+
id: r.id, name: r.name, enabled: true,
|
|
1188
|
+
hour: evHour, minute: evMinute,
|
|
1189
|
+
timeStr: r.allDay ? "All day" : pad(evHour) + ":" + pad(evMinute),
|
|
1190
|
+
allDay: r.allDay || false,
|
|
1191
|
+
color: r.color || null,
|
|
1192
|
+
source: r.source || null,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (!r.cron) continue; // skip non-scheduled without date
|
|
1199
|
+
var parsed = parseCronSimple(r.cron);
|
|
1200
|
+
if (!parsed) continue;
|
|
1201
|
+
// Skip occurrences before the schedule's start date
|
|
1202
|
+
if (r.date) {
|
|
1203
|
+
var sp = r.date.split("-");
|
|
1204
|
+
var startDate = new Date(parseInt(sp[0], 10), parseInt(sp[1], 10) - 1, parseInt(sp[2], 10));
|
|
1205
|
+
startDate.setHours(0, 0, 0, 0);
|
|
1206
|
+
var checkDate = new Date(date);
|
|
1207
|
+
checkDate.setHours(0, 0, 0, 0);
|
|
1208
|
+
if (checkDate < startDate) continue;
|
|
1209
|
+
}
|
|
1210
|
+
// Skip occurrences after the recurrence end date
|
|
1211
|
+
if (r.recurrenceEnd && r.recurrenceEnd.type === "until" && r.recurrenceEnd.date) {
|
|
1212
|
+
var ep = r.recurrenceEnd.date.split("-");
|
|
1213
|
+
var endDate = new Date(parseInt(ep[0], 10), parseInt(ep[1], 10) - 1, parseInt(ep[2], 10));
|
|
1214
|
+
endDate.setHours(23, 59, 59, 999);
|
|
1215
|
+
var checkDate2 = new Date(date);
|
|
1216
|
+
checkDate2.setHours(0, 0, 0, 0);
|
|
1217
|
+
if (checkDate2 > endDate) continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (parsed.months.indexOf(month) === -1) continue;
|
|
1220
|
+
if (parsed.daysOfMonth.indexOf(dom) === -1) continue;
|
|
1221
|
+
if (parsed.daysOfWeek.indexOf(dow) === -1) continue;
|
|
1222
|
+
for (var h = 0; h < parsed.hours.length; h++) {
|
|
1223
|
+
for (var m = 0; m < parsed.minutes.length; m++) {
|
|
1224
|
+
results.push({
|
|
1225
|
+
id: r.id, name: r.name, enabled: r.enabled,
|
|
1226
|
+
hour: parsed.hours[h], minute: parsed.minutes[m],
|
|
1227
|
+
timeStr: pad(parsed.hours[h]) + ":" + pad(parsed.minutes[m]),
|
|
1228
|
+
color: r.color || null,
|
|
1229
|
+
source: r.source || null,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
results.sort(function (a, b) { return a.hour * 60 + a.minute - (b.hour * 60 + b.minute); });
|
|
1235
|
+
return results;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// --- Popover ---
|
|
1239
|
+
|
|
1240
|
+
function showPopover(recId, anchorEl) {
|
|
1241
|
+
var rec = null;
|
|
1242
|
+
for (var i = 0; i < records.length; i++) {
|
|
1243
|
+
if (records[i].id === recId) { rec = records[i]; break; }
|
|
1244
|
+
}
|
|
1245
|
+
if (!rec || !popoverEl) return;
|
|
1246
|
+
|
|
1247
|
+
var nextStr = rec.nextRunAt ? formatDateTime(new Date(rec.nextRunAt)) : "—";
|
|
1248
|
+
var lastStr = rec.lastRunAt ? formatDateTime(new Date(rec.lastRunAt)) : "Never";
|
|
1249
|
+
|
|
1250
|
+
var html = '<div class="schedule-popover-name">' + esc(rec.name) + '</div>';
|
|
1251
|
+
html += '<div class="schedule-popover-meta">Next: <strong>' + nextStr + '</strong></div>';
|
|
1252
|
+
html += '<div class="schedule-popover-meta">Last: <strong>' + lastStr + '</strong></div>';
|
|
1253
|
+
if (rec.lastRunResult) {
|
|
1254
|
+
html += '<div class="schedule-popover-result ' + (rec.lastRunResult === "pass" ? "pass" : "fail") + '">' + rec.lastRunResult + '</div>';
|
|
1255
|
+
}
|
|
1256
|
+
html += '<div class="schedule-popover-meta">' + cronToHuman(rec.cron) + '</div>';
|
|
1257
|
+
html += '<div class="schedule-popover-actions">';
|
|
1258
|
+
html += '<button class="schedule-popover-btn" data-action="edit" data-id="' + rec.id + '">Edit</button>';
|
|
1259
|
+
html += '<button class="schedule-popover-btn" data-action="toggle" data-id="' + rec.id + '">' + (rec.enabled ? "Pause" : "Enable") + '</button>';
|
|
1260
|
+
html += '<button class="schedule-popover-btn" data-action="rerun" data-id="' + rec.id + '">Re-run</button>';
|
|
1261
|
+
html += '<button class="schedule-popover-btn" data-action="move" data-id="' + rec.id + '">Move to\u2026</button>';
|
|
1262
|
+
html += '<button class="schedule-popover-btn danger" data-action="delete" data-id="' + rec.id + '">Delete</button>';
|
|
1263
|
+
html += '</div>';
|
|
1264
|
+
|
|
1265
|
+
popoverEl.innerHTML = html;
|
|
1266
|
+
popoverEl.classList.remove("hidden");
|
|
1267
|
+
|
|
1268
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
1269
|
+
var left = Math.max(8, Math.min(rect.left, window.innerWidth - 268));
|
|
1270
|
+
var top = rect.bottom + 6;
|
|
1271
|
+
if (top + 200 > window.innerHeight) top = rect.top - 200;
|
|
1272
|
+
popoverEl.style.left = left + "px";
|
|
1273
|
+
popoverEl.style.top = top + "px";
|
|
1274
|
+
|
|
1275
|
+
var btns = popoverEl.querySelectorAll(".schedule-popover-btn");
|
|
1276
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1277
|
+
(function (btn) {
|
|
1278
|
+
btn.addEventListener("click", function (e) {
|
|
1279
|
+
e.stopPropagation();
|
|
1280
|
+
var action = btn.dataset.action;
|
|
1281
|
+
var id = btn.dataset.id;
|
|
1282
|
+
popoverEl.classList.add("hidden");
|
|
1283
|
+
if (action === "edit") openEditModal(id);
|
|
1284
|
+
else if (action === "toggle") send({ type: "loop_registry_toggle", id: id });
|
|
1285
|
+
else if (action === "rerun") send({ type: "loop_registry_rerun", id: id });
|
|
1286
|
+
else if (action === "move") showMovePopover(id, btn);
|
|
1287
|
+
else if (action === "delete" && confirm("Delete this schedule?")) send({ type: "loop_registry_remove", id: id });
|
|
1288
|
+
});
|
|
1289
|
+
})(btns[i]);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// --- Move task to another project ---
|
|
1294
|
+
|
|
1295
|
+
function getAvailableProjects(excludeSlug) {
|
|
1296
|
+
var seen = {};
|
|
1297
|
+
var result = [];
|
|
1298
|
+
// First use the project list from the app context (most reliable)
|
|
1299
|
+
if (ctx && typeof ctx.getProjects === "function") {
|
|
1300
|
+
var projects = ctx.getProjects();
|
|
1301
|
+
for (var p = 0; p < projects.length; p++) {
|
|
1302
|
+
var proj = projects[p];
|
|
1303
|
+
if (proj.slug && proj.slug !== excludeSlug && !seen[proj.slug]) {
|
|
1304
|
+
seen[proj.slug] = true;
|
|
1305
|
+
result.push({ slug: proj.slug, title: proj.title || proj.project || proj.slug });
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// Fallback: extract from records
|
|
1310
|
+
if (result.length === 0) {
|
|
1311
|
+
for (var i = 0; i < records.length; i++) {
|
|
1312
|
+
var r = records[i];
|
|
1313
|
+
if (r.projectSlug && !seen[r.projectSlug] && r.projectSlug !== excludeSlug) {
|
|
1314
|
+
seen[r.projectSlug] = true;
|
|
1315
|
+
result.push({ slug: r.projectSlug, title: r.projectTitle || r.projectSlug });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return result;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function showMovePopover(recId, anchorEl) {
|
|
1323
|
+
var rec = null;
|
|
1324
|
+
for (var i = 0; i < records.length; i++) {
|
|
1325
|
+
if (records[i].id === recId) { rec = records[i]; break; }
|
|
1326
|
+
}
|
|
1327
|
+
if (!rec || !popoverEl) return;
|
|
1328
|
+
|
|
1329
|
+
var projects = getAvailableProjects(rec.projectSlug);
|
|
1330
|
+
if (projects.length === 0) {
|
|
1331
|
+
popoverEl.innerHTML = '<div class="schedule-popover-name">No other projects available</div>';
|
|
1332
|
+
popoverEl.classList.remove("hidden");
|
|
1333
|
+
var r2 = anchorEl.getBoundingClientRect();
|
|
1334
|
+
popoverEl.style.left = Math.max(8, r2.left) + "px";
|
|
1335
|
+
popoverEl.style.top = (r2.bottom + 6) + "px";
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
var html = '<div class="schedule-popover-name">Move "' + esc(rec.name) + '" to:</div>';
|
|
1340
|
+
html += '<div class="schedule-popover-actions schedule-move-list">';
|
|
1341
|
+
for (var p = 0; p < projects.length; p++) {
|
|
1342
|
+
html += '<button class="schedule-popover-btn" data-action="move-to" data-slug="' + esc(projects[p].slug) + '">' + esc(projects[p].title) + '</button>';
|
|
1343
|
+
}
|
|
1344
|
+
html += '</div>';
|
|
1345
|
+
|
|
1346
|
+
popoverEl.innerHTML = html;
|
|
1347
|
+
popoverEl.classList.remove("hidden");
|
|
1348
|
+
|
|
1349
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
1350
|
+
popoverEl.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)) + "px";
|
|
1351
|
+
popoverEl.style.top = (rect.bottom + 6) + "px";
|
|
1352
|
+
|
|
1353
|
+
var btns = popoverEl.querySelectorAll('[data-action="move-to"]');
|
|
1354
|
+
for (var b = 0; b < btns.length; b++) {
|
|
1355
|
+
(function (btn) {
|
|
1356
|
+
btn.addEventListener("click", function (e) {
|
|
1357
|
+
e.stopPropagation();
|
|
1358
|
+
popoverEl.classList.add("hidden");
|
|
1359
|
+
send({
|
|
1360
|
+
type: "schedule_move",
|
|
1361
|
+
recordId: recId,
|
|
1362
|
+
fromSlug: rec.projectSlug || currentProjectSlug,
|
|
1363
|
+
toSlug: btn.dataset.slug,
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
})(btns[b]);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function attachEventClicks(container, selector) {
|
|
1371
|
+
var els = container.querySelectorAll(selector);
|
|
1372
|
+
for (var i = 0; i < els.length; i++) {
|
|
1373
|
+
(function (el) {
|
|
1374
|
+
el.addEventListener("click", function (e) {
|
|
1375
|
+
e.stopPropagation();
|
|
1376
|
+
var recId = el.dataset.recId;
|
|
1377
|
+
var rec = null;
|
|
1378
|
+
for (var j = 0; j < records.length; j++) {
|
|
1379
|
+
if (records[j].id === recId) { rec = records[j]; break; }
|
|
1380
|
+
}
|
|
1381
|
+
if (!rec) return;
|
|
1382
|
+
// Schedule-source records: open create popover with pre-filled values
|
|
1383
|
+
if (rec.source === "schedule") {
|
|
1384
|
+
openCreateModalWithRecord(rec, el);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// Other records: go to detail view
|
|
1388
|
+
selectedTaskId = recId;
|
|
1389
|
+
updateSidebarSelection();
|
|
1390
|
+
switchMode("detail");
|
|
1391
|
+
});
|
|
1392
|
+
})(els[i]);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// --- Edit Modal (for changing cron/name on existing records) ---
|
|
1397
|
+
|
|
1398
|
+
function setupEditModal() {
|
|
1399
|
+
if (!editModal) return;
|
|
1400
|
+
document.getElementById("schedule-edit-close").addEventListener("click", function () { closeEditModal(); });
|
|
1401
|
+
document.getElementById("sched-cancel").addEventListener("click", function () { closeEditModal(); });
|
|
1402
|
+
editModal.querySelector(".confirm-backdrop").addEventListener("click", function () { closeEditModal(); });
|
|
1403
|
+
|
|
1404
|
+
// Presets
|
|
1405
|
+
var presetBtns = document.querySelectorAll("#sched-presets .sched-preset-btn");
|
|
1406
|
+
for (var i = 0; i < presetBtns.length; i++) {
|
|
1407
|
+
(function (btn) {
|
|
1408
|
+
btn.addEventListener("click", function () { selectPreset(btn.dataset.preset); });
|
|
1409
|
+
})(presetBtns[i]);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// DOW
|
|
1413
|
+
var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
|
|
1414
|
+
for (var i = 0; i < dowBtns.length; i++) {
|
|
1415
|
+
(function (btn) {
|
|
1416
|
+
btn.addEventListener("click", function () { btn.classList.toggle("active"); updateEditCronPreview(); });
|
|
1417
|
+
})(dowBtns[i]);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
document.getElementById("sched-time").addEventListener("change", function () { updateEditCronPreview(); });
|
|
1421
|
+
document.getElementById("sched-save").addEventListener("click", function () { saveEdit(); });
|
|
1422
|
+
document.getElementById("sched-delete").addEventListener("click", function () {
|
|
1423
|
+
if (editingId && confirm("Delete this job?")) {
|
|
1424
|
+
send({ type: "loop_registry_remove", id: editingId });
|
|
1425
|
+
closeEditModal();
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
var editPreset = "daily";
|
|
1431
|
+
|
|
1432
|
+
function selectPreset(preset) {
|
|
1433
|
+
editPreset = preset;
|
|
1434
|
+
var btns = document.querySelectorAll("#sched-presets .sched-preset-btn");
|
|
1435
|
+
for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i].dataset.preset === preset);
|
|
1436
|
+
var dowField = document.getElementById("sched-dow-field");
|
|
1437
|
+
if (dowField) dowField.style.display = (preset === "custom" || preset === "weekly") ? "" : "none";
|
|
1438
|
+
updateEditCronPreview();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function buildEditCron() {
|
|
1442
|
+
var timeVal = document.getElementById("sched-time").value || "09:00";
|
|
1443
|
+
var parts = timeVal.split(":");
|
|
1444
|
+
var h = parseInt(parts[0], 10);
|
|
1445
|
+
var m = parseInt(parts[1], 10);
|
|
1446
|
+
var dow = "*";
|
|
1447
|
+
if (editPreset === "weekdays") dow = "1-5";
|
|
1448
|
+
else if (editPreset === "weekly" || editPreset === "custom") {
|
|
1449
|
+
var days = [];
|
|
1450
|
+
var btns = document.querySelectorAll("#sched-dow-row .sched-dow-btn.active");
|
|
1451
|
+
for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
|
|
1452
|
+
if (days.length > 0 && days.length < 7) dow = days.sort().join(",");
|
|
1453
|
+
} else if (editPreset === "monthly") {
|
|
1454
|
+
return m + " " + h + " " + new Date().getDate() + " * *";
|
|
1455
|
+
}
|
|
1456
|
+
return m + " " + h + " * * " + dow;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function updateEditCronPreview() {
|
|
1460
|
+
var cron = buildEditCron();
|
|
1461
|
+
var humanEl = document.getElementById("sched-human-text");
|
|
1462
|
+
var cronEl = document.getElementById("sched-cron-text");
|
|
1463
|
+
if (humanEl) humanEl.textContent = cronToHuman(cron);
|
|
1464
|
+
if (cronEl) cronEl.textContent = cron;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function openEditModal(recId) {
|
|
1468
|
+
if (!editModal) return;
|
|
1469
|
+
editingId = recId;
|
|
1470
|
+
var rec = null;
|
|
1471
|
+
for (var i = 0; i < records.length; i++) {
|
|
1472
|
+
if (records[i].id === recId) { rec = records[i]; break; }
|
|
1473
|
+
}
|
|
1474
|
+
if (!rec) return;
|
|
1475
|
+
|
|
1476
|
+
document.getElementById("schedule-edit-title").textContent = "Edit Schedule";
|
|
1477
|
+
document.getElementById("sched-name").value = rec.name || "";
|
|
1478
|
+
document.getElementById("sched-enabled").checked = rec.enabled;
|
|
1479
|
+
document.getElementById("sched-delete").style.display = "";
|
|
1480
|
+
|
|
1481
|
+
// Show job name
|
|
1482
|
+
var jobNameEl = document.getElementById("sched-job-name");
|
|
1483
|
+
if (jobNameEl) jobNameEl.textContent = rec.task ? rec.task.substring(0, 80) : rec.id;
|
|
1484
|
+
|
|
1485
|
+
// History
|
|
1486
|
+
var historyField = document.getElementById("sched-history-field");
|
|
1487
|
+
if (rec.runs && rec.runs.length > 0) {
|
|
1488
|
+
if (historyField) historyField.style.display = "";
|
|
1489
|
+
renderHistory(rec.runs);
|
|
1490
|
+
} else {
|
|
1491
|
+
if (historyField) historyField.style.display = "none";
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Parse cron
|
|
1495
|
+
if (rec.cron) {
|
|
1496
|
+
var parsed = parseCronSimple(rec.cron);
|
|
1497
|
+
if (parsed) {
|
|
1498
|
+
document.getElementById("sched-time").value = pad(parsed.hours[0] || 9) + ":" + pad(parsed.minutes[0] || 0);
|
|
1499
|
+
var dowArr = parsed.daysOfWeek;
|
|
1500
|
+
if (dowArr.length === 7) selectPreset("daily");
|
|
1501
|
+
else if (dowArr.length === 5 && dowArr[0] === 1 && dowArr[4] === 5) selectPreset("weekdays");
|
|
1502
|
+
else {
|
|
1503
|
+
selectPreset("custom");
|
|
1504
|
+
var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
|
|
1505
|
+
for (var j = 0; j < dowBtns.length; j++) {
|
|
1506
|
+
dowBtns[j].classList.toggle("active", dowArr.indexOf(parseInt(dowBtns[j].dataset.dow)) !== -1);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
document.getElementById("sched-time").value = "09:00";
|
|
1512
|
+
selectPreset("daily");
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
updateEditCronPreview();
|
|
1516
|
+
editModal.classList.remove("hidden");
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function closeEditModal() {
|
|
1520
|
+
if (editModal) editModal.classList.add("hidden");
|
|
1521
|
+
editingId = null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function saveEdit() {
|
|
1525
|
+
var name = document.getElementById("sched-name").value.trim();
|
|
1526
|
+
var enabled = document.getElementById("sched-enabled").checked;
|
|
1527
|
+
var cron = buildEditCron();
|
|
1528
|
+
if (!name) { alert("Please enter a name."); return; }
|
|
1529
|
+
|
|
1530
|
+
send({
|
|
1531
|
+
type: "loop_registry_update",
|
|
1532
|
+
id: editingId,
|
|
1533
|
+
data: { name: name, cron: cron, enabled: enabled },
|
|
1534
|
+
});
|
|
1535
|
+
closeEditModal();
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function renderHistory(runs) {
|
|
1539
|
+
var el = document.getElementById("sched-history");
|
|
1540
|
+
if (!el || !runs || runs.length === 0) { if (el) el.innerHTML = '<div class="sched-history-empty">No runs yet</div>'; return; }
|
|
1541
|
+
var html = "";
|
|
1542
|
+
var sorted = runs.slice().reverse();
|
|
1543
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
1544
|
+
var run = sorted[i];
|
|
1545
|
+
html += '<div class="sched-history-item"><span class="sched-history-dot ' + (run.result || "") + '"></span>';
|
|
1546
|
+
html += '<span class="sched-history-date">' + formatDateTime(new Date(run.startedAt)) + '</span>';
|
|
1547
|
+
html += '<span class="sched-history-result">' + (run.result || "?") + '</span>';
|
|
1548
|
+
html += '<span class="sched-history-iterations">' + (run.iterations || 0) + ' iter</span></div>';
|
|
1549
|
+
}
|
|
1550
|
+
el.innerHTML = html;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// --- Public API ---
|
|
1554
|
+
|
|
1555
|
+
export function openSchedulerToTab(tab) {
|
|
1556
|
+
if (!panelOpen) openScheduler();
|
|
1557
|
+
if (tab === "library" || tab === "tasks") {
|
|
1558
|
+
// Just open, sidebar already shows tasks
|
|
1559
|
+
} else {
|
|
1560
|
+
switchMode("calendar");
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
export function isSchedulerOpen() {
|
|
1565
|
+
return panelOpen;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export function enterCraftingMode(sessionId, taskId) {
|
|
1569
|
+
craftingSessionId = sessionId || null;
|
|
1570
|
+
craftingTaskId = taskId || null;
|
|
1571
|
+
if (!panelOpen) openScheduler();
|
|
1572
|
+
if (taskId) {
|
|
1573
|
+
selectedTaskId = taskId;
|
|
1574
|
+
renderSidebar();
|
|
1575
|
+
}
|
|
1576
|
+
switchMode("crafting");
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export function exitCraftingMode(taskId) {
|
|
1580
|
+
if (!panelOpen || currentMode !== "crafting") return;
|
|
1581
|
+
craftingTaskId = null;
|
|
1582
|
+
if (taskId) {
|
|
1583
|
+
selectedTaskId = taskId;
|
|
1584
|
+
switchMode("detail");
|
|
1585
|
+
renderSidebar();
|
|
1586
|
+
} else {
|
|
1587
|
+
switchMode("calendar");
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// --- Message handlers ---
|
|
1592
|
+
|
|
1593
|
+
export function handleLoopRegistryUpdated(msg) {
|
|
1594
|
+
records = msg.records || [];
|
|
1595
|
+
if (panelOpen) {
|
|
1596
|
+
renderSidebar();
|
|
1597
|
+
if (currentMode === "calendar") render();
|
|
1598
|
+
else if (currentMode === "detail") renderDetail();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
export function handleLoopRegistryFiles(msg) {
|
|
1603
|
+
if (!panelOpen || currentMode !== "detail") return;
|
|
1604
|
+
if (msg.id !== selectedTaskId) return;
|
|
1605
|
+
var bodyEl2 = document.getElementById("scheduler-detail-body");
|
|
1606
|
+
if (!bodyEl2) return;
|
|
1607
|
+
var activeTab = contentDetailEl ? contentDetailEl.querySelector(".scheduler-detail-tab.active") : null;
|
|
1608
|
+
var tab = activeTab ? activeTab.dataset.tab : "prompt";
|
|
1609
|
+
if (tab === "prompt") {
|
|
1610
|
+
bodyEl2.innerHTML = msg.prompt ? '<div class="md-content">' + renderMarkdown(msg.prompt) + '</div>' : '<div class="scheduler-empty">No PROMPT.md found</div>';
|
|
1611
|
+
} else if (tab === "judge") {
|
|
1612
|
+
bodyEl2.innerHTML = msg.judge ? '<div class="md-content">' + renderMarkdown(msg.judge) + '</div>' : '<div class="scheduler-empty">No JUDGE.md found</div>';
|
|
1613
|
+
}
|
|
1614
|
+
// Disable "Run now" if PROMPT.md or JUDGE.md is missing
|
|
1615
|
+
var runBtn = contentDetailEl ? contentDetailEl.querySelector('[data-action="run"]') : null;
|
|
1616
|
+
if (runBtn) {
|
|
1617
|
+
var filesReady = !!msg.prompt && !!msg.judge;
|
|
1618
|
+
runBtn.disabled = !filesReady;
|
|
1619
|
+
runBtn.title = filesReady ? "Run now" : "PROMPT.md and JUDGE.md are required to run";
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
export function handleScheduleRunStarted(msg) {
|
|
1624
|
+
if (panelOpen) render();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
export function handleScheduleRunFinished(msg) {
|
|
1628
|
+
send({ type: "loop_registry_list" });
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export function handleLoopScheduled(msg) {
|
|
1632
|
+
// A loop was just registered as scheduled (from approval bar)
|
|
1633
|
+
send({ type: "loop_registry_list" });
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Expose upcoming schedules (within given ms window) for countdown display
|
|
1637
|
+
// Always filters to current project only (countdown is project-specific)
|
|
1638
|
+
export function getUpcomingSchedules(windowMs) {
|
|
1639
|
+
var now = Date.now();
|
|
1640
|
+
var result = [];
|
|
1641
|
+
var filtered = filterByProject(records);
|
|
1642
|
+
for (var i = 0; i < filtered.length; i++) {
|
|
1643
|
+
var r = filtered[i];
|
|
1644
|
+
if (!r.enabled || !r.nextRunAt) continue;
|
|
1645
|
+
var diff = r.nextRunAt - now;
|
|
1646
|
+
if (diff > 0 && diff <= windowMs) {
|
|
1647
|
+
result.push({ id: r.id, name: r.name, nextRunAt: r.nextRunAt, color: r.color || "" });
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return result;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// --- Cell click → open create modal ---
|
|
1654
|
+
|
|
1655
|
+
function attachCellClicks(container) {
|
|
1656
|
+
var cells = container.querySelectorAll(".scheduler-cell[data-date]");
|
|
1657
|
+
for (var i = 0; i < cells.length; i++) {
|
|
1658
|
+
(function (cell) {
|
|
1659
|
+
cell.addEventListener("click", function (e) {
|
|
1660
|
+
// Don't open create if user clicked on an event
|
|
1661
|
+
if (e.target.closest(".scheduler-event")) return;
|
|
1662
|
+
var parts = cell.dataset.date.split("-");
|
|
1663
|
+
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1664
|
+
openCreateModal(d, null, cell);
|
|
1665
|
+
});
|
|
1666
|
+
cell.addEventListener("dragover", function (e) {
|
|
1667
|
+
e.preventDefault();
|
|
1668
|
+
e.dataTransfer.dropEffect = "copy";
|
|
1669
|
+
cell.classList.add("drag-over");
|
|
1670
|
+
if (!previewEl || previewEl.parentNode !== cell) {
|
|
1671
|
+
showPreviewOnCell(cell);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
cell.addEventListener("dragleave", function (e) {
|
|
1675
|
+
if (cell.contains(e.relatedTarget)) return;
|
|
1676
|
+
cell.classList.remove("drag-over");
|
|
1677
|
+
removePreview();
|
|
1678
|
+
});
|
|
1679
|
+
cell.addEventListener("drop", function (e) {
|
|
1680
|
+
e.preventDefault();
|
|
1681
|
+
cell.classList.remove("drag-over");
|
|
1682
|
+
removePreview();
|
|
1683
|
+
var parts = cell.dataset.date.split("-");
|
|
1684
|
+
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1685
|
+
openCreateModal(d, null, cell);
|
|
1686
|
+
applyDraggedTask();
|
|
1687
|
+
});
|
|
1688
|
+
})(cells[i]);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function attachWeekSlotClicks(container) {
|
|
1693
|
+
var slots = container.querySelectorAll(".scheduler-week-slot[data-date]");
|
|
1694
|
+
for (var i = 0; i < slots.length; i++) {
|
|
1695
|
+
(function (slot) {
|
|
1696
|
+
slot.addEventListener("click", function (e) {
|
|
1697
|
+
if (e.target.closest(".scheduler-week-event")) return;
|
|
1698
|
+
var parts = slot.dataset.date.split("-");
|
|
1699
|
+
var hour = parseInt(slot.dataset.hour, 10);
|
|
1700
|
+
var quarter = parseInt(slot.dataset.quarter || "0", 10);
|
|
1701
|
+
var minute = quarter * 15;
|
|
1702
|
+
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), hour, minute, 0);
|
|
1703
|
+
openCreateModal(d, hour, slot);
|
|
1704
|
+
});
|
|
1705
|
+
slot.addEventListener("dragover", function (e) {
|
|
1706
|
+
e.preventDefault();
|
|
1707
|
+
e.dataTransfer.dropEffect = "copy";
|
|
1708
|
+
slot.classList.add("drag-over");
|
|
1709
|
+
if (!previewEl || !slot.closest(".scheduler-week-day-col").contains(previewEl)) {
|
|
1710
|
+
showPreviewOnSlot(slot);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
slot.addEventListener("dragleave", function (e) {
|
|
1714
|
+
if (slot.contains(e.relatedTarget)) return;
|
|
1715
|
+
slot.classList.remove("drag-over");
|
|
1716
|
+
removePreview();
|
|
1717
|
+
});
|
|
1718
|
+
slot.addEventListener("drop", function (e) {
|
|
1719
|
+
e.preventDefault();
|
|
1720
|
+
slot.classList.remove("drag-over");
|
|
1721
|
+
removePreview();
|
|
1722
|
+
var parts = slot.dataset.date.split("-");
|
|
1723
|
+
var hour = parseInt(slot.dataset.hour, 10);
|
|
1724
|
+
var quarter = parseInt(slot.dataset.quarter || "0", 10);
|
|
1725
|
+
var minute = quarter * 15;
|
|
1726
|
+
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), hour, minute, 0);
|
|
1727
|
+
openCreateModal(d, hour, slot);
|
|
1728
|
+
applyDraggedTask();
|
|
1729
|
+
});
|
|
1730
|
+
})(slots[i]);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// --- Create Popover (inline, Akiflow-style) ---
|
|
1735
|
+
|
|
1736
|
+
function setupCreateModal() {
|
|
1737
|
+
if (!createPopover) return;
|
|
1738
|
+
|
|
1739
|
+
// Close
|
|
1740
|
+
document.getElementById("sched-create-cancel").addEventListener("click", function () { closeCreateModal(); });
|
|
1741
|
+
|
|
1742
|
+
// Color picker
|
|
1743
|
+
var colorBtn = document.getElementById("sched-create-color-btn");
|
|
1744
|
+
var colorPalette = document.getElementById("sched-create-color-palette");
|
|
1745
|
+
if (colorBtn && colorPalette) {
|
|
1746
|
+
colorBtn.addEventListener("click", function (e) {
|
|
1747
|
+
e.stopPropagation();
|
|
1748
|
+
colorPalette.classList.toggle("hidden");
|
|
1749
|
+
});
|
|
1750
|
+
var swatches = colorPalette.querySelectorAll(".sched-color-swatch");
|
|
1751
|
+
for (var i = 0; i < swatches.length; i++) {
|
|
1752
|
+
swatches[i].addEventListener("click", function (e) {
|
|
1753
|
+
e.stopPropagation();
|
|
1754
|
+
var c = this.dataset.color;
|
|
1755
|
+
createColor = c;
|
|
1756
|
+
var dot = document.getElementById("sched-create-color-dot");
|
|
1757
|
+
if (dot) dot.style.background = c;
|
|
1758
|
+
// update active state
|
|
1759
|
+
var all = colorPalette.querySelectorAll(".sched-color-swatch");
|
|
1760
|
+
for (var j = 0; j < all.length; j++) {
|
|
1761
|
+
all[j].classList.toggle("active", all[j].dataset.color === c);
|
|
1762
|
+
}
|
|
1763
|
+
colorPalette.classList.add("hidden");
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Date picker change → sync createSelectedDate and recurrence labels
|
|
1769
|
+
var datePickerEl = document.getElementById("sched-create-date-picker");
|
|
1770
|
+
if (datePickerEl) {
|
|
1771
|
+
datePickerEl.addEventListener("change", function () {
|
|
1772
|
+
var parts = this.value.split("-");
|
|
1773
|
+
if (parts.length === 3) {
|
|
1774
|
+
createSelectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1775
|
+
document.getElementById("sched-create-date").value = this.value;
|
|
1776
|
+
updateRecurrenceLabels(createSelectedDate);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Task dropdown
|
|
1782
|
+
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
1783
|
+
var taskList = document.getElementById("sched-create-task-list");
|
|
1784
|
+
if (taskBtn && taskList) {
|
|
1785
|
+
taskBtn.addEventListener("click", function (e) {
|
|
1786
|
+
e.stopPropagation();
|
|
1787
|
+
taskList.classList.toggle("hidden");
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Close task dropdown on outside click
|
|
1792
|
+
document.addEventListener("click", function (e) {
|
|
1793
|
+
var tl = document.getElementById("sched-create-task-list");
|
|
1794
|
+
if (tl && !tl.classList.contains("hidden")) {
|
|
1795
|
+
if (!tl.contains(e.target) && !e.target.closest("#sched-create-task-btn")) {
|
|
1796
|
+
tl.classList.add("hidden");
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
// Recurrence button → toggle dropdown
|
|
1802
|
+
document.getElementById("sched-create-recurrence-btn").addEventListener("click", function (e) {
|
|
1803
|
+
e.stopPropagation();
|
|
1804
|
+
var dd = document.getElementById("sched-create-recurrence-dropdown");
|
|
1805
|
+
var btn = document.getElementById("sched-create-recurrence-btn");
|
|
1806
|
+
if (dd) {
|
|
1807
|
+
var wasHidden = dd.classList.contains("hidden");
|
|
1808
|
+
dd.classList.toggle("hidden");
|
|
1809
|
+
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
1810
|
+
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
1811
|
+
if (wasHidden && btn) {
|
|
1812
|
+
var bRect = btn.getBoundingClientRect();
|
|
1813
|
+
var ddW = 280;
|
|
1814
|
+
var ddLeft = bRect.left;
|
|
1815
|
+
if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
|
|
1816
|
+
if (ddLeft < 10) ddLeft = 10;
|
|
1817
|
+
dd.style.left = ddLeft + "px";
|
|
1818
|
+
dd.style.top = (bRect.bottom + 4) + "px";
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
// Recurrence option clicks
|
|
1824
|
+
var recOptions = createPopover.querySelectorAll(".sched-recurrence-option");
|
|
1825
|
+
for (var i = 0; i < recOptions.length; i++) {
|
|
1826
|
+
(function (opt) {
|
|
1827
|
+
opt.addEventListener("click", function (e) {
|
|
1828
|
+
e.stopPropagation();
|
|
1829
|
+
var rec = opt.dataset.recurrence;
|
|
1830
|
+
if (rec === "custom") {
|
|
1831
|
+
document.getElementById("sched-create-recurrence-list").style.display = "none";
|
|
1832
|
+
document.getElementById("sched-custom-repeat-panel").classList.remove("hidden");
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
for (var j = 0; j < recOptions.length; j++) {
|
|
1836
|
+
recOptions[j].classList.toggle("active", recOptions[j] === opt);
|
|
1837
|
+
}
|
|
1838
|
+
createRecurrence = rec;
|
|
1839
|
+
createCustomConfirmed = false;
|
|
1840
|
+
// Close dropdown
|
|
1841
|
+
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
1842
|
+
updateRecurrenceBtn();
|
|
1843
|
+
});
|
|
1844
|
+
})(recOptions[i]);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Custom repeat: back
|
|
1848
|
+
document.getElementById("sched-custom-back").addEventListener("click", function (e) {
|
|
1849
|
+
e.stopPropagation();
|
|
1850
|
+
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
1851
|
+
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// Custom repeat: cancel
|
|
1855
|
+
document.getElementById("sched-custom-cancel").addEventListener("click", function (e) {
|
|
1856
|
+
e.stopPropagation();
|
|
1857
|
+
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
1858
|
+
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
// Custom repeat: unit change
|
|
1862
|
+
document.getElementById("sched-custom-unit").addEventListener("change", function () {
|
|
1863
|
+
var dowSection = document.getElementById("sched-custom-dow-section");
|
|
1864
|
+
if (dowSection) dowSection.style.display = this.value === "week" ? "" : "none";
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
// Custom repeat: DOW toggle
|
|
1868
|
+
var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
|
|
1869
|
+
for (var i = 0; i < customDowBtns.length; i++) {
|
|
1870
|
+
(function (btn) {
|
|
1871
|
+
btn.addEventListener("click", function (e) { e.stopPropagation(); btn.classList.toggle("active"); });
|
|
1872
|
+
})(customDowBtns[i]);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Custom repeat: End type JS dropdown
|
|
1876
|
+
var endBtn = document.getElementById("sched-custom-end-btn");
|
|
1877
|
+
var endList = document.getElementById("sched-custom-end-list");
|
|
1878
|
+
|
|
1879
|
+
endBtn.addEventListener("click", function (e) {
|
|
1880
|
+
e.stopPropagation();
|
|
1881
|
+
if (endList.classList.contains("hidden")) {
|
|
1882
|
+
var r = endBtn.getBoundingClientRect();
|
|
1883
|
+
endList.style.left = r.left + "px";
|
|
1884
|
+
endList.style.top = (r.bottom + 4) + "px";
|
|
1885
|
+
// If it would overflow bottom, show above
|
|
1886
|
+
endList.classList.remove("hidden");
|
|
1887
|
+
var lr = endList.getBoundingClientRect();
|
|
1888
|
+
if (lr.bottom > window.innerHeight - 8) {
|
|
1889
|
+
endList.style.top = (r.top - lr.height - 4) + "px";
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
endList.classList.add("hidden");
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
var endItems = endList.querySelectorAll(".sched-custom-end-item");
|
|
1897
|
+
for (var ei = 0; ei < endItems.length; ei++) {
|
|
1898
|
+
(function (item) {
|
|
1899
|
+
item.addEventListener("click", function (e) {
|
|
1900
|
+
e.stopPropagation();
|
|
1901
|
+
var val = item.dataset.value;
|
|
1902
|
+
createEndType = val;
|
|
1903
|
+
document.getElementById("sched-custom-end").value = val;
|
|
1904
|
+
document.getElementById("sched-custom-end-label").textContent = item.textContent;
|
|
1905
|
+
|
|
1906
|
+
// Update active state
|
|
1907
|
+
for (var j = 0; j < endItems.length; j++) {
|
|
1908
|
+
endItems[j].classList.toggle("active", endItems[j] === item);
|
|
1909
|
+
}
|
|
1910
|
+
endList.classList.add("hidden");
|
|
1911
|
+
|
|
1912
|
+
// Toggle conditional inputs
|
|
1913
|
+
var dateBtn2 = document.getElementById("sched-custom-end-date-btn");
|
|
1914
|
+
var afterWrap = document.getElementById("sched-custom-end-after-wrap");
|
|
1915
|
+
var calPanel = document.getElementById("sched-custom-end-calendar");
|
|
1916
|
+
|
|
1917
|
+
dateBtn2.classList.add("hidden");
|
|
1918
|
+
afterWrap.classList.add("hidden");
|
|
1919
|
+
calPanel.classList.add("hidden");
|
|
1920
|
+
|
|
1921
|
+
if (val === "until") {
|
|
1922
|
+
dateBtn2.classList.remove("hidden");
|
|
1923
|
+
if (!createEndDate) {
|
|
1924
|
+
createEndDate = new Date(createSelectedDate || new Date());
|
|
1925
|
+
createEndDate.setMonth(createEndDate.getMonth() + 1);
|
|
1926
|
+
}
|
|
1927
|
+
updateEndDateLabel();
|
|
1928
|
+
} else if (val === "after") {
|
|
1929
|
+
afterWrap.classList.remove("hidden");
|
|
1930
|
+
document.getElementById("sched-custom-end-after").value = createEndAfter;
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
})(endItems[ei]);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// Close end dropdown on outside click
|
|
1937
|
+
document.addEventListener("click", function (e) {
|
|
1938
|
+
if (endList && !endList.classList.contains("hidden")) {
|
|
1939
|
+
if (!endList.contains(e.target) && !endBtn.contains(e.target)) {
|
|
1940
|
+
endList.classList.add("hidden");
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// Custom repeat: End date button → toggle inline calendar
|
|
1946
|
+
document.getElementById("sched-custom-end-date-btn").addEventListener("click", function (e) {
|
|
1947
|
+
e.stopPropagation();
|
|
1948
|
+
var calPanel = document.getElementById("sched-custom-end-calendar");
|
|
1949
|
+
if (calPanel.classList.contains("hidden")) {
|
|
1950
|
+
createEndCalMonth = new Date(createEndDate.getFullYear(), createEndDate.getMonth(), 1);
|
|
1951
|
+
renderEndCalendar();
|
|
1952
|
+
calPanel.classList.remove("hidden");
|
|
1953
|
+
try { lucide.createIcons({ node: calPanel }); } catch (ex) {}
|
|
1954
|
+
} else {
|
|
1955
|
+
calPanel.classList.add("hidden");
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// Custom repeat: End calendar prev/next
|
|
1960
|
+
document.getElementById("sched-cal-prev").addEventListener("click", function (e) {
|
|
1961
|
+
e.stopPropagation();
|
|
1962
|
+
createEndCalMonth.setMonth(createEndCalMonth.getMonth() - 1);
|
|
1963
|
+
renderEndCalendar();
|
|
1964
|
+
});
|
|
1965
|
+
document.getElementById("sched-cal-next").addEventListener("click", function (e) {
|
|
1966
|
+
e.stopPropagation();
|
|
1967
|
+
createEndCalMonth.setMonth(createEndCalMonth.getMonth() + 1);
|
|
1968
|
+
renderEndCalendar();
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// Custom repeat: After occurrences input
|
|
1972
|
+
document.getElementById("sched-custom-end-after").addEventListener("change", function () {
|
|
1973
|
+
createEndAfter = parseInt(this.value, 10) || 10;
|
|
1974
|
+
if (createEndAfter < 1) { createEndAfter = 1; this.value = 1; }
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
// Custom repeat: OK
|
|
1978
|
+
document.getElementById("sched-custom-ok").addEventListener("click", function (e) {
|
|
1979
|
+
e.stopPropagation();
|
|
1980
|
+
createRecurrence = "custom";
|
|
1981
|
+
createCustomConfirmed = true;
|
|
1982
|
+
var recOptions = createPopover.querySelectorAll(".sched-recurrence-option");
|
|
1983
|
+
for (var j = 0; j < recOptions.length; j++) {
|
|
1984
|
+
recOptions[j].classList.toggle("active", recOptions[j].dataset.recurrence === "custom");
|
|
1985
|
+
}
|
|
1986
|
+
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
1987
|
+
updateRecurrenceBtn();
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Submit
|
|
1991
|
+
document.getElementById("sched-create-submit").addEventListener("click", function () { submitCreateSchedule(); });
|
|
1992
|
+
|
|
1993
|
+
// Delete button → close popover, then open dialog
|
|
1994
|
+
var deleteBtn = document.getElementById("sched-create-delete");
|
|
1995
|
+
var deleteDialog = document.getElementById("sched-delete-dialog");
|
|
1996
|
+
if (deleteBtn) {
|
|
1997
|
+
deleteBtn.addEventListener("click", function (e) {
|
|
1998
|
+
e.stopPropagation();
|
|
1999
|
+
if (!createEditingRecId) return;
|
|
2000
|
+
var rec = null;
|
|
2001
|
+
for (var j = 0; j < records.length; j++) {
|
|
2002
|
+
if (records[j].id === createEditingRecId) { rec = records[j]; break; }
|
|
2003
|
+
}
|
|
2004
|
+
if (!rec) return;
|
|
2005
|
+
// Save context before closing popover
|
|
2006
|
+
var deleteRecId = createEditingRecId;
|
|
2007
|
+
var deleteDate = createSelectedDate ? new Date(createSelectedDate) : null;
|
|
2008
|
+
closeCreateModal();
|
|
2009
|
+
openDeleteDialog(deleteRecId, deleteDate, !rec.cron);
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Delete dialog option handlers
|
|
2014
|
+
if (deleteDialog) {
|
|
2015
|
+
var deleteOptions = deleteDialog.querySelectorAll(".sched-delete-option");
|
|
2016
|
+
for (var i = 0; i < deleteOptions.length; i++) {
|
|
2017
|
+
(function (opt) {
|
|
2018
|
+
opt.addEventListener("click", function (e) {
|
|
2019
|
+
e.stopPropagation();
|
|
2020
|
+
var action = opt.dataset.delete;
|
|
2021
|
+
if (action === "cancel") {
|
|
2022
|
+
closeDeleteDialog();
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
var recId = deleteDialog.dataset.recId;
|
|
2026
|
+
var dateStr = deleteDialog.dataset.eventDate;
|
|
2027
|
+
if (!recId) return;
|
|
2028
|
+
if (action === "this") {
|
|
2029
|
+
if (dateStr) {
|
|
2030
|
+
var dp = dateStr.split("-");
|
|
2031
|
+
var next = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
|
|
2032
|
+
next.setDate(next.getDate() + 1);
|
|
2033
|
+
var newDate = next.getFullYear() + "-" + pad(next.getMonth() + 1) + "-" + pad(next.getDate());
|
|
2034
|
+
send({ type: "loop_registry_update", id: recId, data: { date: newDate } });
|
|
2035
|
+
}
|
|
2036
|
+
} else if (action === "following") {
|
|
2037
|
+
if (dateStr) {
|
|
2038
|
+
var dp2 = dateStr.split("-");
|
|
2039
|
+
var prev = new Date(parseInt(dp2[0], 10), parseInt(dp2[1], 10) - 1, parseInt(dp2[2], 10));
|
|
2040
|
+
prev.setDate(prev.getDate() - 1);
|
|
2041
|
+
var endDate = prev.getFullYear() + "-" + pad(prev.getMonth() + 1) + "-" + pad(prev.getDate());
|
|
2042
|
+
send({ type: "loop_registry_update", id: recId, data: { recurrenceEnd: { type: "until", date: endDate } } });
|
|
2043
|
+
}
|
|
2044
|
+
} else if (action === "all") {
|
|
2045
|
+
send({ type: "loop_registry_remove", id: recId });
|
|
2046
|
+
}
|
|
2047
|
+
closeDeleteDialog();
|
|
2048
|
+
});
|
|
2049
|
+
})(deleteOptions[i]);
|
|
2050
|
+
}
|
|
2051
|
+
// Close on backdrop click
|
|
2052
|
+
deleteDialog.addEventListener("click", function (e) {
|
|
2053
|
+
if (e.target === deleteDialog) closeDeleteDialog();
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Close color palette on any click outside it
|
|
2058
|
+
document.addEventListener("click", function (e) {
|
|
2059
|
+
var pal = document.getElementById("sched-create-color-palette");
|
|
2060
|
+
if (pal && !pal.classList.contains("hidden")) {
|
|
2061
|
+
if (!pal.contains(e.target) && !e.target.closest("#sched-create-color-btn")) {
|
|
2062
|
+
pal.classList.add("hidden");
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
// Close popover on outside click
|
|
2068
|
+
document.addEventListener("click", function (e) {
|
|
2069
|
+
if (!createPopover || createPopover.classList.contains("hidden")) return;
|
|
2070
|
+
if (createPopover.contains(e.target)) return;
|
|
2071
|
+
// Also ignore clicks on calendar cells (they open the popover)
|
|
2072
|
+
if (e.target.closest(".scheduler-cell") || e.target.closest(".scheduler-week-slot")) return;
|
|
2073
|
+
closeCreateModal();
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
// Escape key
|
|
2077
|
+
document.addEventListener("keydown", function (e) {
|
|
2078
|
+
if (e.key === "Escape" && createPopover && !createPopover.classList.contains("hidden")) {
|
|
2079
|
+
// Close recurrence dropdown first if open
|
|
2080
|
+
var dd = document.getElementById("sched-create-recurrence-dropdown");
|
|
2081
|
+
if (dd && !dd.classList.contains("hidden")) {
|
|
2082
|
+
dd.classList.add("hidden");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
closeCreateModal();
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function updateRecurrenceBtn() {
|
|
2091
|
+
var btn = document.getElementById("sched-create-recurrence-btn");
|
|
2092
|
+
if (btn) {
|
|
2093
|
+
btn.classList.toggle("has-recurrence", createRecurrence !== "none");
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function removePreview() {
|
|
2098
|
+
if (previewEl && previewEl.parentNode) {
|
|
2099
|
+
previewEl.parentNode.removeChild(previewEl);
|
|
2100
|
+
}
|
|
2101
|
+
previewEl = null;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function showPreviewOnCell(cell) {
|
|
2105
|
+
removePreview();
|
|
2106
|
+
var label = draggedTaskName || "(No title)";
|
|
2107
|
+
var el = document.createElement("div");
|
|
2108
|
+
el.className = "scheduler-event preview";
|
|
2109
|
+
el.textContent = label;
|
|
2110
|
+
cell.appendChild(el);
|
|
2111
|
+
previewEl = el;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function showPreviewOnSlot(slot) {
|
|
2115
|
+
removePreview();
|
|
2116
|
+
var label = draggedTaskName || "(No title)";
|
|
2117
|
+
var hour = parseInt(slot.dataset.hour, 10);
|
|
2118
|
+
var quarter = parseInt(slot.dataset.quarter || "0", 10);
|
|
2119
|
+
var minute = quarter * 15;
|
|
2120
|
+
var timeStr = pad(hour) + ":" + pad(minute);
|
|
2121
|
+
var col = slot.closest(".scheduler-week-day-col");
|
|
2122
|
+
if (!col) return;
|
|
2123
|
+
var topPct = ((hour * 60 + minute) / 1440) * 100;
|
|
2124
|
+
var el = document.createElement("div");
|
|
2125
|
+
el.className = "scheduler-week-event preview";
|
|
2126
|
+
el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
|
|
2127
|
+
el.textContent = timeStr + " " + label;
|
|
2128
|
+
col.appendChild(el);
|
|
2129
|
+
previewEl = el;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function showPreviewForCreate(anchorEl, label) {
|
|
2133
|
+
removePreview();
|
|
2134
|
+
if (!anchorEl) return;
|
|
2135
|
+
var text = label || "(No title)";
|
|
2136
|
+
if (anchorEl.classList.contains("scheduler-week-slot")) {
|
|
2137
|
+
var hour = parseInt(anchorEl.dataset.hour, 10);
|
|
2138
|
+
var quarter = parseInt(anchorEl.dataset.quarter || "0", 10);
|
|
2139
|
+
var minute = quarter * 15;
|
|
2140
|
+
var timeStr = pad(hour) + ":" + pad(minute);
|
|
2141
|
+
var col = anchorEl.closest(".scheduler-week-day-col");
|
|
2142
|
+
if (!col) return;
|
|
2143
|
+
var topPct = ((hour * 60 + minute) / 1440) * 100;
|
|
2144
|
+
var el = document.createElement("div");
|
|
2145
|
+
el.className = "scheduler-week-event preview";
|
|
2146
|
+
el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
|
|
2147
|
+
el.textContent = timeStr + " " + text;
|
|
2148
|
+
col.appendChild(el);
|
|
2149
|
+
previewEl = el;
|
|
2150
|
+
} else if (anchorEl.classList.contains("scheduler-cell")) {
|
|
2151
|
+
var el = document.createElement("div");
|
|
2152
|
+
el.className = "scheduler-event preview";
|
|
2153
|
+
el.textContent = text;
|
|
2154
|
+
anchorEl.appendChild(el);
|
|
2155
|
+
previewEl = el;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function applyDraggedTask() {
|
|
2160
|
+
if (!draggedTaskId) return;
|
|
2161
|
+
var taskHidden = document.getElementById("sched-create-task");
|
|
2162
|
+
var taskLabel = document.getElementById("sched-create-task-label");
|
|
2163
|
+
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
2164
|
+
if (taskHidden) taskHidden.value = draggedTaskId;
|
|
2165
|
+
if (taskLabel) taskLabel.textContent = draggedTaskName || draggedTaskId;
|
|
2166
|
+
if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
|
|
2167
|
+
// Mark the matching item as selected in the dropdown list
|
|
2168
|
+
var taskListEl = document.getElementById("sched-create-task-list");
|
|
2169
|
+
if (taskListEl) {
|
|
2170
|
+
var items = taskListEl.querySelectorAll(".sched-create-task-item");
|
|
2171
|
+
for (var k = 0; k < items.length; k++) {
|
|
2172
|
+
items[k].classList.toggle("selected", items[k].dataset.taskId === draggedTaskId);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
// Auto-generate title: "taskName - HH:MM"
|
|
2176
|
+
var titleInput = document.getElementById("sched-create-title");
|
|
2177
|
+
var timeInput = document.getElementById("sched-create-time");
|
|
2178
|
+
if (titleInput && (draggedTaskName || draggedTaskId)) {
|
|
2179
|
+
var name = draggedTaskName || draggedTaskId;
|
|
2180
|
+
var time = timeInput ? timeInput.value : "";
|
|
2181
|
+
titleInput.value = time ? name + " - " + time : name;
|
|
2182
|
+
}
|
|
2183
|
+
// Update preview text to match auto-title
|
|
2184
|
+
if (previewEl && titleInput) {
|
|
2185
|
+
var previewText = titleInput.value || "(No title)";
|
|
2186
|
+
if (previewEl.classList.contains("scheduler-week-event") && timeInput) {
|
|
2187
|
+
previewText = timeInput.value + " " + (titleInput.value || "(No title)");
|
|
2188
|
+
}
|
|
2189
|
+
previewEl.textContent = previewText;
|
|
2190
|
+
}
|
|
2191
|
+
draggedTaskId = null;
|
|
2192
|
+
draggedTaskName = null;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function openCreateModalWithRecord(rec, anchorEl) {
|
|
2196
|
+
// Parse date/time from record
|
|
2197
|
+
var date = null;
|
|
2198
|
+
var hour = null;
|
|
2199
|
+
if (rec.date) {
|
|
2200
|
+
var dp = rec.date.split("-");
|
|
2201
|
+
date = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
|
|
2202
|
+
}
|
|
2203
|
+
if (rec.time) {
|
|
2204
|
+
var tp = rec.time.split(":");
|
|
2205
|
+
hour = parseInt(tp[0], 10) || 0;
|
|
2206
|
+
var mins = parseInt(tp[1], 10) || 0;
|
|
2207
|
+
if (date) { date.setHours(hour, mins, 0); }
|
|
2208
|
+
}
|
|
2209
|
+
// Mark as editing existing record
|
|
2210
|
+
createEditingRecId = rec.id;
|
|
2211
|
+
|
|
2212
|
+
// Open the create modal normally first
|
|
2213
|
+
openCreateModal(date || new Date(), hour, anchorEl);
|
|
2214
|
+
|
|
2215
|
+
// Show delete button
|
|
2216
|
+
var deleteBtn = document.getElementById("sched-create-delete");
|
|
2217
|
+
if (deleteBtn) deleteBtn.classList.remove("hidden");
|
|
2218
|
+
|
|
2219
|
+
// Now override with record values
|
|
2220
|
+
var titleInput = document.getElementById("sched-create-title");
|
|
2221
|
+
if (titleInput) titleInput.value = rec.name || "";
|
|
2222
|
+
|
|
2223
|
+
var descInput = document.getElementById("sched-create-desc");
|
|
2224
|
+
if (descInput) descInput.value = rec.description || "";
|
|
2225
|
+
|
|
2226
|
+
// Set color
|
|
2227
|
+
if (rec.color) {
|
|
2228
|
+
createColor = rec.color;
|
|
2229
|
+
var colorDot = document.getElementById("sched-create-color-dot");
|
|
2230
|
+
if (colorDot) colorDot.style.background = createColor;
|
|
2231
|
+
var swatches = createPopover.querySelectorAll(".sched-color-swatch");
|
|
2232
|
+
for (var si = 0; si < swatches.length; si++) {
|
|
2233
|
+
swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// Set iterations
|
|
2238
|
+
if (rec.maxIterations) {
|
|
2239
|
+
var iterInput = document.getElementById("sched-create-iterations");
|
|
2240
|
+
if (iterInput) iterInput.value = rec.maxIterations;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Set linked task
|
|
2244
|
+
if (rec.linkedTaskId) {
|
|
2245
|
+
var taskHidden = document.getElementById("sched-create-task");
|
|
2246
|
+
var taskLabel = document.getElementById("sched-create-task-label");
|
|
2247
|
+
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
2248
|
+
var taskListEl = document.getElementById("sched-create-task-list");
|
|
2249
|
+
if (taskHidden) taskHidden.value = rec.linkedTaskId;
|
|
2250
|
+
// Find the task name
|
|
2251
|
+
var taskName = rec.linkedTaskId;
|
|
2252
|
+
for (var j = 0; j < records.length; j++) {
|
|
2253
|
+
if (records[j].id === rec.linkedTaskId) { taskName = records[j].name || records[j].id; break; }
|
|
2254
|
+
}
|
|
2255
|
+
if (taskLabel) taskLabel.textContent = taskName;
|
|
2256
|
+
if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
|
|
2257
|
+
if (taskListEl) {
|
|
2258
|
+
var items = taskListEl.querySelectorAll(".sched-create-task-item");
|
|
2259
|
+
for (var k = 0; k < items.length; k++) {
|
|
2260
|
+
items[k].classList.toggle("selected", items[k].dataset.taskId === rec.linkedTaskId);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// Update preview to show record name
|
|
2266
|
+
if (previewEl) {
|
|
2267
|
+
var previewText = rec.name || "(No title)";
|
|
2268
|
+
if (previewEl.classList.contains("scheduler-week-event") && rec.time) {
|
|
2269
|
+
previewText = rec.time + " " + previewText;
|
|
2270
|
+
}
|
|
2271
|
+
previewEl.textContent = previewText;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function openCreateModal(date, hour, anchorEl) {
|
|
2276
|
+
if (!createPopover) return;
|
|
2277
|
+
// Reset editing state (openCreateModalWithRecord sets this before calling us)
|
|
2278
|
+
if (!createEditingRecId) {
|
|
2279
|
+
var deleteBtn = document.getElementById("sched-create-delete");
|
|
2280
|
+
if (deleteBtn) deleteBtn.classList.add("hidden");
|
|
2281
|
+
}
|
|
2282
|
+
createSelectedDate = date || new Date();
|
|
2283
|
+
createRecurrence = "none";
|
|
2284
|
+
createCustomConfirmed = false;
|
|
2285
|
+
createColor = "#ffb86c";
|
|
2286
|
+
|
|
2287
|
+
// Reset form
|
|
2288
|
+
document.getElementById("sched-create-title").value = "";
|
|
2289
|
+
document.getElementById("sched-create-desc").value = "";
|
|
2290
|
+
var iterReset = document.getElementById("sched-create-iterations");
|
|
2291
|
+
if (iterReset) iterReset.value = "3";
|
|
2292
|
+
|
|
2293
|
+
// Reset color
|
|
2294
|
+
var colorDot = document.getElementById("sched-create-color-dot");
|
|
2295
|
+
if (colorDot) colorDot.style.background = createColor;
|
|
2296
|
+
var palette = document.getElementById("sched-create-color-palette");
|
|
2297
|
+
if (palette) palette.classList.add("hidden");
|
|
2298
|
+
var swatches = createPopover.querySelectorAll(".sched-color-swatch");
|
|
2299
|
+
for (var si = 0; si < swatches.length; si++) {
|
|
2300
|
+
swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// Populate task dropdown (only tasks — exclude ralph and schedule)
|
|
2304
|
+
var taskHidden = document.getElementById("sched-create-task");
|
|
2305
|
+
var taskLabel = document.getElementById("sched-create-task-label");
|
|
2306
|
+
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
2307
|
+
var taskListEl = document.getElementById("sched-create-task-list");
|
|
2308
|
+
if (taskHidden) taskHidden.value = "";
|
|
2309
|
+
if (taskLabel) taskLabel.textContent = "Select a task";
|
|
2310
|
+
if (taskBtn) { taskBtn.classList.remove("has-value"); taskBtn.classList.remove("invalid"); }
|
|
2311
|
+
if (taskListEl) {
|
|
2312
|
+
taskListEl.classList.add("hidden");
|
|
2313
|
+
var tasks = records.filter(function (r) { return r.source !== "ralph" && r.source !== "schedule"; });
|
|
2314
|
+
if (tasks.length === 0) {
|
|
2315
|
+
taskListEl.innerHTML = '<div class="sched-create-task-empty">No tasks available</div>';
|
|
2316
|
+
} else {
|
|
2317
|
+
var html = "";
|
|
2318
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
2319
|
+
html += '<div class="sched-create-task-item" data-task-id="' + esc(tasks[i].id) + '">' + esc(tasks[i].name || tasks[i].id) + '</div>';
|
|
2320
|
+
}
|
|
2321
|
+
taskListEl.innerHTML = html;
|
|
2322
|
+
// Bind click handlers
|
|
2323
|
+
var items = taskListEl.querySelectorAll(".sched-create-task-item");
|
|
2324
|
+
for (var j = 0; j < items.length; j++) {
|
|
2325
|
+
(function (item) {
|
|
2326
|
+
item.addEventListener("click", function (e) {
|
|
2327
|
+
e.stopPropagation();
|
|
2328
|
+
var id = item.dataset.taskId;
|
|
2329
|
+
var name = item.textContent;
|
|
2330
|
+
if (taskHidden) taskHidden.value = id;
|
|
2331
|
+
if (taskLabel) taskLabel.textContent = name;
|
|
2332
|
+
if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
|
|
2333
|
+
// Update selected state
|
|
2334
|
+
var all = taskListEl.querySelectorAll(".sched-create-task-item");
|
|
2335
|
+
for (var k = 0; k < all.length; k++) {
|
|
2336
|
+
all[k].classList.toggle("selected", all[k] === item);
|
|
2337
|
+
}
|
|
2338
|
+
taskListEl.classList.add("hidden");
|
|
2339
|
+
});
|
|
2340
|
+
})(items[j]);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// Set date picker
|
|
2346
|
+
var dateStr = createSelectedDate.getFullYear() + "-" + pad(createSelectedDate.getMonth() + 1) + "-" + pad(createSelectedDate.getDate());
|
|
2347
|
+
document.getElementById("sched-create-date").value = dateStr;
|
|
2348
|
+
var datePicker = document.getElementById("sched-create-date-picker");
|
|
2349
|
+
if (datePicker) datePicker.value = dateStr;
|
|
2350
|
+
|
|
2351
|
+
// Time (use minutes from createSelectedDate for 15-min snapping)
|
|
2352
|
+
if (hour !== null && hour !== undefined) {
|
|
2353
|
+
var mins = createSelectedDate.getMinutes ? createSelectedDate.getMinutes() : 0;
|
|
2354
|
+
document.getElementById("sched-create-time").value = pad(hour) + ":" + pad(mins);
|
|
2355
|
+
} else {
|
|
2356
|
+
document.getElementById("sched-create-time").value = "09:00";
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Update recurrence labels
|
|
2360
|
+
updateRecurrenceLabels(createSelectedDate);
|
|
2361
|
+
|
|
2362
|
+
// Reset recurrence
|
|
2363
|
+
var recOptions = createPopover.querySelectorAll(".sched-recurrence-option");
|
|
2364
|
+
for (var i = 0; i < recOptions.length; i++) {
|
|
2365
|
+
recOptions[i].classList.toggle("active", recOptions[i].dataset.recurrence === "none");
|
|
2366
|
+
}
|
|
2367
|
+
updateRecurrenceBtn();
|
|
2368
|
+
|
|
2369
|
+
// Reset custom panel
|
|
2370
|
+
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
2371
|
+
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
2372
|
+
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
2373
|
+
document.getElementById("sched-custom-interval").value = "1";
|
|
2374
|
+
document.getElementById("sched-custom-unit").value = "week";
|
|
2375
|
+
document.getElementById("sched-custom-dow-section").style.display = "";
|
|
2376
|
+
var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
|
|
2377
|
+
for (var i = 0; i < customDowBtns.length; i++) {
|
|
2378
|
+
customDowBtns[i].classList.toggle("active", parseInt(customDowBtns[i].dataset.dow) === createSelectedDate.getDay());
|
|
2379
|
+
}
|
|
2380
|
+
document.getElementById("sched-custom-end").value = "never";
|
|
2381
|
+
document.getElementById("sched-custom-end-label").textContent = "Never";
|
|
2382
|
+
var endItems = document.querySelectorAll(".sched-custom-end-item");
|
|
2383
|
+
for (var ei = 0; ei < endItems.length; ei++) {
|
|
2384
|
+
endItems[ei].classList.toggle("active", endItems[ei].dataset.value === "never");
|
|
2385
|
+
}
|
|
2386
|
+
document.getElementById("sched-custom-end-list").classList.add("hidden");
|
|
2387
|
+
createEndType = "never";
|
|
2388
|
+
createEndDate = null;
|
|
2389
|
+
createEndAfter = 10;
|
|
2390
|
+
document.getElementById("sched-custom-end-date-btn").classList.add("hidden");
|
|
2391
|
+
document.getElementById("sched-custom-end-after-wrap").classList.add("hidden");
|
|
2392
|
+
document.getElementById("sched-custom-end-calendar").classList.add("hidden");
|
|
2393
|
+
|
|
2394
|
+
// Show preview event on the calendar cell
|
|
2395
|
+
showPreviewForCreate(anchorEl, draggedTaskName || null);
|
|
2396
|
+
|
|
2397
|
+
// Position near anchor cell
|
|
2398
|
+
createPopover.classList.remove("hidden");
|
|
2399
|
+
positionCreatePopover(anchorEl);
|
|
2400
|
+
|
|
2401
|
+
try { lucide.createIcons({ node: createPopover }); } catch (e) {}
|
|
2402
|
+
setTimeout(function () { document.getElementById("sched-create-title").focus(); }, 50);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function positionCreatePopover(anchorEl) {
|
|
2406
|
+
if (!createPopover || !anchorEl) {
|
|
2407
|
+
// Fallback: center in scheduler content area
|
|
2408
|
+
if (createPopover && contentCalEl) {
|
|
2409
|
+
var cRect = contentCalEl.getBoundingClientRect();
|
|
2410
|
+
createPopover.style.left = (cRect.left + cRect.width / 2 - 180) + "px";
|
|
2411
|
+
createPopover.style.top = (cRect.top + 60) + "px";
|
|
2412
|
+
}
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
2417
|
+
var popW = 360;
|
|
2418
|
+
var popH = createPopover.offsetHeight || 300;
|
|
2419
|
+
|
|
2420
|
+
// Try to place to the right of the cell
|
|
2421
|
+
var left = rect.right + 8;
|
|
2422
|
+
var top = rect.top;
|
|
2423
|
+
|
|
2424
|
+
// If it overflows right, place to the left
|
|
2425
|
+
if (left + popW > window.innerWidth - 10) {
|
|
2426
|
+
left = rect.left - popW - 8;
|
|
2427
|
+
}
|
|
2428
|
+
// If it still overflows left, center horizontally on the cell
|
|
2429
|
+
if (left < 10) {
|
|
2430
|
+
left = Math.max(10, rect.left + rect.width / 2 - popW / 2);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// Vertical: don't overflow bottom
|
|
2434
|
+
if (top + popH > window.innerHeight - 10) {
|
|
2435
|
+
top = window.innerHeight - popH - 10;
|
|
2436
|
+
}
|
|
2437
|
+
if (top < 10) top = 10;
|
|
2438
|
+
|
|
2439
|
+
createPopover.style.left = left + "px";
|
|
2440
|
+
createPopover.style.top = top + "px";
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function updateRecurrenceLabels(date) {
|
|
2444
|
+
var dow = date.getDay();
|
|
2445
|
+
var dayName = DAY_NAMES[dow];
|
|
2446
|
+
var dom = date.getDate();
|
|
2447
|
+
var monthName = MONTH_NAMES[date.getMonth()];
|
|
2448
|
+
|
|
2449
|
+
// Weekly on {day}
|
|
2450
|
+
var weeklyEl = document.getElementById("sched-recurrence-weekly");
|
|
2451
|
+
if (weeklyEl) weeklyEl.textContent = "Weekly on " + dayName;
|
|
2452
|
+
|
|
2453
|
+
// Every second {day} of the month
|
|
2454
|
+
var weekOfMonth = Math.ceil(dom / 7);
|
|
2455
|
+
var ordinals = ["", "first", "second", "third", "fourth", "fifth"];
|
|
2456
|
+
var biweeklyEl = document.getElementById("sched-recurrence-biweekly");
|
|
2457
|
+
if (biweeklyEl) {
|
|
2458
|
+
var ordStr = ordinals[weekOfMonth] || weekOfMonth + "th";
|
|
2459
|
+
biweeklyEl.textContent = "Every " + ordStr + " " + dayName + " of the mo...";
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// Every year on {month} {date}
|
|
2463
|
+
var yearlyEl = document.getElementById("sched-recurrence-yearly");
|
|
2464
|
+
if (yearlyEl) yearlyEl.textContent = "Every year on " + monthName + " " + dom;
|
|
2465
|
+
|
|
2466
|
+
// Every month on the {date}th
|
|
2467
|
+
var monthlyEl = document.getElementById("sched-recurrence-monthly");
|
|
2468
|
+
if (monthlyEl) {
|
|
2469
|
+
var suffix = "th";
|
|
2470
|
+
if (dom === 1 || dom === 21 || dom === 31) suffix = "st";
|
|
2471
|
+
else if (dom === 2 || dom === 22) suffix = "nd";
|
|
2472
|
+
else if (dom === 3 || dom === 23) suffix = "rd";
|
|
2473
|
+
monthlyEl.textContent = "Every month on the " + dom + suffix;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
function closeCreateModal() {
|
|
2478
|
+
if (createPopover) createPopover.classList.add("hidden");
|
|
2479
|
+
var dd = document.getElementById("sched-create-recurrence-dropdown");
|
|
2480
|
+
if (dd) dd.classList.add("hidden");
|
|
2481
|
+
var pal = document.getElementById("sched-create-color-palette");
|
|
2482
|
+
if (pal) pal.classList.add("hidden");
|
|
2483
|
+
var tl = document.getElementById("sched-create-task-list");
|
|
2484
|
+
if (tl) tl.classList.add("hidden");
|
|
2485
|
+
removePreview();
|
|
2486
|
+
createSelectedDate = null;
|
|
2487
|
+
createEditingRecId = null;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
function openDeleteDialog(recId, eventDate, isOneOff) {
|
|
2491
|
+
var dialog = document.getElementById("sched-delete-dialog");
|
|
2492
|
+
if (!dialog) return;
|
|
2493
|
+
dialog.dataset.recId = recId;
|
|
2494
|
+
if (eventDate) {
|
|
2495
|
+
dialog.dataset.eventDate = eventDate.getFullYear() + "-" + pad(eventDate.getMonth() + 1) + "-" + pad(eventDate.getDate());
|
|
2496
|
+
} else {
|
|
2497
|
+
dialog.dataset.eventDate = "";
|
|
2498
|
+
}
|
|
2499
|
+
// Toggle between one-off and recurring UI
|
|
2500
|
+
var title = dialog.querySelector(".sched-delete-dialog-title");
|
|
2501
|
+
var body = dialog.querySelector(".sched-delete-dialog-body");
|
|
2502
|
+
var footer = dialog.querySelector(".sched-delete-dialog-footer");
|
|
2503
|
+
var cancelBtn = dialog.querySelector('[data-delete="cancel"]');
|
|
2504
|
+
dialog.dataset.oneOff = isOneOff ? "1" : "";
|
|
2505
|
+
if (isOneOff) {
|
|
2506
|
+
if (title) title.textContent = "Delete this event?";
|
|
2507
|
+
if (body) body.classList.add("hidden");
|
|
2508
|
+
if (cancelBtn) cancelBtn.textContent = "Cancel";
|
|
2509
|
+
// Add a "Delete" button next to cancel in footer
|
|
2510
|
+
var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
|
|
2511
|
+
if (!existingDel && footer) {
|
|
2512
|
+
var delBtn = document.createElement("button");
|
|
2513
|
+
delBtn.className = "sched-delete-option danger sched-delete-confirm-btn";
|
|
2514
|
+
delBtn.dataset.delete = "all";
|
|
2515
|
+
delBtn.textContent = "Delete";
|
|
2516
|
+
footer.appendChild(delBtn);
|
|
2517
|
+
delBtn.addEventListener("click", function (e) {
|
|
2518
|
+
e.stopPropagation();
|
|
2519
|
+
var rid = dialog.dataset.recId;
|
|
2520
|
+
if (rid) send({ type: "loop_registry_remove", id: rid });
|
|
2521
|
+
closeDeleteDialog();
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
if (existingDel) existingDel.classList.remove("hidden");
|
|
2525
|
+
} else {
|
|
2526
|
+
if (title) title.textContent = "Delete recurring event";
|
|
2527
|
+
if (body) body.classList.remove("hidden");
|
|
2528
|
+
if (cancelBtn) cancelBtn.textContent = "Cancel";
|
|
2529
|
+
var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
|
|
2530
|
+
if (existingDel) existingDel.classList.add("hidden");
|
|
2531
|
+
}
|
|
2532
|
+
dialog.classList.remove("hidden");
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function closeDeleteDialog() {
|
|
2536
|
+
var dialog = document.getElementById("sched-delete-dialog");
|
|
2537
|
+
if (dialog) {
|
|
2538
|
+
dialog.classList.add("hidden");
|
|
2539
|
+
dialog.dataset.recId = "";
|
|
2540
|
+
dialog.dataset.eventDate = "";
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
function buildCreateCron() {
|
|
2545
|
+
if (!createSelectedDate) return null;
|
|
2546
|
+
|
|
2547
|
+
var timeVal = document.getElementById("sched-create-time").value || "09:00";
|
|
2548
|
+
var timeParts = timeVal.split(":");
|
|
2549
|
+
var h = parseInt(timeParts[0], 10);
|
|
2550
|
+
var m = parseInt(timeParts[1], 10);
|
|
2551
|
+
|
|
2552
|
+
var dow = createSelectedDate.getDay();
|
|
2553
|
+
var dom = createSelectedDate.getDate();
|
|
2554
|
+
var month = createSelectedDate.getMonth() + 1;
|
|
2555
|
+
|
|
2556
|
+
if (createRecurrence === "none") return null;
|
|
2557
|
+
if (createRecurrence === "daily") return m + " " + h + " * * *";
|
|
2558
|
+
if (createRecurrence === "weekly") return m + " " + h + " * * " + dow;
|
|
2559
|
+
if (createRecurrence === "biweekly") {
|
|
2560
|
+
// Nth weekday of month — approximate with cron (run weekly, but it's the closest)
|
|
2561
|
+
var weekNum = Math.ceil(dom / 7);
|
|
2562
|
+
return m + " " + h + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
|
|
2563
|
+
}
|
|
2564
|
+
if (createRecurrence === "yearly") return m + " " + h + " " + dom + " " + month + " *";
|
|
2565
|
+
if (createRecurrence === "monthly") return m + " " + h + " " + dom + " * *";
|
|
2566
|
+
if (createRecurrence === "weekdays") return m + " " + h + " * * 1-5";
|
|
2567
|
+
|
|
2568
|
+
if (createRecurrence === "custom" && createCustomConfirmed) {
|
|
2569
|
+
return buildCustomCron(h, m);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function updateEndDateLabel() {
|
|
2576
|
+
var label = document.getElementById("sched-custom-end-date-label");
|
|
2577
|
+
if (!label || !createEndDate) return;
|
|
2578
|
+
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2579
|
+
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
2580
|
+
label.textContent = days[createEndDate.getDay()] + ", " + months[createEndDate.getMonth()] + " " + createEndDate.getDate();
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
function renderEndCalendar() {
|
|
2584
|
+
var grid = document.getElementById("sched-cal-grid");
|
|
2585
|
+
var titleEl = document.getElementById("sched-cal-title");
|
|
2586
|
+
if (!grid || !createEndCalMonth) return;
|
|
2587
|
+
|
|
2588
|
+
var year = createEndCalMonth.getFullYear();
|
|
2589
|
+
var month = createEndCalMonth.getMonth();
|
|
2590
|
+
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
2591
|
+
titleEl.textContent = months[month] + " " + year;
|
|
2592
|
+
|
|
2593
|
+
var firstDay = new Date(year, month, 1).getDay();
|
|
2594
|
+
var daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
2595
|
+
var prevDays = new Date(year, month, 0).getDate();
|
|
2596
|
+
|
|
2597
|
+
var today = new Date();
|
|
2598
|
+
today.setHours(0, 0, 0, 0);
|
|
2599
|
+
|
|
2600
|
+
grid.innerHTML = "";
|
|
2601
|
+
|
|
2602
|
+
// Previous month filler
|
|
2603
|
+
for (var p = firstDay - 1; p >= 0; p--) {
|
|
2604
|
+
var d = prevDays - p;
|
|
2605
|
+
var btn = document.createElement("button");
|
|
2606
|
+
btn.className = "sched-cal-day other-month";
|
|
2607
|
+
btn.textContent = d;
|
|
2608
|
+
btn.type = "button";
|
|
2609
|
+
var prevDate = new Date(year, month - 1, d);
|
|
2610
|
+
(function (dt) {
|
|
2611
|
+
btn.addEventListener("click", function (e) {
|
|
2612
|
+
e.stopPropagation();
|
|
2613
|
+
createEndDate = dt;
|
|
2614
|
+
updateEndDateLabel();
|
|
2615
|
+
renderEndCalendar();
|
|
2616
|
+
});
|
|
2617
|
+
})(prevDate);
|
|
2618
|
+
grid.appendChild(btn);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Current month
|
|
2622
|
+
for (var i = 1; i <= daysInMonth; i++) {
|
|
2623
|
+
var btn = document.createElement("button");
|
|
2624
|
+
btn.className = "sched-cal-day";
|
|
2625
|
+
btn.textContent = i;
|
|
2626
|
+
btn.type = "button";
|
|
2627
|
+
var cellDate = new Date(year, month, i);
|
|
2628
|
+
if (cellDate.getTime() === today.getTime()) btn.classList.add("today");
|
|
2629
|
+
if (createEndDate && cellDate.getFullYear() === createEndDate.getFullYear() && cellDate.getMonth() === createEndDate.getMonth() && cellDate.getDate() === createEndDate.getDate()) {
|
|
2630
|
+
btn.classList.add("selected");
|
|
2631
|
+
}
|
|
2632
|
+
(function (dt) {
|
|
2633
|
+
btn.addEventListener("click", function (e) {
|
|
2634
|
+
e.stopPropagation();
|
|
2635
|
+
createEndDate = dt;
|
|
2636
|
+
updateEndDateLabel();
|
|
2637
|
+
renderEndCalendar();
|
|
2638
|
+
});
|
|
2639
|
+
})(cellDate);
|
|
2640
|
+
grid.appendChild(btn);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// Next month filler
|
|
2644
|
+
var totalCells = firstDay + daysInMonth;
|
|
2645
|
+
var remaining = (7 - (totalCells % 7)) % 7;
|
|
2646
|
+
for (var n = 1; n <= remaining; n++) {
|
|
2647
|
+
var btn = document.createElement("button");
|
|
2648
|
+
btn.className = "sched-cal-day other-month";
|
|
2649
|
+
btn.textContent = n;
|
|
2650
|
+
btn.type = "button";
|
|
2651
|
+
var nextDate = new Date(year, month + 1, n);
|
|
2652
|
+
(function (dt) {
|
|
2653
|
+
btn.addEventListener("click", function (e) {
|
|
2654
|
+
e.stopPropagation();
|
|
2655
|
+
createEndDate = dt;
|
|
2656
|
+
updateEndDateLabel();
|
|
2657
|
+
renderEndCalendar();
|
|
2658
|
+
});
|
|
2659
|
+
})(nextDate);
|
|
2660
|
+
grid.appendChild(btn);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
function buildCustomCron(h, m) {
|
|
2665
|
+
var interval = parseInt(document.getElementById("sched-custom-interval").value, 10) || 1;
|
|
2666
|
+
var unit = document.getElementById("sched-custom-unit").value;
|
|
2667
|
+
|
|
2668
|
+
if (unit === "day") {
|
|
2669
|
+
if (interval === 1) return m + " " + h + " * * *";
|
|
2670
|
+
return m + " " + h + " */" + interval + " * *";
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
if (unit === "week") {
|
|
2674
|
+
var days = [];
|
|
2675
|
+
var btns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn.active");
|
|
2676
|
+
for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
|
|
2677
|
+
if (days.length === 0) days.push(String(createSelectedDate ? createSelectedDate.getDay() : 0));
|
|
2678
|
+
return m + " " + h + " * * " + days.sort().join(",");
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
if (unit === "month") {
|
|
2682
|
+
var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
|
|
2683
|
+
if (interval === 1) return m + " " + h + " " + dom + " * *";
|
|
2684
|
+
return m + " " + h + " " + dom + " */" + interval + " *";
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (unit === "year") {
|
|
2688
|
+
var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
|
|
2689
|
+
var month = createSelectedDate ? createSelectedDate.getMonth() + 1 : 1;
|
|
2690
|
+
return m + " " + h + " " + dom + " " + month + " *";
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function submitCreateSchedule() {
|
|
2697
|
+
var name = document.getElementById("sched-create-title").value.trim();
|
|
2698
|
+
if (!name) { document.getElementById("sched-create-title").focus(); return; }
|
|
2699
|
+
|
|
2700
|
+
var taskId = document.getElementById("sched-create-task").value || null;
|
|
2701
|
+
if (!taskId) {
|
|
2702
|
+
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
2703
|
+
if (taskBtn) taskBtn.classList.add("invalid");
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
var description = document.getElementById("sched-create-desc").value.trim();
|
|
2707
|
+
var datePicker = document.getElementById("sched-create-date-picker");
|
|
2708
|
+
var dateVal = datePicker ? datePicker.value : document.getElementById("sched-create-date").value;
|
|
2709
|
+
var timeVal = document.getElementById("sched-create-time").value || "09:00";
|
|
2710
|
+
var cron = buildCreateCron();
|
|
2711
|
+
|
|
2712
|
+
// Build recurrence end info
|
|
2713
|
+
var recurrenceEnd = null;
|
|
2714
|
+
if (cron && createRecurrence === "custom" && createCustomConfirmed) {
|
|
2715
|
+
if (createEndType === "until" && createEndDate) {
|
|
2716
|
+
var ey = createEndDate.getFullYear();
|
|
2717
|
+
var em = String(createEndDate.getMonth() + 1).padStart(2, "0");
|
|
2718
|
+
var ed = String(createEndDate.getDate()).padStart(2, "0");
|
|
2719
|
+
recurrenceEnd = { type: "until", date: ey + "-" + em + "-" + ed };
|
|
2720
|
+
} else if (createEndType === "after" && createEndAfter > 0) {
|
|
2721
|
+
recurrenceEnd = { type: "after", count: createEndAfter };
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
var iterInput = document.getElementById("sched-create-iterations");
|
|
2726
|
+
var maxIterations = iterInput ? (parseInt(iterInput.value, 10) || 3) : 3;
|
|
2727
|
+
if (maxIterations < 1) maxIterations = 1;
|
|
2728
|
+
if (maxIterations > 100) maxIterations = 100;
|
|
2729
|
+
|
|
2730
|
+
send({
|
|
2731
|
+
type: "schedule_create",
|
|
2732
|
+
data: {
|
|
2733
|
+
name: name,
|
|
2734
|
+
taskId: taskId,
|
|
2735
|
+
description: description,
|
|
2736
|
+
date: dateVal,
|
|
2737
|
+
time: timeVal,
|
|
2738
|
+
allDay: false,
|
|
2739
|
+
cron: cron,
|
|
2740
|
+
enabled: cron ? true : false,
|
|
2741
|
+
color: createColor,
|
|
2742
|
+
recurrenceEnd: recurrenceEnd,
|
|
2743
|
+
maxIterations: maxIterations,
|
|
2744
|
+
},
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
closeCreateModal();
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// --- Cron parser (client-side) ---
|
|
2751
|
+
|
|
2752
|
+
function parseCronSimple(expr) {
|
|
2753
|
+
if (!expr) return null;
|
|
2754
|
+
var fields = expr.trim().split(/\s+/);
|
|
2755
|
+
if (fields.length !== 5) return null;
|
|
2756
|
+
return {
|
|
2757
|
+
minutes: parseField(fields[0], 0, 59),
|
|
2758
|
+
hours: parseField(fields[1], 0, 23),
|
|
2759
|
+
daysOfMonth: parseField(fields[2], 1, 31),
|
|
2760
|
+
months: parseField(fields[3], 1, 12),
|
|
2761
|
+
daysOfWeek: parseField(fields[4], 0, 6),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
function parseField(field, min, max) {
|
|
2766
|
+
var values = [];
|
|
2767
|
+
var parts = field.split(",");
|
|
2768
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2769
|
+
var part = parts[i].trim();
|
|
2770
|
+
if (part.indexOf("/") !== -1) {
|
|
2771
|
+
var sp = part.split("/");
|
|
2772
|
+
var step = parseInt(sp[1], 10);
|
|
2773
|
+
var rMin = min, rMax = max;
|
|
2774
|
+
if (sp[0] !== "*") { var rp = sp[0].split("-"); rMin = parseInt(rp[0], 10); rMax = rp.length > 1 ? parseInt(rp[1], 10) : rMin; }
|
|
2775
|
+
for (var v = rMin; v <= rMax; v += step) values.push(v);
|
|
2776
|
+
} else if (part === "*") {
|
|
2777
|
+
for (var v = min; v <= max; v++) values.push(v);
|
|
2778
|
+
} else if (part.indexOf("-") !== -1) {
|
|
2779
|
+
var rp = part.split("-");
|
|
2780
|
+
for (var v = parseInt(rp[0], 10); v <= parseInt(rp[1], 10); v++) values.push(v);
|
|
2781
|
+
} else {
|
|
2782
|
+
values.push(parseInt(part, 10));
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return values;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// --- Utility ---
|
|
2789
|
+
|
|
2790
|
+
function getISOWeekNumber(date) {
|
|
2791
|
+
var d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
2792
|
+
var dayNum = d.getUTCDay() || 7;
|
|
2793
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
2794
|
+
var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
2795
|
+
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function getWeekStart(date) {
|
|
2799
|
+
var d = new Date(date);
|
|
2800
|
+
d.setDate(d.getDate() - d.getDay());
|
|
2801
|
+
d.setHours(0, 0, 0, 0);
|
|
2802
|
+
return d;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function pad(n) { return n < 10 ? "0" + n : String(n); }
|
|
2806
|
+
function esc(s) { return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
|
2807
|
+
|
|
2808
|
+
function formatDateTime(d) {
|
|
2809
|
+
return MONTH_NAMES[d.getMonth()].substring(0, 3) + " " + d.getDate() + ", " + pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
function cronToHuman(cron) {
|
|
2813
|
+
if (!cron) return "";
|
|
2814
|
+
var parts = cron.trim().split(/\s+/);
|
|
2815
|
+
if (parts.length !== 5) return cron;
|
|
2816
|
+
var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
|
|
2817
|
+
var dow = parts[4], dom = parts[2];
|
|
2818
|
+
if (dow === "*" && dom === "*") return "Every day at " + t;
|
|
2819
|
+
if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
|
|
2820
|
+
if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
|
|
2821
|
+
if (dow !== "*" && dom === "*") {
|
|
2822
|
+
var ds = dow.split(",").map(function (d) { return DAY_NAMES[parseInt(d, 10)] || d; });
|
|
2823
|
+
return "Every " + ds.join(", ") + " at " + t;
|
|
2824
|
+
}
|
|
2825
|
+
return cron;
|
|
2826
|
+
}
|