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.
Files changed (55) hide show
  1. package/bin/cli.js +31 -17
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1039 -134
  5. package/lib/public/apple-touch-icon-dark.png +0 -0
  6. package/lib/public/apple-touch-icon.png +0 -0
  7. package/lib/public/clay-logo.png +0 -0
  8. package/lib/public/css/base.css +18 -1
  9. package/lib/public/css/filebrowser.css +1 -0
  10. package/lib/public/css/home-hub.css +455 -0
  11. package/lib/public/css/icon-strip.css +6 -5
  12. package/lib/public/css/loop.css +141 -23
  13. package/lib/public/css/messages.css +2 -0
  14. package/lib/public/css/mobile-nav.css +38 -12
  15. package/lib/public/css/overlays.css +205 -169
  16. package/lib/public/css/playbook.css +264 -0
  17. package/lib/public/css/profile.css +268 -0
  18. package/lib/public/css/scheduler-modal.css +1429 -0
  19. package/lib/public/css/scheduler.css +1305 -0
  20. package/lib/public/css/sidebar.css +305 -11
  21. package/lib/public/css/sticky-notes.css +23 -19
  22. package/lib/public/css/stt.css +155 -0
  23. package/lib/public/css/title-bar.css +14 -6
  24. package/lib/public/favicon-banded-32.png +0 -0
  25. package/lib/public/favicon-banded.png +0 -0
  26. package/lib/public/icon-192-dark.png +0 -0
  27. package/lib/public/icon-192.png +0 -0
  28. package/lib/public/icon-512-dark.png +0 -0
  29. package/lib/public/icon-512.png +0 -0
  30. package/lib/public/icon-banded-76.png +0 -0
  31. package/lib/public/icon-banded-96.png +0 -0
  32. package/lib/public/index.html +336 -44
  33. package/lib/public/modules/ascii-logo.js +442 -0
  34. package/lib/public/modules/markdown.js +18 -0
  35. package/lib/public/modules/notifications.js +50 -63
  36. package/lib/public/modules/playbook.js +578 -0
  37. package/lib/public/modules/profile.js +357 -0
  38. package/lib/public/modules/project-settings.js +1 -9
  39. package/lib/public/modules/scheduler.js +2826 -0
  40. package/lib/public/modules/server-settings.js +1 -1
  41. package/lib/public/modules/sidebar.js +376 -32
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/style.css +6 -0
  46. package/lib/public/sw.js +82 -3
  47. package/lib/public/wordmark-banded-20.png +0 -0
  48. package/lib/public/wordmark-banded-32.png +0 -0
  49. package/lib/public/wordmark-banded-64.png +0 -0
  50. package/lib/public/wordmark-banded-80.png +0 -0
  51. package/lib/scheduler.js +402 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server.js +124 -3
  54. package/lib/sessions.js +35 -2
  55. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
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
+ }