clay-server 2.15.0-beta.2 → 2.15.0-beta.3
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 +7 -2
- package/lib/public/app.js +3 -0
- package/lib/public/css/scheduler-modal.css +193 -0
- package/lib/public/index.html +30 -0
- package/lib/public/modules/scheduler.js +218 -17
- package/lib/scheduler.js +2 -0
- package/package.json +1 -1
package/lib/project.js
CHANGED
|
@@ -615,9 +615,13 @@ function createProjectContext(opts) {
|
|
|
615
615
|
var loopRegistry = createLoopRegistry({
|
|
616
616
|
cwd: cwd,
|
|
617
617
|
onTrigger: function (record) {
|
|
618
|
-
//
|
|
618
|
+
// Skip trigger if a loop is already active and skipIfRunning is enabled
|
|
619
619
|
if (loopState.active || loopState.phase === "executing") {
|
|
620
|
-
|
|
620
|
+
if (record.skipIfRunning !== false) {
|
|
621
|
+
console.log("[loop-registry] Skipping trigger for " + record.name + " — loop already active (skipIfRunning)");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
console.log("[loop-registry] Loop active but skipIfRunning disabled for " + record.name + "; deferring");
|
|
621
625
|
return;
|
|
622
626
|
}
|
|
623
627
|
|
|
@@ -3202,6 +3206,7 @@ function createProjectContext(opts) {
|
|
|
3202
3206
|
source: "schedule",
|
|
3203
3207
|
color: sData.color || null,
|
|
3204
3208
|
recurrenceEnd: sData.recurrenceEnd || null,
|
|
3209
|
+
skipIfRunning: sData.skipIfRunning !== undefined ? sData.skipIfRunning : true,
|
|
3205
3210
|
});
|
|
3206
3211
|
return;
|
|
3207
3212
|
}
|
package/lib/public/app.js
CHANGED
|
@@ -6913,6 +6913,9 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
|
|
|
6913
6913
|
connect();
|
|
6914
6914
|
if (!currentSlug) {
|
|
6915
6915
|
showHomeHub();
|
|
6916
|
+
} else if (location.hash === "#scheduler") {
|
|
6917
|
+
// Restore scheduler view after refresh
|
|
6918
|
+
setTimeout(function () { openSchedulerToTab("calendar"); }, 500);
|
|
6916
6919
|
} else {
|
|
6917
6920
|
inputEl.focus();
|
|
6918
6921
|
}
|
|
@@ -596,6 +596,11 @@
|
|
|
596
596
|
box-sizing: border-box;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
.light-theme .sched-create-date-input,
|
|
600
|
+
.light-theme .sched-create-time-input {
|
|
601
|
+
color-scheme: light;
|
|
602
|
+
}
|
|
603
|
+
|
|
599
604
|
.sched-create-date-input:focus,
|
|
600
605
|
.sched-create-time-input:focus {
|
|
601
606
|
border-color: var(--accent);
|
|
@@ -1076,6 +1081,194 @@
|
|
|
1076
1081
|
font-weight: 600;
|
|
1077
1082
|
}
|
|
1078
1083
|
|
|
1084
|
+
.sched-create-interval-btn {
|
|
1085
|
+
background: none;
|
|
1086
|
+
border: 1px solid var(--border);
|
|
1087
|
+
border-radius: 6px;
|
|
1088
|
+
padding: 4px 6px;
|
|
1089
|
+
cursor: pointer;
|
|
1090
|
+
color: var(--text-muted);
|
|
1091
|
+
display: flex;
|
|
1092
|
+
align-items: center;
|
|
1093
|
+
transition: color 0.15s, border-color 0.15s;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.sched-create-interval-btn:hover {
|
|
1097
|
+
color: var(--accent);
|
|
1098
|
+
border-color: var(--accent);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.sched-create-interval-btn.has-recurrence {
|
|
1102
|
+
color: var(--accent);
|
|
1103
|
+
border-color: var(--accent);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.sched-create-interval-btn .lucide {
|
|
1107
|
+
width: 14px;
|
|
1108
|
+
height: 14px;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.sched-create-interval-dropdown {
|
|
1112
|
+
position: fixed;
|
|
1113
|
+
z-index: 1200;
|
|
1114
|
+
background: var(--bg);
|
|
1115
|
+
border: 1px solid var(--border);
|
|
1116
|
+
border-radius: 10px;
|
|
1117
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
|
1118
|
+
min-width: 200px;
|
|
1119
|
+
max-width: 260px;
|
|
1120
|
+
overflow: hidden;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.sched-create-interval-dropdown.hidden {
|
|
1124
|
+
display: none;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.sched-interval-options {
|
|
1128
|
+
padding: 4px;
|
|
1129
|
+
display: flex;
|
|
1130
|
+
flex-direction: column;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.sched-interval-option {
|
|
1134
|
+
background: none;
|
|
1135
|
+
border: none;
|
|
1136
|
+
padding: 8px 12px;
|
|
1137
|
+
text-align: left;
|
|
1138
|
+
cursor: pointer;
|
|
1139
|
+
font-size: 13px;
|
|
1140
|
+
border-radius: 6px;
|
|
1141
|
+
color: var(--text);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.sched-interval-option:hover {
|
|
1145
|
+
background: var(--hover);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.sched-interval-option.active {
|
|
1149
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
1150
|
+
color: var(--accent);
|
|
1151
|
+
font-weight: 600;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.sched-interval-custom-row {
|
|
1155
|
+
display: flex;
|
|
1156
|
+
align-items: center;
|
|
1157
|
+
gap: 6px;
|
|
1158
|
+
padding: 4px 8px 6px;
|
|
1159
|
+
border-top: 1px solid var(--border);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.sched-interval-custom-row .sched-custom-label {
|
|
1163
|
+
font-size: 13px;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.sched-interval-custom-row .sched-custom-interval {
|
|
1167
|
+
width: 48px;
|
|
1168
|
+
padding: 4px 6px;
|
|
1169
|
+
font-size: 13px;
|
|
1170
|
+
border-radius: 6px;
|
|
1171
|
+
border: 1px solid var(--border);
|
|
1172
|
+
background: var(--input-bg);
|
|
1173
|
+
color: var(--text);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.sched-interval-unit-toggle {
|
|
1177
|
+
display: flex;
|
|
1178
|
+
border: 1px solid var(--border);
|
|
1179
|
+
border-radius: 6px;
|
|
1180
|
+
overflow: hidden;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.sched-interval-seg {
|
|
1184
|
+
padding: 4px 10px;
|
|
1185
|
+
font-size: 12px;
|
|
1186
|
+
font-weight: 500;
|
|
1187
|
+
border: none;
|
|
1188
|
+
background: var(--input-bg);
|
|
1189
|
+
color: var(--text-dimmer);
|
|
1190
|
+
cursor: pointer;
|
|
1191
|
+
transition: background 0.15s, color 0.15s;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.sched-interval-seg:first-child {
|
|
1195
|
+
border-right: 1px solid var(--border);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.sched-interval-seg.active {
|
|
1199
|
+
background: var(--accent);
|
|
1200
|
+
color: #fff;
|
|
1201
|
+
font-weight: 600;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.sched-interval-seg:not(.active):hover {
|
|
1205
|
+
background: var(--hover);
|
|
1206
|
+
color: var(--text);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.sched-skip-running-row {
|
|
1210
|
+
padding: 6px 12px;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.sched-skip-running-row.hidden {
|
|
1214
|
+
display: none;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.sched-switch-label {
|
|
1218
|
+
display: flex;
|
|
1219
|
+
align-items: center;
|
|
1220
|
+
gap: 8px;
|
|
1221
|
+
cursor: pointer;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.sched-switch-text {
|
|
1225
|
+
font-size: 11px;
|
|
1226
|
+
color: var(--text-muted);
|
|
1227
|
+
user-select: none;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.sched-switch {
|
|
1231
|
+
position: relative;
|
|
1232
|
+
width: 28px;
|
|
1233
|
+
height: 16px;
|
|
1234
|
+
flex-shrink: 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.sched-switch input {
|
|
1238
|
+
opacity: 0;
|
|
1239
|
+
width: 0;
|
|
1240
|
+
height: 0;
|
|
1241
|
+
position: absolute;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.sched-switch-slider {
|
|
1245
|
+
position: absolute;
|
|
1246
|
+
inset: 0;
|
|
1247
|
+
background: var(--border);
|
|
1248
|
+
border-radius: 8px;
|
|
1249
|
+
transition: background 0.2s;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.sched-switch-slider::before {
|
|
1253
|
+
content: "";
|
|
1254
|
+
position: absolute;
|
|
1255
|
+
left: 2px;
|
|
1256
|
+
top: 2px;
|
|
1257
|
+
width: 12px;
|
|
1258
|
+
height: 12px;
|
|
1259
|
+
background: #fff;
|
|
1260
|
+
border-radius: 50%;
|
|
1261
|
+
transition: transform 0.2s;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.sched-switch input:checked + .sched-switch-slider {
|
|
1265
|
+
background: var(--accent);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.sched-switch input:checked + .sched-switch-slider::before {
|
|
1269
|
+
transform: translateX(12px);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1079
1272
|
/* --- Custom repeat panel --- */
|
|
1080
1273
|
.sched-custom-repeat-panel {
|
|
1081
1274
|
padding: 0;
|
package/lib/public/index.html
CHANGED
|
@@ -1625,6 +1625,7 @@
|
|
|
1625
1625
|
<input type="date" class="sched-create-date-input" id="sched-create-date-picker" value="">
|
|
1626
1626
|
<input type="time" class="sched-create-time-input" id="sched-create-time" value="09:00">
|
|
1627
1627
|
<button class="sched-create-recurrence-btn" id="sched-create-recurrence-btn" title="Recurrence"><i data-lucide="repeat"></i></button>
|
|
1628
|
+
<button class="sched-create-interval-btn" id="sched-create-interval-btn" title="Interval"><i data-lucide="timer"></i></button>
|
|
1628
1629
|
</div>
|
|
1629
1630
|
<button class="sched-create-close-btn" id="sched-create-cancel" title="Close"><i data-lucide="x"></i></button>
|
|
1630
1631
|
</div>
|
|
@@ -1666,6 +1667,15 @@
|
|
|
1666
1667
|
<i data-lucide="align-left" class="sched-create-row-icon"></i>
|
|
1667
1668
|
<textarea class="sched-create-row-textarea" id="sched-create-desc" rows="3" placeholder="Description"></textarea>
|
|
1668
1669
|
</div>
|
|
1670
|
+
<div class="sched-skip-running-row hidden" id="sched-skip-running-row">
|
|
1671
|
+
<label class="sched-switch-label" for="sched-skip-running">
|
|
1672
|
+
<div class="sched-switch">
|
|
1673
|
+
<input type="checkbox" id="sched-skip-running" checked>
|
|
1674
|
+
<span class="sched-switch-slider"></span>
|
|
1675
|
+
</div>
|
|
1676
|
+
<span class="sched-switch-text">Skip if previous run still active</span>
|
|
1677
|
+
</label>
|
|
1678
|
+
</div>
|
|
1669
1679
|
<!-- Bottom bar -->
|
|
1670
1680
|
<div class="sched-create-bottom">
|
|
1671
1681
|
<div class="sched-create-bottom-left">
|
|
@@ -1703,6 +1713,8 @@
|
|
|
1703
1713
|
<span class="sched-custom-label">Every</span>
|
|
1704
1714
|
<input type="number" class="sched-custom-interval" id="sched-custom-interval" value="1" min="1" max="99">
|
|
1705
1715
|
<select class="sched-field-select sched-custom-unit" id="sched-custom-unit">
|
|
1716
|
+
<option value="minute">Minute</option>
|
|
1717
|
+
<option value="hour">Hour</option>
|
|
1706
1718
|
<option value="day">Day</option>
|
|
1707
1719
|
<option value="week" selected>Week</option>
|
|
1708
1720
|
<option value="month">Month</option>
|
|
@@ -1763,6 +1775,24 @@
|
|
|
1763
1775
|
</div>
|
|
1764
1776
|
</div>
|
|
1765
1777
|
</div>
|
|
1778
|
+
<!-- Interval dropdown -->
|
|
1779
|
+
<div class="sched-create-interval-dropdown hidden" id="sched-create-interval-dropdown">
|
|
1780
|
+
<div class="sched-interval-options" id="sched-create-interval-list">
|
|
1781
|
+
<button class="sched-interval-option active" data-interval="none">No interval</button>
|
|
1782
|
+
<button class="sched-interval-option" data-interval="1">Every minute</button>
|
|
1783
|
+
<button class="sched-interval-option" data-interval="5">Every 5 minutes</button>
|
|
1784
|
+
<button class="sched-interval-option" data-interval="15">Every 15 minutes</button>
|
|
1785
|
+
<button class="sched-interval-option" data-interval="30">Every 30 minutes</button>
|
|
1786
|
+
<button class="sched-interval-option" data-interval="60">Every hour</button>
|
|
1787
|
+
</div>
|
|
1788
|
+
<div class="sched-interval-custom-row">
|
|
1789
|
+
<span class="sched-custom-label">Every</span>
|
|
1790
|
+
<input type="number" class="sched-custom-interval" id="sched-interval-custom-value" value="10" min="1" max="999">
|
|
1791
|
+
<div class="sched-interval-unit-toggle" id="sched-interval-custom-unit">
|
|
1792
|
+
<button type="button" class="sched-interval-seg active" data-unit="minute">min</button><button type="button" class="sched-interval-seg" data-unit="hour">hrs</button>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>
|
|
1795
|
+
</div>
|
|
1766
1796
|
</div>
|
|
1767
1797
|
|
|
1768
1798
|
<!-- Delete recurring event dialog -->
|
|
@@ -54,6 +54,8 @@ var createPopover = null;
|
|
|
54
54
|
var createSelectedDate = null; // Date object for clicked calendar date
|
|
55
55
|
var createRecurrence = "none"; // current recurrence selection
|
|
56
56
|
var createCustomConfirmed = false; // whether custom repeat was confirmed via OK
|
|
57
|
+
var createInterval = "none"; // current interval selection: "none", "1", "5", "15", "30", "60", "custom"
|
|
58
|
+
var createIntervalCustom = null; // { value: N, unit: "minute"|"hour" } for custom interval
|
|
57
59
|
var createColor = "#ffb86c"; // selected event color (default: accent)
|
|
58
60
|
var createEndType = "never"; // "never" | "until" | "after"
|
|
59
61
|
var createEndDate = null; // Date for "until" end type
|
|
@@ -368,6 +370,11 @@ function openScheduler() {
|
|
|
368
370
|
|
|
369
371
|
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
370
372
|
if (sidebarBtn) sidebarBtn.classList.add("active");
|
|
373
|
+
|
|
374
|
+
// Persist scheduler state in URL hash
|
|
375
|
+
if (location.hash !== "#scheduler") {
|
|
376
|
+
history.replaceState(null, "", location.pathname + "#scheduler");
|
|
377
|
+
}
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
export function closeScheduler() {
|
|
@@ -403,6 +410,11 @@ export function closeScheduler() {
|
|
|
403
410
|
// Un-mark sidebar button
|
|
404
411
|
var sidebarBtn = document.getElementById("scheduler-btn");
|
|
405
412
|
if (sidebarBtn) sidebarBtn.classList.remove("active");
|
|
413
|
+
|
|
414
|
+
// Remove scheduler hash from URL
|
|
415
|
+
if (location.hash === "#scheduler") {
|
|
416
|
+
history.replaceState(null, "", location.pathname);
|
|
417
|
+
}
|
|
406
418
|
}
|
|
407
419
|
|
|
408
420
|
// Reset state on project switch (SPA navigation, no full reload)
|
|
@@ -995,6 +1007,15 @@ function renderWeekView() {
|
|
|
995
1007
|
}
|
|
996
1008
|
for (var e = 0; e < events.length; e++) {
|
|
997
1009
|
var ev = events[e];
|
|
1010
|
+
if (ev.intervalBadge) {
|
|
1011
|
+
var badgeStyle = "";
|
|
1012
|
+
if (ev.color) badgeStyle = "background:" + ev.color;
|
|
1013
|
+
html += '<div class="scheduler-week-event interval-badge ' + (ev.enabled ? "enabled" : "disabled") + '" data-rec-id="' + ev.id + '" style="position:relative;top:0;left:0;width:85%;height:auto;' + badgeStyle + '">';
|
|
1014
|
+
html += '<span class="scheduler-week-event-title">' + esc(ev.name) + '</span>';
|
|
1015
|
+
html += '<span class="scheduler-week-event-time">' + esc(ev.timeStr) + '</span>';
|
|
1016
|
+
html += '</div>';
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
998
1019
|
var topPct = ((ev.hour * 60 + ev.minute) / 1440) * 100;
|
|
999
1020
|
var evColor = ev.color || "";
|
|
1000
1021
|
var assign = colAssign[ev.id] || { col: 0, totalCols: 1 };
|
|
@@ -1178,15 +1199,32 @@ function getEventsForDate(date) {
|
|
|
1178
1199
|
if (parsed.months.indexOf(month) === -1) continue;
|
|
1179
1200
|
if (parsed.daysOfMonth.indexOf(dom) === -1) continue;
|
|
1180
1201
|
if (parsed.daysOfWeek.indexOf(dow) === -1) continue;
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1202
|
+
// Detect sub-daily interval mode to prevent calendar item explosion
|
|
1203
|
+
var cronParts = r.cron.trim().split(/\s+/);
|
|
1204
|
+
var isIntervalMode = (cronParts[0].indexOf("/") !== -1 && cronParts[1] === "*")
|
|
1205
|
+
|| (cronParts[1].indexOf("/") !== -1)
|
|
1206
|
+
|| (parsed.minutes.length * parsed.hours.length > 24);
|
|
1207
|
+
if (isIntervalMode) {
|
|
1208
|
+
results.push({
|
|
1209
|
+
id: r.id, name: r.name, enabled: r.enabled,
|
|
1210
|
+
hour: 0, minute: 0,
|
|
1211
|
+
timeStr: cronToHuman(r.cron) || "Interval",
|
|
1212
|
+
allDay: true,
|
|
1213
|
+
intervalBadge: true,
|
|
1214
|
+
color: r.color || null,
|
|
1215
|
+
source: r.source || null,
|
|
1216
|
+
});
|
|
1217
|
+
} else {
|
|
1218
|
+
for (var h = 0; h < parsed.hours.length; h++) {
|
|
1219
|
+
for (var m = 0; m < parsed.minutes.length; m++) {
|
|
1220
|
+
results.push({
|
|
1221
|
+
id: r.id, name: r.name, enabled: r.enabled,
|
|
1222
|
+
hour: parsed.hours[h], minute: parsed.minutes[m],
|
|
1223
|
+
timeStr: pad(parsed.hours[h]) + ":" + pad(parsed.minutes[m]),
|
|
1224
|
+
color: r.color || null,
|
|
1225
|
+
source: r.source || null,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1190
1228
|
}
|
|
1191
1229
|
}
|
|
1192
1230
|
}
|
|
@@ -1766,6 +1804,8 @@ function setupCreateModal() {
|
|
|
1766
1804
|
e.stopPropagation();
|
|
1767
1805
|
var dd = document.getElementById("sched-create-recurrence-dropdown");
|
|
1768
1806
|
var btn = document.getElementById("sched-create-recurrence-btn");
|
|
1807
|
+
// Close interval dropdown if open
|
|
1808
|
+
document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
|
|
1769
1809
|
if (dd) {
|
|
1770
1810
|
var wasHidden = dd.classList.contains("hidden");
|
|
1771
1811
|
dd.classList.toggle("hidden");
|
|
@@ -1807,6 +1847,81 @@ function setupCreateModal() {
|
|
|
1807
1847
|
})(recOptions[i]);
|
|
1808
1848
|
}
|
|
1809
1849
|
|
|
1850
|
+
// --- Interval button + dropdown ---
|
|
1851
|
+
document.getElementById("sched-create-interval-btn").addEventListener("click", function (e) {
|
|
1852
|
+
e.stopPropagation();
|
|
1853
|
+
var dd = document.getElementById("sched-create-interval-dropdown");
|
|
1854
|
+
var btn = document.getElementById("sched-create-interval-btn");
|
|
1855
|
+
// Close recurrence dropdown if open
|
|
1856
|
+
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
1857
|
+
if (dd) {
|
|
1858
|
+
var wasHidden = dd.classList.contains("hidden");
|
|
1859
|
+
dd.classList.toggle("hidden");
|
|
1860
|
+
if (wasHidden && btn) {
|
|
1861
|
+
var bRect = btn.getBoundingClientRect();
|
|
1862
|
+
var ddW = 220;
|
|
1863
|
+
var ddLeft = bRect.left;
|
|
1864
|
+
if (ddLeft + ddW > window.innerWidth - 10) ddLeft = window.innerWidth - ddW - 10;
|
|
1865
|
+
if (ddLeft < 10) ddLeft = 10;
|
|
1866
|
+
dd.style.left = ddLeft + "px";
|
|
1867
|
+
dd.style.top = (bRect.bottom + 4) + "px";
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// Interval option clicks
|
|
1873
|
+
var intOptions = document.querySelectorAll(".sched-interval-option");
|
|
1874
|
+
for (var ii = 0; ii < intOptions.length; ii++) {
|
|
1875
|
+
(function (opt) {
|
|
1876
|
+
opt.addEventListener("click", function (e) {
|
|
1877
|
+
e.stopPropagation();
|
|
1878
|
+
var val = opt.dataset.interval;
|
|
1879
|
+
for (var j = 0; j < intOptions.length; j++) {
|
|
1880
|
+
intOptions[j].classList.toggle("active", intOptions[j] === opt);
|
|
1881
|
+
}
|
|
1882
|
+
createInterval = val;
|
|
1883
|
+
createIntervalCustom = null;
|
|
1884
|
+
document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
|
|
1885
|
+
updateIntervalBtn();
|
|
1886
|
+
});
|
|
1887
|
+
})(intOptions[ii]);
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Interval inline custom input
|
|
1891
|
+
var intCustomValue = document.getElementById("sched-interval-custom-value");
|
|
1892
|
+
var intUnitSegs = document.querySelectorAll(".sched-interval-seg");
|
|
1893
|
+
function getIntervalUnit() {
|
|
1894
|
+
for (var s = 0; s < intUnitSegs.length; s++) {
|
|
1895
|
+
if (intUnitSegs[s].classList.contains("active")) return intUnitSegs[s].dataset.unit;
|
|
1896
|
+
}
|
|
1897
|
+
return "minute";
|
|
1898
|
+
}
|
|
1899
|
+
function applyInlineInterval() {
|
|
1900
|
+
var v = parseInt(intCustomValue.value, 10) || 1;
|
|
1901
|
+
var u = getIntervalUnit();
|
|
1902
|
+
createInterval = "custom";
|
|
1903
|
+
createIntervalCustom = { value: v, unit: u };
|
|
1904
|
+
for (var j = 0; j < intOptions.length; j++) {
|
|
1905
|
+
intOptions[j].classList.remove("active");
|
|
1906
|
+
}
|
|
1907
|
+
updateIntervalBtn();
|
|
1908
|
+
}
|
|
1909
|
+
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
|
+
for (var si = 0; si < intUnitSegs.length; si++) {
|
|
1914
|
+
(function (seg) {
|
|
1915
|
+
seg.addEventListener("click", function (e) {
|
|
1916
|
+
e.stopPropagation();
|
|
1917
|
+
for (var s = 0; s < intUnitSegs.length; s++) {
|
|
1918
|
+
intUnitSegs[s].classList.toggle("active", intUnitSegs[s] === seg);
|
|
1919
|
+
}
|
|
1920
|
+
applyInlineInterval();
|
|
1921
|
+
});
|
|
1922
|
+
})(intUnitSegs[si]);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1810
1925
|
// Custom repeat: back
|
|
1811
1926
|
document.getElementById("sched-custom-back").addEventListener("click", function (e) {
|
|
1812
1927
|
e.stopPropagation();
|
|
@@ -2069,6 +2184,24 @@ function updateRecurrenceBtn() {
|
|
|
2069
2184
|
if (btn) {
|
|
2070
2185
|
btn.classList.toggle("has-recurrence", createRecurrence !== "none");
|
|
2071
2186
|
}
|
|
2187
|
+
var skipRow = document.getElementById("sched-skip-running-row");
|
|
2188
|
+
if (skipRow) {
|
|
2189
|
+
skipRow.classList.toggle("hidden", createInterval === "none");
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function updateIntervalBtn() {
|
|
2194
|
+
var btn = document.getElementById("sched-create-interval-btn");
|
|
2195
|
+
if (btn) {
|
|
2196
|
+
btn.classList.toggle("has-recurrence", createInterval !== "none");
|
|
2197
|
+
}
|
|
2198
|
+
// Hide time picker when interval is set
|
|
2199
|
+
var timeInput = document.getElementById("sched-create-time");
|
|
2200
|
+
if (timeInput) {
|
|
2201
|
+
timeInput.style.display = createInterval !== "none" ? "none" : "";
|
|
2202
|
+
}
|
|
2203
|
+
// Update skip-if-running visibility
|
|
2204
|
+
updateRecurrenceBtn();
|
|
2072
2205
|
}
|
|
2073
2206
|
|
|
2074
2207
|
function removePreview() {
|
|
@@ -2211,6 +2344,10 @@ function openCreateModalWithRecord(rec, anchorEl) {
|
|
|
2211
2344
|
}
|
|
2212
2345
|
}
|
|
2213
2346
|
|
|
2347
|
+
// Set skip-if-running
|
|
2348
|
+
var skipRunningEl = document.getElementById("sched-skip-running");
|
|
2349
|
+
if (skipRunningEl) skipRunningEl.checked = rec.skipIfRunning !== false;
|
|
2350
|
+
|
|
2214
2351
|
// Set run mode and iterations
|
|
2215
2352
|
var editRunMode = (rec.maxIterations && rec.maxIterations > 1) ? "multi" : "single";
|
|
2216
2353
|
var editRunBtns = createPopover.querySelectorAll(".sched-run-mode-btn");
|
|
@@ -2266,6 +2403,8 @@ function openCreateModal(date, hour, anchorEl) {
|
|
|
2266
2403
|
createSelectedDate = date || new Date();
|
|
2267
2404
|
createRecurrence = "none";
|
|
2268
2405
|
createCustomConfirmed = false;
|
|
2406
|
+
createInterval = "none";
|
|
2407
|
+
createIntervalCustom = null;
|
|
2269
2408
|
createColor = "#ffb86c";
|
|
2270
2409
|
|
|
2271
2410
|
// Reset form
|
|
@@ -2357,6 +2496,16 @@ function openCreateModal(date, hour, anchorEl) {
|
|
|
2357
2496
|
}
|
|
2358
2497
|
updateRecurrenceBtn();
|
|
2359
2498
|
|
|
2499
|
+
// 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
|
+
document.getElementById("sched-create-interval-dropdown").classList.add("hidden");
|
|
2505
|
+
var timeInput = document.getElementById("sched-create-time");
|
|
2506
|
+
if (timeInput) timeInput.style.display = "";
|
|
2507
|
+
updateIntervalBtn();
|
|
2508
|
+
|
|
2360
2509
|
// Reset custom panel
|
|
2361
2510
|
document.getElementById("sched-create-recurrence-dropdown").classList.add("hidden");
|
|
2362
2511
|
document.getElementById("sched-custom-repeat-panel").classList.add("hidden");
|
|
@@ -2544,17 +2693,48 @@ function buildCreateCron() {
|
|
|
2544
2693
|
var dom = createSelectedDate.getDate();
|
|
2545
2694
|
var month = createSelectedDate.getMonth() + 1;
|
|
2546
2695
|
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
if (
|
|
2696
|
+
// Determine interval minutes
|
|
2697
|
+
var intervalMins = 0;
|
|
2698
|
+
if (createInterval !== "none") {
|
|
2699
|
+
if (createInterval === "custom" && createIntervalCustom) {
|
|
2700
|
+
intervalMins = createIntervalCustom.unit === "hour"
|
|
2701
|
+
? createIntervalCustom.value * 60
|
|
2702
|
+
: createIntervalCustom.value;
|
|
2703
|
+
} else {
|
|
2704
|
+
intervalMins = parseInt(createInterval, 10) || 0;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Interval only (no recurrence) = interval every day
|
|
2709
|
+
if (intervalMins > 0 && createRecurrence === "none") {
|
|
2710
|
+
if (intervalMins < 60) return "*/" + intervalMins + " * * * *";
|
|
2711
|
+
var intHrs = Math.floor(intervalMins / 60);
|
|
2712
|
+
return "0 */" + intHrs + " * * *";
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
if (createRecurrence === "none" && intervalMins === 0) return null;
|
|
2716
|
+
|
|
2717
|
+
// Build minute/hour fields from interval or time
|
|
2718
|
+
var minField = String(m);
|
|
2719
|
+
var hourField = String(h);
|
|
2720
|
+
if (intervalMins > 0 && intervalMins < 60) {
|
|
2721
|
+
minField = "*/" + intervalMins;
|
|
2722
|
+
hourField = "*";
|
|
2723
|
+
} else if (intervalMins >= 60) {
|
|
2724
|
+
var intHrs2 = Math.floor(intervalMins / 60);
|
|
2725
|
+
minField = String(m);
|
|
2726
|
+
hourField = "*/" + intHrs2;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (createRecurrence === "daily") return minField + " " + hourField + " * * *";
|
|
2730
|
+
if (createRecurrence === "weekly") return minField + " " + hourField + " * * " + dow;
|
|
2550
2731
|
if (createRecurrence === "biweekly") {
|
|
2551
|
-
// Nth weekday of month — approximate with cron (run weekly, but it's the closest)
|
|
2552
2732
|
var weekNum = Math.ceil(dom / 7);
|
|
2553
|
-
return
|
|
2733
|
+
return minField + " " + hourField + " " + ((weekNum - 1) * 7 + 1) + "-" + (weekNum * 7) + " * " + dow;
|
|
2554
2734
|
}
|
|
2555
|
-
if (createRecurrence === "yearly") return
|
|
2556
|
-
if (createRecurrence === "monthly") return
|
|
2557
|
-
if (createRecurrence === "weekdays") return
|
|
2735
|
+
if (createRecurrence === "yearly") return minField + " " + hourField + " " + dom + " " + month + " *";
|
|
2736
|
+
if (createRecurrence === "monthly") return minField + " " + hourField + " " + dom + " * *";
|
|
2737
|
+
if (createRecurrence === "weekdays") return minField + " " + hourField + " * * 1-5";
|
|
2558
2738
|
|
|
2559
2739
|
if (createRecurrence === "custom" && createCustomConfirmed) {
|
|
2560
2740
|
return buildCustomCron(h, m);
|
|
@@ -2656,6 +2836,12 @@ function buildCustomCron(h, m) {
|
|
|
2656
2836
|
var interval = parseInt(document.getElementById("sched-custom-interval").value, 10) || 1;
|
|
2657
2837
|
var unit = document.getElementById("sched-custom-unit").value;
|
|
2658
2838
|
|
|
2839
|
+
if (unit === "minute") {
|
|
2840
|
+
return interval === 1 ? "*/1 * * * *" : "*/" + interval + " * * * *";
|
|
2841
|
+
}
|
|
2842
|
+
if (unit === "hour") {
|
|
2843
|
+
return interval === 1 ? "0 */1 * * *" : "0 */" + interval + " * * *";
|
|
2844
|
+
}
|
|
2659
2845
|
if (unit === "day") {
|
|
2660
2846
|
if (interval === 1) return m + " " + h + " * * *";
|
|
2661
2847
|
return m + " " + h + " */" + interval + " * *";
|
|
@@ -2715,6 +2901,9 @@ function submitCreateSchedule() {
|
|
|
2715
2901
|
}
|
|
2716
2902
|
}
|
|
2717
2903
|
|
|
2904
|
+
var skipRunningEl = document.getElementById("sched-skip-running");
|
|
2905
|
+
var skipIfRunning = skipRunningEl ? skipRunningEl.checked : true;
|
|
2906
|
+
|
|
2718
2907
|
var activeRunMode = createPopover.querySelector(".sched-run-mode-btn.active");
|
|
2719
2908
|
var runMode = activeRunMode ? activeRunMode.dataset.mode : "single";
|
|
2720
2909
|
var maxIterations = 1;
|
|
@@ -2740,6 +2929,7 @@ function submitCreateSchedule() {
|
|
|
2740
2929
|
color: createColor,
|
|
2741
2930
|
recurrenceEnd: recurrenceEnd,
|
|
2742
2931
|
maxIterations: maxIterations,
|
|
2932
|
+
skipIfRunning: skipIfRunning,
|
|
2743
2933
|
},
|
|
2744
2934
|
});
|
|
2745
2935
|
} else {
|
|
@@ -2757,6 +2947,7 @@ function submitCreateSchedule() {
|
|
|
2757
2947
|
color: createColor,
|
|
2758
2948
|
recurrenceEnd: recurrenceEnd,
|
|
2759
2949
|
maxIterations: maxIterations,
|
|
2950
|
+
skipIfRunning: skipIfRunning,
|
|
2760
2951
|
},
|
|
2761
2952
|
});
|
|
2762
2953
|
}
|
|
@@ -2831,6 +3022,16 @@ function cronToHuman(cron) {
|
|
|
2831
3022
|
if (!cron) return "";
|
|
2832
3023
|
var parts = cron.trim().split(/\s+/);
|
|
2833
3024
|
if (parts.length !== 5) return cron;
|
|
3025
|
+
// Minute interval patterns (e.g. */5 * * * *)
|
|
3026
|
+
if (parts[0].indexOf("/") !== -1 && parts[1] === "*" && parts[2] === "*") {
|
|
3027
|
+
var minStep = parseInt(parts[0].split("/")[1], 10);
|
|
3028
|
+
return minStep === 1 ? "Every minute" : "Every " + minStep + " minutes";
|
|
3029
|
+
}
|
|
3030
|
+
// Hour interval patterns (e.g. 0 */2 * * *)
|
|
3031
|
+
if (parts[1].indexOf("/") !== -1 && parts[2] === "*") {
|
|
3032
|
+
var hrStep = parseInt(parts[1].split("/")[1], 10);
|
|
3033
|
+
return hrStep === 1 ? "Every hour" : "Every " + hrStep + " hours";
|
|
3034
|
+
}
|
|
2834
3035
|
var t = pad(parseInt(parts[1], 10)) + ":" + pad(parseInt(parts[0], 10));
|
|
2835
3036
|
var dow = parts[4], dom = parts[2];
|
|
2836
3037
|
if (dow === "*" && dom === "*") return "Every day at " + t;
|
package/lib/scheduler.js
CHANGED
|
@@ -264,6 +264,7 @@ function createLoopRegistry(opts) {
|
|
|
264
264
|
recurrenceEnd: data.recurrenceEnd || null,
|
|
265
265
|
mode: data.mode || "loop",
|
|
266
266
|
prompt: data.prompt || null,
|
|
267
|
+
skipIfRunning: data.skipIfRunning !== undefined ? data.skipIfRunning : true,
|
|
267
268
|
runs: [],
|
|
268
269
|
};
|
|
269
270
|
if (rec.cron && rec.enabled) {
|
|
@@ -299,6 +300,7 @@ function createLoopRegistry(opts) {
|
|
|
299
300
|
if (data.color !== undefined) rec.color = data.color;
|
|
300
301
|
if (data.allDay !== undefined) rec.allDay = data.allDay;
|
|
301
302
|
if (data.linkedTaskId !== undefined) rec.linkedTaskId = data.linkedTaskId;
|
|
303
|
+
if (data.skipIfRunning !== undefined) rec.skipIfRunning = data.skipIfRunning;
|
|
302
304
|
rec.updatedAt = Date.now();
|
|
303
305
|
if (rec.cron && rec.enabled) {
|
|
304
306
|
rec.nextRunAt = nextRunTime(rec.cron);
|