clay-server 2.18.0-beta.9 → 2.18.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.
- package/lib/project.js +1445 -32
- package/lib/public/app.js +309 -125
- package/lib/public/css/debate.css +1039 -0
- package/lib/public/css/mates.css +125 -0
- package/lib/public/css/scheduler-modal.css +110 -392
- package/lib/public/css/scheduler.css +10 -3
- package/lib/public/css/sidebar.css +80 -7
- package/lib/public/index.html +53 -106
- package/lib/public/modules/debate.js +633 -0
- package/lib/public/modules/input.js +14 -5
- package/lib/public/modules/mate-sidebar.js +169 -2
- package/lib/public/modules/mention.js +6 -3
- package/lib/public/modules/scheduler.js +373 -252
- package/lib/public/modules/sidebar.js +158 -28
- package/lib/public/modules/tools.js +13 -3
- package/lib/public/modules/tooltip.js +2 -0
- package/lib/public/style.css +1 -0
- package/lib/scheduler.js +59 -1
- package/lib/sessions.js +9 -2
- package/lib/user-presence.js +92 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { renderMarkdown } from './markdown.js';
|
|
9
9
|
import { iconHtml } from './icons.js';
|
|
10
|
+
import { showToast } from './utils.js';
|
|
10
11
|
|
|
11
12
|
var ctx = null;
|
|
12
13
|
var records = []; // all loop registry records
|
|
@@ -33,7 +34,6 @@ var panel = null; // #scheduler-panel
|
|
|
33
34
|
var bodyEl = null;
|
|
34
35
|
var monthLabel = null;
|
|
35
36
|
var calHeader = null;
|
|
36
|
-
var editModal = null;
|
|
37
37
|
var popoverEl = null;
|
|
38
38
|
var panelOpen = false;
|
|
39
39
|
|
|
@@ -46,7 +46,6 @@ var messagesOrigParent = null; // for reparenting
|
|
|
46
46
|
var inputOrigNextSibling = null; // anchor for restoring input-area position
|
|
47
47
|
|
|
48
48
|
// Edit state
|
|
49
|
-
var editingId = null;
|
|
50
49
|
|
|
51
50
|
// Create popover state
|
|
52
51
|
var createEditingRecId = null; // non-null when editing existing schedule
|
|
@@ -56,6 +55,9 @@ var createRecurrence = "none"; // current recurrence selection
|
|
|
56
55
|
var createCustomConfirmed = false; // whether custom repeat was confirmed via OK
|
|
57
56
|
var createInterval = "none"; // current interval selection: "none", "1", "5", "15", "30", "60", "custom"
|
|
58
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
|
|
59
61
|
var createColor = "#ffb86c"; // selected event color (default: accent)
|
|
60
62
|
var createEndType = "never"; // "never" | "until" | "after"
|
|
61
63
|
var createEndDate = null; // Date for "until" end type
|
|
@@ -77,7 +79,6 @@ var MONTH_NAMES = [
|
|
|
77
79
|
export function initScheduler(_ctx) {
|
|
78
80
|
ctx = _ctx;
|
|
79
81
|
currentProjectSlug = ctx.currentSlug || null;
|
|
80
|
-
editModal = document.getElementById("schedule-edit-modal");
|
|
81
82
|
createPopover = document.getElementById("schedule-create-popover");
|
|
82
83
|
popoverEl = document.getElementById("schedule-popover");
|
|
83
84
|
|
|
@@ -95,9 +96,6 @@ export function initScheduler(_ctx) {
|
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
// Edit modal
|
|
99
|
-
setupEditModal();
|
|
100
|
-
|
|
101
99
|
// Create modal
|
|
102
100
|
setupCreateModal();
|
|
103
101
|
|
|
@@ -751,6 +749,10 @@ function renderDetailBody(tab, rec) {
|
|
|
751
749
|
html += '<span class="scheduler-detail-meta-value">' + esc(createdStr) + '</span>';
|
|
752
750
|
html += '<span class="scheduler-detail-meta-label">Last Run</span>';
|
|
753
751
|
html += '<span class="scheduler-detail-meta-value">' + esc(lastRunStr) + '</span>';
|
|
752
|
+
if (isScheduled && rec.nextRunAt) {
|
|
753
|
+
html += '<span class="scheduler-detail-meta-label">Next Run</span>';
|
|
754
|
+
html += '<span class="scheduler-detail-meta-value">' + esc(formatDateTime(new Date(rec.nextRunAt))) + '</span>';
|
|
755
|
+
}
|
|
754
756
|
html += '</div>';
|
|
755
757
|
bodyEl2.innerHTML = html;
|
|
756
758
|
} else {
|
|
@@ -874,8 +876,9 @@ function renderMonthView() {
|
|
|
874
876
|
var dateStr = cursor.getFullYear() + "-" + pad(cursor.getMonth() + 1) + "-" + pad(cursor.getDate());
|
|
875
877
|
var isOther = cursor.getMonth() !== month;
|
|
876
878
|
var isToday = dateStr === todayStr;
|
|
879
|
+
var isPast = dateStr < todayStr;
|
|
877
880
|
var isWeekend = d === 0 || d === 6;
|
|
878
|
-
var cls = "scheduler-cell" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (isWeekend ? " weekend" : "");
|
|
881
|
+
var cls = "scheduler-cell" + (isOther ? " other-month" : "") + (isToday ? " today" : "") + (isPast ? " past" : "") + (isWeekend ? " weekend" : "");
|
|
879
882
|
html += '<div class="' + cls + '" data-date="' + dateStr + '">';
|
|
880
883
|
var dayLabel = cursor.getDate() === 1
|
|
881
884
|
? MONTH_NAMES[cursor.getMonth()].substring(0, 3) + ", " + cursor.getDate()
|
|
@@ -884,7 +887,8 @@ function renderMonthView() {
|
|
|
884
887
|
var events = getEventsForDate(cursor);
|
|
885
888
|
for (var e = 0; e < events.length && e < 3; e++) {
|
|
886
889
|
var ev = events[e];
|
|
887
|
-
|
|
890
|
+
var evFullText = ev.timeStr + " " + ev.name;
|
|
891
|
+
html += '<div class="scheduler-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" data-tip="' + esc(evFullText) + '">';
|
|
888
892
|
html += '<span class="scheduler-event-time">' + ev.timeStr + '</span> ' + esc(ev.name);
|
|
889
893
|
html += '</div>';
|
|
890
894
|
}
|
|
@@ -1010,9 +1014,10 @@ function renderWeekView() {
|
|
|
1010
1014
|
if (ev.intervalBadge) {
|
|
1011
1015
|
var badgeStyle = "";
|
|
1012
1016
|
if (ev.color) badgeStyle = "background:" + ev.color;
|
|
1013
|
-
|
|
1014
|
-
html += '<
|
|
1017
|
+
var weekIntFullText = ev.timeStr + " " + ev.name;
|
|
1018
|
+
html += '<div class="scheduler-week-event interval-badge ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" data-tip="' + esc(weekIntFullText) + '" style="position:relative;top:0;left:0;width:85%;height:auto;' + badgeStyle + '">';
|
|
1015
1019
|
html += '<span class="scheduler-week-event-time">' + esc(ev.timeStr) + '</span>';
|
|
1020
|
+
html += '<span class="scheduler-week-event-title">' + esc(ev.name) + '</span>';
|
|
1016
1021
|
html += '</div>';
|
|
1017
1022
|
continue;
|
|
1018
1023
|
}
|
|
@@ -1026,7 +1031,8 @@ function renderWeekView() {
|
|
|
1026
1031
|
var evStyle = "top:" + topPct + "%;height:calc(160vh / 48)";
|
|
1027
1032
|
evStyle += ";left:" + leftPct + "%;width:" + (colWidth - 1) + "%";
|
|
1028
1033
|
if (evColor) evStyle += ";background:" + evColor;
|
|
1029
|
-
|
|
1034
|
+
var weekEvFullText = ev.timeStr + " " + ev.name;
|
|
1035
|
+
html += '<div class="scheduler-week-event ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" data-tip="' + esc(weekEvFullText) + '" style="' + evStyle + '">';
|
|
1030
1036
|
html += '<span class="scheduler-week-event-title">' + esc(ev.name) + '</span>';
|
|
1031
1037
|
html += '<span class="scheduler-week-event-time">' + ev.timeStr + '</span>';
|
|
1032
1038
|
html += '</div>';
|
|
@@ -1208,7 +1214,7 @@ function getEventsForDate(date) {
|
|
|
1208
1214
|
results.push({
|
|
1209
1215
|
id: r.id, name: r.name, enabled: r.enabled,
|
|
1210
1216
|
hour: 0, minute: 0,
|
|
1211
|
-
timeStr: cronToHuman(r.cron) || "Interval",
|
|
1217
|
+
timeStr: (r.time || "00:00") + " " + (cronToHuman(r.cron) || "Interval"),
|
|
1212
1218
|
allDay: true,
|
|
1213
1219
|
intervalBadge: true,
|
|
1214
1220
|
color: r.color || null,
|
|
@@ -1277,7 +1283,13 @@ function showPopover(recId, anchorEl) {
|
|
|
1277
1283
|
var action = btn.dataset.action;
|
|
1278
1284
|
var id = btn.dataset.id;
|
|
1279
1285
|
popoverEl.classList.add("hidden");
|
|
1280
|
-
if (action === "edit")
|
|
1286
|
+
if (action === "edit") {
|
|
1287
|
+
var rec = null;
|
|
1288
|
+
for (var ri = 0; ri < records.length; ri++) {
|
|
1289
|
+
if (records[ri].id === id) { rec = records[ri]; break; }
|
|
1290
|
+
}
|
|
1291
|
+
if (rec) openCreateModalWithRecord(rec, btn);
|
|
1292
|
+
}
|
|
1281
1293
|
else if (action === "toggle") send({ type: "loop_registry_toggle", id: id });
|
|
1282
1294
|
else if (action === "rerun") send({ type: "loop_registry_rerun", id: id });
|
|
1283
1295
|
else if (action === "move") showMovePopover(id, btn);
|
|
@@ -1390,148 +1402,6 @@ function attachEventClicks(container, selector) {
|
|
|
1390
1402
|
}
|
|
1391
1403
|
}
|
|
1392
1404
|
|
|
1393
|
-
// --- Edit Modal (for changing cron/name on existing records) ---
|
|
1394
|
-
|
|
1395
|
-
function setupEditModal() {
|
|
1396
|
-
if (!editModal) return;
|
|
1397
|
-
document.getElementById("schedule-edit-close").addEventListener("click", function () { closeEditModal(); });
|
|
1398
|
-
document.getElementById("sched-cancel").addEventListener("click", function () { closeEditModal(); });
|
|
1399
|
-
editModal.querySelector(".confirm-backdrop").addEventListener("click", function () { closeEditModal(); });
|
|
1400
|
-
|
|
1401
|
-
// Presets
|
|
1402
|
-
var presetBtns = document.querySelectorAll("#sched-presets .sched-preset-btn");
|
|
1403
|
-
for (var i = 0; i < presetBtns.length; i++) {
|
|
1404
|
-
(function (btn) {
|
|
1405
|
-
btn.addEventListener("click", function () { selectPreset(btn.dataset.preset); });
|
|
1406
|
-
})(presetBtns[i]);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// DOW
|
|
1410
|
-
var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
|
|
1411
|
-
for (var i = 0; i < dowBtns.length; i++) {
|
|
1412
|
-
(function (btn) {
|
|
1413
|
-
btn.addEventListener("click", function () { btn.classList.toggle("active"); updateEditCronPreview(); });
|
|
1414
|
-
})(dowBtns[i]);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
document.getElementById("sched-time").addEventListener("change", function () { updateEditCronPreview(); });
|
|
1418
|
-
document.getElementById("sched-save").addEventListener("click", function () { saveEdit(); });
|
|
1419
|
-
document.getElementById("sched-delete").addEventListener("click", function () {
|
|
1420
|
-
if (editingId && confirm("Delete this job?")) {
|
|
1421
|
-
send({ type: "loop_registry_remove", id: editingId });
|
|
1422
|
-
closeEditModal();
|
|
1423
|
-
}
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
var editPreset = "daily";
|
|
1428
|
-
|
|
1429
|
-
function selectPreset(preset) {
|
|
1430
|
-
editPreset = preset;
|
|
1431
|
-
var btns = document.querySelectorAll("#sched-presets .sched-preset-btn");
|
|
1432
|
-
for (var i = 0; i < btns.length; i++) btns[i].classList.toggle("active", btns[i].dataset.preset === preset);
|
|
1433
|
-
var dowField = document.getElementById("sched-dow-field");
|
|
1434
|
-
if (dowField) dowField.style.display = (preset === "custom" || preset === "weekly") ? "" : "none";
|
|
1435
|
-
updateEditCronPreview();
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
function buildEditCron() {
|
|
1439
|
-
var timeVal = document.getElementById("sched-time").value || "09:00";
|
|
1440
|
-
var parts = timeVal.split(":");
|
|
1441
|
-
var h = parseInt(parts[0], 10);
|
|
1442
|
-
var m = parseInt(parts[1], 10);
|
|
1443
|
-
var dow = "*";
|
|
1444
|
-
if (editPreset === "weekdays") dow = "1-5";
|
|
1445
|
-
else if (editPreset === "weekly" || editPreset === "custom") {
|
|
1446
|
-
var days = [];
|
|
1447
|
-
var btns = document.querySelectorAll("#sched-dow-row .sched-dow-btn.active");
|
|
1448
|
-
for (var i = 0; i < btns.length; i++) days.push(btns[i].dataset.dow);
|
|
1449
|
-
if (days.length > 0 && days.length < 7) dow = days.sort().join(",");
|
|
1450
|
-
} else if (editPreset === "monthly") {
|
|
1451
|
-
return m + " " + h + " " + new Date().getDate() + " * *";
|
|
1452
|
-
}
|
|
1453
|
-
return m + " " + h + " * * " + dow;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
function updateEditCronPreview() {
|
|
1457
|
-
var cron = buildEditCron();
|
|
1458
|
-
var humanEl = document.getElementById("sched-human-text");
|
|
1459
|
-
var cronEl = document.getElementById("sched-cron-text");
|
|
1460
|
-
if (humanEl) humanEl.textContent = cronToHuman(cron);
|
|
1461
|
-
if (cronEl) cronEl.textContent = cron;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
function openEditModal(recId) {
|
|
1465
|
-
if (!editModal) return;
|
|
1466
|
-
editingId = recId;
|
|
1467
|
-
var rec = null;
|
|
1468
|
-
for (var i = 0; i < records.length; i++) {
|
|
1469
|
-
if (records[i].id === recId) { rec = records[i]; break; }
|
|
1470
|
-
}
|
|
1471
|
-
if (!rec) return;
|
|
1472
|
-
|
|
1473
|
-
document.getElementById("schedule-edit-title").textContent = "Edit Schedule";
|
|
1474
|
-
document.getElementById("sched-name").value = rec.name || "";
|
|
1475
|
-
document.getElementById("sched-enabled").checked = rec.enabled;
|
|
1476
|
-
document.getElementById("sched-delete").style.display = "";
|
|
1477
|
-
|
|
1478
|
-
// Show job name
|
|
1479
|
-
var jobNameEl = document.getElementById("sched-job-name");
|
|
1480
|
-
if (jobNameEl) jobNameEl.textContent = rec.task ? rec.task.substring(0, 80) : rec.id;
|
|
1481
|
-
|
|
1482
|
-
// History
|
|
1483
|
-
var historyField = document.getElementById("sched-history-field");
|
|
1484
|
-
if (rec.runs && rec.runs.length > 0) {
|
|
1485
|
-
if (historyField) historyField.style.display = "";
|
|
1486
|
-
renderHistory(rec.runs);
|
|
1487
|
-
} else {
|
|
1488
|
-
if (historyField) historyField.style.display = "none";
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// Parse cron
|
|
1492
|
-
if (rec.cron) {
|
|
1493
|
-
var parsed = parseCronSimple(rec.cron);
|
|
1494
|
-
if (parsed) {
|
|
1495
|
-
document.getElementById("sched-time").value = pad(parsed.hours[0] || 9) + ":" + pad(parsed.minutes[0] || 0);
|
|
1496
|
-
var dowArr = parsed.daysOfWeek;
|
|
1497
|
-
if (dowArr.length === 7) selectPreset("daily");
|
|
1498
|
-
else if (dowArr.length === 5 && dowArr[0] === 1 && dowArr[4] === 5) selectPreset("weekdays");
|
|
1499
|
-
else {
|
|
1500
|
-
selectPreset("custom");
|
|
1501
|
-
var dowBtns = document.querySelectorAll("#sched-dow-row .sched-dow-btn");
|
|
1502
|
-
for (var j = 0; j < dowBtns.length; j++) {
|
|
1503
|
-
dowBtns[j].classList.toggle("active", dowArr.indexOf(parseInt(dowBtns[j].dataset.dow)) !== -1);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
} else {
|
|
1508
|
-
document.getElementById("sched-time").value = "09:00";
|
|
1509
|
-
selectPreset("daily");
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
updateEditCronPreview();
|
|
1513
|
-
editModal.classList.remove("hidden");
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
function closeEditModal() {
|
|
1517
|
-
if (editModal) editModal.classList.add("hidden");
|
|
1518
|
-
editingId = null;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function saveEdit() {
|
|
1522
|
-
var name = document.getElementById("sched-name").value.trim();
|
|
1523
|
-
var enabled = document.getElementById("sched-enabled").checked;
|
|
1524
|
-
var cron = buildEditCron();
|
|
1525
|
-
if (!name) { alert("Please enter a name."); return; }
|
|
1526
|
-
|
|
1527
|
-
send({
|
|
1528
|
-
type: "loop_registry_update",
|
|
1529
|
-
id: editingId,
|
|
1530
|
-
data: { name: name, cron: cron, enabled: enabled },
|
|
1531
|
-
});
|
|
1532
|
-
closeEditModal();
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
1405
|
function renderHistory(runs) {
|
|
1536
1406
|
var el = document.getElementById("sched-history");
|
|
1537
1407
|
if (!el || !runs || runs.length === 0) { if (el) el.innerHTML = '<div class="sched-history-empty">No runs yet</div>'; return; }
|
|
@@ -1615,9 +1485,9 @@ export function handleLoopRegistryFiles(msg) {
|
|
|
1615
1485
|
// Disable "Run now" if PROMPT.md or JUDGE.md is missing
|
|
1616
1486
|
var runBtn = contentDetailEl ? contentDetailEl.querySelector('[data-action="run"]') : null;
|
|
1617
1487
|
if (runBtn) {
|
|
1618
|
-
var filesReady = !!msg.prompt
|
|
1488
|
+
var filesReady = !!msg.prompt;
|
|
1619
1489
|
runBtn.disabled = !filesReady;
|
|
1620
|
-
runBtn.title = filesReady ? "Run now" : "PROMPT.md
|
|
1490
|
+
runBtn.title = filesReady ? "Run now" : "PROMPT.md is required to run";
|
|
1621
1491
|
}
|
|
1622
1492
|
}
|
|
1623
1493
|
|
|
@@ -1662,6 +1532,13 @@ function attachCellClicks(container) {
|
|
|
1662
1532
|
if (e.target.closest(".scheduler-event")) return;
|
|
1663
1533
|
var parts = cell.dataset.date.split("-");
|
|
1664
1534
|
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1535
|
+
// Block creating tasks on past dates
|
|
1536
|
+
var now = new Date();
|
|
1537
|
+
var todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1538
|
+
if (d < todayStart) {
|
|
1539
|
+
showToast("Cannot schedule a task in the past", "error");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1665
1542
|
openCreateModal(d, null, cell);
|
|
1666
1543
|
});
|
|
1667
1544
|
cell.addEventListener("dragover", function (e) {
|
|
@@ -1683,6 +1560,12 @@ function attachCellClicks(container) {
|
|
|
1683
1560
|
removePreview();
|
|
1684
1561
|
var parts = cell.dataset.date.split("-");
|
|
1685
1562
|
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1563
|
+
var now = new Date();
|
|
1564
|
+
var todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1565
|
+
if (d < todayStart) {
|
|
1566
|
+
showToast("Cannot schedule a task in the past", "error");
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1686
1569
|
openCreateModal(d, null, cell);
|
|
1687
1570
|
applyDraggedTask();
|
|
1688
1571
|
});
|
|
@@ -1701,6 +1584,11 @@ function attachWeekSlotClicks(container) {
|
|
|
1701
1584
|
var quarter = parseInt(slot.dataset.quarter || "0", 10);
|
|
1702
1585
|
var minute = quarter * 15;
|
|
1703
1586
|
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), hour, minute, 0);
|
|
1587
|
+
// Block creating tasks in the past
|
|
1588
|
+
if (d < new Date()) {
|
|
1589
|
+
showToast("Cannot schedule a task in the past", "error");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1704
1592
|
openCreateModal(d, hour, slot);
|
|
1705
1593
|
});
|
|
1706
1594
|
slot.addEventListener("dragover", function (e) {
|
|
@@ -1725,6 +1613,10 @@ function attachWeekSlotClicks(container) {
|
|
|
1725
1613
|
var quarter = parseInt(slot.dataset.quarter || "0", 10);
|
|
1726
1614
|
var minute = quarter * 15;
|
|
1727
1615
|
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10), hour, minute, 0);
|
|
1616
|
+
if (d < new Date()) {
|
|
1617
|
+
showToast("Cannot schedule a task in the past", "error");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1728
1620
|
openCreateModal(d, hour, slot);
|
|
1729
1621
|
applyDraggedTask();
|
|
1730
1622
|
});
|
|
@@ -1775,10 +1667,19 @@ function setupCreateModal() {
|
|
|
1775
1667
|
createSelectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
1776
1668
|
document.getElementById("sched-create-date").value = this.value;
|
|
1777
1669
|
updateRecurrenceLabels(createSelectedDate);
|
|
1670
|
+
enforceMinTime();
|
|
1778
1671
|
}
|
|
1779
1672
|
});
|
|
1780
1673
|
}
|
|
1781
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
|
+
|
|
1782
1683
|
// Task dropdown
|
|
1783
1684
|
var taskBtn = document.getElementById("sched-create-task-btn");
|
|
1784
1685
|
var taskList = document.getElementById("sched-create-task-list");
|
|
@@ -1809,8 +1710,6 @@ function setupCreateModal() {
|
|
|
1809
1710
|
if (dd) {
|
|
1810
1711
|
var wasHidden = dd.classList.contains("hidden");
|
|
1811
1712
|
dd.classList.toggle("hidden");
|
|
1812
|
-
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
1813
|
-
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
1814
1713
|
if (wasHidden && btn) {
|
|
1815
1714
|
var bRect = btn.getBoundingClientRect();
|
|
1816
1715
|
var ddW = 280;
|
|
@@ -1823,31 +1722,8 @@ function setupCreateModal() {
|
|
|
1823
1722
|
}
|
|
1824
1723
|
});
|
|
1825
1724
|
|
|
1826
|
-
// Recurrence option clicks
|
|
1827
|
-
var recOptions = createPopover.querySelectorAll(".sched-recurrence-option");
|
|
1828
|
-
for (var i = 0; i < recOptions.length; i++) {
|
|
1829
|
-
(function (opt) {
|
|
1830
|
-
opt.addEventListener("click", function (e) {
|
|
1831
|
-
e.stopPropagation();
|
|
1832
|
-
var rec = opt.dataset.recurrence;
|
|
1833
|
-
if (rec === "custom") {
|
|
1834
|
-
document.getElementById("sched-create-recurrence-list").style.display = "none";
|
|
1835
|
-
document.getElementById("sched-custom-repeat-panel").classList.remove("hidden");
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
for (var j = 0; j < recOptions.length; j++) {
|
|
1839
|
-
recOptions[j].classList.toggle("active", recOptions[j] === opt);
|
|
1840
|
-
}
|
|
1841
|
-
createRecurrence = rec;
|
|
1842
|
-
createCustomConfirmed = false;
|
|
1843
|
-
// Close dropdown
|
|
1844
|
-
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
1845
|
-
updateRecurrenceBtn();
|
|
1846
|
-
});
|
|
1847
|
-
})(recOptions[i]);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
1725
|
// --- Interval button + dropdown ---
|
|
1726
|
+
var intervalSnapshot = null;
|
|
1851
1727
|
document.getElementById("sched-create-interval-btn").addEventListener("click", function (e) {
|
|
1852
1728
|
e.stopPropagation();
|
|
1853
1729
|
var dd = document.getElementById("sched-create-interval-dropdown");
|
|
@@ -1858,6 +1734,14 @@ function setupCreateModal() {
|
|
|
1858
1734
|
var wasHidden = dd.classList.contains("hidden");
|
|
1859
1735
|
dd.classList.toggle("hidden");
|
|
1860
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
|
+
};
|
|
1861
1745
|
var bRect = btn.getBoundingClientRect();
|
|
1862
1746
|
var ddW = 220;
|
|
1863
1747
|
var ddLeft = bRect.left;
|
|
@@ -1865,29 +1749,36 @@ function setupCreateModal() {
|
|
|
1865
1749
|
if (ddLeft < 10) ddLeft = 10;
|
|
1866
1750
|
dd.style.left = ddLeft + "px";
|
|
1867
1751
|
dd.style.top = (bRect.bottom + 4) + "px";
|
|
1752
|
+
// Auto-apply custom interval when opening
|
|
1753
|
+
if (createInterval === "none") {
|
|
1754
|
+
applyInlineInterval();
|
|
1755
|
+
}
|
|
1868
1756
|
}
|
|
1869
1757
|
}
|
|
1870
1758
|
});
|
|
1871
1759
|
|
|
1872
|
-
// Interval
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
updateIntervalBtn();
|
|
1886
|
-
});
|
|
1887
|
-
})(intOptions[ii]);
|
|
1888
|
-
}
|
|
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
|
+
});
|
|
1889
1773
|
|
|
1890
|
-
// Interval
|
|
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
|
|
1891
1782
|
var intCustomValue = document.getElementById("sched-interval-custom-value");
|
|
1892
1783
|
var intUnitSegs = document.querySelectorAll(".sched-interval-seg");
|
|
1893
1784
|
function getIntervalUnit() {
|
|
@@ -1901,15 +1792,9 @@ function setupCreateModal() {
|
|
|
1901
1792
|
var u = getIntervalUnit();
|
|
1902
1793
|
createInterval = "custom";
|
|
1903
1794
|
createIntervalCustom = { value: v, unit: u };
|
|
1904
|
-
for (var j = 0; j < intOptions.length; j++) {
|
|
1905
|
-
intOptions[j].classList.remove("active");
|
|
1906
|
-
}
|
|
1907
1795
|
updateIntervalBtn();
|
|
1908
1796
|
}
|
|
1909
1797
|
intCustomValue.addEventListener("change", applyInlineInterval);
|
|
1910
|
-
intCustomValue.addEventListener("keydown", function (e) { e.stopPropagation(); });
|
|
1911
|
-
intCustomValue.addEventListener("keyup", function (e) { e.stopPropagation(); });
|
|
1912
|
-
intCustomValue.addEventListener("keypress", function (e) { e.stopPropagation(); });
|
|
1913
1798
|
for (var si = 0; si < intUnitSegs.length; si++) {
|
|
1914
1799
|
(function (seg) {
|
|
1915
1800
|
seg.addEventListener("click", function (e) {
|
|
@@ -1922,18 +1807,44 @@ function setupCreateModal() {
|
|
|
1922
1807
|
})(intUnitSegs[si]);
|
|
1923
1808
|
}
|
|
1924
1809
|
|
|
1925
|
-
//
|
|
1926
|
-
document.
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
+
}
|
|
1931
1843
|
|
|
1932
1844
|
// Custom repeat: cancel
|
|
1933
1845
|
document.getElementById("sched-custom-cancel").addEventListener("click", function (e) {
|
|
1934
1846
|
e.stopPropagation();
|
|
1935
|
-
document.getElementById("sched-
|
|
1936
|
-
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
1847
|
+
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
1937
1848
|
});
|
|
1938
1849
|
|
|
1939
1850
|
// Custom repeat: unit change
|
|
@@ -2057,10 +1968,6 @@ function setupCreateModal() {
|
|
|
2057
1968
|
e.stopPropagation();
|
|
2058
1969
|
createRecurrence = "custom";
|
|
2059
1970
|
createCustomConfirmed = true;
|
|
2060
|
-
var recOptions = createPopover.querySelectorAll(".sched-recurrence-option");
|
|
2061
|
-
for (var j = 0; j < recOptions.length; j++) {
|
|
2062
|
-
recOptions[j].classList.toggle("active", recOptions[j].dataset.recurrence === "custom");
|
|
2063
|
-
}
|
|
2064
1971
|
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
2065
1972
|
updateRecurrenceBtn();
|
|
2066
1973
|
});
|
|
@@ -2184,9 +2091,36 @@ function updateRecurrenceBtn() {
|
|
|
2184
2091
|
if (btn) {
|
|
2185
2092
|
btn.classList.toggle("has-recurrence", createRecurrence !== "none");
|
|
2186
2093
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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");
|
|
2190
2124
|
}
|
|
2191
2125
|
}
|
|
2192
2126
|
|
|
@@ -2195,11 +2129,12 @@ function updateIntervalBtn() {
|
|
|
2195
2129
|
if (btn) {
|
|
2196
2130
|
btn.classList.toggle("has-recurrence", createInterval !== "none");
|
|
2197
2131
|
}
|
|
2198
|
-
//
|
|
2199
|
-
var
|
|
2200
|
-
if (
|
|
2201
|
-
|
|
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");
|
|
2202
2136
|
}
|
|
2137
|
+
// Always show time picker (start time is needed even with interval)
|
|
2203
2138
|
// Update skip-if-running visibility
|
|
2204
2139
|
updateRecurrenceBtn();
|
|
2205
2140
|
}
|
|
@@ -2383,6 +2318,104 @@ function openCreateModalWithRecord(rec, anchorEl) {
|
|
|
2383
2318
|
}
|
|
2384
2319
|
}
|
|
2385
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
|
+
|
|
2386
2419
|
// Update preview to show record name
|
|
2387
2420
|
if (previewEl) {
|
|
2388
2421
|
var previewText = rec.name || "(No title)";
|
|
@@ -2405,6 +2438,9 @@ function openCreateModal(date, hour, anchorEl) {
|
|
|
2405
2438
|
createCustomConfirmed = false;
|
|
2406
2439
|
createInterval = "none";
|
|
2407
2440
|
createIntervalCustom = null;
|
|
2441
|
+
createIntervalEnd = "allday";
|
|
2442
|
+
createIntervalEndAfter = 5;
|
|
2443
|
+
createIntervalEndTime = "";
|
|
2408
2444
|
createColor = "#ffb86c";
|
|
2409
2445
|
|
|
2410
2446
|
// Reset form
|
|
@@ -2476,40 +2512,62 @@ function openCreateModal(date, hour, anchorEl) {
|
|
|
2476
2512
|
var dateStr = createSelectedDate.getFullYear() + "-" + pad(createSelectedDate.getMonth() + 1) + "-" + pad(createSelectedDate.getDate());
|
|
2477
2513
|
document.getElementById("sched-create-date").value = dateStr;
|
|
2478
2514
|
var datePicker = document.getElementById("sched-create-date-picker");
|
|
2479
|
-
if (datePicker)
|
|
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
|
+
}
|
|
2480
2521
|
|
|
2481
2522
|
// Time (use minutes from createSelectedDate for 15-min snapping)
|
|
2482
2523
|
if (hour !== null && hour !== undefined) {
|
|
2483
2524
|
var mins = createSelectedDate.getMinutes ? createSelectedDate.getMinutes() : 0;
|
|
2484
2525
|
document.getElementById("sched-create-time").value = pad(hour) + ":" + pad(mins);
|
|
2485
2526
|
} else {
|
|
2486
|
-
|
|
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);
|
|
2487
2535
|
}
|
|
2488
2536
|
|
|
2489
2537
|
// Update recurrence labels
|
|
2490
2538
|
updateRecurrenceLabels(createSelectedDate);
|
|
2491
2539
|
|
|
2540
|
+
// Enforce min time for today
|
|
2541
|
+
enforceMinTime();
|
|
2542
|
+
|
|
2492
2543
|
// Reset recurrence
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
recOptions[i].classList.toggle("active", recOptions[i].dataset.recurrence === "none");
|
|
2496
|
-
}
|
|
2544
|
+
createRecurrence = "none";
|
|
2545
|
+
createCustomConfirmed = false;
|
|
2497
2546
|
updateRecurrenceBtn();
|
|
2498
2547
|
|
|
2499
2548
|
// Reset interval
|
|
2500
|
-
var intOpts = document.querySelectorAll(".sched-interval-option");
|
|
2501
|
-
for (var io = 0; io < intOpts.length; io++) {
|
|
2502
|
-
intOpts[io].classList.toggle("active", intOpts[io].dataset.interval === "none");
|
|
2503
|
-
}
|
|
2504
2549
|
document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
|
|
2505
2550
|
var timeInput = document.getElementById("sched-create-time");
|
|
2506
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
|
+
|
|
2507
2567
|
updateIntervalBtn();
|
|
2508
2568
|
|
|
2509
2569
|
// Reset custom panel
|
|
2510
2570
|
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
2511
|
-
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
2512
|
-
document.getElementById("sched-create-recurrence-list").style.display = "";
|
|
2513
2571
|
document.getElementById("sched-custom-interval").value = "1";
|
|
2514
2572
|
document.getElementById("sched-custom-unit").value = "week";
|
|
2515
2573
|
document.getElementById("sched-custom-dow-section").style.display = "";
|
|
@@ -2681,6 +2739,18 @@ function closeDeleteDialog() {
|
|
|
2681
2739
|
}
|
|
2682
2740
|
}
|
|
2683
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
|
+
|
|
2684
2754
|
function buildCreateCron() {
|
|
2685
2755
|
if (!createSelectedDate) return null;
|
|
2686
2756
|
|
|
@@ -2707,9 +2777,9 @@ function buildCreateCron() {
|
|
|
2707
2777
|
|
|
2708
2778
|
// Interval only (no recurrence) = interval every day
|
|
2709
2779
|
if (intervalMins > 0 && createRecurrence === "none") {
|
|
2710
|
-
if (intervalMins < 60) return
|
|
2780
|
+
if (intervalMins < 60) return buildOffsetList(m, intervalMins, 60) + " * * * *";
|
|
2711
2781
|
var intHrs = Math.floor(intervalMins / 60);
|
|
2712
|
-
return "
|
|
2782
|
+
return String(m) + " " + buildOffsetList(h, intHrs, 24) + " * * *";
|
|
2713
2783
|
}
|
|
2714
2784
|
|
|
2715
2785
|
if (createRecurrence === "none" && intervalMins === 0) return null;
|
|
@@ -2718,12 +2788,12 @@ function buildCreateCron() {
|
|
|
2718
2788
|
var minField = String(m);
|
|
2719
2789
|
var hourField = String(h);
|
|
2720
2790
|
if (intervalMins > 0 && intervalMins < 60) {
|
|
2721
|
-
minField =
|
|
2791
|
+
minField = buildOffsetList(m, intervalMins, 60);
|
|
2722
2792
|
hourField = "*";
|
|
2723
2793
|
} else if (intervalMins >= 60) {
|
|
2724
2794
|
var intHrs2 = Math.floor(intervalMins / 60);
|
|
2725
2795
|
minField = String(m);
|
|
2726
|
-
hourField =
|
|
2796
|
+
hourField = buildOffsetList(h, intHrs2, 24);
|
|
2727
2797
|
}
|
|
2728
2798
|
|
|
2729
2799
|
if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
|
|
@@ -2837,10 +2907,10 @@ function buildCustomCron(h, m) {
|
|
|
2837
2907
|
var unit = document.getElementById("sched-custom-unit").value;
|
|
2838
2908
|
|
|
2839
2909
|
if (unit === "minute") {
|
|
2840
|
-
return interval === 1 ? "*/1 * * * *" :
|
|
2910
|
+
return interval === 1 ? "*/1 * * * *" : buildOffsetList(m, interval, 60) + " * * * *";
|
|
2841
2911
|
}
|
|
2842
2912
|
if (unit === "hour") {
|
|
2843
|
-
return interval === 1 ? "
|
|
2913
|
+
return interval === 1 ? m + " */1 * * *" : m + " " + buildOffsetList(h, interval, 24) + " * * *";
|
|
2844
2914
|
}
|
|
2845
2915
|
if (unit === "day") {
|
|
2846
2916
|
if (interval === 1) return m + " " + h + " * * *";
|
|
@@ -2886,10 +2956,31 @@ function submitCreateSchedule() {
|
|
|
2886
2956
|
var datePicker = document.getElementById("sched-create-date-picker");
|
|
2887
2957
|
var dateVal = datePicker ? datePicker.value : document.getElementById("sched-create-date").value;
|
|
2888
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
|
+
|
|
2889
2976
|
var cron = buildCreateCron();
|
|
2890
2977
|
|
|
2891
2978
|
// Build recurrence end info
|
|
2892
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
|
+
}
|
|
2893
2984
|
if (cron && createRecurrence === "custom" && createCustomConfirmed) {
|
|
2894
2985
|
if (createEndType === "until" && createEndDate) {
|
|
2895
2986
|
var ey = createEndDate.getFullYear();
|
|
@@ -2901,6 +2992,17 @@ function submitCreateSchedule() {
|
|
|
2901
2992
|
}
|
|
2902
2993
|
}
|
|
2903
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
|
+
|
|
2904
3006
|
var skipRunningEl = document.getElementById("sched-skip-running");
|
|
2905
3007
|
var skipIfRunning = skipRunningEl ? skipRunningEl.checked : true;
|
|
2906
3008
|
|
|
@@ -2928,6 +3030,7 @@ function submitCreateSchedule() {
|
|
|
2928
3030
|
enabled: cron ? true : false,
|
|
2929
3031
|
color: createColor,
|
|
2930
3032
|
recurrenceEnd: recurrenceEnd,
|
|
3033
|
+
intervalEnd: intervalEnd,
|
|
2931
3034
|
maxIterations: maxIterations,
|
|
2932
3035
|
skipIfRunning: skipIfRunning,
|
|
2933
3036
|
},
|
|
@@ -2946,6 +3049,7 @@ function submitCreateSchedule() {
|
|
|
2946
3049
|
enabled: cron ? true : false,
|
|
2947
3050
|
color: createColor,
|
|
2948
3051
|
recurrenceEnd: recurrenceEnd,
|
|
3052
|
+
intervalEnd: intervalEnd,
|
|
2949
3053
|
maxIterations: maxIterations,
|
|
2950
3054
|
skipIfRunning: skipIfRunning,
|
|
2951
3055
|
},
|
|
@@ -3022,15 +3126,15 @@ function cronToHuman(cron) {
|
|
|
3022
3126
|
if (!cron) return "";
|
|
3023
3127
|
var parts = cron.trim().split(/\s+/);
|
|
3024
3128
|
if (parts.length !== 5) return cron;
|
|
3025
|
-
// Minute interval patterns (e.g. */5 * * * *)
|
|
3026
|
-
if (parts[
|
|
3027
|
-
var minStep =
|
|
3028
|
-
return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
|
|
3129
|
+
// Minute interval patterns (e.g. */5 * * * * or 0,15,30,45 * * * *)
|
|
3130
|
+
if (parts[1] === "*" && parts[2] === "*") {
|
|
3131
|
+
var minStep = detectInterval(parts[0], 60);
|
|
3132
|
+
if (minStep) return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
|
|
3029
3133
|
}
|
|
3030
|
-
// Hour interval patterns (e.g. 0 */2 * * *)
|
|
3031
|
-
if (parts[
|
|
3032
|
-
var hrStep =
|
|
3033
|
-
return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
|
|
3134
|
+
// Hour interval patterns (e.g. 0 */2 * * * or 0 1,5,9,13,17,21 * * *)
|
|
3135
|
+
if (parts[2] === "*") {
|
|
3136
|
+
var hrStep = detectInterval(parts[1], 24);
|
|
3137
|
+
if (hrStep) return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
|
|
3034
3138
|
}
|
|
3035
3139
|
var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
|
|
3036
3140
|
var dow = parts[4], dom = parts[2];
|
|
@@ -3043,3 +3147,20 @@ function cronToHuman(cron) {
|
|
|
3043
3147
|
}
|
|
3044
3148
|
return cron;
|
|
3045
3149
|
}
|
|
3150
|
+
|
|
3151
|
+
// Detect if a cron field represents an evenly-spaced interval (*/N or comma-separated offset list)
|
|
3152
|
+
function detectInterval(field, max) {
|
|
3153
|
+
if (field.indexOf("/") !== -1) return parseInt(field.split("/")[1], 10) || null;
|
|
3154
|
+
if (field.indexOf(",") === -1) return null;
|
|
3155
|
+
var vals = field.split(",").map(function (v) { return parseInt(v, 10); }).sort(function (a, b) { return a - b; });
|
|
3156
|
+
if (vals.length < 2) return null;
|
|
3157
|
+
var step = vals[1] - vals[0];
|
|
3158
|
+
if (step <= 0) return null;
|
|
3159
|
+
// Verify all values are evenly spaced (wrapping around max)
|
|
3160
|
+
for (var i = 1; i < vals.length; i++) {
|
|
3161
|
+
if (vals[i] - vals[i - 1] !== step) return null;
|
|
3162
|
+
}
|
|
3163
|
+
// Check the wrap-around gap matches too
|
|
3164
|
+
if ((max - vals[vals.length - 1] + vals[0]) !== step) return null;
|
|
3165
|
+
return step;
|
|
3166
|
+
}
|