clay-server 2.7.1 → 2.7.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.
@@ -1,1240 +0,0 @@
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
-
10
- var ctx = null;
11
- var records = []; // all loop registry records
12
-
13
- // Calendar state
14
- var currentView = "month";
15
- var viewDate = new Date();
16
-
17
- // Mode state
18
- var currentMode = "calendar"; // "calendar" | "detail" | "crafting"
19
- var selectedTaskId = null;
20
- var craftingTaskId = null; // task ID currently being crafted
21
- var craftingSessionId = null; // session ID used for crafting
22
- var logPreviousSessionId = null; // session to restore when leaving log mode
23
-
24
- // DOM refs
25
- var panel = null; // #scheduler-panel
26
- var bodyEl = null;
27
- var monthLabel = null;
28
- var calHeader = null;
29
- var editModal = null;
30
- var popoverEl = null;
31
- var panelOpen = false;
32
-
33
- // Split-panel DOM refs
34
- var sidebarListEl = null;
35
- var contentCalEl = null;
36
- var contentDetailEl = null;
37
- var contentCraftEl = null;
38
- var messagesOrigParent = null; // for reparenting
39
- var inputOrigNextSibling = null; // anchor for restoring input-area position
40
-
41
- // Edit state
42
- var editingId = null;
43
-
44
- // Day names
45
- var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
46
- var DAY_SHORT = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
47
- var MONTH_NAMES = [
48
- "January", "February", "March", "April", "May", "June",
49
- "July", "August", "September", "October", "November", "December"
50
- ];
51
-
52
- // --- Init ---
53
-
54
- export function initScheduler(_ctx) {
55
- ctx = _ctx;
56
- editModal = document.getElementById("schedule-edit-modal");
57
- popoverEl = document.getElementById("schedule-popover");
58
-
59
- // Sidebar button
60
- var btn = document.getElementById("scheduler-btn");
61
- if (btn) {
62
- btn.addEventListener("click", function () {
63
- if (panelOpen) {
64
- closeScheduler();
65
- } else {
66
- openScheduler();
67
- }
68
- });
69
- }
70
-
71
- // Edit modal
72
- setupEditModal();
73
-
74
- // Close popover on outside click
75
- document.addEventListener("click", function (e) {
76
- if (popoverEl && !popoverEl.classList.contains("hidden") && !popoverEl.contains(e.target)) {
77
- popoverEl.classList.add("hidden");
78
- }
79
- });
80
- }
81
-
82
- function ensurePanel() {
83
- if (panel) return;
84
-
85
- var appEl = document.getElementById("app");
86
- if (!appEl) return;
87
-
88
- panel = document.createElement("div");
89
- panel.id = "scheduler-panel";
90
- panel.className = "hidden";
91
-
92
- // --- Top header bar ---
93
- var topBar = document.createElement("div");
94
- topBar.className = "scheduler-top-bar";
95
- topBar.innerHTML =
96
- '<span class="scheduler-top-title">Scheduled Tasks</span>' +
97
- '<button class="scheduler-close-btn" id="scheduler-panel-close" title="Close"><i data-lucide="x"></i></button>';
98
- panel.appendChild(topBar);
99
-
100
- // --- Body row (sidebar + content) ---
101
- var bodyRow = document.createElement("div");
102
- bodyRow.className = "scheduler-body-row";
103
-
104
- // --- Sidebar ---
105
- var sidebar = document.createElement("div");
106
- sidebar.className = "scheduler-sidebar";
107
-
108
- // Sidebar header
109
- var sidebarHeader = document.createElement("div");
110
- sidebarHeader.className = "scheduler-sidebar-header";
111
- sidebarHeader.innerHTML =
112
- '<span class="scheduler-sidebar-title">Tasks</span>' +
113
- '<span class="scheduler-sidebar-count">0</span>';
114
- sidebar.appendChild(sidebarHeader);
115
-
116
- // Inline add task
117
- var addRow = document.createElement("div");
118
- addRow.className = "scheduler-add-row";
119
- addRow.innerHTML =
120
- '<div class="scheduler-add-trigger" id="scheduler-add-trigger">' +
121
- '<i data-lucide="plus-circle"></i> <span>Add new task</span>' +
122
- '</div>' +
123
- '<div class="scheduler-add-form hidden" id="scheduler-add-form">' +
124
- '<textarea id="scheduler-add-input" rows="2" placeholder="Describe what to build..."></textarea>' +
125
- '<div class="scheduler-add-actions">' +
126
- '<button type="button" class="scheduler-add-submit" id="scheduler-add-submit">Add</button>' +
127
- '<button type="button" class="scheduler-add-cancel" id="scheduler-add-cancel">Cancel</button>' +
128
- '</div>' +
129
- '</div>';
130
- sidebar.appendChild(addRow);
131
-
132
- // Sidebar list
133
- var sidebarList = document.createElement("div");
134
- sidebarList.className = "scheduler-sidebar-list";
135
- sidebar.appendChild(sidebarList);
136
- sidebarListEl = sidebarList;
137
-
138
- bodyRow.appendChild(sidebar);
139
-
140
- // --- Content ---
141
- var content = document.createElement("div");
142
- content.className = "scheduler-content";
143
-
144
- // Content: calendar
145
- var contentCal = document.createElement("div");
146
- contentCal.className = "scheduler-content-calendar";
147
-
148
- // Calendar header (nav, month label, view toggle)
149
- var calHdr = document.createElement("div");
150
- calHdr.className = "scheduler-header";
151
- calHdr.id = "scheduler-cal-header";
152
- calHdr.innerHTML =
153
- '<div class="scheduler-nav">' +
154
- '<button class="scheduler-nav-btn" id="scheduler-prev"><i data-lucide="chevron-left"></i></button>' +
155
- '<button class="scheduler-nav-btn" id="scheduler-next"><i data-lucide="chevron-right"></i></button>' +
156
- '</div>' +
157
- '<span class="scheduler-month-label" id="scheduler-month-label"></span>' +
158
- '<button class="scheduler-today-btn" id="scheduler-today">Today</button>' +
159
- '<div class="scheduler-view-toggle">' +
160
- '<button class="scheduler-view-btn active" data-view="month">Month</button>' +
161
- '<button class="scheduler-view-btn" data-view="week">Week</button>' +
162
- '</div>';
163
- contentCal.appendChild(calHdr);
164
- calHeader = calHdr;
165
- monthLabel = calHdr.querySelector("#scheduler-month-label");
166
-
167
- // Calendar body
168
- var body = document.createElement("div");
169
- body.className = "scheduler-body";
170
- body.id = "scheduler-body";
171
- contentCal.appendChild(body);
172
- bodyEl = body;
173
-
174
- content.appendChild(contentCal);
175
- contentCalEl = contentCal;
176
-
177
- // Content: detail
178
- var contentDetail = document.createElement("div");
179
- contentDetail.className = "scheduler-content-detail hidden";
180
- content.appendChild(contentDetail);
181
- contentDetailEl = contentDetail;
182
-
183
- // Content: crafting
184
- var contentCraft = document.createElement("div");
185
- contentCraft.className = "scheduler-content-crafting hidden";
186
- content.appendChild(contentCraft);
187
- contentCraftEl = contentCraft;
188
-
189
- bodyRow.appendChild(content);
190
- panel.appendChild(bodyRow);
191
-
192
- appEl.appendChild(panel);
193
-
194
- // --- Close button (in top bar) ---
195
- panel.querySelector("#scheduler-panel-close").addEventListener("click", function () {
196
- closeScheduler();
197
- });
198
-
199
- // Inline add task
200
- var addTrigger = addRow.querySelector("#scheduler-add-trigger");
201
- var addForm = addRow.querySelector("#scheduler-add-form");
202
- var addInput = addRow.querySelector("#scheduler-add-input");
203
- var addSubmitBtn = addRow.querySelector("#scheduler-add-submit");
204
- var addCancelBtn = addRow.querySelector("#scheduler-add-cancel");
205
-
206
- addTrigger.addEventListener("click", function () {
207
- addTrigger.classList.add("hidden");
208
- addForm.classList.remove("hidden");
209
- addInput.value = "";
210
- addInput.focus();
211
- });
212
-
213
- addCancelBtn.addEventListener("click", function () {
214
- addForm.classList.add("hidden");
215
- addTrigger.classList.remove("hidden");
216
- });
217
-
218
- addInput.addEventListener("keydown", function (e) {
219
- if (e.key === "Enter" && !e.shiftKey) {
220
- e.preventDefault();
221
- submitInlineTask();
222
- }
223
- if (e.key === "Escape") {
224
- addForm.classList.add("hidden");
225
- addTrigger.classList.remove("hidden");
226
- }
227
- });
228
-
229
- addSubmitBtn.addEventListener("click", function () {
230
- submitInlineTask();
231
- });
232
-
233
- var addSubmitting = false;
234
- function submitInlineTask() {
235
- if (addSubmitting) return;
236
- var task = addInput.value.trim();
237
- if (!task) { addInput.focus(); return; }
238
- addSubmitting = true;
239
- addInput.value = "";
240
- addForm.classList.add("hidden");
241
- addTrigger.classList.remove("hidden");
242
- // Send wizard complete directly (skip modal)
243
- send({
244
- type: "ralph_wizard_complete",
245
- data: { name: task, task: task, maxIterations: 25, cron: null }
246
- });
247
- setTimeout(function () { addSubmitting = false; }, 1000);
248
- }
249
-
250
- // Calendar controls
251
- calHdr.querySelector("#scheduler-prev").addEventListener("click", function () { navigate(-1); });
252
- calHdr.querySelector("#scheduler-next").addEventListener("click", function () { navigate(1); });
253
- calHdr.querySelector("#scheduler-today").addEventListener("click", function () { viewDate = new Date(); render(); });
254
-
255
- // View toggle
256
- var viewBtns = calHdr.querySelectorAll(".scheduler-view-btn");
257
- for (var i = 0; i < viewBtns.length; i++) {
258
- (function (vbtn) {
259
- vbtn.addEventListener("click", function () {
260
- currentView = vbtn.dataset.view;
261
- for (var j = 0; j < viewBtns.length; j++) {
262
- viewBtns[j].classList.toggle("active", viewBtns[j] === vbtn);
263
- }
264
- render();
265
- });
266
- })(viewBtns[i]);
267
- }
268
-
269
- try { lucide.createIcons({ node: panel }); } catch (e) {}
270
- }
271
-
272
- // --- Mode switching ---
273
-
274
- function switchMode(mode) {
275
- currentMode = mode;
276
- if (contentCalEl) contentCalEl.classList.toggle("hidden", mode !== "calendar");
277
- if (contentDetailEl) contentDetailEl.classList.toggle("hidden", mode !== "detail");
278
- if (contentCraftEl) contentCraftEl.classList.toggle("hidden", mode !== "crafting");
279
-
280
- if (mode === "calendar") {
281
- selectedTaskId = null;
282
- updateSidebarSelection();
283
- unparentChat();
284
- if (contentDetailEl) contentDetailEl.innerHTML = "";
285
- render();
286
- } else if (mode === "detail") {
287
- unparentChat();
288
- renderDetail();
289
- } else if (mode === "crafting") {
290
- reparentChat();
291
- updateCraftingHeader();
292
- }
293
- }
294
-
295
- function updateCraftingHeader() {
296
- if (!contentCraftEl) return;
297
- var existing = contentCraftEl.querySelector(".scheduler-crafting-header");
298
- if (existing) existing.remove();
299
-
300
- var isLog = !!logPreviousSessionId;
301
- var hdr = document.createElement("div");
302
- hdr.className = "scheduler-crafting-header";
303
-
304
- var backBtn = document.createElement("button");
305
- backBtn.className = "scheduler-crafting-back";
306
- backBtn.innerHTML = '<i data-lucide="arrow-left"></i> <span>' + (isLog ? "Back to task" : "Back to tasks") + '</span>';
307
- backBtn.addEventListener("click", function () {
308
- if (isLog) {
309
- switchMode("detail");
310
- } else {
311
- switchMode("calendar");
312
- }
313
- });
314
- hdr.appendChild(backBtn);
315
-
316
- var label = document.createElement("span");
317
- label.className = "scheduler-crafting-label";
318
- if (isLog) {
319
- label.innerHTML = '<i data-lucide="message-square"></i> Session Log';
320
- } else {
321
- label.innerHTML = '<i data-lucide="radio"></i> Crafting in progress';
322
- }
323
- hdr.appendChild(label);
324
-
325
- contentCraftEl.insertBefore(hdr, contentCraftEl.firstChild);
326
- try { lucide.createIcons({ node: hdr }); } catch (e) {}
327
- }
328
-
329
- // --- Open/Close ---
330
-
331
- function openScheduler() {
332
- if (panelOpen) return;
333
- panelOpen = true;
334
- ensurePanel();
335
- if (!panel) return;
336
-
337
- var messagesEl = document.getElementById("messages");
338
- var inputArea = document.getElementById("input-area");
339
- var titleBar = document.querySelector("#main-column > .title-bar-content");
340
- var notesContainer = document.getElementById("sticky-notes-container");
341
- var notesArchive = document.getElementById("notes-archive");
342
-
343
- if (messagesEl) messagesEl.classList.add("hidden");
344
- if (inputArea) inputArea.classList.add("hidden");
345
- if (titleBar) titleBar.classList.add("hidden");
346
- if (notesContainer) notesContainer.classList.add("hidden");
347
- if (notesArchive) notesArchive.classList.add("hidden");
348
-
349
- panel.classList.remove("hidden");
350
- viewDate = new Date();
351
- currentMode = "calendar";
352
- selectedTaskId = null;
353
- send({ type: "loop_registry_list" });
354
- switchMode("calendar");
355
- renderSidebar();
356
- try { lucide.createIcons({ node: panel }); } catch (e) {}
357
-
358
- var sidebarBtn = document.getElementById("scheduler-btn");
359
- if (sidebarBtn) sidebarBtn.classList.add("active");
360
- }
361
-
362
- export function closeScheduler() {
363
- if (!panelOpen) return;
364
- panelOpen = false;
365
- if (currentMode === "crafting") unparentChat();
366
-
367
- if (panel) panel.classList.add("hidden");
368
- if (popoverEl) popoverEl.classList.add("hidden");
369
-
370
- var messagesEl = document.getElementById("messages");
371
- var inputArea = document.getElementById("input-area");
372
- var titleBar = document.querySelector("#main-column > .title-bar-content");
373
-
374
- if (messagesEl) messagesEl.classList.remove("hidden");
375
- if (inputArea) inputArea.classList.remove("hidden");
376
- if (titleBar) titleBar.classList.remove("hidden");
377
-
378
- currentMode = "calendar";
379
- selectedTaskId = null;
380
-
381
- // Un-mark sidebar button
382
- var sidebarBtn = document.getElementById("scheduler-btn");
383
- if (sidebarBtn) sidebarBtn.classList.remove("active");
384
- }
385
-
386
- function send(msg) {
387
- if (ctx && ctx.ws && ctx.ws.readyState === 1) {
388
- ctx.ws.send(JSON.stringify(msg));
389
- }
390
- }
391
-
392
- // --- Sidebar ---
393
-
394
- function renderSidebar() {
395
- if (!sidebarListEl) return;
396
-
397
- // Update count badge
398
- var countEl = panel ? panel.querySelector(".scheduler-sidebar-count") : null;
399
- if (countEl) countEl.textContent = records.length;
400
-
401
- if (records.length === 0) {
402
- sidebarListEl.innerHTML = '<div class="scheduler-empty">No tasks yet</div>';
403
- return;
404
- }
405
-
406
- var sorted = records.slice().sort(function (a, b) { return (b.createdAt || 0) - (a.createdAt || 0); });
407
- var html = "";
408
- for (var i = 0; i < sorted.length; i++) {
409
- var rec = sorted[i];
410
- var isScheduled = !!rec.cron;
411
- var selected = rec.id === selectedTaskId ? " selected" : "";
412
- var isCrafting = craftingTaskId === rec.id;
413
- var showBadge = isCrafting || (isScheduled && rec.enabled);
414
- var badgeCls = isCrafting ? "crafting" : "scheduled";
415
- var badgeLabel = isCrafting ? "Crafting" : "Scheduled";
416
-
417
- html += '<div class="scheduler-task-item' + selected + '" data-rec-id="' + rec.id + '">';
418
- html += '<div class="scheduler-task-name-row">';
419
- html += '<div class="scheduler-task-name">' + esc(rec.name || rec.id) + '</div>';
420
- if (!isCrafting) {
421
- html += '<button class="scheduler-task-edit-btn" data-edit-id="' + rec.id + '" type="button" title="Rename">✏</button>';
422
- }
423
- html += '</div>';
424
- if (showBadge) {
425
- html += '<div class="scheduler-task-row">';
426
- html += '<span class="scheduler-task-badge ' + badgeCls + '">' + badgeLabel + '</span>';
427
- html += '</div>';
428
- }
429
- html += '</div>';
430
- }
431
- sidebarListEl.innerHTML = html;
432
-
433
- // Attach click handlers
434
- var items = sidebarListEl.querySelectorAll(".scheduler-task-item");
435
- for (var i = 0; i < items.length; i++) {
436
- (function (item) {
437
- item.addEventListener("click", function () {
438
- var clickedId = item.dataset.recId;
439
- if (selectedTaskId === clickedId) {
440
- if (currentMode === "detail") {
441
- // Toggle: detail → crafting (if this task is being crafted) or calendar
442
- if (craftingTaskId === clickedId) {
443
- switchMode("crafting");
444
- } else {
445
- switchMode("calendar");
446
- renderSidebar();
447
- }
448
- return;
449
- } else if (currentMode === "crafting") {
450
- // Toggle: crafting → detail
451
- switchMode("detail");
452
- return;
453
- }
454
- }
455
- selectedTaskId = clickedId;
456
- updateSidebarSelection();
457
- switchMode("detail");
458
- });
459
- })(items[i]);
460
- }
461
-
462
- // Attach pencil edit handlers
463
- var editBtns = sidebarListEl.querySelectorAll(".scheduler-task-edit-btn");
464
- for (var i = 0; i < editBtns.length; i++) {
465
- (function (btn) {
466
- btn.addEventListener("click", function (e) {
467
- e.stopPropagation();
468
- var editId = btn.dataset.editId;
469
- var rec = null;
470
- for (var j = 0; j < records.length; j++) {
471
- if (records[j].id === editId) { rec = records[j]; break; }
472
- }
473
- if (!rec) return;
474
- var nameEl = btn.parentElement.querySelector(".scheduler-task-name");
475
- var original = rec.name || rec.id;
476
- var input = document.createElement("input");
477
- input.type = "text";
478
- input.className = "scheduler-task-name-input";
479
- input.value = original;
480
- nameEl.replaceWith(input);
481
- btn.classList.add("hidden");
482
- input.focus();
483
- input.select();
484
-
485
- function finishEdit() {
486
- var newName = input.value.trim();
487
- if (newName && newName !== original) {
488
- send({ type: "loop_registry_update", id: editId, data: { name: newName } });
489
- }
490
- renderSidebar();
491
- }
492
- input.addEventListener("keydown", function (ev) {
493
- if (ev.key === "Enter") { ev.preventDefault(); finishEdit(); }
494
- if (ev.key === "Escape") { ev.preventDefault(); renderSidebar(); }
495
- });
496
- input.addEventListener("blur", finishEdit);
497
- });
498
- })(editBtns[i]);
499
- }
500
- }
501
-
502
- function updateSidebarSelection() {
503
- if (!sidebarListEl) return;
504
- var items = sidebarListEl.querySelectorAll(".scheduler-task-item");
505
- for (var i = 0; i < items.length; i++) {
506
- items[i].classList.toggle("selected", items[i].dataset.recId === selectedTaskId);
507
- }
508
- }
509
-
510
- // --- Detail view ---
511
-
512
- function renderDetail() {
513
- if (!contentDetailEl || !selectedTaskId) return;
514
- var rec = null;
515
- for (var i = 0; i < records.length; i++) {
516
- if (records[i].id === selectedTaskId) { rec = records[i]; break; }
517
- }
518
- if (!rec) {
519
- // Task not found — fall back to calendar view
520
- selectedTaskId = null;
521
- switchMode("calendar");
522
- renderSidebar();
523
- render();
524
- return;
525
- }
526
-
527
- var isScheduled = !!rec.cron;
528
- var lastRun = rec.runs && rec.runs.length > 0 ? rec.runs[rec.runs.length - 1] : null;
529
-
530
- var isCraftingThis = craftingTaskId === rec.id;
531
- var hasSession = rec.craftingSessionId || null;
532
-
533
- var html = '<div class="scheduler-detail-header">';
534
- html += '<button class="scheduler-crafting-back" data-action="close" title="Back to tasks"><i data-lucide="arrow-left"></i></button>';
535
- html += '<span class="scheduler-detail-name">' + esc(rec.name || rec.id) + '</span>';
536
- html += '<div class="scheduler-detail-actions">';
537
- if (isCraftingThis || hasSession) {
538
- html += '<button class="scheduler-detail-btn" data-action="session">';
539
- html += '<i data-lucide="' + (isCraftingThis ? "radio" : "message-square") + '"></i> ';
540
- html += isCraftingThis ? "Live session" : "Session log";
541
- html += '</button>';
542
- }
543
- html += '<button class="scheduler-detail-btn primary" data-action="run">Run now</button>';
544
- html += '<button class="scheduler-detail-icon-btn" data-action="delete" title="Delete task"><i data-lucide="trash-2"></i></button>';
545
- html += '</div>';
546
- html += '</div>';
547
-
548
- html += '<div class="scheduler-detail-tabs">';
549
- html += '<button class="scheduler-detail-tab active" data-tab="prompt">PROMPT.md</button>';
550
- html += '<button class="scheduler-detail-tab" data-tab="judge">JUDGE.md</button>';
551
- html += '<button class="scheduler-detail-tab" data-tab="meta">Info</button>';
552
- html += '</div>';
553
-
554
- html += '<div class="scheduler-detail-body" id="scheduler-detail-body">';
555
- html += '<div class="scheduler-detail-loading">Loading...</div>';
556
- html += '</div>';
557
-
558
- contentDetailEl.innerHTML = html;
559
-
560
- // Bind action handlers
561
- var actionBtns = contentDetailEl.querySelectorAll("[data-action]");
562
- for (var i = 0; i < actionBtns.length; i++) {
563
- (function (btn) {
564
- btn.addEventListener("click", function (e) {
565
- e.stopPropagation();
566
- var action = btn.dataset.action;
567
- if (action === "run") {
568
- send({ type: "loop_registry_rerun", id: selectedTaskId });
569
- } else if (action === "delete") {
570
- if (confirm("Delete this task?")) {
571
- send({ type: "loop_registry_remove", id: selectedTaskId });
572
- }
573
- } else if (action === "close") {
574
- switchMode("calendar");
575
- renderSidebar();
576
- } else if (action === "session") {
577
- if (craftingTaskId === rec.id) {
578
- switchMode("crafting");
579
- } else if (rec.craftingSessionId) {
580
- logPreviousSessionId = ctx.activeSessionId || null;
581
- send({ type: "switch_session", id: rec.craftingSessionId });
582
- switchMode("crafting");
583
- var inputArea = document.getElementById("input-area");
584
- if (inputArea && contentCraftEl && contentCraftEl.contains(inputArea)) {
585
- inputArea.classList.add("hidden");
586
- }
587
- }
588
- }
589
- });
590
- })(actionBtns[i]);
591
- }
592
-
593
- // Bind tab switching
594
- var tabBtns = contentDetailEl.querySelectorAll(".scheduler-detail-tab");
595
- for (var i = 0; i < tabBtns.length; i++) {
596
- (function (tabBtn) {
597
- tabBtn.addEventListener("click", function () {
598
- for (var j = 0; j < tabBtns.length; j++) {
599
- tabBtns[j].classList.toggle("active", tabBtns[j] === tabBtn);
600
- }
601
- renderDetailBody(tabBtn.dataset.tab, rec);
602
- });
603
- })(tabBtns[i]);
604
- }
605
-
606
- // Request files for prompt tab (default)
607
- send({ type: "loop_registry_files", id: selectedTaskId });
608
-
609
- try { lucide.createIcons({ node: contentDetailEl }); } catch (e) {}
610
- }
611
-
612
- function renderDetailBody(tab, rec) {
613
- var bodyEl2 = document.getElementById("scheduler-detail-body");
614
- if (!bodyEl2) return;
615
-
616
- if (tab === "meta") {
617
- var isScheduled = !!rec.cron;
618
- var lastRun = rec.runs && rec.runs.length > 0 ? rec.runs[rec.runs.length - 1] : null;
619
- var scheduleStr = isScheduled ? cronToHuman(rec.cron) : "One-off";
620
- var statusStr = isScheduled ? (rec.enabled ? "Enabled" : "Paused") : "One-off";
621
- var createdStr = rec.createdAt ? formatDateTime(new Date(rec.createdAt)) : "—";
622
- var lastRunStr = "Never";
623
- if (lastRun) {
624
- var resultStr = lastRun.result || "?";
625
- var iterStr = (lastRun.iterations || 0) + " iter";
626
- lastRunStr = formatDateTime(new Date(lastRun.finishedAt || lastRun.startedAt)) + " — " + resultStr + " (" + iterStr + ")";
627
- }
628
-
629
- var html = '<div class="scheduler-detail-meta">';
630
- html += '<span class="scheduler-detail-meta-label">Schedule</span>';
631
- html += '<span class="scheduler-detail-meta-value">' + esc(scheduleStr) + '</span>';
632
- html += '<span class="scheduler-detail-meta-label">Status</span>';
633
- html += '<span class="scheduler-detail-meta-value">' + esc(statusStr) + '</span>';
634
- html += '<span class="scheduler-detail-meta-label">Max Iterations</span>';
635
- html += '<span class="scheduler-detail-meta-value">' + (rec.maxIterations || "—") + '</span>';
636
- html += '<span class="scheduler-detail-meta-label">Created</span>';
637
- html += '<span class="scheduler-detail-meta-value">' + esc(createdStr) + '</span>';
638
- html += '<span class="scheduler-detail-meta-label">Last Run</span>';
639
- html += '<span class="scheduler-detail-meta-value">' + esc(lastRunStr) + '</span>';
640
- html += '</div>';
641
- bodyEl2.innerHTML = html;
642
- } else {
643
- // prompt or judge — request files from server
644
- bodyEl2.innerHTML = '<div class="scheduler-detail-loading">Loading...</div>';
645
- send({ type: "loop_registry_files", id: selectedTaskId });
646
- }
647
- }
648
-
649
- // --- Chat reparenting ---
650
-
651
- function reparentChat() {
652
- var messagesEl = document.getElementById("messages");
653
- var inputArea = document.getElementById("input-area");
654
- if (!messagesEl || !inputArea || !contentCraftEl) return;
655
- if (messagesOrigParent) return; // already reparented
656
- messagesOrigParent = messagesEl.parentNode;
657
- inputOrigNextSibling = inputArea.nextSibling;
658
- contentCraftEl.appendChild(messagesEl);
659
- contentCraftEl.appendChild(inputArea);
660
- messagesEl.classList.remove("hidden");
661
- inputArea.classList.remove("hidden");
662
- }
663
-
664
- function unparentChat() {
665
- var messagesEl = document.getElementById("messages");
666
- var inputArea = document.getElementById("input-area");
667
- if (!messagesOrigParent) return;
668
- var infoPanels = messagesOrigParent.querySelector("#info-panels");
669
- if (infoPanels) {
670
- messagesOrigParent.insertBefore(messagesEl, infoPanels);
671
- } else {
672
- messagesOrigParent.appendChild(messagesEl);
673
- }
674
- if (inputOrigNextSibling) {
675
- messagesOrigParent.insertBefore(inputArea, inputOrigNextSibling);
676
- } else {
677
- messagesOrigParent.appendChild(inputArea);
678
- }
679
- messagesOrigParent = null;
680
- inputOrigNextSibling = null;
681
-
682
- // Restore input-area visibility (may have been hidden in log mode)
683
- if (inputArea) inputArea.classList.remove("hidden");
684
-
685
- // Remove crafting header
686
- if (contentCraftEl) {
687
- var craftHdr = contentCraftEl.querySelector(".scheduler-crafting-header");
688
- if (craftHdr) craftHdr.remove();
689
- }
690
-
691
- // If we were in log mode, switch back to the original session
692
- if (logPreviousSessionId) {
693
- send({ type: "switch_session", id: logPreviousSessionId });
694
- logPreviousSessionId = null;
695
- }
696
- }
697
-
698
- // --- Navigation ---
699
-
700
- function navigate(dir) {
701
- if (currentView === "month") {
702
- viewDate.setMonth(viewDate.getMonth() + dir);
703
- } else {
704
- viewDate.setDate(viewDate.getDate() + dir * 7);
705
- }
706
- render();
707
- }
708
-
709
- // --- Render ---
710
-
711
- function render() {
712
- if (!bodyEl) return;
713
- updateMonthLabel();
714
- if (currentView === "month") {
715
- renderMonthView();
716
- } else {
717
- renderWeekView();
718
- }
719
- }
720
-
721
- function updateMonthLabel() {
722
- if (!monthLabel) return;
723
- if (currentView === "month") {
724
- monthLabel.textContent = MONTH_NAMES[viewDate.getMonth()] + " " + viewDate.getFullYear();
725
- } else {
726
- var weekStart = getWeekStart(viewDate);
727
- var weekEnd = new Date(weekStart);
728
- weekEnd.setDate(weekEnd.getDate() + 6);
729
- monthLabel.textContent = MONTH_NAMES[weekStart.getMonth()].substring(0, 3) + " " + weekStart.getDate() + " – " + MONTH_NAMES[weekEnd.getMonth()].substring(0, 3) + " " + weekEnd.getDate() + ", " + weekEnd.getFullYear();
730
- }
731
- }
732
-
733
- // --- Month View ---
734
-
735
- function renderMonthView() {
736
- var year = viewDate.getFullYear();
737
- var month = viewDate.getMonth();
738
- var today = new Date();
739
- var todayStr = today.getFullYear() + "-" + pad(today.getMonth() + 1) + "-" + pad(today.getDate());
740
-
741
- var firstDay = new Date(year, month, 1);
742
- var startDay = new Date(firstDay);
743
- startDay.setDate(startDay.getDate() - firstDay.getDay());
744
-
745
- var html = '<div class="scheduler-weekdays">';
746
- html += '<div class="scheduler-weekday scheduler-week-num-hdr"></div>';
747
- for (var d = 0; d < 7; d++) {
748
- var wkdCls = "scheduler-weekday" + (d === 0 || d === 6 ? " weekend" : "");
749
- html += '<div class="' + wkdCls + '">' + DAY_NAMES[d] + '</div>';
750
- }
751
- html += '</div><div class="scheduler-grid">';
752
-
753
- var cursor = new Date(startDay);
754
- for (var w = 0; w < 6; w++) {
755
- // Week number label
756
- var wn = getISOWeekNumber(cursor);
757
- html += '<div class="scheduler-week-num">W' + wn + '</div>';
758
- for (var d = 0; d < 7; d++) {
759
- var dateStr = cursor.getFullYear() + "-" + pad(cursor.getMonth() + 1) + "-" + pad(cursor.getDate());
760
- var isOther = cursor.getMonth() !== month;
761
- var isToday = dateStr === todayStr;
762
- var isWeekend = d === 0 || d === 6;
763
- var cls = "scheduler-cell" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (isWeekend ? " weekend" : "");
764
- html += '<div class="' + cls + '">';
765
- var dayLabel = cursor.getDate() === 1
766
- ? MONTH_NAMES[cursor.getMonth()].substring(0, 3) + ", " + cursor.getDate()
767
- : String(cursor.getDate());
768
- html += '<div class="scheduler-day-num">' + dayLabel + '</div>';
769
- var events = getEventsForDate(cursor);
770
- for (var e = 0; e < events.length && e < 3; e++) {
771
- var ev = events[e];
772
- html += '<div class="scheduler-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '">';
773
- html += '<span class="scheduler-event-time">' + ev.timeStr + '</span> ' + esc(ev.name);
774
- html += '</div>';
775
- }
776
- if (events.length > 3) {
777
- html += '<div class="scheduler-event" style="opacity:0.6;font-size:10px">+' + (events.length - 3) + ' more</div>';
778
- }
779
- html += '</div>';
780
- cursor.setDate(cursor.getDate() + 1);
781
- }
782
- }
783
- html += '</div>';
784
- bodyEl.innerHTML = html;
785
- attachEventClicks(bodyEl, ".scheduler-event[data-rec-id]");
786
- }
787
-
788
- // --- Week View ---
789
-
790
- function renderWeekView() {
791
- var weekStart = getWeekStart(viewDate);
792
- var today = new Date();
793
- var todayStr = today.getFullYear() + "-" + pad(today.getMonth() + 1) + "-" + pad(today.getDate());
794
-
795
- var html = '<div class="scheduler-week-header"><div></div>';
796
- for (var d = 0; d < 7; d++) {
797
- var day = new Date(weekStart);
798
- day.setDate(day.getDate() + d);
799
- var dateStr = day.getFullYear() + "-" + pad(day.getMonth() + 1) + "-" + pad(day.getDate());
800
- html += '<div class="scheduler-week-header-cell' + (dateStr === todayStr ? ' today' : '') + '">';
801
- html += '<div class="wday">' + DAY_NAMES[d].toUpperCase() + '</div>';
802
- html += '<div class="wdate">' + day.getDate() + '</div></div>';
803
- }
804
- html += '</div><div class="scheduler-week-view">';
805
- html += '<div class="scheduler-week-time-col">';
806
- for (var h = 0; h < 24; h++) {
807
- html += '<div class="scheduler-week-time-label">' + (h === 0 ? "" : pad(h) + ":00") + '</div>';
808
- }
809
- html += '</div>';
810
- for (var d = 0; d < 7; d++) {
811
- var day = new Date(weekStart);
812
- day.setDate(day.getDate() + d);
813
- html += '<div class="scheduler-week-day-col">';
814
- for (var h = 0; h < 24; h++) {
815
- html += '<div class="scheduler-week-slot"></div>';
816
- }
817
- var events = getEventsForDate(day);
818
- for (var e = 0; e < events.length; e++) {
819
- var ev = events[e];
820
- var topPx = ev.hour * 48 + (ev.minute / 60) * 48;
821
- html += '<div class="scheduler-week-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" style="top:' + topPx + 'px;height:24px">';
822
- html += ev.timeStr + " " + esc(ev.name) + '</div>';
823
- }
824
- html += '</div>';
825
- }
826
- html += '</div>';
827
- bodyEl.innerHTML = html;
828
-
829
- var weekView = bodyEl.querySelector(".scheduler-week-view");
830
- if (weekView) weekView.scrollTop = Math.max(0, today.getHours() - 2) * 48;
831
- attachEventClicks(bodyEl, ".scheduler-week-event[data-rec-id]");
832
- }
833
-
834
- // --- Events for calendar ---
835
-
836
- function getEventsForDate(date) {
837
- var results = [];
838
- var dow = date.getDay();
839
- var dom = date.getDate();
840
- var month = date.getMonth() + 1;
841
-
842
- for (var i = 0; i < records.length; i++) {
843
- var r = records[i];
844
- if (!r.cron) continue; // only scheduled
845
- var parsed = parseCronSimple(r.cron);
846
- if (!parsed) continue;
847
- if (parsed.months.indexOf(month) === -1) continue;
848
- if (parsed.daysOfMonth.indexOf(dom) === -1) continue;
849
- if (parsed.daysOfWeek.indexOf(dow) === -1) continue;
850
- for (var h = 0; h < parsed.hours.length; h++) {
851
- for (var m = 0; m < parsed.minutes.length; m++) {
852
- results.push({
853
- id: r.id, name: r.name, enabled: r.enabled,
854
- hour: parsed.hours[h], minute: parsed.minutes[m],
855
- timeStr: pad(parsed.hours[h]) + ":" + pad(parsed.minutes[m]),
856
- });
857
- }
858
- }
859
- }
860
- results.sort(function (a, b) { return a.hour * 60 + a.minute - (b.hour * 60 + b.minute); });
861
- return results;
862
- }
863
-
864
- // --- Popover ---
865
-
866
- function showPopover(recId, anchorEl) {
867
- var rec = null;
868
- for (var i = 0; i < records.length; i++) {
869
- if (records[i].id === recId) { rec = records[i]; break; }
870
- }
871
- if (!rec || !popoverEl) return;
872
-
873
- var nextStr = rec.nextRunAt ? formatDateTime(new Date(rec.nextRunAt)) : "—";
874
- var lastStr = rec.lastRunAt ? formatDateTime(new Date(rec.lastRunAt)) : "Never";
875
-
876
- var html = '<div class="schedule-popover-name">' + esc(rec.name) + '</div>';
877
- html += '<div class="schedule-popover-meta">Next: <strong>' + nextStr + '</strong></div>';
878
- html += '<div class="schedule-popover-meta">Last: <strong>' + lastStr + '</strong></div>';
879
- if (rec.lastRunResult) {
880
- html += '<div class="schedule-popover-result ' + (rec.lastRunResult === "pass" ? "pass" : "fail") + '">' + rec.lastRunResult + '</div>';
881
- }
882
- html += '<div class="schedule-popover-meta">' + cronToHuman(rec.cron) + '</div>';
883
- html += '<div class="schedule-popover-actions">';
884
- html += '<button class="schedule-popover-btn" data-action="edit" data-id="' + rec.id + '">Edit</button>';
885
- html += '<button class="schedule-popover-btn" data-action="toggle" data-id="' + rec.id + '">' + (rec.enabled ? "Pause" : "Enable") + '</button>';
886
- html += '<button class="schedule-popover-btn" data-action="rerun" data-id="' + rec.id + '">Re-run</button>';
887
- html += '<button class="schedule-popover-btn danger" data-action="delete" data-id="' + rec.id + '">Delete</button>';
888
- html += '</div>';
889
-
890
- popoverEl.innerHTML = html;
891
- popoverEl.classList.remove("hidden");
892
-
893
- var rect = anchorEl.getBoundingClientRect();
894
- var left = Math.max(8, Math.min(rect.left, window.innerWidth - 268));
895
- var top = rect.bottom + 6;
896
- if (top + 200 > window.innerHeight) top = rect.top - 200;
897
- popoverEl.style.left = left + "px";
898
- popoverEl.style.top = top + "px";
899
-
900
- var btns = popoverEl.querySelectorAll(".schedule-popover-btn");
901
- for (var i = 0; i < btns.length; i++) {
902
- (function (btn) {
903
- btn.addEventListener("click", function (e) {
904
- e.stopPropagation();
905
- var action = btn.dataset.action;
906
- var id = btn.dataset.id;
907
- popoverEl.classList.add("hidden");
908
- if (action === "edit") openEditModal(id);
909
- else if (action === "toggle") send({ type: "loop_registry_toggle", id: id });
910
- else if (action === "rerun") send({ type: "loop_registry_rerun", id: id });
911
- else if (action === "delete" && confirm("Delete this schedule?")) send({ type: "loop_registry_remove", id: id });
912
- });
913
- })(btns[i]);
914
- }
915
- }
916
-
917
- function attachEventClicks(container, selector) {
918
- var els = container.querySelectorAll(selector);
919
- for (var i = 0; i < els.length; i++) {
920
- (function (el) {
921
- el.addEventListener("click", function (e) {
922
- e.stopPropagation();
923
- selectedTaskId = el.dataset.recId;
924
- updateSidebarSelection();
925
- switchMode("detail");
926
- });
927
- })(els[i]);
928
- }
929
- }
930
-
931
- // --- Edit Modal (for changing cron/name on existing records) ---
932
-
933
- function setupEditModal() {
934
- if (!editModal) return;
935
- document.getElementById("schedule-edit-close").addEventListener("click", function () { closeEditModal(); });
936
- document.getElementById("sched-cancel").addEventListener("click", function () { closeEditModal(); });
937
- editModal.querySelector(".confirm-backdrop").addEventListener("click", function () { closeEditModal(); });
938
-
939
- // Presets
940
- var presetBtns = document.querySelectorAll("#sched-presets .sched-preset-btn");
941
- for (var i = 0; i < presetBtns.length; i++) {
942
- (function (btn) {
943
- btn.addEventListener("click", function () { selectPreset(btn.dataset.preset); });
944
- })(presetBtns[i]);
945
- }
946
-
947
- // DOW
948
- var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
949
- for (var i = 0; i < dowBtns.length; i++) {
950
- (function (btn) {
951
- btn.addEventListener("click", function () { btn.classList.toggle("active"); updateEditCronPreview(); });
952
- })(dowBtns[i]);
953
- }
954
-
955
- document.getElementById("sched-time").addEventListener("change", function () { updateEditCronPreview(); });
956
- document.getElementById("sched-save").addEventListener("click", function () { saveEdit(); });
957
- document.getElementById("sched-delete").addEventListener("click", function () {
958
- if (editingId && confirm("Delete this job?")) {
959
- send({ type: "loop_registry_remove", id: editingId });
960
- closeEditModal();
961
- }
962
- });
963
- }
964
-
965
- var editPreset = "daily";
966
-
967
- function selectPreset(preset) {
968
- editPreset = preset;
969
- var btns = document.querySelectorAll("#sched-presets .sched-preset-btn");
970
- for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i].dataset.preset === preset);
971
- var dowField = document.getElementById("sched-dow-field");
972
- if (dowField) dowField.style.display = (preset === "custom" || preset === "weekly") ? "" : "none";
973
- updateEditCronPreview();
974
- }
975
-
976
- function buildEditCron() {
977
- var timeVal = document.getElementById("sched-time").value || "09:00";
978
- var parts = timeVal.split(":");
979
- var h = parseInt(parts[0], 10);
980
- var m = parseInt(parts[1], 10);
981
- var dow = "*";
982
- if (editPreset === "weekdays") dow = "1-5";
983
- else if (editPreset === "weekly" || editPreset === "custom") {
984
- var days = [];
985
- var btns = document.querySelectorAll("#sched-dow-row .sched-dow-btn.active");
986
- for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
987
- if (days.length > 0 && days.length < 7) dow = days.sort().join(",");
988
- } else if (editPreset === "monthly") {
989
- return m + " " + h + " " + new Date().getDate() + " * *";
990
- }
991
- return m + " " + h + " * * " + dow;
992
- }
993
-
994
- function updateEditCronPreview() {
995
- var cron = buildEditCron();
996
- var humanEl = document.getElementById("sched-human-text");
997
- var cronEl = document.getElementById("sched-cron-text");
998
- if (humanEl) humanEl.textContent = cronToHuman(cron);
999
- if (cronEl) cronEl.textContent = cron;
1000
- }
1001
-
1002
- function openEditModal(recId) {
1003
- if (!editModal) return;
1004
- editingId = recId;
1005
- var rec = null;
1006
- for (var i = 0; i < records.length; i++) {
1007
- if (records[i].id === recId) { rec = records[i]; break; }
1008
- }
1009
- if (!rec) return;
1010
-
1011
- document.getElementById("schedule-edit-title").textContent = "Edit Schedule";
1012
- document.getElementById("sched-name").value = rec.name || "";
1013
- document.getElementById("sched-enabled").checked = rec.enabled;
1014
- document.getElementById("sched-delete").style.display = "";
1015
-
1016
- // Show job name
1017
- var jobNameEl = document.getElementById("sched-job-name");
1018
- if (jobNameEl) jobNameEl.textContent = rec.task ? rec.task.substring(0, 80) : rec.id;
1019
-
1020
- // History
1021
- var historyField = document.getElementById("sched-history-field");
1022
- if (rec.runs && rec.runs.length > 0) {
1023
- if (historyField) historyField.style.display = "";
1024
- renderHistory(rec.runs);
1025
- } else {
1026
- if (historyField) historyField.style.display = "none";
1027
- }
1028
-
1029
- // Parse cron
1030
- if (rec.cron) {
1031
- var parsed = parseCronSimple(rec.cron);
1032
- if (parsed) {
1033
- document.getElementById("sched-time").value = pad(parsed.hours[0] || 9) + ":" + pad(parsed.minutes[0] || 0);
1034
- var dowArr = parsed.daysOfWeek;
1035
- if (dowArr.length === 7) selectPreset("daily");
1036
- else if (dowArr.length === 5 && dowArr[0] === 1 && dowArr[4] === 5) selectPreset("weekdays");
1037
- else {
1038
- selectPreset("custom");
1039
- var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
1040
- for (var j = 0; j < dowBtns.length; j++) {
1041
- dowBtns[j].classList.toggle("active", dowArr.indexOf(parseInt(dowBtns[j].dataset.dow)) !== -1);
1042
- }
1043
- }
1044
- }
1045
- } else {
1046
- document.getElementById("sched-time").value = "09:00";
1047
- selectPreset("daily");
1048
- }
1049
-
1050
- updateEditCronPreview();
1051
- editModal.classList.remove("hidden");
1052
- }
1053
-
1054
- function closeEditModal() {
1055
- if (editModal) editModal.classList.add("hidden");
1056
- editingId = null;
1057
- }
1058
-
1059
- function saveEdit() {
1060
- var name = document.getElementById("sched-name").value.trim();
1061
- var enabled = document.getElementById("sched-enabled").checked;
1062
- var cron = buildEditCron();
1063
- if (!name) { alert("Please enter a name."); return; }
1064
-
1065
- send({
1066
- type: "loop_registry_update",
1067
- id: editingId,
1068
- data: { name: name, cron: cron, enabled: enabled },
1069
- });
1070
- closeEditModal();
1071
- }
1072
-
1073
- function renderHistory(runs) {
1074
- var el = document.getElementById("sched-history");
1075
- if (!el || !runs || runs.length === 0) { if (el) el.innerHTML = '<div class="sched-history-empty">No runs yet</div>'; return; }
1076
- var html = "";
1077
- var sorted = runs.slice().reverse();
1078
- for (var i = 0; i < sorted.length; i++) {
1079
- var run = sorted[i];
1080
- html += '<div class="sched-history-item"><span class="sched-history-dot ' + (run.result || "") + '"></span>';
1081
- html += '<span class="sched-history-date">' + formatDateTime(new Date(run.startedAt)) + '</span>';
1082
- html += '<span class="sched-history-result">' + (run.result || "?") + '</span>';
1083
- html += '<span class="sched-history-iterations">' + (run.iterations || 0) + ' iter</span></div>';
1084
- }
1085
- el.innerHTML = html;
1086
- }
1087
-
1088
- // --- Public API ---
1089
-
1090
- export function openSchedulerToTab(tab) {
1091
- if (!panelOpen) openScheduler();
1092
- if (tab === "library" || tab === "tasks") {
1093
- // Just open, sidebar already shows tasks
1094
- } else {
1095
- switchMode("calendar");
1096
- }
1097
- }
1098
-
1099
- export function isSchedulerOpen() {
1100
- return panelOpen;
1101
- }
1102
-
1103
- export function enterCraftingMode(sessionId, taskId) {
1104
- craftingSessionId = sessionId || null;
1105
- craftingTaskId = taskId || null;
1106
- if (!panelOpen) openScheduler();
1107
- if (taskId) {
1108
- selectedTaskId = taskId;
1109
- renderSidebar();
1110
- }
1111
- switchMode("crafting");
1112
- }
1113
-
1114
- export function exitCraftingMode(taskId) {
1115
- if (!panelOpen || currentMode !== "crafting") return;
1116
- craftingTaskId = null;
1117
- if (taskId) {
1118
- selectedTaskId = taskId;
1119
- switchMode("detail");
1120
- renderSidebar();
1121
- } else {
1122
- switchMode("calendar");
1123
- }
1124
- }
1125
-
1126
- // --- Message handlers ---
1127
-
1128
- export function handleLoopRegistryUpdated(msg) {
1129
- records = msg.records || [];
1130
- if (panelOpen) {
1131
- renderSidebar();
1132
- if (currentMode === "calendar") render();
1133
- else if (currentMode === "detail") renderDetail();
1134
- }
1135
- }
1136
-
1137
- export function handleLoopRegistryFiles(msg) {
1138
- if (!panelOpen || currentMode !== "detail") return;
1139
- if (msg.id !== selectedTaskId) return;
1140
- var bodyEl2 = document.getElementById("scheduler-detail-body");
1141
- if (!bodyEl2) return;
1142
- var activeTab = contentDetailEl ? contentDetailEl.querySelector(".scheduler-detail-tab.active") : null;
1143
- var tab = activeTab ? activeTab.dataset.tab : "prompt";
1144
- if (tab === "prompt") {
1145
- bodyEl2.innerHTML = msg.prompt ? '<div class="md-content">' + renderMarkdown(msg.prompt) + '</div>' : '<div class="scheduler-empty">No PROMPT.md found</div>';
1146
- } else if (tab === "judge") {
1147
- bodyEl2.innerHTML = msg.judge ? '<div class="md-content">' + renderMarkdown(msg.judge) + '</div>' : '<div class="scheduler-empty">No JUDGE.md found</div>';
1148
- }
1149
- }
1150
-
1151
- export function handleScheduleRunStarted(msg) {
1152
- if (panelOpen) render();
1153
- }
1154
-
1155
- export function handleScheduleRunFinished(msg) {
1156
- send({ type: "loop_registry_list" });
1157
- }
1158
-
1159
- export function handleLoopScheduled(msg) {
1160
- // A loop was just registered as scheduled (from approval bar)
1161
- send({ type: "loop_registry_list" });
1162
- }
1163
-
1164
- // --- Cron parser (client-side) ---
1165
-
1166
- function parseCronSimple(expr) {
1167
- if (!expr) return null;
1168
- var fields = expr.trim().split(/\s+/);
1169
- if (fields.length !== 5) return null;
1170
- return {
1171
- minutes: parseField(fields[0], 0, 59),
1172
- hours: parseField(fields[1], 0, 23),
1173
- daysOfMonth: parseField(fields[2], 1, 31),
1174
- months: parseField(fields[3], 1, 12),
1175
- daysOfWeek: parseField(fields[4], 0, 6),
1176
- };
1177
- }
1178
-
1179
- function parseField(field, min, max) {
1180
- var values = [];
1181
- var parts = field.split(",");
1182
- for (var i = 0; i < parts.length; i++) {
1183
- var part = parts[i].trim();
1184
- if (part.indexOf("/") !== -1) {
1185
- var sp = part.split("/");
1186
- var step = parseInt(sp[1], 10);
1187
- var rMin = min, rMax = max;
1188
- if (sp[0] !== "*") { var rp = sp[0].split("-"); rMin = parseInt(rp[0], 10); rMax = rp.length > 1 ? parseInt(rp[1], 10) : rMin; }
1189
- for (var v = rMin; v <= rMax; v += step) values.push(v);
1190
- } else if (part === "*") {
1191
- for (var v = min; v <= max; v++) values.push(v);
1192
- } else if (part.indexOf("-") !== -1) {
1193
- var rp = part.split("-");
1194
- for (var v = parseInt(rp[0], 10); v <= parseInt(rp[1], 10); v++) values.push(v);
1195
- } else {
1196
- values.push(parseInt(part, 10));
1197
- }
1198
- }
1199
- return values;
1200
- }
1201
-
1202
- // --- Utility ---
1203
-
1204
- function getISOWeekNumber(date) {
1205
- var d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
1206
- var dayNum = d.getUTCDay() || 7;
1207
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
1208
- var yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
1209
- return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
1210
- }
1211
-
1212
- function getWeekStart(date) {
1213
- var d = new Date(date);
1214
- d.setDate(d.getDate() - d.getDay());
1215
- d.setHours(0, 0, 0, 0);
1216
- return d;
1217
- }
1218
-
1219
- function pad(n) { return n < 10 ? "0" + n : String(n); }
1220
- function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); }
1221
-
1222
- function formatDateTime(d) {
1223
- return MONTH_NAMES[d.getMonth()].substring(0, 3) + " " + d.getDate() + ", " + pad(d.getHours()) + ":" + pad(d.getMinutes());
1224
- }
1225
-
1226
- function cronToHuman(cron) {
1227
- if (!cron) return "";
1228
- var parts = cron.trim().split(/\s+/);
1229
- if (parts.length !== 5) return cron;
1230
- var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
1231
- var dow = parts[4], dom = parts[2];
1232
- if (dow === "*" && dom === "*") return "Every day at " + t;
1233
- if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
1234
- if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
1235
- if (dow !== "*" && dom === "*") {
1236
- var ds = dow.split(",").map(function (d) { return DAY_NAMES[parseInt(d, 10)] || d; });
1237
- return "Every " + ds.join(", ") + " at " + t;
1238
- }
1239
- return cron;
1240
- }