clay-server 2.27.0-beta.8 → 2.27.0

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 (72) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-debate.js +19 -12
  10. package/lib/project-http.js +4 -2
  11. package/lib/project-loop.js +110 -48
  12. package/lib/project-mate-interaction.js +4 -0
  13. package/lib/project-notifications.js +210 -0
  14. package/lib/project-sessions.js +5 -2
  15. package/lib/project-user-message.js +2 -1
  16. package/lib/project.js +26 -2
  17. package/lib/public/app.js +1193 -8521
  18. package/lib/public/css/command-palette.css +14 -0
  19. package/lib/public/css/loop.css +301 -0
  20. package/lib/public/css/notifications-center.css +190 -0
  21. package/lib/public/css/rewind.css +6 -0
  22. package/lib/public/index.html +89 -35
  23. package/lib/public/modules/app-connection.js +160 -0
  24. package/lib/public/modules/app-cursors.js +473 -0
  25. package/lib/public/modules/app-debate-ui.js +389 -0
  26. package/lib/public/modules/app-dm.js +627 -0
  27. package/lib/public/modules/app-favicon.js +212 -0
  28. package/lib/public/modules/app-header.js +229 -0
  29. package/lib/public/modules/app-home-hub.js +600 -0
  30. package/lib/public/modules/app-loop-ui.js +589 -0
  31. package/lib/public/modules/app-loop-wizard.js +439 -0
  32. package/lib/public/modules/app-messages.js +1560 -0
  33. package/lib/public/modules/app-misc.js +299 -0
  34. package/lib/public/modules/app-notifications.js +372 -0
  35. package/lib/public/modules/app-panels.js +888 -0
  36. package/lib/public/modules/app-projects.js +798 -0
  37. package/lib/public/modules/app-rate-limit.js +451 -0
  38. package/lib/public/modules/app-rendering.js +597 -0
  39. package/lib/public/modules/app-skills-install.js +234 -0
  40. package/lib/public/modules/command-palette.js +27 -4
  41. package/lib/public/modules/input.js +31 -20
  42. package/lib/public/modules/scheduler-config.js +1532 -0
  43. package/lib/public/modules/scheduler-history.js +79 -0
  44. package/lib/public/modules/scheduler.js +33 -1554
  45. package/lib/public/modules/session-search.js +13 -1
  46. package/lib/public/modules/sidebar-mates.js +812 -0
  47. package/lib/public/modules/sidebar-mobile.js +1269 -0
  48. package/lib/public/modules/sidebar-projects.js +1449 -0
  49. package/lib/public/modules/sidebar-sessions.js +986 -0
  50. package/lib/public/modules/sidebar.js +232 -4591
  51. package/lib/public/modules/store.js +27 -0
  52. package/lib/public/modules/ws-ref.js +7 -0
  53. package/lib/public/style.css +1 -0
  54. package/lib/sdk-bridge.js +96 -717
  55. package/lib/sdk-message-processor.js +587 -0
  56. package/lib/sdk-message-queue.js +42 -0
  57. package/lib/sdk-skill-discovery.js +131 -0
  58. package/lib/server-admin.js +712 -0
  59. package/lib/server-auth.js +737 -0
  60. package/lib/server-dm.js +221 -0
  61. package/lib/server-mates.js +281 -0
  62. package/lib/server-palette.js +110 -0
  63. package/lib/server-settings.js +479 -0
  64. package/lib/server-skills.js +280 -0
  65. package/lib/server.js +246 -2755
  66. package/lib/sessions.js +11 -4
  67. package/lib/users-auth.js +146 -0
  68. package/lib/users-permissions.js +118 -0
  69. package/lib/users-preferences.js +210 -0
  70. package/lib/users.js +48 -398
  71. package/lib/ws-schema.js +498 -0
  72. package/package.json +1 -1
@@ -0,0 +1,1532 @@
1
+ /**
2
+ * Scheduler config module — Create/edit modal, delete dialog, cron builder, preview.
3
+ *
4
+ * Extracted from scheduler.js to keep module sizes manageable.
5
+ */
6
+
7
+ import { showToast } from './utils.js';
8
+
9
+ // Constants (duplicated from scheduler.js — small arrays)
10
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
11
+ var MONTH_NAMES = [
12
+ "January", "February", "March", "April", "May", "June",
13
+ "July", "August", "September", "October", "November", "December"
14
+ ];
15
+
16
+ // Module-private state
17
+ var configCtx = null;
18
+ var previewEl = null;
19
+ var createEditingRecId = null;
20
+ var createSelectedDate = null;
21
+ var createRecurrence = "none";
22
+ var createCustomConfirmed = false;
23
+ var createInterval = "none";
24
+ var createIntervalCustom = null;
25
+ var createIntervalEnd = "allday";
26
+ var createIntervalEndAfter = 5;
27
+ var createIntervalEndTime = "";
28
+ var createColor = "#ffb86c";
29
+ var createEndType = "never";
30
+ var createEndDate = null;
31
+ var createEndCalMonth = null;
32
+ var createEndAfter = 10;
33
+
34
+ // --- Init ---
35
+
36
+ export function initSchedulerConfig(_configCtx) {
37
+ configCtx = _configCtx;
38
+ }
39
+
40
+ // --- Create Popover (inline, Akiflow-style) ---
41
+
42
+ export function setupCreateModal() {
43
+ var createPopover = configCtx.getCreatePopover();
44
+ if (!createPopover) return;
45
+
46
+ // Close
47
+ document.getElementById("sched-create-cancel").addEventListener("click", function () { closeCreateModal(); });
48
+
49
+ // Color picker
50
+ var colorBtn = document.getElementById("sched-create-color-btn");
51
+ var colorPalette = document.getElementById("sched-create-color-palette");
52
+ if (colorBtn && colorPalette) {
53
+ colorBtn.addEventListener("click", function (e) {
54
+ e.stopPropagation();
55
+ colorPalette.classList.toggle("hidden");
56
+ });
57
+ var swatches = colorPalette.querySelectorAll(".sched-color-swatch");
58
+ for (var i = 0; i < swatches.length; i++) {
59
+ swatches[i].addEventListener("click", function (e) {
60
+ e.stopPropagation();
61
+ var c = this.dataset.color;
62
+ createColor = c;
63
+ var dot = document.getElementById("sched-create-color-dot");
64
+ if (dot) dot.style.background = c;
65
+ // update active state
66
+ var all = colorPalette.querySelectorAll(".sched-color-swatch");
67
+ for (var j = 0; j < all.length; j++) {
68
+ all[j].classList.toggle("active", all[j].dataset.color === c);
69
+ }
70
+ colorPalette.classList.add("hidden");
71
+ });
72
+ }
73
+ }
74
+
75
+ // Date picker change -> sync createSelectedDate and recurrence labels
76
+ var datePickerEl = document.getElementById("sched-create-date-picker");
77
+ if (datePickerEl) {
78
+ datePickerEl.addEventListener("change", function () {
79
+ var parts = this.value.split("-");
80
+ if (parts.length === 3) {
81
+ createSelectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
82
+ document.getElementById("sched-create-date").value = this.value;
83
+ updateRecurrenceLabels(createSelectedDate);
84
+ enforceMinTime();
85
+ }
86
+ });
87
+ }
88
+
89
+ // Time input change -- enforce min time when today is selected
90
+ var timeInputEl = document.getElementById("sched-create-time");
91
+ if (timeInputEl) {
92
+ timeInputEl.addEventListener("blur", function () {
93
+ enforceMinTime();
94
+ });
95
+ }
96
+
97
+ // Task dropdown
98
+ var taskBtn = document.getElementById("sched-create-task-btn");
99
+ var taskList = document.getElementById("sched-create-task-list");
100
+ if (taskBtn && taskList) {
101
+ taskBtn.addEventListener("click", function (e) {
102
+ e.stopPropagation();
103
+ taskList.classList.toggle("hidden");
104
+ });
105
+ }
106
+
107
+ // Close task dropdown on outside click
108
+ document.addEventListener("click", function (e) {
109
+ var tl = document.getElementById("sched-create-task-list");
110
+ if (tl && !tl.classList.contains("hidden")) {
111
+ if (!tl.contains(e.target) && !e.target.closest("#sched-create-task-btn")) {
112
+ tl.classList.add("hidden");
113
+ }
114
+ }
115
+ });
116
+
117
+ // Recurrence button -> toggle dropdown
118
+ document.getElementById("sched-create-recurrence-btn").addEventListener("click", function (e) {
119
+ e.stopPropagation();
120
+ var dd = document.getElementById("sched-create-recurrence-dropdown");
121
+ var btn = document.getElementById("sched-create-recurrence-btn");
122
+ // Close interval dropdown if open
123
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
124
+ if (dd) {
125
+ var wasHidden = dd.classList.contains("hidden");
126
+ dd.classList.toggle("hidden");
127
+ if (wasHidden && btn) {
128
+ var bRect = btn.getBoundingClientRect();
129
+ var ddW = 280;
130
+ var ddLeft = bRect.left;
131
+ if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
132
+ if (ddLeft < 10) ddLeft = 10;
133
+ dd.style.left = ddLeft + "px";
134
+ dd.style.top = (bRect.bottom + 4) + "px";
135
+ }
136
+ }
137
+ });
138
+
139
+ // --- Interval button + dropdown ---
140
+ var intervalSnapshot = null;
141
+ document.getElementById("sched-create-interval-btn").addEventListener("click", function (e) {
142
+ e.stopPropagation();
143
+ var dd = document.getElementById("sched-create-interval-dropdown");
144
+ var btn = document.getElementById("sched-create-interval-btn");
145
+ // Close recurrence dropdown if open
146
+ document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
147
+ if (dd) {
148
+ var wasHidden = dd.classList.contains("hidden");
149
+ dd.classList.toggle("hidden");
150
+ if (wasHidden && btn) {
151
+ // Save snapshot for cancel
152
+ intervalSnapshot = {
153
+ interval: createInterval,
154
+ custom: createIntervalCustom ? { value: createIntervalCustom.value, unit: createIntervalCustom.unit } : null,
155
+ end: createIntervalEnd,
156
+ endAfter: createIntervalEndAfter,
157
+ endTime: createIntervalEndTime
158
+ };
159
+ var bRect = btn.getBoundingClientRect();
160
+ var ddW = 220;
161
+ var ddLeft = bRect.left;
162
+ if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
163
+ if (ddLeft < 10) ddLeft = 10;
164
+ dd.style.left = ddLeft + "px";
165
+ dd.style.top = (bRect.bottom + 4) + "px";
166
+ // Auto-apply custom interval when opening
167
+ if (createInterval === "none") {
168
+ applyInlineInterval();
169
+ }
170
+ }
171
+ }
172
+ });
173
+
174
+ // Interval Cancel button - revert to snapshot
175
+ document.getElementById("sched-interval-cancel").addEventListener("click", function (e) {
176
+ e.stopPropagation();
177
+ if (intervalSnapshot) {
178
+ createInterval = intervalSnapshot.interval;
179
+ createIntervalCustom = intervalSnapshot.custom;
180
+ createIntervalEnd = intervalSnapshot.end;
181
+ createIntervalEndAfter = intervalSnapshot.endAfter;
182
+ createIntervalEndTime = intervalSnapshot.endTime;
183
+ updateIntervalBtn();
184
+ }
185
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
186
+ });
187
+
188
+ // Interval OK button - accept and close
189
+ document.getElementById("sched-interval-ok").addEventListener("click", function (e) {
190
+ e.stopPropagation();
191
+ applyInlineInterval();
192
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
193
+ });
194
+
195
+ // Interval custom input
196
+ var intCustomValue = document.getElementById("sched-interval-custom-value");
197
+ var intUnitSegs = document.querySelectorAll(".sched-interval-seg");
198
+ function getIntervalUnit() {
199
+ for (var s = 0; s < intUnitSegs.length; s++) {
200
+ if (intUnitSegs[s].classList.contains("active")) return intUnitSegs[s].dataset.unit;
201
+ }
202
+ return "minute";
203
+ }
204
+ function applyInlineInterval() {
205
+ var v = parseInt(intCustomValue.value, 10) || 1;
206
+ var u = getIntervalUnit();
207
+ createInterval = "custom";
208
+ createIntervalCustom = { value: v, unit: u };
209
+ updateIntervalBtn();
210
+ }
211
+ intCustomValue.addEventListener("change", applyInlineInterval);
212
+ for (var si = 0; si < intUnitSegs.length; si++) {
213
+ (function (seg) {
214
+ seg.addEventListener("click", function (e) {
215
+ e.stopPropagation();
216
+ for (var s = 0; s < intUnitSegs.length; s++) {
217
+ intUnitSegs[s].classList.toggle("active", intUnitSegs[s] === seg);
218
+ }
219
+ applyInlineInterval();
220
+ });
221
+ })(intUnitSegs[si]);
222
+ }
223
+
224
+ // Interval end condition options
225
+ var iendOpts = document.querySelectorAll(".sched-interval-end-opt");
226
+ for (var ie = 0; ie < iendOpts.length; ie++) {
227
+ (function (opt) {
228
+ opt.addEventListener("click", function (e) {
229
+ e.stopPropagation();
230
+ var val = opt.dataset.iend;
231
+ for (var j = 0; j < iendOpts.length; j++) {
232
+ iendOpts[j].classList.toggle("active", iendOpts[j] === opt);
233
+ }
234
+ createIntervalEnd = val;
235
+ var afterRow = document.getElementById("sched-interval-end-after-row");
236
+ var untilRow = document.getElementById("sched-interval-end-until-row");
237
+ if (afterRow) afterRow.classList.toggle("hidden", val !== "after");
238
+ if (untilRow) untilRow.classList.toggle("hidden", val !== "until");
239
+ });
240
+ })(iendOpts[ie]);
241
+ }
242
+
243
+ var iendAfterInput = document.getElementById("sched-interval-end-after");
244
+ if (iendAfterInput) {
245
+ iendAfterInput.addEventListener("change", function () {
246
+ createIntervalEndAfter = parseInt(this.value, 10) || 5;
247
+ if (createIntervalEndAfter < 1) createIntervalEndAfter = 1;
248
+ });
249
+ }
250
+
251
+ var iendTimeInput = document.getElementById("sched-interval-end-time");
252
+ if (iendTimeInput) {
253
+ iendTimeInput.addEventListener("change", function () {
254
+ createIntervalEndTime = this.value;
255
+ });
256
+ }
257
+
258
+ // Custom repeat: cancel
259
+ document.getElementById("sched-custom-cancel").addEventListener("click", function (e) {
260
+ e.stopPropagation();
261
+ document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
262
+ });
263
+
264
+ // Custom repeat: unit change
265
+ document.getElementById("sched-custom-unit").addEventListener("change", function () {
266
+ var dowSection = document.getElementById("sched-custom-dow-section");
267
+ if (dowSection) dowSection.style.display = this.value === "week" ? "" : "none";
268
+ });
269
+
270
+ // Custom repeat: DOW toggle
271
+ var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
272
+ for (var i = 0; i < customDowBtns.length; i++) {
273
+ (function (btn) {
274
+ btn.addEventListener("click", function (e) { e.stopPropagation(); btn.classList.toggle("active"); });
275
+ })(customDowBtns[i]);
276
+ }
277
+
278
+ // Custom repeat: End type JS dropdown
279
+ var endBtn = document.getElementById("sched-custom-end-btn");
280
+ var endList = document.getElementById("sched-custom-end-list");
281
+
282
+ endBtn.addEventListener("click", function (e) {
283
+ e.stopPropagation();
284
+ if (endList.classList.contains("hidden")) {
285
+ var r = endBtn.getBoundingClientRect();
286
+ endList.style.left = r.left + "px";
287
+ endList.style.top = (r.bottom + 4) + "px";
288
+ // If it would overflow bottom, show above
289
+ endList.classList.remove("hidden");
290
+ var lr = endList.getBoundingClientRect();
291
+ if (lr.bottom > window.innerHeight - 8) {
292
+ endList.style.top = (r.top - lr.height - 4) + "px";
293
+ }
294
+ } else {
295
+ endList.classList.add("hidden");
296
+ }
297
+ });
298
+
299
+ var endItems = endList.querySelectorAll(".sched-custom-end-item");
300
+ for (var ei = 0; ei < endItems.length; ei++) {
301
+ (function (item) {
302
+ item.addEventListener("click", function (e) {
303
+ e.stopPropagation();
304
+ var val = item.dataset.value;
305
+ createEndType = val;
306
+ document.getElementById("sched-custom-end").value = val;
307
+ document.getElementById("sched-custom-end-label").textContent = item.textContent;
308
+
309
+ // Update active state
310
+ for (var j = 0; j < endItems.length; j++) {
311
+ endItems[j].classList.toggle("active", endItems[j] === item);
312
+ }
313
+ endList.classList.add("hidden");
314
+
315
+ // Toggle conditional inputs
316
+ var dateBtn2 = document.getElementById("sched-custom-end-date-btn");
317
+ var afterWrap = document.getElementById("sched-custom-end-after-wrap");
318
+ var calPanel = document.getElementById("sched-custom-end-calendar");
319
+
320
+ dateBtn2.classList.add("hidden");
321
+ afterWrap.classList.add("hidden");
322
+ calPanel.classList.add("hidden");
323
+
324
+ if (val === "until") {
325
+ dateBtn2.classList.remove("hidden");
326
+ if (!createEndDate) {
327
+ createEndDate = new Date(createSelectedDate || new Date());
328
+ createEndDate.setMonth(createEndDate.getMonth() + 1);
329
+ }
330
+ updateEndDateLabel();
331
+ } else if (val === "after") {
332
+ afterWrap.classList.remove("hidden");
333
+ document.getElementById("sched-custom-end-after").value = createEndAfter;
334
+ }
335
+ });
336
+ })(endItems[ei]);
337
+ }
338
+
339
+ // Close end dropdown on outside click
340
+ document.addEventListener("click", function (e) {
341
+ if (endList && !endList.classList.contains("hidden")) {
342
+ if (!endList.contains(e.target) && !endBtn.contains(e.target)) {
343
+ endList.classList.add("hidden");
344
+ }
345
+ }
346
+ });
347
+
348
+ // Custom repeat: End date button -> toggle inline calendar
349
+ document.getElementById("sched-custom-end-date-btn").addEventListener("click", function (e) {
350
+ e.stopPropagation();
351
+ var calPanel = document.getElementById("sched-custom-end-calendar");
352
+ if (calPanel.classList.contains("hidden")) {
353
+ createEndCalMonth = new Date(createEndDate.getFullYear(), createEndDate.getMonth(), 1);
354
+ renderEndCalendar();
355
+ calPanel.classList.remove("hidden");
356
+ try { lucide.createIcons({ node: calPanel }); } catch (ex) {}
357
+ } else {
358
+ calPanel.classList.add("hidden");
359
+ }
360
+ });
361
+
362
+ // Custom repeat: End calendar prev/next
363
+ document.getElementById("sched-cal-prev").addEventListener("click", function (e) {
364
+ e.stopPropagation();
365
+ createEndCalMonth.setMonth(createEndCalMonth.getMonth() - 1);
366
+ renderEndCalendar();
367
+ });
368
+ document.getElementById("sched-cal-next").addEventListener("click", function (e) {
369
+ e.stopPropagation();
370
+ createEndCalMonth.setMonth(createEndCalMonth.getMonth() + 1);
371
+ renderEndCalendar();
372
+ });
373
+
374
+ // Custom repeat: After occurrences input
375
+ document.getElementById("sched-custom-end-after").addEventListener("change", function () {
376
+ createEndAfter = parseInt(this.value, 10) || 10;
377
+ if (createEndAfter < 1) { createEndAfter = 1; this.value = 1; }
378
+ });
379
+
380
+ // Custom repeat: OK
381
+ document.getElementById("sched-custom-ok").addEventListener("click", function (e) {
382
+ e.stopPropagation();
383
+ createRecurrence = "custom";
384
+ createCustomConfirmed = true;
385
+ document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
386
+ updateRecurrenceBtn();
387
+ });
388
+
389
+ // Run mode toggle (single vs multi-round)
390
+ var runModeContainer = createPopover.querySelector(".sched-create-run-mode");
391
+ if (runModeContainer) {
392
+ runModeContainer.addEventListener("click", function (e) {
393
+ var btn = e.target.closest(".sched-run-mode-btn");
394
+ if (!btn) return;
395
+ var mode = btn.dataset.mode;
396
+ var btns = runModeContainer.querySelectorAll(".sched-run-mode-btn");
397
+ for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i] === btn);
398
+ var iterGroup = document.getElementById("sched-create-iter-group");
399
+ if (iterGroup) iterGroup.classList.toggle("hidden", mode !== "multi");
400
+ });
401
+ }
402
+
403
+ // Submit
404
+ document.getElementById("sched-create-submit").addEventListener("click", function () { submitCreateSchedule(); });
405
+
406
+ // Delete button -> close popover, then open dialog
407
+ var deleteBtn = document.getElementById("sched-create-delete");
408
+ var deleteDialog = document.getElementById("sched-delete-dialog");
409
+ if (deleteBtn) {
410
+ deleteBtn.addEventListener("click", function (e) {
411
+ e.stopPropagation();
412
+ if (!createEditingRecId) return;
413
+ var records = configCtx.getRecords();
414
+ var rec = null;
415
+ for (var j = 0; j < records.length; j++) {
416
+ if (records[j].id === createEditingRecId) { rec = records[j]; break; }
417
+ }
418
+ if (!rec) return;
419
+ // Save context before closing popover
420
+ var deleteRecId = createEditingRecId;
421
+ var deleteDate = createSelectedDate ? new Date(createSelectedDate) : null;
422
+ closeCreateModal();
423
+ openDeleteDialog(deleteRecId, deleteDate, !rec.cron);
424
+ });
425
+ }
426
+
427
+ // Delete dialog option handlers
428
+ if (deleteDialog) {
429
+ var deleteOptions = deleteDialog.querySelectorAll(".sched-delete-option");
430
+ for (var i = 0; i < deleteOptions.length; i++) {
431
+ (function (opt) {
432
+ opt.addEventListener("click", function (e) {
433
+ e.stopPropagation();
434
+ var action = opt.dataset.delete;
435
+ if (action === "cancel") {
436
+ closeDeleteDialog();
437
+ return;
438
+ }
439
+ var recId = deleteDialog.dataset.recId;
440
+ var dateStr = deleteDialog.dataset.eventDate;
441
+ if (!recId) return;
442
+ if (action === "this") {
443
+ if (dateStr) {
444
+ var dp = dateStr.split("-");
445
+ var next = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
446
+ next.setDate(next.getDate() + 1);
447
+ var newDate = next.getFullYear() + "-" + configCtx.pad(next.getMonth() + 1) + "-" + configCtx.pad(next.getDate());
448
+ configCtx.send({ type: "loop_registry_update", id: recId, data: { date: newDate } });
449
+ }
450
+ } else if (action === "following") {
451
+ if (dateStr) {
452
+ var dp2 = dateStr.split("-");
453
+ var prev = new Date(parseInt(dp2[0], 10), parseInt(dp2[1], 10) - 1, parseInt(dp2[2], 10));
454
+ prev.setDate(prev.getDate() - 1);
455
+ var endDate = prev.getFullYear() + "-" + configCtx.pad(prev.getMonth() + 1) + "-" + configCtx.pad(prev.getDate());
456
+ configCtx.send({ type: "loop_registry_update", id: recId, data: { recurrenceEnd: { type: "until", date: endDate } } });
457
+ }
458
+ } else if (action === "all") {
459
+ configCtx.send({ type: "loop_registry_remove", id: recId });
460
+ }
461
+ closeDeleteDialog();
462
+ });
463
+ })(deleteOptions[i]);
464
+ }
465
+ // Close on backdrop click
466
+ deleteDialog.addEventListener("click", function (e) {
467
+ if (e.target === deleteDialog) closeDeleteDialog();
468
+ });
469
+ }
470
+
471
+ // Close color palette on any click outside it
472
+ document.addEventListener("click", function (e) {
473
+ var pal = document.getElementById("sched-create-color-palette");
474
+ if (pal && !pal.classList.contains("hidden")) {
475
+ if (!pal.contains(e.target) && !e.target.closest("#sched-create-color-btn")) {
476
+ pal.classList.add("hidden");
477
+ }
478
+ }
479
+ });
480
+
481
+ // Close popover on outside click
482
+ document.addEventListener("click", function (e) {
483
+ var cp = configCtx.getCreatePopover();
484
+ if (!cp || cp.classList.contains("hidden")) return;
485
+ if (cp.contains(e.target)) return;
486
+ // Also ignore clicks on calendar cells (they open the popover)
487
+ if (e.target.closest(".scheduler-cell") || e.target.closest(".scheduler-week-slot")) return;
488
+ closeCreateModal();
489
+ });
490
+
491
+ // Escape key
492
+ document.addEventListener("keydown", function (e) {
493
+ var cp = configCtx.getCreatePopover();
494
+ if (e.key === "Escape" && cp && !cp.classList.contains("hidden")) {
495
+ // Close recurrence dropdown first if open
496
+ var dd = document.getElementById("sched-create-recurrence-dropdown");
497
+ if (dd && !dd.classList.contains("hidden")) {
498
+ dd.classList.add("hidden");
499
+ return;
500
+ }
501
+ closeCreateModal();
502
+ }
503
+ });
504
+ }
505
+
506
+ function updateRecurrenceBtn() {
507
+ var btn = document.getElementById("sched-create-recurrence-btn");
508
+ if (btn) {
509
+ btn.classList.toggle("has-recurrence", createRecurrence !== "none");
510
+ }
511
+ }
512
+
513
+ /**
514
+ * When today is selected, set min on the time input so past times appear disabled.
515
+ * If the current time value is before now, bump it to the next quarter-hour.
516
+ */
517
+ function enforceMinTime() {
518
+ var timeInput = document.getElementById("sched-create-time");
519
+ var datePicker = document.getElementById("sched-create-date-picker");
520
+ if (!timeInput || !datePicker) return;
521
+
522
+ var now = new Date();
523
+ var todayStr = now.getFullYear() + "-" + configCtx.pad(now.getMonth() + 1) + "-" + configCtx.pad(now.getDate());
524
+ var isToday = datePicker.value === todayStr;
525
+
526
+ if (isToday) {
527
+ // Round up to the next minute for min
528
+ var minMinutes = now.getHours() * 60 + now.getMinutes() + 1;
529
+ var minH = Math.floor(minMinutes / 60);
530
+ var minM = minMinutes % 60;
531
+ if (minH >= 24) { minH = 23; minM = 59; }
532
+ var minVal = configCtx.pad(minH) + ":" + configCtx.pad(minM);
533
+ timeInput.min = minVal;
534
+
535
+ // If current value is before min, bump it
536
+ if (timeInput.value < minVal) {
537
+ timeInput.value = minVal;
538
+ }
539
+ } else {
540
+ timeInput.removeAttribute("min");
541
+ }
542
+ }
543
+
544
+ function updateIntervalBtn() {
545
+ var btn = document.getElementById("sched-create-interval-btn");
546
+ if (btn) {
547
+ btn.classList.toggle("has-recurrence", createInterval !== "none");
548
+ }
549
+ // Show/hide interval end conditions section
550
+ var endSection = document.getElementById("sched-interval-end-section");
551
+ if (endSection) {
552
+ endSection.classList.toggle("hidden", createInterval === "none");
553
+ }
554
+ // Always show time picker (start time is needed even with interval)
555
+ // Update skip-if-running visibility
556
+ updateRecurrenceBtn();
557
+ }
558
+
559
+ export function getPreviewEl() {
560
+ return previewEl;
561
+ }
562
+
563
+ export function removePreview() {
564
+ if (previewEl && previewEl.parentNode) {
565
+ previewEl.parentNode.removeChild(previewEl);
566
+ }
567
+ previewEl = null;
568
+ }
569
+
570
+ export function showPreviewOnCell(cell) {
571
+ removePreview();
572
+ var dragState = configCtx.getDragState();
573
+ var label = dragState.draggedTaskName || "(No title)";
574
+ var el = document.createElement("div");
575
+ el.className = "scheduler-event preview";
576
+ el.textContent = label;
577
+ cell.appendChild(el);
578
+ previewEl = el;
579
+ }
580
+
581
+ export function showPreviewOnSlot(slot) {
582
+ removePreview();
583
+ var dragState = configCtx.getDragState();
584
+ var label = dragState.draggedTaskName || "(No title)";
585
+ var hour = parseInt(slot.dataset.hour, 10);
586
+ var quarter = parseInt(slot.dataset.quarter || "0", 10);
587
+ var minute = quarter * 15;
588
+ var timeStr = configCtx.pad(hour) + ":" + configCtx.pad(minute);
589
+ var col = slot.closest(".scheduler-week-day-col");
590
+ if (!col) return;
591
+ var topPct = ((hour * 60 + minute) / 1440) * 100;
592
+ var el = document.createElement("div");
593
+ el.className = "scheduler-week-event preview";
594
+ el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
595
+ el.textContent = timeStr + " " + label;
596
+ col.appendChild(el);
597
+ previewEl = el;
598
+ }
599
+
600
+ export function showPreviewForCreate(anchorEl, label) {
601
+ removePreview();
602
+ if (!anchorEl) return;
603
+ var text = label || "(No title)";
604
+ if (anchorEl.classList.contains("scheduler-week-slot")) {
605
+ var hour = parseInt(anchorEl.dataset.hour, 10);
606
+ var quarter = parseInt(anchorEl.dataset.quarter || "0", 10);
607
+ var minute = quarter * 15;
608
+ var timeStr = configCtx.pad(hour) + ":" + configCtx.pad(minute);
609
+ var col = anchorEl.closest(".scheduler-week-day-col");
610
+ if (!col) return;
611
+ var topPct = ((hour * 60 + minute) / 1440) * 100;
612
+ var el = document.createElement("div");
613
+ el.className = "scheduler-week-event preview";
614
+ el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
615
+ el.textContent = timeStr + " " + text;
616
+ col.appendChild(el);
617
+ previewEl = el;
618
+ } else if (anchorEl.classList.contains("scheduler-cell")) {
619
+ var el = document.createElement("div");
620
+ el.className = "scheduler-event preview";
621
+ el.textContent = text;
622
+ anchorEl.appendChild(el);
623
+ previewEl = el;
624
+ }
625
+ }
626
+
627
+ export function applyDraggedTask() {
628
+ var dragState = configCtx.getDragState();
629
+ if (!dragState.draggedTaskId) return;
630
+ var taskHidden = document.getElementById("sched-create-task");
631
+ var taskLabel = document.getElementById("sched-create-task-label");
632
+ var taskBtn = document.getElementById("sched-create-task-btn");
633
+ if (taskHidden) taskHidden.value = dragState.draggedTaskId;
634
+ if (taskLabel) taskLabel.textContent = dragState.draggedTaskName || dragState.draggedTaskId;
635
+ if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
636
+ // Mark the matching item as selected in the dropdown list
637
+ var taskListEl = document.getElementById("sched-create-task-list");
638
+ if (taskListEl) {
639
+ var items = taskListEl.querySelectorAll(".sched-create-task-item");
640
+ for (var k = 0; k < items.length; k++) {
641
+ items[k].classList.toggle("selected", items[k].dataset.taskId === dragState.draggedTaskId);
642
+ }
643
+ }
644
+ // Auto-generate title: "taskName - HH:MM"
645
+ var titleInput = document.getElementById("sched-create-title");
646
+ var timeInput = document.getElementById("sched-create-time");
647
+ if (titleInput && (dragState.draggedTaskName || dragState.draggedTaskId)) {
648
+ var name = dragState.draggedTaskName || dragState.draggedTaskId;
649
+ var time = timeInput ? timeInput.value : "";
650
+ titleInput.value = time ? name + " - " + time : name;
651
+ }
652
+ // Update preview text to match auto-title
653
+ if (previewEl && titleInput) {
654
+ var previewText = titleInput.value || "(No title)";
655
+ if (previewEl.classList.contains("scheduler-week-event") && timeInput) {
656
+ previewText = timeInput.value + " " + (titleInput.value || "(No title)");
657
+ }
658
+ previewEl.textContent = previewText;
659
+ }
660
+ configCtx.clearDragState();
661
+ }
662
+
663
+ export function openCreateModalWithRecord(rec, anchorEl) {
664
+ var createPopover = configCtx.getCreatePopover();
665
+ // Parse date/time from record
666
+ var date = null;
667
+ var hour = null;
668
+ if (rec.date) {
669
+ var dp = rec.date.split("-");
670
+ date = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
671
+ }
672
+ if (rec.time) {
673
+ var tp = rec.time.split(":");
674
+ hour = parseInt(tp[0], 10) || 0;
675
+ var mins = parseInt(tp[1], 10) || 0;
676
+ if (date) { date.setHours(hour, mins, 0); }
677
+ }
678
+ // Mark as editing existing record
679
+ createEditingRecId = rec.id;
680
+
681
+ // Open the create modal normally first
682
+ openCreateModal(date || new Date(), hour, anchorEl);
683
+
684
+ // Show delete button
685
+ var deleteBtn = document.getElementById("sched-create-delete");
686
+ if (deleteBtn) deleteBtn.classList.remove("hidden");
687
+
688
+ // Now override with record values
689
+ var titleInput = document.getElementById("sched-create-title");
690
+ if (titleInput) titleInput.value = rec.name || "";
691
+
692
+ var descInput = document.getElementById("sched-create-desc");
693
+ if (descInput) descInput.value = rec.description || "";
694
+
695
+ // Set color
696
+ if (rec.color) {
697
+ createColor = rec.color;
698
+ var colorDot = document.getElementById("sched-create-color-dot");
699
+ if (colorDot) colorDot.style.background = createColor;
700
+ var swatches = createPopover.querySelectorAll(".sched-color-swatch");
701
+ for (var si = 0; si < swatches.length; si++) {
702
+ swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
703
+ }
704
+ }
705
+
706
+ // Set skip-if-running
707
+ var skipRunningEl = document.getElementById("sched-skip-running");
708
+ if (skipRunningEl) skipRunningEl.checked = rec.skipIfRunning !== false;
709
+
710
+ // Set run mode and iterations
711
+ var editRunMode = (rec.maxIterations && rec.maxIterations > 1) ? "multi" : "single";
712
+ var editRunBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
713
+ for (var rb = 0; rb < editRunBtns.length; rb++) {
714
+ editRunBtns[rb].classList.toggle("active", editRunBtns[rb].dataset.mode === editRunMode);
715
+ }
716
+ var editIterGroup = document.getElementById("sched-create-iter-group");
717
+ if (editIterGroup) editIterGroup.classList.toggle("hidden", editRunMode !== "multi");
718
+ if (rec.maxIterations && rec.maxIterations > 1) {
719
+ var iterInput = document.getElementById("sched-create-iterations");
720
+ if (iterInput) iterInput.value = rec.maxIterations;
721
+ }
722
+
723
+ // Set linked task
724
+ if (rec.linkedTaskId) {
725
+ var taskHidden = document.getElementById("sched-create-task");
726
+ var taskLabel2 = document.getElementById("sched-create-task-label");
727
+ var taskBtn = document.getElementById("sched-create-task-btn");
728
+ var taskListEl = document.getElementById("sched-create-task-list");
729
+ var records = configCtx.getRecords();
730
+ if (taskHidden) taskHidden.value = rec.linkedTaskId;
731
+ // Find the task name
732
+ var taskName = rec.linkedTaskId;
733
+ for (var j = 0; j < records.length; j++) {
734
+ if (records[j].id === rec.linkedTaskId) { taskName = records[j].name || records[j].id; break; }
735
+ }
736
+ if (taskLabel2) taskLabel2.textContent = taskName;
737
+ if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
738
+ if (taskListEl) {
739
+ var items = taskListEl.querySelectorAll(".sched-create-task-item");
740
+ for (var k = 0; k < items.length; k++) {
741
+ items[k].classList.toggle("selected", items[k].dataset.taskId === rec.linkedTaskId);
742
+ }
743
+ }
744
+ }
745
+
746
+ // Restore interval from cron
747
+ if (rec.cron) {
748
+ var cronParts = rec.cron.trim().split(/\s+/);
749
+ if (cronParts.length === 5) {
750
+ var detectedMinInterval = null;
751
+ var detectedHrInterval = null;
752
+ // Detect minute-level interval: e.g. "0,5,10,... * * * *" or "*/5 * * * *"
753
+ if (cronParts[1] === "*" && cronParts[2] === "*") {
754
+ detectedMinInterval = configCtx.detectInterval(cronParts[0], 60);
755
+ }
756
+ // Detect hour-level interval: e.g. "0 1,3,5,... * * *"
757
+ if (!detectedMinInterval && cronParts[2] === "*") {
758
+ detectedHrInterval = configCtx.detectInterval(cronParts[1], 24);
759
+ }
760
+
761
+ if (detectedMinInterval) {
762
+ createInterval = "custom";
763
+ createIntervalCustom = { value: detectedMinInterval, unit: "minute" };
764
+ var intValEl = document.getElementById("sched-interval-custom-value");
765
+ if (intValEl) intValEl.value = detectedMinInterval;
766
+ var intUnitBtns = document.querySelectorAll("#sched-interval-custom-unit .sched-interval-seg");
767
+ for (var iu = 0; iu < intUnitBtns.length; iu++) {
768
+ intUnitBtns[iu].classList.toggle("active", intUnitBtns[iu].dataset.unit === "minute");
769
+ }
770
+ updateIntervalBtn();
771
+ } else if (detectedHrInterval) {
772
+ createInterval = "custom";
773
+ createIntervalCustom = { value: detectedHrInterval, unit: "hour" };
774
+ var intValEl2 = document.getElementById("sched-interval-custom-value");
775
+ if (intValEl2) intValEl2.value = detectedHrInterval;
776
+ var intUnitBtns2 = document.querySelectorAll("#sched-interval-custom-unit .sched-interval-seg");
777
+ for (var iu2 = 0; iu2 < intUnitBtns2.length; iu2++) {
778
+ intUnitBtns2[iu2].classList.toggle("active", intUnitBtns2[iu2].dataset.unit === "hour");
779
+ }
780
+ updateIntervalBtn();
781
+ }
782
+
783
+ // Restore recurrence from cron (if no interval detected, or combined with interval)
784
+ if (!detectedMinInterval && !detectedHrInterval) {
785
+ var cronDow = cronParts[4];
786
+ var cronDom = cronParts[2];
787
+ var cronMonth = cronParts[3];
788
+ if (cronDow === "*" && cronDom === "*" && cronMonth === "*") {
789
+ createRecurrence = "daily";
790
+ } else if (cronDow === "1-5" && cronDom === "*") {
791
+ createRecurrence = "weekdays";
792
+ } else if (cronDom !== "*" && cronMonth !== "*") {
793
+ createRecurrence = "yearly";
794
+ } else if (cronDom !== "*" && cronDow === "*") {
795
+ createRecurrence = "monthly";
796
+ } else if (cronDow !== "*" && cronDom === "*") {
797
+ // Check if it matches a single day (weekly)
798
+ var dowVals = cronDow.split(",");
799
+ if (dowVals.length === 1) {
800
+ createRecurrence = "weekly";
801
+ } else if (dowVals.length === 7) {
802
+ createRecurrence = "daily";
803
+ } else {
804
+ createRecurrence = "custom";
805
+ createCustomConfirmed = true;
806
+ // Set custom panel values
807
+ document.getElementById("sched-custom-interval").value = "1";
808
+ document.getElementById("sched-custom-unit").value = "week";
809
+ var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
810
+ for (var cd = 0; cd < customDowBtns.length; cd++) {
811
+ customDowBtns[cd].classList.toggle("active", dowVals.indexOf(customDowBtns[cd].dataset.dow) !== -1);
812
+ }
813
+ }
814
+ }
815
+ updateRecurrenceBtn();
816
+ }
817
+ }
818
+ }
819
+
820
+ // Restore interval end conditions
821
+ if (rec.intervalEnd) {
822
+ createIntervalEnd = rec.intervalEnd.type || "allday";
823
+ var editIendOpts = document.querySelectorAll(".sched-interval-end-opt");
824
+ for (var ei = 0; ei < editIendOpts.length; ei++) {
825
+ editIendOpts[ei].classList.toggle("active", editIendOpts[ei].dataset.iend === createIntervalEnd);
826
+ }
827
+ var editAfterRow = document.getElementById("sched-interval-end-after-row");
828
+ var editUntilRow = document.getElementById("sched-interval-end-until-row");
829
+ if (createIntervalEnd === "after") {
830
+ createIntervalEndAfter = rec.intervalEnd.count || 5;
831
+ if (editAfterRow) editAfterRow.classList.remove("hidden");
832
+ if (editUntilRow) editUntilRow.classList.add("hidden");
833
+ var editAfterInput = document.getElementById("sched-interval-end-after");
834
+ if (editAfterInput) editAfterInput.value = createIntervalEndAfter;
835
+ } else if (createIntervalEnd === "until") {
836
+ createIntervalEndTime = rec.intervalEnd.time || "18:00";
837
+ if (editAfterRow) editAfterRow.classList.add("hidden");
838
+ if (editUntilRow) editUntilRow.classList.remove("hidden");
839
+ var editTimeInput = document.getElementById("sched-interval-end-time");
840
+ if (editTimeInput) editTimeInput.value = createIntervalEndTime;
841
+ }
842
+ }
843
+
844
+ // Update preview to show record name
845
+ if (previewEl) {
846
+ var previewText = rec.name || "(No title)";
847
+ if (previewEl.classList.contains("scheduler-week-event") && rec.time) {
848
+ previewText = rec.time + " " + previewText;
849
+ }
850
+ previewEl.textContent = previewText;
851
+ }
852
+ }
853
+
854
+ export function openCreateModal(date, hour, anchorEl) {
855
+ var createPopover = configCtx.getCreatePopover();
856
+ if (!createPopover) return;
857
+ // Reset editing state (openCreateModalWithRecord sets this before calling us)
858
+ if (!createEditingRecId) {
859
+ var deleteBtn = document.getElementById("sched-create-delete");
860
+ if (deleteBtn) deleteBtn.classList.add("hidden");
861
+ }
862
+ createSelectedDate = date || new Date();
863
+ createRecurrence = "none";
864
+ createCustomConfirmed = false;
865
+ createInterval = "none";
866
+ createIntervalCustom = null;
867
+ createIntervalEnd = "allday";
868
+ createIntervalEndAfter = 5;
869
+ createIntervalEndTime = "";
870
+ createColor = "#ffb86c";
871
+
872
+ // Reset form
873
+ document.getElementById("sched-create-title").value = "";
874
+ document.getElementById("sched-create-desc").value = "";
875
+ var iterReset = document.getElementById("sched-create-iterations");
876
+ if (iterReset) iterReset.value = "3";
877
+ // Reset run mode to single
878
+ var runModeBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
879
+ for (var rm = 0; rm < runModeBtns.length; rm++) {
880
+ runModeBtns[rm].classList.toggle("active", runModeBtns[rm].dataset.mode === "single");
881
+ }
882
+ var iterGroup = document.getElementById("sched-create-iter-group");
883
+ if (iterGroup) iterGroup.classList.add("hidden");
884
+
885
+ // Reset color
886
+ var colorDot = document.getElementById("sched-create-color-dot");
887
+ if (colorDot) colorDot.style.background = createColor;
888
+ var palette = document.getElementById("sched-create-color-palette");
889
+ if (palette) palette.classList.add("hidden");
890
+ var swatches = createPopover.querySelectorAll(".sched-color-swatch");
891
+ for (var si = 0; si < swatches.length; si++) {
892
+ swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
893
+ }
894
+
895
+ // Populate task dropdown (only tasks -- exclude ralph and schedule)
896
+ var taskHidden = document.getElementById("sched-create-task");
897
+ var taskLabel = document.getElementById("sched-create-task-label");
898
+ var taskBtn = document.getElementById("sched-create-task-btn");
899
+ var taskListEl = document.getElementById("sched-create-task-list");
900
+ var records = configCtx.getRecords();
901
+ if (taskHidden) taskHidden.value = "";
902
+ if (taskLabel) taskLabel.textContent = "Select a task";
903
+ if (taskBtn) { taskBtn.classList.remove("has-value"); taskBtn.classList.remove("invalid"); }
904
+ if (taskListEl) {
905
+ taskListEl.classList.add("hidden");
906
+ var tasks = records.filter(function (r) { return r.source !== "ralph" && r.source !== "schedule"; });
907
+ if (tasks.length === 0) {
908
+ taskListEl.innerHTML = '<div class="sched-create-task-empty">No tasks available</div>';
909
+ } else {
910
+ var html = "";
911
+ for (var i = 0; i < tasks.length; i++) {
912
+ html += '<div class="sched-create-task-item" data-task-id="' + configCtx.esc(tasks[i].id) + '">' + configCtx.esc(tasks[i].name || tasks[i].id) + '</div>';
913
+ }
914
+ taskListEl.innerHTML = html;
915
+ // Bind click handlers
916
+ var items = taskListEl.querySelectorAll(".sched-create-task-item");
917
+ for (var j = 0; j < items.length; j++) {
918
+ (function (item) {
919
+ item.addEventListener("click", function (e) {
920
+ e.stopPropagation();
921
+ var id = item.dataset.taskId;
922
+ var name = item.textContent;
923
+ if (taskHidden) taskHidden.value = id;
924
+ if (taskLabel) taskLabel.textContent = name;
925
+ if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
926
+ // Update selected state
927
+ var all = taskListEl.querySelectorAll(".sched-create-task-item");
928
+ for (var k = 0; k < all.length; k++) {
929
+ all[k].classList.toggle("selected", all[k] === item);
930
+ }
931
+ taskListEl.classList.add("hidden");
932
+ });
933
+ })(items[j]);
934
+ }
935
+ }
936
+ }
937
+
938
+ // Set date picker
939
+ var dateStr = createSelectedDate.getFullYear() + "-" + configCtx.pad(createSelectedDate.getMonth() + 1) + "-" + configCtx.pad(createSelectedDate.getDate());
940
+ document.getElementById("sched-create-date").value = dateStr;
941
+ var datePicker = document.getElementById("sched-create-date-picker");
942
+ if (datePicker) {
943
+ datePicker.value = dateStr;
944
+ var todayNow = new Date();
945
+ var todayMin = todayNow.getFullYear() + "-" + configCtx.pad(todayNow.getMonth() + 1) + "-" + configCtx.pad(todayNow.getDate());
946
+ datePicker.min = todayMin;
947
+ }
948
+
949
+ // Time (use minutes from createSelectedDate for 15-min snapping)
950
+ if (hour !== null && hour !== undefined) {
951
+ var mins = createSelectedDate.getMinutes ? createSelectedDate.getMinutes() : 0;
952
+ document.getElementById("sched-create-time").value = configCtx.pad(hour) + ":" + configCtx.pad(mins);
953
+ } else {
954
+ // Default to current time (next quarter-hour)
955
+ var nowT = new Date();
956
+ var nowMins = nowT.getHours() * 60 + nowT.getMinutes();
957
+ var nextQ = Math.ceil(nowMins / 15) * 15;
958
+ var defH = Math.floor(nextQ / 60);
959
+ var defM = nextQ % 60;
960
+ if (defH >= 24) { defH = 23; defM = 45; }
961
+ document.getElementById("sched-create-time").value = configCtx.pad(defH) + ":" + configCtx.pad(defM);
962
+ }
963
+
964
+ // Update recurrence labels
965
+ updateRecurrenceLabels(createSelectedDate);
966
+
967
+ // Enforce min time for today
968
+ enforceMinTime();
969
+
970
+ // Reset recurrence
971
+ createRecurrence = "none";
972
+ createCustomConfirmed = false;
973
+ updateRecurrenceBtn();
974
+
975
+ // Reset interval
976
+ document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
977
+ var timeInput = document.getElementById("sched-create-time");
978
+ if (timeInput) timeInput.style.display = "";
979
+
980
+ // Reset interval end conditions
981
+ var iendOpts = document.querySelectorAll(".sched-interval-end-opt");
982
+ for (var ie = 0; ie < iendOpts.length; ie++) {
983
+ iendOpts[ie].classList.toggle("active", iendOpts[ie].dataset.iend === "allday");
984
+ }
985
+ var iendAfterRow = document.getElementById("sched-interval-end-after-row");
986
+ if (iendAfterRow) iendAfterRow.classList.add("hidden");
987
+ var iendUntilRow = document.getElementById("sched-interval-end-until-row");
988
+ if (iendUntilRow) iendUntilRow.classList.add("hidden");
989
+ var iendAfterInput = document.getElementById("sched-interval-end-after");
990
+ if (iendAfterInput) iendAfterInput.value = "5";
991
+ var iendTimeInput = document.getElementById("sched-interval-end-time");
992
+ if (iendTimeInput) iendTimeInput.value = "18:00";
993
+
994
+ updateIntervalBtn();
995
+
996
+ // Reset custom panel
997
+ document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
998
+ document.getElementById("sched-custom-interval").value = "1";
999
+ document.getElementById("sched-custom-unit").value = "week";
1000
+ document.getElementById("sched-custom-dow-section").style.display = "";
1001
+ var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
1002
+ for (var i = 0; i < customDowBtns.length; i++) {
1003
+ customDowBtns[i].classList.toggle("active", parseInt(customDowBtns[i].dataset.dow) === createSelectedDate.getDay());
1004
+ }
1005
+ document.getElementById("sched-custom-end").value = "never";
1006
+ document.getElementById("sched-custom-end-label").textContent = "Never";
1007
+ var endItems = document.querySelectorAll(".sched-custom-end-item");
1008
+ for (var ei = 0; ei < endItems.length; ei++) {
1009
+ endItems[ei].classList.toggle("active", endItems[ei].dataset.value === "never");
1010
+ }
1011
+ document.getElementById("sched-custom-end-list").classList.add("hidden");
1012
+ createEndType = "never";
1013
+ createEndDate = null;
1014
+ createEndAfter = 10;
1015
+ document.getElementById("sched-custom-end-date-btn").classList.add("hidden");
1016
+ document.getElementById("sched-custom-end-after-wrap").classList.add("hidden");
1017
+ document.getElementById("sched-custom-end-calendar").classList.add("hidden");
1018
+
1019
+ // Show preview event on the calendar cell
1020
+ var dragState = configCtx.getDragState();
1021
+ showPreviewForCreate(anchorEl, dragState.draggedTaskName || null);
1022
+
1023
+ // Position near anchor cell
1024
+ createPopover.classList.remove("hidden");
1025
+ positionCreatePopover(anchorEl);
1026
+
1027
+ try { lucide.createIcons({ node: createPopover }); } catch (e) {}
1028
+ setTimeout(function () { document.getElementById("sched-create-title").focus(); }, 50);
1029
+ }
1030
+
1031
+ function positionCreatePopover(anchorEl) {
1032
+ var createPopover = configCtx.getCreatePopover();
1033
+ var contentCalEl = configCtx.getContentCalEl();
1034
+ if (!createPopover || !anchorEl) {
1035
+ // Fallback: center in scheduler content area
1036
+ if (createPopover && contentCalEl) {
1037
+ var cRect = contentCalEl.getBoundingClientRect();
1038
+ createPopover.style.left = (cRect.left + cRect.width / 2 - 180) + "px";
1039
+ createPopover.style.top = (cRect.top + 60) + "px";
1040
+ }
1041
+ return;
1042
+ }
1043
+
1044
+ var rect = anchorEl.getBoundingClientRect();
1045
+ var popW = 360;
1046
+ var popH = createPopover.offsetHeight || 300;
1047
+
1048
+ // Try to place to the right of the cell
1049
+ var left = rect.right + 8;
1050
+ var top = rect.top;
1051
+
1052
+ // If it overflows right, place to the left
1053
+ if (left + popW > window.innerWidth - 10) {
1054
+ left = rect.left - popW - 8;
1055
+ }
1056
+ // If it still overflows left, center horizontally on the cell
1057
+ if (left < 10) {
1058
+ left = Math.max(10, rect.left + rect.width / 2 - popW / 2);
1059
+ }
1060
+
1061
+ // Vertical: don't overflow bottom
1062
+ if (top + popH > window.innerHeight - 10) {
1063
+ top = window.innerHeight - popH - 10;
1064
+ }
1065
+ if (top < 10) top = 10;
1066
+
1067
+ createPopover.style.left = left + "px";
1068
+ createPopover.style.top = top + "px";
1069
+ }
1070
+
1071
+ function updateRecurrenceLabels(date) {
1072
+ var dow = date.getDay();
1073
+ var dayName = DAY_NAMES[dow];
1074
+ var dom = date.getDate();
1075
+ var monthName = MONTH_NAMES[date.getMonth()];
1076
+
1077
+ // Weekly on {day}
1078
+ var weeklyEl = document.getElementById("sched-recurrence-weekly");
1079
+ if (weeklyEl) weeklyEl.textContent = "Weekly on " + dayName;
1080
+
1081
+ // Every second {day} of the month
1082
+ var weekOfMonth = Math.ceil(dom / 7);
1083
+ var ordinals = ["", "first", "second", "third", "fourth", "fifth"];
1084
+ var biweeklyEl = document.getElementById("sched-recurrence-biweekly");
1085
+ if (biweeklyEl) {
1086
+ var ordStr = ordinals[weekOfMonth] || weekOfMonth + "th";
1087
+ biweeklyEl.textContent = "Every " + ordStr + " " + dayName + " of the mo...";
1088
+ }
1089
+
1090
+ // Every year on {month} {date}
1091
+ var yearlyEl = document.getElementById("sched-recurrence-yearly");
1092
+ if (yearlyEl) yearlyEl.textContent = "Every year on " + monthName + " " + dom;
1093
+
1094
+ // Every month on the {date}th
1095
+ var monthlyEl = document.getElementById("sched-recurrence-monthly");
1096
+ if (monthlyEl) {
1097
+ var suffix = "th";
1098
+ if (dom === 1 || dom === 21 || dom === 31) suffix = "st";
1099
+ else if (dom === 2 || dom === 22) suffix = "nd";
1100
+ else if (dom === 3 || dom === 23) suffix = "rd";
1101
+ monthlyEl.textContent = "Every month on the " + dom + suffix;
1102
+ }
1103
+ }
1104
+
1105
+ export function closeCreateModal() {
1106
+ var createPopover = configCtx.getCreatePopover();
1107
+ if (createPopover) createPopover.classList.add("hidden");
1108
+ var dd = document.getElementById("sched-create-recurrence-dropdown");
1109
+ if (dd) dd.classList.add("hidden");
1110
+ var pal = document.getElementById("sched-create-color-palette");
1111
+ if (pal) pal.classList.add("hidden");
1112
+ var tl = document.getElementById("sched-create-task-list");
1113
+ if (tl) tl.classList.add("hidden");
1114
+ removePreview();
1115
+ createSelectedDate = null;
1116
+ createEditingRecId = null;
1117
+ }
1118
+
1119
+ function openDeleteDialog(recId, eventDate, isOneOff) {
1120
+ var dialog = document.getElementById("sched-delete-dialog");
1121
+ if (!dialog) return;
1122
+ dialog.dataset.recId = recId;
1123
+ if (eventDate) {
1124
+ dialog.dataset.eventDate = eventDate.getFullYear() + "-" + configCtx.pad(eventDate.getMonth() + 1) + "-" + configCtx.pad(eventDate.getDate());
1125
+ } else {
1126
+ dialog.dataset.eventDate = "";
1127
+ }
1128
+ // Toggle between one-off and recurring UI
1129
+ var title = dialog.querySelector(".sched-delete-dialog-title");
1130
+ var body = dialog.querySelector(".sched-delete-dialog-body");
1131
+ var footer = dialog.querySelector(".sched-delete-dialog-footer");
1132
+ var cancelBtn = dialog.querySelector('[data-delete="cancel"]');
1133
+ dialog.dataset.oneOff = isOneOff ? "1" : "";
1134
+ if (isOneOff) {
1135
+ if (title) title.textContent = "Delete this event?";
1136
+ if (body) body.classList.add("hidden");
1137
+ if (cancelBtn) cancelBtn.textContent = "Cancel";
1138
+ // Add a "Delete" button next to cancel in footer
1139
+ var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
1140
+ if (!existingDel && footer) {
1141
+ var delBtn = document.createElement("button");
1142
+ delBtn.className = "sched-delete-option danger sched-delete-confirm-btn";
1143
+ delBtn.dataset.delete = "all";
1144
+ delBtn.textContent = "Delete";
1145
+ footer.appendChild(delBtn);
1146
+ delBtn.addEventListener("click", function (e) {
1147
+ e.stopPropagation();
1148
+ var rid = dialog.dataset.recId;
1149
+ if (rid) configCtx.send({ type: "loop_registry_remove", id: rid });
1150
+ closeDeleteDialog();
1151
+ });
1152
+ }
1153
+ if (existingDel) existingDel.classList.remove("hidden");
1154
+ } else {
1155
+ if (title) title.textContent = "Delete recurring event";
1156
+ if (body) body.classList.remove("hidden");
1157
+ if (cancelBtn) cancelBtn.textContent = "Cancel";
1158
+ var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
1159
+ if (existingDel) existingDel.classList.add("hidden");
1160
+ }
1161
+ dialog.classList.remove("hidden");
1162
+ }
1163
+
1164
+ function closeDeleteDialog() {
1165
+ var dialog = document.getElementById("sched-delete-dialog");
1166
+ if (dialog) {
1167
+ dialog.classList.add("hidden");
1168
+ dialog.dataset.recId = "";
1169
+ dialog.dataset.eventDate = "";
1170
+ }
1171
+ }
1172
+
1173
+ // Build an explicit list of values offset from a start value with a given step, wrapping at max
1174
+ function buildOffsetList(start, step, max) {
1175
+ var vals = [];
1176
+ var v = start % max;
1177
+ for (var i = 0; i < max; i += step) {
1178
+ vals.push(v);
1179
+ v = (v + step) % max;
1180
+ }
1181
+ vals.sort(function (a, b) { return a - b; });
1182
+ return vals.join(",");
1183
+ }
1184
+
1185
+ function buildCreateCron() {
1186
+ if (!createSelectedDate) return null;
1187
+
1188
+ var timeVal = document.getElementById("sched-create-time").value || "09:00";
1189
+ var timeParts = timeVal.split(":");
1190
+ var h = parseInt(timeParts[0], 10);
1191
+ var m = parseInt(timeParts[1], 10);
1192
+
1193
+ var dow = createSelectedDate.getDay();
1194
+ var dom = createSelectedDate.getDate();
1195
+ var month = createSelectedDate.getMonth() + 1;
1196
+
1197
+ // Determine interval minutes
1198
+ var intervalMins = 0;
1199
+ if (createInterval !== "none") {
1200
+ if (createInterval === "custom" && createIntervalCustom) {
1201
+ intervalMins = createIntervalCustom.unit === "hour"
1202
+ ? createIntervalCustom.value * 60
1203
+ : createIntervalCustom.value;
1204
+ } else {
1205
+ intervalMins = parseInt(createInterval, 10) || 0;
1206
+ }
1207
+ }
1208
+
1209
+ // Interval only (no recurrence) = interval every day
1210
+ if (intervalMins > 0 && createRecurrence === "none") {
1211
+ if (intervalMins < 60) return buildOffsetList(m, intervalMins, 60) + " * * * *";
1212
+ var intHrs = Math.floor(intervalMins / 60);
1213
+ return String(m) + " " + buildOffsetList(h, intHrs, 24) + " * * *";
1214
+ }
1215
+
1216
+ if (createRecurrence === "none" && intervalMins === 0) return null;
1217
+
1218
+ // Build minute/hour fields from interval or time
1219
+ var minField = String(m);
1220
+ var hourField = String(h);
1221
+ if (intervalMins > 0 && intervalMins < 60) {
1222
+ minField = buildOffsetList(m, intervalMins, 60);
1223
+ hourField = "*";
1224
+ } else if (intervalMins >= 60) {
1225
+ var intHrs2 = Math.floor(intervalMins / 60);
1226
+ minField = String(m);
1227
+ hourField = buildOffsetList(h, intHrs2, 24);
1228
+ }
1229
+
1230
+ if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
1231
+ if (createRecurrence === "weekly") return minField + " " + hourField + " * * " + dow;
1232
+ if (createRecurrence === "biweekly") {
1233
+ var weekNum = Math.ceil(dom / 7);
1234
+ return minField + " " + hourField + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
1235
+ }
1236
+ if (createRecurrence === "yearly") return minField + " " + hourField + " " + dom + " " + month + " *";
1237
+ if (createRecurrence === "monthly") return minField + " " + hourField + " " + dom + " * *";
1238
+ if (createRecurrence === "weekdays") return minField + " " + hourField + " * * 1-5";
1239
+
1240
+ if (createRecurrence === "custom" && createCustomConfirmed) {
1241
+ return buildCustomCron(h, m);
1242
+ }
1243
+
1244
+ return null;
1245
+ }
1246
+
1247
+ function updateEndDateLabel() {
1248
+ var label = document.getElementById("sched-custom-end-date-label");
1249
+ if (!label || !createEndDate) return;
1250
+ var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1251
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1252
+ label.textContent = days[createEndDate.getDay()] + ", " + months[createEndDate.getMonth()] + " " + createEndDate.getDate();
1253
+ }
1254
+
1255
+ function renderEndCalendar() {
1256
+ var grid = document.getElementById("sched-cal-grid");
1257
+ var titleEl = document.getElementById("sched-cal-title");
1258
+ if (!grid || !createEndCalMonth) return;
1259
+
1260
+ var year = createEndCalMonth.getFullYear();
1261
+ var month = createEndCalMonth.getMonth();
1262
+ var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
1263
+ titleEl.textContent = months[month] + " " + year;
1264
+
1265
+ var firstDay = new Date(year, month, 1).getDay();
1266
+ var daysInMonth = new Date(year, month + 1, 0).getDate();
1267
+ var prevDays = new Date(year, month, 0).getDate();
1268
+
1269
+ var today = new Date();
1270
+ today.setHours(0, 0, 0, 0);
1271
+
1272
+ grid.innerHTML = "";
1273
+
1274
+ // Previous month filler
1275
+ for (var p = firstDay - 1; p >= 0; p--) {
1276
+ var d = prevDays - p;
1277
+ var btn = document.createElement("button");
1278
+ btn.className = "sched-cal-day other-month";
1279
+ btn.textContent = d;
1280
+ btn.type = "button";
1281
+ var prevDate = new Date(year, month - 1, d);
1282
+ (function (dt) {
1283
+ btn.addEventListener("click", function (e) {
1284
+ e.stopPropagation();
1285
+ createEndDate = dt;
1286
+ updateEndDateLabel();
1287
+ renderEndCalendar();
1288
+ });
1289
+ })(prevDate);
1290
+ grid.appendChild(btn);
1291
+ }
1292
+
1293
+ // Current month
1294
+ for (var i = 1; i <= daysInMonth; i++) {
1295
+ var btn = document.createElement("button");
1296
+ btn.className = "sched-cal-day";
1297
+ btn.textContent = i;
1298
+ btn.type = "button";
1299
+ var cellDate = new Date(year, month, i);
1300
+ if (cellDate.getTime() === today.getTime()) btn.classList.add("today");
1301
+ if (createEndDate && cellDate.getFullYear() === createEndDate.getFullYear() && cellDate.getMonth() === createEndDate.getMonth() && cellDate.getDate() === createEndDate.getDate()) {
1302
+ btn.classList.add("selected");
1303
+ }
1304
+ (function (dt) {
1305
+ btn.addEventListener("click", function (e) {
1306
+ e.stopPropagation();
1307
+ createEndDate = dt;
1308
+ updateEndDateLabel();
1309
+ renderEndCalendar();
1310
+ });
1311
+ })(cellDate);
1312
+ grid.appendChild(btn);
1313
+ }
1314
+
1315
+ // Next month filler
1316
+ var totalCells = firstDay + daysInMonth;
1317
+ var remaining = (7 - (totalCells % 7)) % 7;
1318
+ for (var n = 1; n <= remaining; n++) {
1319
+ var btn = document.createElement("button");
1320
+ btn.className = "sched-cal-day other-month";
1321
+ btn.textContent = n;
1322
+ btn.type = "button";
1323
+ var nextDate = new Date(year, month + 1, n);
1324
+ (function (dt) {
1325
+ btn.addEventListener("click", function (e) {
1326
+ e.stopPropagation();
1327
+ createEndDate = dt;
1328
+ updateEndDateLabel();
1329
+ renderEndCalendar();
1330
+ });
1331
+ })(nextDate);
1332
+ grid.appendChild(btn);
1333
+ }
1334
+ }
1335
+
1336
+ function buildCustomCron(h, m) {
1337
+ var interval = parseInt(document.getElementById("sched-custom-interval").value, 10) || 1;
1338
+ var unit = document.getElementById("sched-custom-unit").value;
1339
+
1340
+ if (unit === "minute") {
1341
+ return interval === 1 ? "*/1 * * * *" : buildOffsetList(m, interval, 60) + " * * * *";
1342
+ }
1343
+ if (unit === "hour") {
1344
+ return interval === 1 ? m + " */1 * * *" : m + " " + buildOffsetList(h, interval, 24) + " * * *";
1345
+ }
1346
+ if (unit === "day") {
1347
+ if (interval === 1) return m + " " + h + " * * *";
1348
+ return m + " " + h + " */" + interval + " * *";
1349
+ }
1350
+
1351
+ if (unit === "week") {
1352
+ var days = [];
1353
+ var btns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn.active");
1354
+ for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
1355
+ if (days.length === 0) days.push(String(createSelectedDate ? createSelectedDate.getDay() : 0));
1356
+ return m + " " + h + " * * " + days.sort().join(",");
1357
+ }
1358
+
1359
+ if (unit === "month") {
1360
+ var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
1361
+ if (interval === 1) return m + " " + h + " " + dom + " * *";
1362
+ return m + " " + h + " " + dom + " */" + interval + " *";
1363
+ }
1364
+
1365
+ if (unit === "year") {
1366
+ var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
1367
+ var month = createSelectedDate ? createSelectedDate.getMonth() + 1 : 1;
1368
+ return m + " " + h + " " + dom + " " + month + " *";
1369
+ }
1370
+
1371
+ return null;
1372
+ }
1373
+
1374
+ function submitCreateSchedule() {
1375
+ var name = document.getElementById("sched-create-title").value.trim();
1376
+ if (!name) { document.getElementById("sched-create-title").focus(); return; }
1377
+
1378
+ var taskId = document.getElementById("sched-create-task").value || null;
1379
+ if (!taskId) {
1380
+ var taskBtn = document.getElementById("sched-create-task-btn");
1381
+ if (taskBtn) taskBtn.classList.add("invalid");
1382
+ return;
1383
+ }
1384
+
1385
+ var ctx = configCtx.ctx;
1386
+ ctx.requireClayRalph(function () {
1387
+ var createPopover = configCtx.getCreatePopover();
1388
+ var description = document.getElementById("sched-create-desc").value.trim();
1389
+ var datePicker = document.getElementById("sched-create-date-picker");
1390
+ var dateVal = datePicker ? datePicker.value : document.getElementById("sched-create-date").value;
1391
+ var timeVal = document.getElementById("sched-create-time").value || "09:00";
1392
+
1393
+ // Reject scheduling in the past
1394
+ if (dateVal && timeVal) {
1395
+ var dp = dateVal.split("-");
1396
+ var tp = timeVal.split(":");
1397
+ if (dp.length === 3 && tp.length >= 2) {
1398
+ var schedDate = new Date(
1399
+ parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10),
1400
+ parseInt(tp[0], 10), parseInt(tp[1], 10), 0
1401
+ );
1402
+ if (schedDate < new Date()) {
1403
+ showToast("Cannot schedule a task in the past", "error");
1404
+ return;
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ var cron = buildCreateCron();
1410
+
1411
+ // Build recurrence end info
1412
+ var recurrenceEnd = null;
1413
+ // Interval-only (no recurrence): limit to the scheduled date only
1414
+ if (cron && createRecurrence === "none" && createInterval !== "none" && dateVal) {
1415
+ recurrenceEnd = { type: "until", date: dateVal };
1416
+ }
1417
+ if (cron && createRecurrence === "custom" && createCustomConfirmed) {
1418
+ if (createEndType === "until" && createEndDate) {
1419
+ var ey = createEndDate.getFullYear();
1420
+ var em = String(createEndDate.getMonth() + 1).padStart(2, "0");
1421
+ var ed = String(createEndDate.getDate()).padStart(2, "0");
1422
+ recurrenceEnd = { type: "until", date: ey + "-" + em + "-" + ed };
1423
+ } else if (createEndType === "after" && createEndAfter > 0) {
1424
+ recurrenceEnd = { type: "after", count: createEndAfter };
1425
+ }
1426
+ }
1427
+
1428
+ // Build interval end info
1429
+ var intervalEnd = null;
1430
+ if (createInterval !== "none") {
1431
+ if (createIntervalEnd === "after" && createIntervalEndAfter > 0) {
1432
+ intervalEnd = { type: "after", count: createIntervalEndAfter };
1433
+ } else if (createIntervalEnd === "until" && createIntervalEndTime) {
1434
+ intervalEnd = { type: "until", time: createIntervalEndTime };
1435
+ }
1436
+ // "allday" = null (no limit)
1437
+ }
1438
+
1439
+ var skipRunningEl = document.getElementById("sched-skip-running");
1440
+ var skipIfRunning = skipRunningEl ? skipRunningEl.checked : true;
1441
+
1442
+ var activeRunMode = createPopover.querySelector(".sched-run-mode-btn.active");
1443
+ var runMode = activeRunMode ? activeRunMode.dataset.mode : "single";
1444
+ var maxIterations = 1;
1445
+ if (runMode === "multi") {
1446
+ var iterInput = document.getElementById("sched-create-iterations");
1447
+ maxIterations = iterInput ? (parseInt(iterInput.value, 10) || 3) : 3;
1448
+ if (maxIterations < 2) maxIterations = 2;
1449
+ if (maxIterations > 100) maxIterations = 100;
1450
+ }
1451
+
1452
+ if (createEditingRecId) {
1453
+ configCtx.send({
1454
+ type: "loop_registry_update",
1455
+ id: createEditingRecId,
1456
+ data: {
1457
+ name: name,
1458
+ description: description,
1459
+ date: dateVal,
1460
+ time: timeVal,
1461
+ allDay: false,
1462
+ cron: cron,
1463
+ enabled: cron ? true : false,
1464
+ color: createColor,
1465
+ recurrenceEnd: recurrenceEnd,
1466
+ intervalEnd: intervalEnd,
1467
+ maxIterations: maxIterations,
1468
+ skipIfRunning: skipIfRunning,
1469
+ },
1470
+ });
1471
+ } else {
1472
+ configCtx.send({
1473
+ type: "schedule_create",
1474
+ data: {
1475
+ name: name,
1476
+ taskId: taskId,
1477
+ description: description,
1478
+ date: dateVal,
1479
+ time: timeVal,
1480
+ allDay: false,
1481
+ cron: cron,
1482
+ enabled: cron ? true : false,
1483
+ color: createColor,
1484
+ recurrenceEnd: recurrenceEnd,
1485
+ intervalEnd: intervalEnd,
1486
+ maxIterations: maxIterations,
1487
+ skipIfRunning: skipIfRunning,
1488
+ },
1489
+ });
1490
+ }
1491
+
1492
+ closeCreateModal();
1493
+ });
1494
+ }
1495
+
1496
+ // --- Cron parser (client-side) ---
1497
+
1498
+ export function parseCronSimple(expr) {
1499
+ if (!expr) return null;
1500
+ var fields = expr.trim().split(/\s+/);
1501
+ if (fields.length !== 5) return null;
1502
+ return {
1503
+ minutes: parseField(fields[0], 0, 59),
1504
+ hours: parseField(fields[1], 0, 23),
1505
+ daysOfMonth: parseField(fields[2], 1, 31),
1506
+ months: parseField(fields[3], 1, 12),
1507
+ daysOfWeek: parseField(fields[4], 0, 6),
1508
+ };
1509
+ }
1510
+
1511
+ function parseField(field, min, max) {
1512
+ var values = [];
1513
+ var parts = field.split(",");
1514
+ for (var i = 0; i < parts.length; i++) {
1515
+ var part = parts[i].trim();
1516
+ if (part.indexOf("/") !== -1) {
1517
+ var sp = part.split("/");
1518
+ var step = parseInt(sp[1], 10);
1519
+ var rMin = min, rMax = max;
1520
+ if (sp[0] !== "*") { var rp = sp[0].split("-"); rMin = parseInt(rp[0], 10); rMax = rp.length > 1 ? parseInt(rp[1], 10) : rMin; }
1521
+ for (var v = rMin; v <= rMax; v += step) values.push(v);
1522
+ } else if (part === "*") {
1523
+ for (var v = min; v <= max; v++) values.push(v);
1524
+ } else if (part.indexOf("-") !== -1) {
1525
+ var rp = part.split("-");
1526
+ for (var v = parseInt(rp[0], 10); v <= parseInt(rp[1], 10); v++) values.push(v);
1527
+ } else {
1528
+ values.push(parseInt(part, 10));
1529
+ }
1530
+ }
1531
+ return values;
1532
+ }