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
@@ -5,9 +5,11 @@
5
5
  * Edit modal: change cron/name/enabled for existing records.
6
6
  */
7
7
 
8
- import { renderMarkdown } from './markdown.js';
9
8
  import { iconHtml } from './icons.js';
10
9
  import { showToast } from './utils.js';
10
+ import { initSchedulerConfig, setupCreateModal, openCreateModal, openCreateModalWithRecord, closeCreateModal, removePreview, getPreviewEl, showPreviewOnCell, showPreviewOnSlot, showPreviewForCreate, applyDraggedTask, parseCronSimple } from './scheduler-config.js';
11
+ import { initSchedulerHistory, renderHistory } from './scheduler-history.js';
12
+ export { handleLoopRegistryUpdated, handleLoopRegistryFiles, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled } from './scheduler-history.js';
11
13
 
12
14
  var ctx = null;
13
15
  var records = []; // all loop registry records
@@ -24,7 +26,6 @@ var showAllProjects = false; // toggle: show tasks from all projects (defa
24
26
  var currentProjectSlug = null; // derived from basePath on init
25
27
  var draggedTaskId = null; // drag-and-drop: task ID being dragged
26
28
  var draggedTaskName = null; // drag-and-drop: task name being dragged
27
- var previewEl = null; // temporary preview event element on calendar
28
29
  var craftingTaskId = null; // task ID currently being crafted
29
30
  var craftingSessionId = null; // session ID used for crafting
30
31
  var logPreviousSessionId = null; // session to restore when leaving log mode
@@ -47,22 +48,8 @@ var inputOrigNextSibling = null; // anchor for restoring input-area position
47
48
 
48
49
  // Edit state
49
50
 
50
- // Create popover state
51
- var createEditingRecId = null; // non-null when editing existing schedule
51
+ // Create popover state (most create* vars moved to scheduler-config.js)
52
52
  var createPopover = null;
53
- var createSelectedDate = null; // Date object for clicked calendar date
54
- var createRecurrence = "none"; // current recurrence selection
55
- var createCustomConfirmed = false; // whether custom repeat was confirmed via OK
56
- var createInterval = "none"; // current interval selection: "none", "1", "5", "15", "30", "60", "custom"
57
- var createIntervalCustom = null; // { value: N, unit: "minute"|"hour" } for custom interval
58
- var createIntervalEnd = "allday"; // "allday" | "after" | "until"
59
- var createIntervalEndAfter = 5; // occurrence count for "after" end type
60
- var createIntervalEndTime = ""; // HH:MM string for "until" end type
61
- var createColor = "#ffb86c"; // selected event color (default: accent)
62
- var createEndType = "never"; // "never" | "until" | "after"
63
- var createEndDate = null; // Date for "until" end type
64
- var createEndCalMonth = null; // Date tracking displayed month in end calendar
65
- var createEndAfter = 10; // occurrence count for "after" end type
66
53
  var weekTzAbbr = ""; // cached timezone abbreviation for week view
67
54
  var nowLineTimer = null; // interval timer for updating current-time indicator
68
55
 
@@ -96,9 +83,35 @@ export function initScheduler(_ctx) {
96
83
  });
97
84
  }
98
85
 
99
- // Create modal
86
+ // Create modal (extracted to scheduler-config.js)
87
+ initSchedulerConfig({
88
+ ctx: ctx,
89
+ getRecords: function () { return records; },
90
+ getCreatePopover: function () { return createPopover; },
91
+ getContentCalEl: function () { return contentCalEl; },
92
+ getDragState: function () { return { draggedTaskId: draggedTaskId, draggedTaskName: draggedTaskName }; },
93
+ clearDragState: function () { draggedTaskId = null; draggedTaskName = null; },
94
+ send: send,
95
+ pad: pad,
96
+ esc: esc,
97
+ detectInterval: detectInterval,
98
+ });
100
99
  setupCreateModal();
101
100
 
101
+ // History handlers (extracted to scheduler-history.js)
102
+ initSchedulerHistory({
103
+ isPanelOpen: function () { return panelOpen; },
104
+ getCurrentMode: function () { return currentMode; },
105
+ getSelectedTaskId: function () { return selectedTaskId; },
106
+ getContentDetailEl: function () { return contentDetailEl; },
107
+ setRecords: function (recs) { records = recs; },
108
+ renderSidebar: function () { renderSidebar(); },
109
+ render: function () { render(); },
110
+ renderDetail: function () { renderDetail(); },
111
+ send: send,
112
+ formatDateTime: formatDateTime,
113
+ });
114
+
102
115
  // Close popover on outside click
103
116
  document.addEventListener("click", function (e) {
104
117
  if (popoverEl && !popoverEl.classList.contains("hidden") && !popoverEl.contains(e.target)) {
@@ -1402,21 +1415,6 @@ function attachEventClicks(container, selector) {
1402
1415
  }
1403
1416
  }
1404
1417
 
1405
- function renderHistory(runs) {
1406
- var el = document.getElementById("sched-history");
1407
- if (!el || !runs || runs.length === 0) { if (el) el.innerHTML = '<div class="sched-history-empty">No runs yet</div>'; return; }
1408
- var html = "";
1409
- var sorted = runs.slice().reverse();
1410
- for (var i = 0; i < sorted.length; i++) {
1411
- var run = sorted[i];
1412
- html += '<div class="sched-history-item"><span class="sched-history-dot ' + (run.result || "") + '"></span>';
1413
- html += '<span class="sched-history-date">' + formatDateTime(new Date(run.startedAt)) + '</span>';
1414
- html += '<span class="sched-history-result">' + (run.result || "?") + '</span>';
1415
- html += '<span class="sched-history-iterations">' + (run.iterations || 0) + ' iter</span></div>';
1416
- }
1417
- el.innerHTML = html;
1418
- }
1419
-
1420
1418
  // --- Public API ---
1421
1419
 
1422
1420
  export function openSchedulerToTab(tab) {
@@ -1459,51 +1457,6 @@ export function exitCraftingMode(taskId) {
1459
1457
  }
1460
1458
  }
1461
1459
 
1462
- // --- Message handlers ---
1463
-
1464
- export function handleLoopRegistryUpdated(msg) {
1465
- records = msg.records || [];
1466
- if (panelOpen) {
1467
- renderSidebar();
1468
- if (currentMode === "calendar") render();
1469
- else if (currentMode === "detail") renderDetail();
1470
- }
1471
- }
1472
-
1473
- export function handleLoopRegistryFiles(msg) {
1474
- if (!panelOpen || currentMode !== "detail") return;
1475
- if (msg.id !== selectedTaskId) return;
1476
- var bodyEl2 = document.getElementById("scheduler-detail-body");
1477
- if (!bodyEl2) return;
1478
- var activeTab = contentDetailEl ? contentDetailEl.querySelector(".scheduler-detail-tab.active") : null;
1479
- var tab = activeTab ? activeTab.dataset.tab : "prompt";
1480
- if (tab === "prompt") {
1481
- bodyEl2.innerHTML = msg.prompt ? '<div class="md-content">' + renderMarkdown(msg.prompt) + '</div>' : '<div class="scheduler-empty">No PROMPT.md found</div>';
1482
- } else if (tab === "judge") {
1483
- bodyEl2.innerHTML = msg.judge ? '<div class="md-content">' + renderMarkdown(msg.judge) + '</div>' : '<div class="scheduler-empty">No JUDGE.md found</div>';
1484
- }
1485
- // Disable "Run now" if PROMPT.md or JUDGE.md is missing
1486
- var runBtn = contentDetailEl ? contentDetailEl.querySelector('[data-action="run"]') : null;
1487
- if (runBtn) {
1488
- var filesReady = !!msg.prompt;
1489
- runBtn.disabled = !filesReady;
1490
- runBtn.title = filesReady ? "Run now" : "PROMPT.md is required to run";
1491
- }
1492
- }
1493
-
1494
- export function handleScheduleRunStarted(msg) {
1495
- if (panelOpen) render();
1496
- }
1497
-
1498
- export function handleScheduleRunFinished(msg) {
1499
- send({ type: "loop_registry_list" });
1500
- }
1501
-
1502
- export function handleLoopScheduled(msg) {
1503
- // A loop was just registered as scheduled (from approval bar)
1504
- send({ type: "loop_registry_list" });
1505
- }
1506
-
1507
1460
  // Expose upcoming schedules (within given ms window) for countdown display
1508
1461
  // Always filters to current project only (countdown is project-specific)
1509
1462
  export function getUpcomingSchedules(windowMs) {
@@ -1545,7 +1498,7 @@ function attachCellClicks(container) {
1545
1498
  e.preventDefault();
1546
1499
  e.dataTransfer.dropEffect = "copy";
1547
1500
  cell.classList.add("drag-over");
1548
- if (!previewEl || previewEl.parentNode !== cell) {
1501
+ if (!getPreviewEl() || getPreviewEl().parentNode !== cell) {
1549
1502
  showPreviewOnCell(cell);
1550
1503
  }
1551
1504
  });
@@ -1595,7 +1548,7 @@ function attachWeekSlotClicks(container) {
1595
1548
  e.preventDefault();
1596
1549
  e.dataTransfer.dropEffect = "copy";
1597
1550
  slot.classList.add("drag-over");
1598
- if (!previewEl || !slot.closest(".scheduler-week-day-col").contains(previewEl)) {
1551
+ if (!getPreviewEl() || !slot.closest(".scheduler-week-day-col").contains(getPreviewEl())) {
1599
1552
  showPreviewOnSlot(slot);
1600
1553
  }
1601
1554
  });
@@ -1624,1480 +1577,6 @@ function attachWeekSlotClicks(container) {
1624
1577
  }
1625
1578
  }
1626
1579
 
1627
- // --- Create Popover (inline, Akiflow-style) ---
1628
-
1629
- function setupCreateModal() {
1630
- if (!createPopover) return;
1631
-
1632
- // Close
1633
- document.getElementById("sched-create-cancel").addEventListener("click", function () { closeCreateModal(); });
1634
-
1635
- // Color picker
1636
- var colorBtn = document.getElementById("sched-create-color-btn");
1637
- var colorPalette = document.getElementById("sched-create-color-palette");
1638
- if (colorBtn && colorPalette) {
1639
- colorBtn.addEventListener("click", function (e) {
1640
- e.stopPropagation();
1641
- colorPalette.classList.toggle("hidden");
1642
- });
1643
- var swatches = colorPalette.querySelectorAll(".sched-color-swatch");
1644
- for (var i = 0; i < swatches.length; i++) {
1645
- swatches[i].addEventListener("click", function (e) {
1646
- e.stopPropagation();
1647
- var c = this.dataset.color;
1648
- createColor = c;
1649
- var dot = document.getElementById("sched-create-color-dot");
1650
- if (dot) dot.style.background = c;
1651
- // update active state
1652
- var all = colorPalette.querySelectorAll(".sched-color-swatch");
1653
- for (var j = 0; j < all.length; j++) {
1654
- all[j].classList.toggle("active", all[j].dataset.color === c);
1655
- }
1656
- colorPalette.classList.add("hidden");
1657
- });
1658
- }
1659
- }
1660
-
1661
- // Date picker change → sync createSelectedDate and recurrence labels
1662
- var datePickerEl = document.getElementById("sched-create-date-picker");
1663
- if (datePickerEl) {
1664
- datePickerEl.addEventListener("change", function () {
1665
- var parts = this.value.split("-");
1666
- if (parts.length === 3) {
1667
- createSelectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
1668
- document.getElementById("sched-create-date").value = this.value;
1669
- updateRecurrenceLabels(createSelectedDate);
1670
- enforceMinTime();
1671
- }
1672
- });
1673
- }
1674
-
1675
- // Time input change — enforce min time when today is selected
1676
- var timeInputEl = document.getElementById("sched-create-time");
1677
- if (timeInputEl) {
1678
- timeInputEl.addEventListener("blur", function () {
1679
- enforceMinTime();
1680
- });
1681
- }
1682
-
1683
- // Task dropdown
1684
- var taskBtn = document.getElementById("sched-create-task-btn");
1685
- var taskList = document.getElementById("sched-create-task-list");
1686
- if (taskBtn && taskList) {
1687
- taskBtn.addEventListener("click", function (e) {
1688
- e.stopPropagation();
1689
- taskList.classList.toggle("hidden");
1690
- });
1691
- }
1692
-
1693
- // Close task dropdown on outside click
1694
- document.addEventListener("click", function (e) {
1695
- var tl = document.getElementById("sched-create-task-list");
1696
- if (tl && !tl.classList.contains("hidden")) {
1697
- if (!tl.contains(e.target) && !e.target.closest("#sched-create-task-btn")) {
1698
- tl.classList.add("hidden");
1699
- }
1700
- }
1701
- });
1702
-
1703
- // Recurrence button → toggle dropdown
1704
- document.getElementById("sched-create-recurrence-btn").addEventListener("click", function (e) {
1705
- e.stopPropagation();
1706
- var dd = document.getElementById("sched-create-recurrence-dropdown");
1707
- var btn = document.getElementById("sched-create-recurrence-btn");
1708
- // Close interval dropdown if open
1709
- document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
1710
- if (dd) {
1711
- var wasHidden = dd.classList.contains("hidden");
1712
- dd.classList.toggle("hidden");
1713
- if (wasHidden && btn) {
1714
- var bRect = btn.getBoundingClientRect();
1715
- var ddW = 280;
1716
- var ddLeft = bRect.left;
1717
- if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
1718
- if (ddLeft < 10) ddLeft = 10;
1719
- dd.style.left = ddLeft + "px";
1720
- dd.style.top = (bRect.bottom + 4) + "px";
1721
- }
1722
- }
1723
- });
1724
-
1725
- // --- Interval button + dropdown ---
1726
- var intervalSnapshot = null;
1727
- document.getElementById("sched-create-interval-btn").addEventListener("click", function (e) {
1728
- e.stopPropagation();
1729
- var dd = document.getElementById("sched-create-interval-dropdown");
1730
- var btn = document.getElementById("sched-create-interval-btn");
1731
- // Close recurrence dropdown if open
1732
- document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
1733
- if (dd) {
1734
- var wasHidden = dd.classList.contains("hidden");
1735
- dd.classList.toggle("hidden");
1736
- if (wasHidden && btn) {
1737
- // Save snapshot for cancel
1738
- intervalSnapshot = {
1739
- interval: createInterval,
1740
- custom: createIntervalCustom ? { value: createIntervalCustom.value, unit: createIntervalCustom.unit } : null,
1741
- end: createIntervalEnd,
1742
- endAfter: createIntervalEndAfter,
1743
- endTime: createIntervalEndTime
1744
- };
1745
- var bRect = btn.getBoundingClientRect();
1746
- var ddW = 220;
1747
- var ddLeft = bRect.left;
1748
- if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
1749
- if (ddLeft < 10) ddLeft = 10;
1750
- dd.style.left = ddLeft + "px";
1751
- dd.style.top = (bRect.bottom + 4) + "px";
1752
- // Auto-apply custom interval when opening
1753
- if (createInterval === "none") {
1754
- applyInlineInterval();
1755
- }
1756
- }
1757
- }
1758
- });
1759
-
1760
- // Interval Cancel button - revert to snapshot
1761
- document.getElementById("sched-interval-cancel").addEventListener("click", function (e) {
1762
- e.stopPropagation();
1763
- if (intervalSnapshot) {
1764
- createInterval = intervalSnapshot.interval;
1765
- createIntervalCustom = intervalSnapshot.custom;
1766
- createIntervalEnd = intervalSnapshot.end;
1767
- createIntervalEndAfter = intervalSnapshot.endAfter;
1768
- createIntervalEndTime = intervalSnapshot.endTime;
1769
- updateIntervalBtn();
1770
- }
1771
- document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
1772
- });
1773
-
1774
- // Interval OK button - accept and close
1775
- document.getElementById("sched-interval-ok").addEventListener("click", function (e) {
1776
- e.stopPropagation();
1777
- applyInlineInterval();
1778
- document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
1779
- });
1780
-
1781
- // Interval custom input
1782
- var intCustomValue = document.getElementById("sched-interval-custom-value");
1783
- var intUnitSegs = document.querySelectorAll(".sched-interval-seg");
1784
- function getIntervalUnit() {
1785
- for (var s = 0; s < intUnitSegs.length; s++) {
1786
- if (intUnitSegs[s].classList.contains("active")) return intUnitSegs[s].dataset.unit;
1787
- }
1788
- return "minute";
1789
- }
1790
- function applyInlineInterval() {
1791
- var v = parseInt(intCustomValue.value, 10) || 1;
1792
- var u = getIntervalUnit();
1793
- createInterval = "custom";
1794
- createIntervalCustom = { value: v, unit: u };
1795
- updateIntervalBtn();
1796
- }
1797
- intCustomValue.addEventListener("change", applyInlineInterval);
1798
- for (var si = 0; si < intUnitSegs.length; si++) {
1799
- (function (seg) {
1800
- seg.addEventListener("click", function (e) {
1801
- e.stopPropagation();
1802
- for (var s = 0; s < intUnitSegs.length; s++) {
1803
- intUnitSegs[s].classList.toggle("active", intUnitSegs[s] === seg);
1804
- }
1805
- applyInlineInterval();
1806
- });
1807
- })(intUnitSegs[si]);
1808
- }
1809
-
1810
- // Interval end condition options
1811
- var iendOpts = document.querySelectorAll(".sched-interval-end-opt");
1812
- for (var ie = 0; ie < iendOpts.length; ie++) {
1813
- (function (opt) {
1814
- opt.addEventListener("click", function (e) {
1815
- e.stopPropagation();
1816
- var val = opt.dataset.iend;
1817
- for (var j = 0; j < iendOpts.length; j++) {
1818
- iendOpts[j].classList.toggle("active", iendOpts[j] === opt);
1819
- }
1820
- createIntervalEnd = val;
1821
- var afterRow = document.getElementById("sched-interval-end-after-row");
1822
- var untilRow = document.getElementById("sched-interval-end-until-row");
1823
- if (afterRow) afterRow.classList.toggle("hidden", val !== "after");
1824
- if (untilRow) untilRow.classList.toggle("hidden", val !== "until");
1825
- });
1826
- })(iendOpts[ie]);
1827
- }
1828
-
1829
- var iendAfterInput = document.getElementById("sched-interval-end-after");
1830
- if (iendAfterInput) {
1831
- iendAfterInput.addEventListener("change", function () {
1832
- createIntervalEndAfter = parseInt(this.value, 10) || 5;
1833
- if (createIntervalEndAfter < 1) createIntervalEndAfter = 1;
1834
- });
1835
- }
1836
-
1837
- var iendTimeInput = document.getElementById("sched-interval-end-time");
1838
- if (iendTimeInput) {
1839
- iendTimeInput.addEventListener("change", function () {
1840
- createIntervalEndTime = this.value;
1841
- });
1842
- }
1843
-
1844
- // Custom repeat: cancel
1845
- document.getElementById("sched-custom-cancel").addEventListener("click", function (e) {
1846
- e.stopPropagation();
1847
- document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
1848
- });
1849
-
1850
- // Custom repeat: unit change
1851
- document.getElementById("sched-custom-unit").addEventListener("change", function () {
1852
- var dowSection = document.getElementById("sched-custom-dow-section");
1853
- if (dowSection) dowSection.style.display = this.value === "week" ? "" : "none";
1854
- });
1855
-
1856
- // Custom repeat: DOW toggle
1857
- var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
1858
- for (var i = 0; i < customDowBtns.length; i++) {
1859
- (function (btn) {
1860
- btn.addEventListener("click", function (e) { e.stopPropagation(); btn.classList.toggle("active"); });
1861
- })(customDowBtns[i]);
1862
- }
1863
-
1864
- // Custom repeat: End type JS dropdown
1865
- var endBtn = document.getElementById("sched-custom-end-btn");
1866
- var endList = document.getElementById("sched-custom-end-list");
1867
-
1868
- endBtn.addEventListener("click", function (e) {
1869
- e.stopPropagation();
1870
- if (endList.classList.contains("hidden")) {
1871
- var r = endBtn.getBoundingClientRect();
1872
- endList.style.left = r.left + "px";
1873
- endList.style.top = (r.bottom + 4) + "px";
1874
- // If it would overflow bottom, show above
1875
- endList.classList.remove("hidden");
1876
- var lr = endList.getBoundingClientRect();
1877
- if (lr.bottom > window.innerHeight - 8) {
1878
- endList.style.top = (r.top - lr.height - 4) + "px";
1879
- }
1880
- } else {
1881
- endList.classList.add("hidden");
1882
- }
1883
- });
1884
-
1885
- var endItems = endList.querySelectorAll(".sched-custom-end-item");
1886
- for (var ei = 0; ei < endItems.length; ei++) {
1887
- (function (item) {
1888
- item.addEventListener("click", function (e) {
1889
- e.stopPropagation();
1890
- var val = item.dataset.value;
1891
- createEndType = val;
1892
- document.getElementById("sched-custom-end").value = val;
1893
- document.getElementById("sched-custom-end-label").textContent = item.textContent;
1894
-
1895
- // Update active state
1896
- for (var j = 0; j < endItems.length; j++) {
1897
- endItems[j].classList.toggle("active", endItems[j] === item);
1898
- }
1899
- endList.classList.add("hidden");
1900
-
1901
- // Toggle conditional inputs
1902
- var dateBtn2 = document.getElementById("sched-custom-end-date-btn");
1903
- var afterWrap = document.getElementById("sched-custom-end-after-wrap");
1904
- var calPanel = document.getElementById("sched-custom-end-calendar");
1905
-
1906
- dateBtn2.classList.add("hidden");
1907
- afterWrap.classList.add("hidden");
1908
- calPanel.classList.add("hidden");
1909
-
1910
- if (val === "until") {
1911
- dateBtn2.classList.remove("hidden");
1912
- if (!createEndDate) {
1913
- createEndDate = new Date(createSelectedDate || new Date());
1914
- createEndDate.setMonth(createEndDate.getMonth() + 1);
1915
- }
1916
- updateEndDateLabel();
1917
- } else if (val === "after") {
1918
- afterWrap.classList.remove("hidden");
1919
- document.getElementById("sched-custom-end-after").value = createEndAfter;
1920
- }
1921
- });
1922
- })(endItems[ei]);
1923
- }
1924
-
1925
- // Close end dropdown on outside click
1926
- document.addEventListener("click", function (e) {
1927
- if (endList && !endList.classList.contains("hidden")) {
1928
- if (!endList.contains(e.target) && !endBtn.contains(e.target)) {
1929
- endList.classList.add("hidden");
1930
- }
1931
- }
1932
- });
1933
-
1934
- // Custom repeat: End date button → toggle inline calendar
1935
- document.getElementById("sched-custom-end-date-btn").addEventListener("click", function (e) {
1936
- e.stopPropagation();
1937
- var calPanel = document.getElementById("sched-custom-end-calendar");
1938
- if (calPanel.classList.contains("hidden")) {
1939
- createEndCalMonth = new Date(createEndDate.getFullYear(), createEndDate.getMonth(), 1);
1940
- renderEndCalendar();
1941
- calPanel.classList.remove("hidden");
1942
- try { lucide.createIcons({ node: calPanel }); } catch (ex) {}
1943
- } else {
1944
- calPanel.classList.add("hidden");
1945
- }
1946
- });
1947
-
1948
- // Custom repeat: End calendar prev/next
1949
- document.getElementById("sched-cal-prev").addEventListener("click", function (e) {
1950
- e.stopPropagation();
1951
- createEndCalMonth.setMonth(createEndCalMonth.getMonth() - 1);
1952
- renderEndCalendar();
1953
- });
1954
- document.getElementById("sched-cal-next").addEventListener("click", function (e) {
1955
- e.stopPropagation();
1956
- createEndCalMonth.setMonth(createEndCalMonth.getMonth() + 1);
1957
- renderEndCalendar();
1958
- });
1959
-
1960
- // Custom repeat: After occurrences input
1961
- document.getElementById("sched-custom-end-after").addEventListener("change", function () {
1962
- createEndAfter = parseInt(this.value, 10) || 10;
1963
- if (createEndAfter < 1) { createEndAfter = 1; this.value = 1; }
1964
- });
1965
-
1966
- // Custom repeat: OK
1967
- document.getElementById("sched-custom-ok").addEventListener("click", function (e) {
1968
- e.stopPropagation();
1969
- createRecurrence = "custom";
1970
- createCustomConfirmed = true;
1971
- document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
1972
- updateRecurrenceBtn();
1973
- });
1974
-
1975
- // Run mode toggle (single vs multi-round)
1976
- var runModeContainer = createPopover.querySelector(".sched-create-run-mode");
1977
- if (runModeContainer) {
1978
- runModeContainer.addEventListener("click", function (e) {
1979
- var btn = e.target.closest(".sched-run-mode-btn");
1980
- if (!btn) return;
1981
- var mode = btn.dataset.mode;
1982
- var btns = runModeContainer.querySelectorAll(".sched-run-mode-btn");
1983
- for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i] === btn);
1984
- var iterGroup = document.getElementById("sched-create-iter-group");
1985
- if (iterGroup) iterGroup.classList.toggle("hidden", mode !== "multi");
1986
- });
1987
- }
1988
-
1989
- // Submit
1990
- document.getElementById("sched-create-submit").addEventListener("click", function () { submitCreateSchedule(); });
1991
-
1992
- // Delete button → close popover, then open dialog
1993
- var deleteBtn = document.getElementById("sched-create-delete");
1994
- var deleteDialog = document.getElementById("sched-delete-dialog");
1995
- if (deleteBtn) {
1996
- deleteBtn.addEventListener("click", function (e) {
1997
- e.stopPropagation();
1998
- if (!createEditingRecId) return;
1999
- var rec = null;
2000
- for (var j = 0; j < records.length; j++) {
2001
- if (records[j].id === createEditingRecId) { rec = records[j]; break; }
2002
- }
2003
- if (!rec) return;
2004
- // Save context before closing popover
2005
- var deleteRecId = createEditingRecId;
2006
- var deleteDate = createSelectedDate ? new Date(createSelectedDate) : null;
2007
- closeCreateModal();
2008
- openDeleteDialog(deleteRecId, deleteDate, !rec.cron);
2009
- });
2010
- }
2011
-
2012
- // Delete dialog option handlers
2013
- if (deleteDialog) {
2014
- var deleteOptions = deleteDialog.querySelectorAll(".sched-delete-option");
2015
- for (var i = 0; i < deleteOptions.length; i++) {
2016
- (function (opt) {
2017
- opt.addEventListener("click", function (e) {
2018
- e.stopPropagation();
2019
- var action = opt.dataset.delete;
2020
- if (action === "cancel") {
2021
- closeDeleteDialog();
2022
- return;
2023
- }
2024
- var recId = deleteDialog.dataset.recId;
2025
- var dateStr = deleteDialog.dataset.eventDate;
2026
- if (!recId) return;
2027
- if (action === "this") {
2028
- if (dateStr) {
2029
- var dp = dateStr.split("-");
2030
- var next = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
2031
- next.setDate(next.getDate() + 1);
2032
- var newDate = next.getFullYear() + "-" + pad(next.getMonth() + 1) + "-" + pad(next.getDate());
2033
- send({ type: "loop_registry_update", id: recId, data: { date: newDate } });
2034
- }
2035
- } else if (action === "following") {
2036
- if (dateStr) {
2037
- var dp2 = dateStr.split("-");
2038
- var prev = new Date(parseInt(dp2[0], 10), parseInt(dp2[1], 10) - 1, parseInt(dp2[2], 10));
2039
- prev.setDate(prev.getDate() - 1);
2040
- var endDate = prev.getFullYear() + "-" + pad(prev.getMonth() + 1) + "-" + pad(prev.getDate());
2041
- send({ type: "loop_registry_update", id: recId, data: { recurrenceEnd: { type: "until", date: endDate } } });
2042
- }
2043
- } else if (action === "all") {
2044
- send({ type: "loop_registry_remove", id: recId });
2045
- }
2046
- closeDeleteDialog();
2047
- });
2048
- })(deleteOptions[i]);
2049
- }
2050
- // Close on backdrop click
2051
- deleteDialog.addEventListener("click", function (e) {
2052
- if (e.target === deleteDialog) closeDeleteDialog();
2053
- });
2054
- }
2055
-
2056
- // Close color palette on any click outside it
2057
- document.addEventListener("click", function (e) {
2058
- var pal = document.getElementById("sched-create-color-palette");
2059
- if (pal && !pal.classList.contains("hidden")) {
2060
- if (!pal.contains(e.target) && !e.target.closest("#sched-create-color-btn")) {
2061
- pal.classList.add("hidden");
2062
- }
2063
- }
2064
- });
2065
-
2066
- // Close popover on outside click
2067
- document.addEventListener("click", function (e) {
2068
- if (!createPopover || createPopover.classList.contains("hidden")) return;
2069
- if (createPopover.contains(e.target)) return;
2070
- // Also ignore clicks on calendar cells (they open the popover)
2071
- if (e.target.closest(".scheduler-cell") || e.target.closest(".scheduler-week-slot")) return;
2072
- closeCreateModal();
2073
- });
2074
-
2075
- // Escape key
2076
- document.addEventListener("keydown", function (e) {
2077
- if (e.key === "Escape" && createPopover && !createPopover.classList.contains("hidden")) {
2078
- // Close recurrence dropdown first if open
2079
- var dd = document.getElementById("sched-create-recurrence-dropdown");
2080
- if (dd && !dd.classList.contains("hidden")) {
2081
- dd.classList.add("hidden");
2082
- return;
2083
- }
2084
- closeCreateModal();
2085
- }
2086
- });
2087
- }
2088
-
2089
- function updateRecurrenceBtn() {
2090
- var btn = document.getElementById("sched-create-recurrence-btn");
2091
- if (btn) {
2092
- btn.classList.toggle("has-recurrence", createRecurrence !== "none");
2093
- }
2094
- }
2095
-
2096
- /**
2097
- * When today is selected, set min on the time input so past times appear disabled.
2098
- * If the current time value is before now, bump it to the next quarter-hour.
2099
- */
2100
- function enforceMinTime() {
2101
- var timeInput = document.getElementById("sched-create-time");
2102
- var datePicker = document.getElementById("sched-create-date-picker");
2103
- if (!timeInput || !datePicker) return;
2104
-
2105
- var now = new Date();
2106
- var todayStr = now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate());
2107
- var isToday = datePicker.value === todayStr;
2108
-
2109
- if (isToday) {
2110
- // Round up to the next minute for min
2111
- var minMinutes = now.getHours() * 60 + now.getMinutes() + 1;
2112
- var minH = Math.floor(minMinutes / 60);
2113
- var minM = minMinutes % 60;
2114
- if (minH >= 24) { minH = 23; minM = 59; }
2115
- var minVal = pad(minH) + ":" + pad(minM);
2116
- timeInput.min = minVal;
2117
-
2118
- // If current value is before min, bump it
2119
- if (timeInput.value < minVal) {
2120
- timeInput.value = minVal;
2121
- }
2122
- } else {
2123
- timeInput.removeAttribute("min");
2124
- }
2125
- }
2126
-
2127
- function updateIntervalBtn() {
2128
- var btn = document.getElementById("sched-create-interval-btn");
2129
- if (btn) {
2130
- btn.classList.toggle("has-recurrence", createInterval !== "none");
2131
- }
2132
- // Show/hide interval end conditions section
2133
- var endSection = document.getElementById("sched-interval-end-section");
2134
- if (endSection) {
2135
- endSection.classList.toggle("hidden", createInterval === "none");
2136
- }
2137
- // Always show time picker (start time is needed even with interval)
2138
- // Update skip-if-running visibility
2139
- updateRecurrenceBtn();
2140
- }
2141
-
2142
- function removePreview() {
2143
- if (previewEl && previewEl.parentNode) {
2144
- previewEl.parentNode.removeChild(previewEl);
2145
- }
2146
- previewEl = null;
2147
- }
2148
-
2149
- function showPreviewOnCell(cell) {
2150
- removePreview();
2151
- var label = draggedTaskName || "(No title)";
2152
- var el = document.createElement("div");
2153
- el.className = "scheduler-event preview";
2154
- el.textContent = label;
2155
- cell.appendChild(el);
2156
- previewEl = el;
2157
- }
2158
-
2159
- function showPreviewOnSlot(slot) {
2160
- removePreview();
2161
- var label = draggedTaskName || "(No title)";
2162
- var hour = parseInt(slot.dataset.hour, 10);
2163
- var quarter = parseInt(slot.dataset.quarter || "0", 10);
2164
- var minute = quarter * 15;
2165
- var timeStr = pad(hour) + ":" + pad(minute);
2166
- var col = slot.closest(".scheduler-week-day-col");
2167
- if (!col) return;
2168
- var topPct = ((hour * 60 + minute) / 1440) * 100;
2169
- var el = document.createElement("div");
2170
- el.className = "scheduler-week-event preview";
2171
- el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
2172
- el.textContent = timeStr + " " + label;
2173
- col.appendChild(el);
2174
- previewEl = el;
2175
- }
2176
-
2177
- function showPreviewForCreate(anchorEl, label) {
2178
- removePreview();
2179
- if (!anchorEl) return;
2180
- var text = label || "(No title)";
2181
- if (anchorEl.classList.contains("scheduler-week-slot")) {
2182
- var hour = parseInt(anchorEl.dataset.hour, 10);
2183
- var quarter = parseInt(anchorEl.dataset.quarter || "0", 10);
2184
- var minute = quarter * 15;
2185
- var timeStr = pad(hour) + ":" + pad(minute);
2186
- var col = anchorEl.closest(".scheduler-week-day-col");
2187
- if (!col) return;
2188
- var topPct = ((hour * 60 + minute) / 1440) * 100;
2189
- var el = document.createElement("div");
2190
- el.className = "scheduler-week-event preview";
2191
- el.style.cssText = "top:" + topPct + "%;height:calc(160vh / 48)";
2192
- el.textContent = timeStr + " " + text;
2193
- col.appendChild(el);
2194
- previewEl = el;
2195
- } else if (anchorEl.classList.contains("scheduler-cell")) {
2196
- var el = document.createElement("div");
2197
- el.className = "scheduler-event preview";
2198
- el.textContent = text;
2199
- anchorEl.appendChild(el);
2200
- previewEl = el;
2201
- }
2202
- }
2203
-
2204
- function applyDraggedTask() {
2205
- if (!draggedTaskId) return;
2206
- var taskHidden = document.getElementById("sched-create-task");
2207
- var taskLabel = document.getElementById("sched-create-task-label");
2208
- var taskBtn = document.getElementById("sched-create-task-btn");
2209
- if (taskHidden) taskHidden.value = draggedTaskId;
2210
- if (taskLabel) taskLabel.textContent = draggedTaskName || draggedTaskId;
2211
- if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
2212
- // Mark the matching item as selected in the dropdown list
2213
- var taskListEl = document.getElementById("sched-create-task-list");
2214
- if (taskListEl) {
2215
- var items = taskListEl.querySelectorAll(".sched-create-task-item");
2216
- for (var k = 0; k < items.length; k++) {
2217
- items[k].classList.toggle("selected", items[k].dataset.taskId === draggedTaskId);
2218
- }
2219
- }
2220
- // Auto-generate title: "taskName - HH:MM"
2221
- var titleInput = document.getElementById("sched-create-title");
2222
- var timeInput = document.getElementById("sched-create-time");
2223
- if (titleInput && (draggedTaskName || draggedTaskId)) {
2224
- var name = draggedTaskName || draggedTaskId;
2225
- var time = timeInput ? timeInput.value : "";
2226
- titleInput.value = time ? name + " - " + time : name;
2227
- }
2228
- // Update preview text to match auto-title
2229
- if (previewEl && titleInput) {
2230
- var previewText = titleInput.value || "(No title)";
2231
- if (previewEl.classList.contains("scheduler-week-event") && timeInput) {
2232
- previewText = timeInput.value + " " + (titleInput.value || "(No title)");
2233
- }
2234
- previewEl.textContent = previewText;
2235
- }
2236
- draggedTaskId = null;
2237
- draggedTaskName = null;
2238
- }
2239
-
2240
- function openCreateModalWithRecord(rec, anchorEl) {
2241
- // Parse date/time from record
2242
- var date = null;
2243
- var hour = null;
2244
- if (rec.date) {
2245
- var dp = rec.date.split("-");
2246
- date = new Date(parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10));
2247
- }
2248
- if (rec.time) {
2249
- var tp = rec.time.split(":");
2250
- hour = parseInt(tp[0], 10) || 0;
2251
- var mins = parseInt(tp[1], 10) || 0;
2252
- if (date) { date.setHours(hour, mins, 0); }
2253
- }
2254
- // Mark as editing existing record
2255
- createEditingRecId = rec.id;
2256
-
2257
- // Open the create modal normally first
2258
- openCreateModal(date || new Date(), hour, anchorEl);
2259
-
2260
- // Show delete button
2261
- var deleteBtn = document.getElementById("sched-create-delete");
2262
- if (deleteBtn) deleteBtn.classList.remove("hidden");
2263
-
2264
- // Now override with record values
2265
- var titleInput = document.getElementById("sched-create-title");
2266
- if (titleInput) titleInput.value = rec.name || "";
2267
-
2268
- var descInput = document.getElementById("sched-create-desc");
2269
- if (descInput) descInput.value = rec.description || "";
2270
-
2271
- // Set color
2272
- if (rec.color) {
2273
- createColor = rec.color;
2274
- var colorDot = document.getElementById("sched-create-color-dot");
2275
- if (colorDot) colorDot.style.background = createColor;
2276
- var swatches = createPopover.querySelectorAll(".sched-color-swatch");
2277
- for (var si = 0; si < swatches.length; si++) {
2278
- swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
2279
- }
2280
- }
2281
-
2282
- // Set skip-if-running
2283
- var skipRunningEl = document.getElementById("sched-skip-running");
2284
- if (skipRunningEl) skipRunningEl.checked = rec.skipIfRunning !== false;
2285
-
2286
- // Set run mode and iterations
2287
- var editRunMode = (rec.maxIterations && rec.maxIterations > 1) ? "multi" : "single";
2288
- var editRunBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
2289
- for (var rb = 0; rb < editRunBtns.length; rb++) {
2290
- editRunBtns[rb].classList.toggle("active", editRunBtns[rb].dataset.mode === editRunMode);
2291
- }
2292
- var editIterGroup = document.getElementById("sched-create-iter-group");
2293
- if (editIterGroup) editIterGroup.classList.toggle("hidden", editRunMode !== "multi");
2294
- if (rec.maxIterations && rec.maxIterations > 1) {
2295
- var iterInput = document.getElementById("sched-create-iterations");
2296
- if (iterInput) iterInput.value = rec.maxIterations;
2297
- }
2298
-
2299
- // Set linked task
2300
- if (rec.linkedTaskId) {
2301
- var taskHidden = document.getElementById("sched-create-task");
2302
- var taskLabel = document.getElementById("sched-create-task-label");
2303
- var taskBtn = document.getElementById("sched-create-task-btn");
2304
- var taskListEl = document.getElementById("sched-create-task-list");
2305
- if (taskHidden) taskHidden.value = rec.linkedTaskId;
2306
- // Find the task name
2307
- var taskName = rec.linkedTaskId;
2308
- for (var j = 0; j < records.length; j++) {
2309
- if (records[j].id === rec.linkedTaskId) { taskName = records[j].name || records[j].id; break; }
2310
- }
2311
- if (taskLabel) taskLabel.textContent = taskName;
2312
- if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
2313
- if (taskListEl) {
2314
- var items = taskListEl.querySelectorAll(".sched-create-task-item");
2315
- for (var k = 0; k < items.length; k++) {
2316
- items[k].classList.toggle("selected", items[k].dataset.taskId === rec.linkedTaskId);
2317
- }
2318
- }
2319
- }
2320
-
2321
- // Restore interval from cron
2322
- if (rec.cron) {
2323
- var cronParts = rec.cron.trim().split(/\s+/);
2324
- if (cronParts.length === 5) {
2325
- var detectedMinInterval = null;
2326
- var detectedHrInterval = null;
2327
- // Detect minute-level interval: e.g. "0,5,10,... * * * *" or "*/5 * * * *"
2328
- if (cronParts[1] === "*" && cronParts[2] === "*") {
2329
- detectedMinInterval = detectInterval(cronParts[0], 60);
2330
- }
2331
- // Detect hour-level interval: e.g. "0 1,3,5,... * * *"
2332
- if (!detectedMinInterval && cronParts[2] === "*") {
2333
- detectedHrInterval = detectInterval(cronParts[1], 24);
2334
- }
2335
-
2336
- if (detectedMinInterval) {
2337
- createInterval = "custom";
2338
- createIntervalCustom = { value: detectedMinInterval, unit: "minute" };
2339
- var intValEl = document.getElementById("sched-interval-custom-value");
2340
- if (intValEl) intValEl.value = detectedMinInterval;
2341
- var intUnitBtns = document.querySelectorAll("#sched-interval-custom-unit .sched-interval-seg");
2342
- for (var iu = 0; iu < intUnitBtns.length; iu++) {
2343
- intUnitBtns[iu].classList.toggle("active", intUnitBtns[iu].dataset.unit === "minute");
2344
- }
2345
- updateIntervalBtn();
2346
- } else if (detectedHrInterval) {
2347
- createInterval = "custom";
2348
- createIntervalCustom = { value: detectedHrInterval, unit: "hour" };
2349
- var intValEl2 = document.getElementById("sched-interval-custom-value");
2350
- if (intValEl2) intValEl2.value = detectedHrInterval;
2351
- var intUnitBtns2 = document.querySelectorAll("#sched-interval-custom-unit .sched-interval-seg");
2352
- for (var iu2 = 0; iu2 < intUnitBtns2.length; iu2++) {
2353
- intUnitBtns2[iu2].classList.toggle("active", intUnitBtns2[iu2].dataset.unit === "hour");
2354
- }
2355
- updateIntervalBtn();
2356
- }
2357
-
2358
- // Restore recurrence from cron (if no interval detected, or combined with interval)
2359
- if (!detectedMinInterval && !detectedHrInterval) {
2360
- var cronDow = cronParts[4];
2361
- var cronDom = cronParts[2];
2362
- var cronMonth = cronParts[3];
2363
- if (cronDow === "*" && cronDom === "*" && cronMonth === "*") {
2364
- createRecurrence = "daily";
2365
- } else if (cronDow === "1-5" && cronDom === "*") {
2366
- createRecurrence = "weekdays";
2367
- } else if (cronDom !== "*" && cronMonth !== "*") {
2368
- createRecurrence = "yearly";
2369
- } else if (cronDom !== "*" && cronDow === "*") {
2370
- createRecurrence = "monthly";
2371
- } else if (cronDow !== "*" && cronDom === "*") {
2372
- // Check if it matches a single day (weekly)
2373
- var dowVals = cronDow.split(",");
2374
- if (dowVals.length === 1) {
2375
- createRecurrence = "weekly";
2376
- } else if (dowVals.length === 7) {
2377
- createRecurrence = "daily";
2378
- } else {
2379
- createRecurrence = "custom";
2380
- createCustomConfirmed = true;
2381
- // Set custom panel values
2382
- document.getElementById("sched-custom-interval").value = "1";
2383
- document.getElementById("sched-custom-unit").value = "week";
2384
- var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
2385
- for (var cd = 0; cd < customDowBtns.length; cd++) {
2386
- customDowBtns[cd].classList.toggle("active", dowVals.indexOf(customDowBtns[cd].dataset.dow) !== -1);
2387
- }
2388
- }
2389
- }
2390
- updateRecurrenceBtn();
2391
- }
2392
- }
2393
- }
2394
-
2395
- // Restore interval end conditions
2396
- if (rec.intervalEnd) {
2397
- createIntervalEnd = rec.intervalEnd.type || "allday";
2398
- var editIendOpts = document.querySelectorAll(".sched-interval-end-opt");
2399
- for (var ei = 0; ei < editIendOpts.length; ei++) {
2400
- editIendOpts[ei].classList.toggle("active", editIendOpts[ei].dataset.iend === createIntervalEnd);
2401
- }
2402
- var editAfterRow = document.getElementById("sched-interval-end-after-row");
2403
- var editUntilRow = document.getElementById("sched-interval-end-until-row");
2404
- if (createIntervalEnd === "after") {
2405
- createIntervalEndAfter = rec.intervalEnd.count || 5;
2406
- if (editAfterRow) editAfterRow.classList.remove("hidden");
2407
- if (editUntilRow) editUntilRow.classList.add("hidden");
2408
- var editAfterInput = document.getElementById("sched-interval-end-after");
2409
- if (editAfterInput) editAfterInput.value = createIntervalEndAfter;
2410
- } else if (createIntervalEnd === "until") {
2411
- createIntervalEndTime = rec.intervalEnd.time || "18:00";
2412
- if (editAfterRow) editAfterRow.classList.add("hidden");
2413
- if (editUntilRow) editUntilRow.classList.remove("hidden");
2414
- var editTimeInput = document.getElementById("sched-interval-end-time");
2415
- if (editTimeInput) editTimeInput.value = createIntervalEndTime;
2416
- }
2417
- }
2418
-
2419
- // Update preview to show record name
2420
- if (previewEl) {
2421
- var previewText = rec.name || "(No title)";
2422
- if (previewEl.classList.contains("scheduler-week-event") && rec.time) {
2423
- previewText = rec.time + " " + previewText;
2424
- }
2425
- previewEl.textContent = previewText;
2426
- }
2427
- }
2428
-
2429
- function openCreateModal(date, hour, anchorEl) {
2430
- if (!createPopover) return;
2431
- // Reset editing state (openCreateModalWithRecord sets this before calling us)
2432
- if (!createEditingRecId) {
2433
- var deleteBtn = document.getElementById("sched-create-delete");
2434
- if (deleteBtn) deleteBtn.classList.add("hidden");
2435
- }
2436
- createSelectedDate = date || new Date();
2437
- createRecurrence = "none";
2438
- createCustomConfirmed = false;
2439
- createInterval = "none";
2440
- createIntervalCustom = null;
2441
- createIntervalEnd = "allday";
2442
- createIntervalEndAfter = 5;
2443
- createIntervalEndTime = "";
2444
- createColor = "#ffb86c";
2445
-
2446
- // Reset form
2447
- document.getElementById("sched-create-title").value = "";
2448
- document.getElementById("sched-create-desc").value = "";
2449
- var iterReset = document.getElementById("sched-create-iterations");
2450
- if (iterReset) iterReset.value = "3";
2451
- // Reset run mode to single
2452
- var runModeBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
2453
- for (var rm = 0; rm < runModeBtns.length; rm++) {
2454
- runModeBtns[rm].classList.toggle("active", runModeBtns[rm].dataset.mode === "single");
2455
- }
2456
- var iterGroup = document.getElementById("sched-create-iter-group");
2457
- if (iterGroup) iterGroup.classList.add("hidden");
2458
-
2459
- // Reset color
2460
- var colorDot = document.getElementById("sched-create-color-dot");
2461
- if (colorDot) colorDot.style.background = createColor;
2462
- var palette = document.getElementById("sched-create-color-palette");
2463
- if (palette) palette.classList.add("hidden");
2464
- var swatches = createPopover.querySelectorAll(".sched-color-swatch");
2465
- for (var si = 0; si < swatches.length; si++) {
2466
- swatches[si].classList.toggle("active", swatches[si].dataset.color === createColor);
2467
- }
2468
-
2469
- // Populate task dropdown (only tasks — exclude ralph and schedule)
2470
- var taskHidden = document.getElementById("sched-create-task");
2471
- var taskLabel = document.getElementById("sched-create-task-label");
2472
- var taskBtn = document.getElementById("sched-create-task-btn");
2473
- var taskListEl = document.getElementById("sched-create-task-list");
2474
- if (taskHidden) taskHidden.value = "";
2475
- if (taskLabel) taskLabel.textContent = "Select a task";
2476
- if (taskBtn) { taskBtn.classList.remove("has-value"); taskBtn.classList.remove("invalid"); }
2477
- if (taskListEl) {
2478
- taskListEl.classList.add("hidden");
2479
- var tasks = records.filter(function (r) { return r.source !== "ralph" && r.source !== "schedule"; });
2480
- if (tasks.length === 0) {
2481
- taskListEl.innerHTML = '<div class="sched-create-task-empty">No tasks available</div>';
2482
- } else {
2483
- var html = "";
2484
- for (var i = 0; i < tasks.length; i++) {
2485
- html += '<div class="sched-create-task-item" data-task-id="' + esc(tasks[i].id) + '">' + esc(tasks[i].name || tasks[i].id) + '</div>';
2486
- }
2487
- taskListEl.innerHTML = html;
2488
- // Bind click handlers
2489
- var items = taskListEl.querySelectorAll(".sched-create-task-item");
2490
- for (var j = 0; j < items.length; j++) {
2491
- (function (item) {
2492
- item.addEventListener("click", function (e) {
2493
- e.stopPropagation();
2494
- var id = item.dataset.taskId;
2495
- var name = item.textContent;
2496
- if (taskHidden) taskHidden.value = id;
2497
- if (taskLabel) taskLabel.textContent = name;
2498
- if (taskBtn) { taskBtn.classList.add("has-value"); taskBtn.classList.remove("invalid"); }
2499
- // Update selected state
2500
- var all = taskListEl.querySelectorAll(".sched-create-task-item");
2501
- for (var k = 0; k < all.length; k++) {
2502
- all[k].classList.toggle("selected", all[k] === item);
2503
- }
2504
- taskListEl.classList.add("hidden");
2505
- });
2506
- })(items[j]);
2507
- }
2508
- }
2509
- }
2510
-
2511
- // Set date picker
2512
- var dateStr = createSelectedDate.getFullYear() + "-" + pad(createSelectedDate.getMonth() + 1) + "-" + pad(createSelectedDate.getDate());
2513
- document.getElementById("sched-create-date").value = dateStr;
2514
- var datePicker = document.getElementById("sched-create-date-picker");
2515
- if (datePicker) {
2516
- datePicker.value = dateStr;
2517
- var todayNow = new Date();
2518
- var todayMin = todayNow.getFullYear() + "-" + pad(todayNow.getMonth() + 1) + "-" + pad(todayNow.getDate());
2519
- datePicker.min = todayMin;
2520
- }
2521
-
2522
- // Time (use minutes from createSelectedDate for 15-min snapping)
2523
- if (hour !== null && hour !== undefined) {
2524
- var mins = createSelectedDate.getMinutes ? createSelectedDate.getMinutes() : 0;
2525
- document.getElementById("sched-create-time").value = pad(hour) + ":" + pad(mins);
2526
- } else {
2527
- // Default to current time (next quarter-hour)
2528
- var nowT = new Date();
2529
- var nowMins = nowT.getHours() * 60 + nowT.getMinutes();
2530
- var nextQ = Math.ceil(nowMins / 15) * 15;
2531
- var defH = Math.floor(nextQ / 60);
2532
- var defM = nextQ % 60;
2533
- if (defH >= 24) { defH = 23; defM = 45; }
2534
- document.getElementById("sched-create-time").value = pad(defH) + ":" + pad(defM);
2535
- }
2536
-
2537
- // Update recurrence labels
2538
- updateRecurrenceLabels(createSelectedDate);
2539
-
2540
- // Enforce min time for today
2541
- enforceMinTime();
2542
-
2543
- // Reset recurrence
2544
- createRecurrence = "none";
2545
- createCustomConfirmed = false;
2546
- updateRecurrenceBtn();
2547
-
2548
- // Reset interval
2549
- document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
2550
- var timeInput = document.getElementById("sched-create-time");
2551
- if (timeInput) timeInput.style.display = "";
2552
-
2553
- // Reset interval end conditions
2554
- var iendOpts = document.querySelectorAll(".sched-interval-end-opt");
2555
- for (var ie = 0; ie < iendOpts.length; ie++) {
2556
- iendOpts[ie].classList.toggle("active", iendOpts[ie].dataset.iend === "allday");
2557
- }
2558
- var iendAfterRow = document.getElementById("sched-interval-end-after-row");
2559
- if (iendAfterRow) iendAfterRow.classList.add("hidden");
2560
- var iendUntilRow = document.getElementById("sched-interval-end-until-row");
2561
- if (iendUntilRow) iendUntilRow.classList.add("hidden");
2562
- var iendAfterInput = document.getElementById("sched-interval-end-after");
2563
- if (iendAfterInput) iendAfterInput.value = "5";
2564
- var iendTimeInput = document.getElementById("sched-interval-end-time");
2565
- if (iendTimeInput) iendTimeInput.value = "18:00";
2566
-
2567
- updateIntervalBtn();
2568
-
2569
- // Reset custom panel
2570
- document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
2571
- document.getElementById("sched-custom-interval").value = "1";
2572
- document.getElementById("sched-custom-unit").value = "week";
2573
- document.getElementById("sched-custom-dow-section").style.display = "";
2574
- var customDowBtns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn");
2575
- for (var i = 0; i < customDowBtns.length; i++) {
2576
- customDowBtns[i].classList.toggle("active", parseInt(customDowBtns[i].dataset.dow) === createSelectedDate.getDay());
2577
- }
2578
- document.getElementById("sched-custom-end").value = "never";
2579
- document.getElementById("sched-custom-end-label").textContent = "Never";
2580
- var endItems = document.querySelectorAll(".sched-custom-end-item");
2581
- for (var ei = 0; ei < endItems.length; ei++) {
2582
- endItems[ei].classList.toggle("active", endItems[ei].dataset.value === "never");
2583
- }
2584
- document.getElementById("sched-custom-end-list").classList.add("hidden");
2585
- createEndType = "never";
2586
- createEndDate = null;
2587
- createEndAfter = 10;
2588
- document.getElementById("sched-custom-end-date-btn").classList.add("hidden");
2589
- document.getElementById("sched-custom-end-after-wrap").classList.add("hidden");
2590
- document.getElementById("sched-custom-end-calendar").classList.add("hidden");
2591
-
2592
- // Show preview event on the calendar cell
2593
- showPreviewForCreate(anchorEl, draggedTaskName || null);
2594
-
2595
- // Position near anchor cell
2596
- createPopover.classList.remove("hidden");
2597
- positionCreatePopover(anchorEl);
2598
-
2599
- try { lucide.createIcons({ node: createPopover }); } catch (e) {}
2600
- setTimeout(function () { document.getElementById("sched-create-title").focus(); }, 50);
2601
- }
2602
-
2603
- function positionCreatePopover(anchorEl) {
2604
- if (!createPopover || !anchorEl) {
2605
- // Fallback: center in scheduler content area
2606
- if (createPopover && contentCalEl) {
2607
- var cRect = contentCalEl.getBoundingClientRect();
2608
- createPopover.style.left = (cRect.left + cRect.width / 2 - 180) + "px";
2609
- createPopover.style.top = (cRect.top + 60) + "px";
2610
- }
2611
- return;
2612
- }
2613
-
2614
- var rect = anchorEl.getBoundingClientRect();
2615
- var popW = 360;
2616
- var popH = createPopover.offsetHeight || 300;
2617
-
2618
- // Try to place to the right of the cell
2619
- var left = rect.right + 8;
2620
- var top = rect.top;
2621
-
2622
- // If it overflows right, place to the left
2623
- if (left + popW > window.innerWidth - 10) {
2624
- left = rect.left - popW - 8;
2625
- }
2626
- // If it still overflows left, center horizontally on the cell
2627
- if (left < 10) {
2628
- left = Math.max(10, rect.left + rect.width / 2 - popW / 2);
2629
- }
2630
-
2631
- // Vertical: don't overflow bottom
2632
- if (top + popH > window.innerHeight - 10) {
2633
- top = window.innerHeight - popH - 10;
2634
- }
2635
- if (top < 10) top = 10;
2636
-
2637
- createPopover.style.left = left + "px";
2638
- createPopover.style.top = top + "px";
2639
- }
2640
-
2641
- function updateRecurrenceLabels(date) {
2642
- var dow = date.getDay();
2643
- var dayName = DAY_NAMES[dow];
2644
- var dom = date.getDate();
2645
- var monthName = MONTH_NAMES[date.getMonth()];
2646
-
2647
- // Weekly on {day}
2648
- var weeklyEl = document.getElementById("sched-recurrence-weekly");
2649
- if (weeklyEl) weeklyEl.textContent = "Weekly on " + dayName;
2650
-
2651
- // Every second {day} of the month
2652
- var weekOfMonth = Math.ceil(dom / 7);
2653
- var ordinals = ["", "first", "second", "third", "fourth", "fifth"];
2654
- var biweeklyEl = document.getElementById("sched-recurrence-biweekly");
2655
- if (biweeklyEl) {
2656
- var ordStr = ordinals[weekOfMonth] || weekOfMonth + "th";
2657
- biweeklyEl.textContent = "Every " + ordStr + " " + dayName + " of the mo...";
2658
- }
2659
-
2660
- // Every year on {month} {date}
2661
- var yearlyEl = document.getElementById("sched-recurrence-yearly");
2662
- if (yearlyEl) yearlyEl.textContent = "Every year on " + monthName + " " + dom;
2663
-
2664
- // Every month on the {date}th
2665
- var monthlyEl = document.getElementById("sched-recurrence-monthly");
2666
- if (monthlyEl) {
2667
- var suffix = "th";
2668
- if (dom === 1 || dom === 21 || dom === 31) suffix = "st";
2669
- else if (dom === 2 || dom === 22) suffix = "nd";
2670
- else if (dom === 3 || dom === 23) suffix = "rd";
2671
- monthlyEl.textContent = "Every month on the " + dom + suffix;
2672
- }
2673
- }
2674
-
2675
- function closeCreateModal() {
2676
- if (createPopover) createPopover.classList.add("hidden");
2677
- var dd = document.getElementById("sched-create-recurrence-dropdown");
2678
- if (dd) dd.classList.add("hidden");
2679
- var pal = document.getElementById("sched-create-color-palette");
2680
- if (pal) pal.classList.add("hidden");
2681
- var tl = document.getElementById("sched-create-task-list");
2682
- if (tl) tl.classList.add("hidden");
2683
- removePreview();
2684
- createSelectedDate = null;
2685
- createEditingRecId = null;
2686
- }
2687
-
2688
- function openDeleteDialog(recId, eventDate, isOneOff) {
2689
- var dialog = document.getElementById("sched-delete-dialog");
2690
- if (!dialog) return;
2691
- dialog.dataset.recId = recId;
2692
- if (eventDate) {
2693
- dialog.dataset.eventDate = eventDate.getFullYear() + "-" + pad(eventDate.getMonth() + 1) + "-" + pad(eventDate.getDate());
2694
- } else {
2695
- dialog.dataset.eventDate = "";
2696
- }
2697
- // Toggle between one-off and recurring UI
2698
- var title = dialog.querySelector(".sched-delete-dialog-title");
2699
- var body = dialog.querySelector(".sched-delete-dialog-body");
2700
- var footer = dialog.querySelector(".sched-delete-dialog-footer");
2701
- var cancelBtn = dialog.querySelector('[data-delete="cancel"]');
2702
- dialog.dataset.oneOff = isOneOff ? "1" : "";
2703
- if (isOneOff) {
2704
- if (title) title.textContent = "Delete this event?";
2705
- if (body) body.classList.add("hidden");
2706
- if (cancelBtn) cancelBtn.textContent = "Cancel";
2707
- // Add a "Delete" button next to cancel in footer
2708
- var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
2709
- if (!existingDel && footer) {
2710
- var delBtn = document.createElement("button");
2711
- delBtn.className = "sched-delete-option danger sched-delete-confirm-btn";
2712
- delBtn.dataset.delete = "all";
2713
- delBtn.textContent = "Delete";
2714
- footer.appendChild(delBtn);
2715
- delBtn.addEventListener("click", function (e) {
2716
- e.stopPropagation();
2717
- var rid = dialog.dataset.recId;
2718
- if (rid) send({ type: "loop_registry_remove", id: rid });
2719
- closeDeleteDialog();
2720
- });
2721
- }
2722
- if (existingDel) existingDel.classList.remove("hidden");
2723
- } else {
2724
- if (title) title.textContent = "Delete recurring event";
2725
- if (body) body.classList.remove("hidden");
2726
- if (cancelBtn) cancelBtn.textContent = "Cancel";
2727
- var existingDel = footer ? footer.querySelector(".sched-delete-confirm-btn") : null;
2728
- if (existingDel) existingDel.classList.add("hidden");
2729
- }
2730
- dialog.classList.remove("hidden");
2731
- }
2732
-
2733
- function closeDeleteDialog() {
2734
- var dialog = document.getElementById("sched-delete-dialog");
2735
- if (dialog) {
2736
- dialog.classList.add("hidden");
2737
- dialog.dataset.recId = "";
2738
- dialog.dataset.eventDate = "";
2739
- }
2740
- }
2741
-
2742
- // Build an explicit list of values offset from a start value with a given step, wrapping at max
2743
- function buildOffsetList(start, step, max) {
2744
- var vals = [];
2745
- var v = start % max;
2746
- for (var i = 0; i < max; i += step) {
2747
- vals.push(v);
2748
- v = (v + step) % max;
2749
- }
2750
- vals.sort(function (a, b) { return a - b; });
2751
- return vals.join(",");
2752
- }
2753
-
2754
- function buildCreateCron() {
2755
- if (!createSelectedDate) return null;
2756
-
2757
- var timeVal = document.getElementById("sched-create-time").value || "09:00";
2758
- var timeParts = timeVal.split(":");
2759
- var h = parseInt(timeParts[0], 10);
2760
- var m = parseInt(timeParts[1], 10);
2761
-
2762
- var dow = createSelectedDate.getDay();
2763
- var dom = createSelectedDate.getDate();
2764
- var month = createSelectedDate.getMonth() + 1;
2765
-
2766
- // Determine interval minutes
2767
- var intervalMins = 0;
2768
- if (createInterval !== "none") {
2769
- if (createInterval === "custom" && createIntervalCustom) {
2770
- intervalMins = createIntervalCustom.unit === "hour"
2771
- ? createIntervalCustom.value * 60
2772
- : createIntervalCustom.value;
2773
- } else {
2774
- intervalMins = parseInt(createInterval, 10) || 0;
2775
- }
2776
- }
2777
-
2778
- // Interval only (no recurrence) = interval every day
2779
- if (intervalMins > 0 && createRecurrence === "none") {
2780
- if (intervalMins < 60) return buildOffsetList(m, intervalMins, 60) + " * * * *";
2781
- var intHrs = Math.floor(intervalMins / 60);
2782
- return String(m) + " " + buildOffsetList(h, intHrs, 24) + " * * *";
2783
- }
2784
-
2785
- if (createRecurrence === "none" && intervalMins === 0) return null;
2786
-
2787
- // Build minute/hour fields from interval or time
2788
- var minField = String(m);
2789
- var hourField = String(h);
2790
- if (intervalMins > 0 && intervalMins < 60) {
2791
- minField = buildOffsetList(m, intervalMins, 60);
2792
- hourField = "*";
2793
- } else if (intervalMins >= 60) {
2794
- var intHrs2 = Math.floor(intervalMins / 60);
2795
- minField = String(m);
2796
- hourField = buildOffsetList(h, intHrs2, 24);
2797
- }
2798
-
2799
- if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
2800
- if (createRecurrence === "weekly") return minField + " " + hourField + " * * " + dow;
2801
- if (createRecurrence === "biweekly") {
2802
- var weekNum = Math.ceil(dom / 7);
2803
- return minField + " " + hourField + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
2804
- }
2805
- if (createRecurrence === "yearly") return minField + " " + hourField + " " + dom + " " + month + " *";
2806
- if (createRecurrence === "monthly") return minField + " " + hourField + " " + dom + " * *";
2807
- if (createRecurrence === "weekdays") return minField + " " + hourField + " * * 1-5";
2808
-
2809
- if (createRecurrence === "custom" && createCustomConfirmed) {
2810
- return buildCustomCron(h, m);
2811
- }
2812
-
2813
- return null;
2814
- }
2815
-
2816
- function updateEndDateLabel() {
2817
- var label = document.getElementById("sched-custom-end-date-label");
2818
- if (!label || !createEndDate) return;
2819
- var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2820
- var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2821
- label.textContent = days[createEndDate.getDay()] + ", " + months[createEndDate.getMonth()] + " " + createEndDate.getDate();
2822
- }
2823
-
2824
- function renderEndCalendar() {
2825
- var grid = document.getElementById("sched-cal-grid");
2826
- var titleEl = document.getElementById("sched-cal-title");
2827
- if (!grid || !createEndCalMonth) return;
2828
-
2829
- var year = createEndCalMonth.getFullYear();
2830
- var month = createEndCalMonth.getMonth();
2831
- var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
2832
- titleEl.textContent = months[month] + " " + year;
2833
-
2834
- var firstDay = new Date(year, month, 1).getDay();
2835
- var daysInMonth = new Date(year, month + 1, 0).getDate();
2836
- var prevDays = new Date(year, month, 0).getDate();
2837
-
2838
- var today = new Date();
2839
- today.setHours(0, 0, 0, 0);
2840
-
2841
- grid.innerHTML = "";
2842
-
2843
- // Previous month filler
2844
- for (var p = firstDay - 1; p >= 0; p--) {
2845
- var d = prevDays - p;
2846
- var btn = document.createElement("button");
2847
- btn.className = "sched-cal-day other-month";
2848
- btn.textContent = d;
2849
- btn.type = "button";
2850
- var prevDate = new Date(year, month - 1, d);
2851
- (function (dt) {
2852
- btn.addEventListener("click", function (e) {
2853
- e.stopPropagation();
2854
- createEndDate = dt;
2855
- updateEndDateLabel();
2856
- renderEndCalendar();
2857
- });
2858
- })(prevDate);
2859
- grid.appendChild(btn);
2860
- }
2861
-
2862
- // Current month
2863
- for (var i = 1; i <= daysInMonth; i++) {
2864
- var btn = document.createElement("button");
2865
- btn.className = "sched-cal-day";
2866
- btn.textContent = i;
2867
- btn.type = "button";
2868
- var cellDate = new Date(year, month, i);
2869
- if (cellDate.getTime() === today.getTime()) btn.classList.add("today");
2870
- if (createEndDate && cellDate.getFullYear() === createEndDate.getFullYear() && cellDate.getMonth() === createEndDate.getMonth() && cellDate.getDate() === createEndDate.getDate()) {
2871
- btn.classList.add("selected");
2872
- }
2873
- (function (dt) {
2874
- btn.addEventListener("click", function (e) {
2875
- e.stopPropagation();
2876
- createEndDate = dt;
2877
- updateEndDateLabel();
2878
- renderEndCalendar();
2879
- });
2880
- })(cellDate);
2881
- grid.appendChild(btn);
2882
- }
2883
-
2884
- // Next month filler
2885
- var totalCells = firstDay + daysInMonth;
2886
- var remaining = (7 - (totalCells % 7)) % 7;
2887
- for (var n = 1; n <= remaining; n++) {
2888
- var btn = document.createElement("button");
2889
- btn.className = "sched-cal-day other-month";
2890
- btn.textContent = n;
2891
- btn.type = "button";
2892
- var nextDate = new Date(year, month + 1, n);
2893
- (function (dt) {
2894
- btn.addEventListener("click", function (e) {
2895
- e.stopPropagation();
2896
- createEndDate = dt;
2897
- updateEndDateLabel();
2898
- renderEndCalendar();
2899
- });
2900
- })(nextDate);
2901
- grid.appendChild(btn);
2902
- }
2903
- }
2904
-
2905
- function buildCustomCron(h, m) {
2906
- var interval = parseInt(document.getElementById("sched-custom-interval").value, 10) || 1;
2907
- var unit = document.getElementById("sched-custom-unit").value;
2908
-
2909
- if (unit === "minute") {
2910
- return interval === 1 ? "*/1 * * * *" : buildOffsetList(m, interval, 60) + " * * * *";
2911
- }
2912
- if (unit === "hour") {
2913
- return interval === 1 ? m + " */1 * * *" : m + " " + buildOffsetList(h, interval, 24) + " * * *";
2914
- }
2915
- if (unit === "day") {
2916
- if (interval === 1) return m + " " + h + " * * *";
2917
- return m + " " + h + " */" + interval + " * *";
2918
- }
2919
-
2920
- if (unit === "week") {
2921
- var days = [];
2922
- var btns = document.querySelectorAll("#sched-custom-dow-row .sched-dow-btn.active");
2923
- for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
2924
- if (days.length === 0) days.push(String(createSelectedDate ? createSelectedDate.getDay() : 0));
2925
- return m + " " + h + " * * " + days.sort().join(",");
2926
- }
2927
-
2928
- if (unit === "month") {
2929
- var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
2930
- if (interval === 1) return m + " " + h + " " + dom + " * *";
2931
- return m + " " + h + " " + dom + " */" + interval + " *";
2932
- }
2933
-
2934
- if (unit === "year") {
2935
- var dom = createSelectedDate ? createSelectedDate.getDate() : 1;
2936
- var month = createSelectedDate ? createSelectedDate.getMonth() + 1 : 1;
2937
- return m + " " + h + " " + dom + " " + month + " *";
2938
- }
2939
-
2940
- return null;
2941
- }
2942
-
2943
- function submitCreateSchedule() {
2944
- var name = document.getElementById("sched-create-title").value.trim();
2945
- if (!name) { document.getElementById("sched-create-title").focus(); return; }
2946
-
2947
- var taskId = document.getElementById("sched-create-task").value || null;
2948
- if (!taskId) {
2949
- var taskBtn = document.getElementById("sched-create-task-btn");
2950
- if (taskBtn) taskBtn.classList.add("invalid");
2951
- return;
2952
- }
2953
-
2954
- ctx.requireClayRalph(function () {
2955
- var description = document.getElementById("sched-create-desc").value.trim();
2956
- var datePicker = document.getElementById("sched-create-date-picker");
2957
- var dateVal = datePicker ? datePicker.value : document.getElementById("sched-create-date").value;
2958
- var timeVal = document.getElementById("sched-create-time").value || "09:00";
2959
-
2960
- // Reject scheduling in the past
2961
- if (dateVal && timeVal) {
2962
- var dp = dateVal.split("-");
2963
- var tp = timeVal.split(":");
2964
- if (dp.length === 3 && tp.length >= 2) {
2965
- var schedDate = new Date(
2966
- parseInt(dp[0], 10), parseInt(dp[1], 10) - 1, parseInt(dp[2], 10),
2967
- parseInt(tp[0], 10), parseInt(tp[1], 10), 0
2968
- );
2969
- if (schedDate < new Date()) {
2970
- showToast("Cannot schedule a task in the past", "error");
2971
- return;
2972
- }
2973
- }
2974
- }
2975
-
2976
- var cron = buildCreateCron();
2977
-
2978
- // Build recurrence end info
2979
- var recurrenceEnd = null;
2980
- // Interval-only (no recurrence): limit to the scheduled date only
2981
- if (cron && createRecurrence === "none" && createInterval !== "none" && dateVal) {
2982
- recurrenceEnd = { type: "until", date: dateVal };
2983
- }
2984
- if (cron && createRecurrence === "custom" && createCustomConfirmed) {
2985
- if (createEndType === "until" && createEndDate) {
2986
- var ey = createEndDate.getFullYear();
2987
- var em = String(createEndDate.getMonth() + 1).padStart(2, "0");
2988
- var ed = String(createEndDate.getDate()).padStart(2, "0");
2989
- recurrenceEnd = { type: "until", date: ey + "-" + em + "-" + ed };
2990
- } else if (createEndType === "after" && createEndAfter > 0) {
2991
- recurrenceEnd = { type: "after", count: createEndAfter };
2992
- }
2993
- }
2994
-
2995
- // Build interval end info
2996
- var intervalEnd = null;
2997
- if (createInterval !== "none") {
2998
- if (createIntervalEnd === "after" && createIntervalEndAfter > 0) {
2999
- intervalEnd = { type: "after", count: createIntervalEndAfter };
3000
- } else if (createIntervalEnd === "until" && createIntervalEndTime) {
3001
- intervalEnd = { type: "until", time: createIntervalEndTime };
3002
- }
3003
- // "allday" = null (no limit)
3004
- }
3005
-
3006
- var skipRunningEl = document.getElementById("sched-skip-running");
3007
- var skipIfRunning = skipRunningEl ? skipRunningEl.checked : true;
3008
-
3009
- var activeRunMode = createPopover.querySelector(".sched-run-mode-btn.active");
3010
- var runMode = activeRunMode ? activeRunMode.dataset.mode : "single";
3011
- var maxIterations = 1;
3012
- if (runMode === "multi") {
3013
- var iterInput = document.getElementById("sched-create-iterations");
3014
- maxIterations = iterInput ? (parseInt(iterInput.value, 10) || 3) : 3;
3015
- if (maxIterations < 2) maxIterations = 2;
3016
- if (maxIterations > 100) maxIterations = 100;
3017
- }
3018
-
3019
- if (createEditingRecId) {
3020
- send({
3021
- type: "loop_registry_update",
3022
- id: createEditingRecId,
3023
- data: {
3024
- name: name,
3025
- description: description,
3026
- date: dateVal,
3027
- time: timeVal,
3028
- allDay: false,
3029
- cron: cron,
3030
- enabled: cron ? true : false,
3031
- color: createColor,
3032
- recurrenceEnd: recurrenceEnd,
3033
- intervalEnd: intervalEnd,
3034
- maxIterations: maxIterations,
3035
- skipIfRunning: skipIfRunning,
3036
- },
3037
- });
3038
- } else {
3039
- send({
3040
- type: "schedule_create",
3041
- data: {
3042
- name: name,
3043
- taskId: taskId,
3044
- description: description,
3045
- date: dateVal,
3046
- time: timeVal,
3047
- allDay: false,
3048
- cron: cron,
3049
- enabled: cron ? true : false,
3050
- color: createColor,
3051
- recurrenceEnd: recurrenceEnd,
3052
- intervalEnd: intervalEnd,
3053
- maxIterations: maxIterations,
3054
- skipIfRunning: skipIfRunning,
3055
- },
3056
- });
3057
- }
3058
-
3059
- closeCreateModal();
3060
- });
3061
- }
3062
-
3063
- // --- Cron parser (client-side) ---
3064
-
3065
- function parseCronSimple(expr) {
3066
- if (!expr) return null;
3067
- var fields = expr.trim().split(/\s+/);
3068
- if (fields.length !== 5) return null;
3069
- return {
3070
- minutes: parseField(fields[0], 0, 59),
3071
- hours: parseField(fields[1], 0, 23),
3072
- daysOfMonth: parseField(fields[2], 1, 31),
3073
- months: parseField(fields[3], 1, 12),
3074
- daysOfWeek: parseField(fields[4], 0, 6),
3075
- };
3076
- }
3077
-
3078
- function parseField(field, min, max) {
3079
- var values = [];
3080
- var parts = field.split(",");
3081
- for (var i = 0; i < parts.length; i++) {
3082
- var part = parts[i].trim();
3083
- if (part.indexOf("/") !== -1) {
3084
- var sp = part.split("/");
3085
- var step = parseInt(sp[1], 10);
3086
- var rMin = min, rMax = max;
3087
- if (sp[0] !== "*") { var rp = sp[0].split("-"); rMin = parseInt(rp[0], 10); rMax = rp.length > 1 ? parseInt(rp[1], 10) : rMin; }
3088
- for (var v = rMin; v <= rMax; v += step) values.push(v);
3089
- } else if (part === "*") {
3090
- for (var v = min; v <= max; v++) values.push(v);
3091
- } else if (part.indexOf("-") !== -1) {
3092
- var rp = part.split("-");
3093
- for (var v = parseInt(rp[0], 10); v <= parseInt(rp[1], 10); v++) values.push(v);
3094
- } else {
3095
- values.push(parseInt(part, 10));
3096
- }
3097
- }
3098
- return values;
3099
- }
3100
-
3101
1580
  // --- Utility ---
3102
1581
 
3103
1582
  function getISOWeekNumber(date) {