bosun 0.40.3 → 0.40.4

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.
@@ -2,12 +2,13 @@
2
2
  * Kanban Board Component — GitHub Projects-style task board
3
3
  * ────────────────────────────────────────────────────────────── */
4
4
  import { h } from "preact";
5
- import { useState, useCallback, useRef, useEffect, useMemo } from "preact/hooks";
5
+ import { useState, useCallback, useRef, useEffect, useMemo, useLayoutEffect } from "preact/hooks";
6
6
  import htm from "htm";
7
7
  import { signal, computed } from "@preact/signals";
8
8
  import {
9
9
  tasksData,
10
10
  tasksLoaded,
11
+ tasksPage,
11
12
  showToast,
12
13
  runOptimistic,
13
14
  loadTasks,
@@ -97,6 +98,27 @@ function getTaskBaseBranch(task) {
97
98
  );
98
99
  }
99
100
 
101
+ function getTaskRuntimeSnapshot(task) {
102
+ return task?.runtimeSnapshot || task?.meta?.runtimeSnapshot || null;
103
+ }
104
+
105
+ function getTaskEpic(task) {
106
+ return String(task?.epic || task?.epicName || task?.meta?.epic || task?.meta?.epicName || "").trim();
107
+ }
108
+
109
+ function getTaskSprint(task) {
110
+ return String(task?.sprintName || task?.sprint || task?.meta?.sprintName || task?.meta?.sprintId || "").trim();
111
+ }
112
+
113
+ function getTaskStoryPoints(task) {
114
+ const value = task?.storyPoints ?? task?.story_points ?? task?.points ?? task?.meta?.storyPoints;
115
+ return Number.isFinite(Number(value)) && String(value).trim() !== "" ? String(value) : "";
116
+ }
117
+
118
+ function getTaskDueDate(task) {
119
+ return String(task?.dueDate || task?.due_date || task?.meta?.dueDate || "").trim();
120
+ }
121
+
100
122
  /* ─── Derived column data ─── */
101
123
  const columnData = computed(() => {
102
124
  const tasks = tasksData.value || [];
@@ -130,6 +152,14 @@ const TOUCH_DRAG_DELAY_MS = 180;
130
152
  const TOUCH_DRAG_START_PX = 6;
131
153
  const TOUCH_CANCEL_PX = 14;
132
154
 
155
+ function queueBoardTasksRefresh() {
156
+ const page = Number(tasksPage?.value ?? 0);
157
+ const append = Number.isFinite(page) && page > 0;
158
+ setTimeout(() => {
159
+ void loadTasks({ append });
160
+ }, 500);
161
+ }
162
+
133
163
  /* ─── Touch drag helpers ─── */
134
164
 
135
165
  function _createTouchClone(el) {
@@ -269,7 +299,7 @@ async function executeBoardTransition(task, newStatus, columnLabel) {
269
299
  );
270
300
 
271
301
  showToast(`Moved to ${columnLabel || "updated status"}`, "success");
272
- setTimeout(() => loadTasks(), 500);
302
+ queueBoardTasksRefresh();
273
303
  return { ok: true, cancelled: false, action: decision.action, status: optimisticStatus };
274
304
  }
275
305
 
@@ -411,6 +441,11 @@ function KanbanCard({ task, onOpen }) {
411
441
  const priorityLabel = PRIORITY_LABELS[task.priority] || null;
412
442
  const tags = getTaskTags(task);
413
443
  const baseBranch = getTaskBaseBranch(task);
444
+ const runtime = getTaskRuntimeSnapshot(task);
445
+ const epic = getTaskEpic(task);
446
+ const sprint = getTaskSprint(task);
447
+ const storyPoints = getTaskStoryPoints(task);
448
+ const dueDate = getTaskDueDate(task);
414
449
  const repoName = task.repo || task.repository || "";
415
450
  const issueNum = task.issueNumber || task.issue_number || (typeof task.id === "string" && /^\d+$/.test(task.id) ? task.id : null);
416
451
  const hasAgent = Boolean(
@@ -459,11 +494,25 @@ function KanbanCard({ task, onOpen }) {
459
494
  ${priorityLabel && html`
460
495
  <${Chip} label=${priorityLabel} size="small" sx=${{ backgroundColor: priorityColor, color: '#fff', height: 18, fontSize: '0.65rem' }} />
461
496
  `}
497
+ ${runtime?.state === "running" && html`
498
+ <${Chip} label="LIVE" size="small" color="success" sx=${{ height: 18, fontSize: '0.65rem' }} />
499
+ `}
500
+ ${runtime?.state === "queued" && html`
501
+ <${Chip} label="QUEUED" size="small" color="warning" sx=${{ height: 18, fontSize: '0.65rem' }} />
502
+ `}
462
503
  </${Stack}>
463
504
  <${Typography} variant="body2" fontWeight=${500}>${truncate(task.title || "(untitled)", 80)}</${Typography}>
464
505
  ${task.description && html`
465
506
  <${Typography} variant="caption" color="text.secondary" sx=${{ display: 'block', mt: 0.5 }}>${truncate(task.description, 72)}</${Typography}>
466
507
  `}
508
+ ${(epic || sprint || storyPoints || dueDate) && html`
509
+ <${Stack} direction="row" spacing=${0.5} flexWrap="wrap" sx=${{ mt: 0.75 }}>
510
+ ${epic && html`<${Chip} label=${`Epic: ${truncate(epic, 18)}`} size="small" variant="outlined" sx=${{ height: 20, fontSize: '0.65rem' }} />`}
511
+ ${sprint && html`<${Chip} label=${`Sprint: ${truncate(sprint, 18)}`} size="small" variant="outlined" sx=${{ height: 20, fontSize: '0.65rem' }} />`}
512
+ ${storyPoints && html`<${Chip} label=${`${storyPoints} pts`} size="small" variant="outlined" sx=${{ height: 20, fontSize: '0.65rem' }} />`}
513
+ ${dueDate && html`<${Chip} label=${`Due: ${truncate(dueDate, 18)}`} size="small" variant="outlined" color="warning" sx=${{ height: 20, fontSize: '0.65rem' }} />`}
514
+ </${Stack}>
515
+ `}
467
516
  ${baseBranch && html`
468
517
  <${Typography} variant="caption" color="text.secondary" sx=${{ display: 'block', mt: 0.5 }}>Base: ${truncate(baseBranch, 24)}</${Typography}>
469
518
  `}
@@ -498,19 +547,80 @@ function KanbanColumn({
498
547
  }) {
499
548
  const [showCreate, setShowCreate] = useState(false);
500
549
  const inputRef = useRef(null);
550
+ const cardsRef = useRef(null);
551
+ const tailSentinelRef = useRef(null);
552
+ const lastAutoLoadCountRef = useRef(-1);
501
553
 
502
554
  useEffect(() => {
503
555
  if (showCreate && inputRef.current) inputRef.current.focus();
504
556
  }, [showCreate]);
505
557
 
558
+ const triggerLoadMore = useCallback(() => {
559
+ if (!hasMoreTasks || loadingMoreTasks || typeof onLoadMoreTasks !== "function") return false;
560
+ void onLoadMoreTasks();
561
+ return true;
562
+ }, [hasMoreTasks, loadingMoreTasks, onLoadMoreTasks]);
563
+
564
+ useEffect(() => {
565
+ if (!hasMoreTasks || typeof onLoadMoreTasks !== "function") {
566
+ lastAutoLoadCountRef.current = -1;
567
+ return;
568
+ }
569
+ const root = cardsRef.current;
570
+ const sentinel = tailSentinelRef.current;
571
+ if (!root || !sentinel || typeof IntersectionObserver !== "function") return;
572
+ const observer = new IntersectionObserver(
573
+ (entries) => {
574
+ for (const entry of entries) {
575
+ if (!entry.isIntersecting) continue;
576
+ const key = tasks.length;
577
+ if (lastAutoLoadCountRef.current === key || loadingMoreTasks) continue;
578
+ lastAutoLoadCountRef.current = key;
579
+ void onLoadMoreTasks();
580
+ }
581
+ },
582
+ {
583
+ root,
584
+ rootMargin: "0px 0px 160px 0px",
585
+ threshold: 0,
586
+ },
587
+ );
588
+ observer.observe(sentinel);
589
+ return () => observer.disconnect();
590
+ }, [hasMoreTasks, loadingMoreTasks, onLoadMoreTasks, tasks.length]);
591
+
592
+ useLayoutEffect(() => {
593
+ const root = cardsRef.current;
594
+ if (!root || !hasMoreTasks || loadingMoreTasks || typeof onLoadMoreTasks !== "function") return;
595
+ const remaining = root.scrollHeight - root.scrollTop - root.clientHeight;
596
+ const underfilled = root.scrollHeight <= root.clientHeight + LOAD_MORE_THRESHOLD_PX;
597
+ if (!underfilled && remaining > LOAD_MORE_THRESHOLD_PX) return;
598
+ const key = tasks.length;
599
+ if (lastAutoLoadCountRef.current === key) return;
600
+ lastAutoLoadCountRef.current = key;
601
+ void onLoadMoreTasks();
602
+ }, [hasMoreTasks, loadingMoreTasks, onLoadMoreTasks, tasks.length, showCreate]);
603
+
506
604
  const onCardsScroll = useCallback((event) => {
507
605
  const el = event?.currentTarget;
508
606
  if (!el) return;
509
607
  const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
510
608
  if (remaining > LOAD_MORE_THRESHOLD_PX) return;
511
- if (!hasMoreTasks || loadingMoreTasks || typeof onLoadMoreTasks !== "function") return;
512
- void onLoadMoreTasks();
513
- }, [hasMoreTasks, loadingMoreTasks, onLoadMoreTasks]);
609
+ lastAutoLoadCountRef.current = tasks.length;
610
+ triggerLoadMore();
611
+ }, [tasks.length, triggerLoadMore]);
612
+
613
+ const onCardsWheel = useCallback((event) => {
614
+ const el = event?.currentTarget;
615
+ if (!el) return;
616
+ if (Math.abs(event.deltaY) < Math.abs(event.deltaX || 0)) return;
617
+ const canScroll = el.scrollHeight > el.clientHeight + 1;
618
+ if (!canScroll) return;
619
+ const atTop = el.scrollTop <= 0;
620
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
621
+ if ((event.deltaY < 0 && atTop) || (event.deltaY > 0 && atBottom)) return;
622
+ event.stopPropagation();
623
+ }, []);
514
624
 
515
625
  const onDragOver = useCallback((e) => {
516
626
  e.preventDefault();
@@ -562,7 +672,7 @@ function KanbanColumn({
562
672
  },
563
673
  );
564
674
  showToast(`Moved to ${col.title}`, "success");
565
- setTimeout(() => loadTasks(), 500);
675
+ queueBoardTasksRefresh();
566
676
  } catch (err) {
567
677
  showToast(err?.message || "Failed to move task", "error");
568
678
  }
@@ -597,7 +707,12 @@ function KanbanColumn({
597
707
  <${Chip} label=${countLabel} size="small" />
598
708
  <${IconButton} size="small" onClick=${() => { setShowCreate(!showCreate); haptic(); }} title=${"Add task to " + col.title}>+</${IconButton}>
599
709
  </${Box}>
600
- <div class="kanban-cards" onScroll=${onCardsScroll}>
710
+ <div
711
+ ref=${cardsRef}
712
+ class="kanban-cards"
713
+ onScroll=${onCardsScroll}
714
+ onWheel=${onCardsWheel}
715
+ >
601
716
  ${showCreate && html`
602
717
  <${TextField}
603
718
  inputRef=${inputRef}
@@ -616,10 +731,23 @@ function KanbanColumn({
616
731
  : html`<${Typography} variant="body2" color="text.secondary" sx=${{ textAlign: 'center', py: 2 }}>Drop tasks here</${Typography}>`
617
732
  }
618
733
  ${hasMoreTasks && html`
619
- <div class="kanban-tail-sentinel"></div>
620
- <div class="kanban-load-more">${loadingMoreTasks ? "Loading more tasks..." : "Scroll down to load more"}</div>
734
+ <div ref=${tailSentinelRef} class="kanban-tail-sentinel"></div>
621
735
  `}
622
736
  </div>
737
+ ${hasMoreTasks && html`
738
+ <div class="kanban-column-footer">
739
+ <button
740
+ type="button"
741
+ class="kanban-load-more"
742
+ onClick=${() => triggerLoadMore()}
743
+ disabled=${loadingMoreTasks}
744
+ aria-label=${loadingMoreTasks ? `Loading more ${col.title} tasks` : `Load more ${col.title} tasks`}
745
+ >
746
+ <span class="kanban-load-more-label">${loadingMoreTasks ? "Loading more tasks..." : `Load more ${col.title}`}</span>
747
+ <span class="kanban-load-more-icon" aria-hidden="true">⌄</span>
748
+ </button>
749
+ </div>
750
+ `}
623
751
  <div class="kanban-scroll-fade"></div>
624
752
  </div>
625
753
  `;
@@ -45,7 +45,6 @@ import {
45
45
  ListItemText,
46
46
  ListItemSecondaryAction,
47
47
  IconButton,
48
- Fade,
49
48
  } from "@mui/material";
50
49
 
51
50
  import { ICONS } from "../modules/icons.js";
@@ -227,7 +226,13 @@ export function SkeletonCard({ height = "80px", className = "" }) {
227
226
  * onSaveBeforeClose?: (() => Promise<boolean|{closed?: boolean}|void>)|null,
228
227
  * onDiscardBeforeClose?: (() => Promise<boolean|{closed?: boolean}|void>)|null,
229
228
  * activeOperationLabel?: string,
230
- * closeGuard?: boolean
229
+ * closeGuard?: boolean,
230
+ * layout?: "sheet"|"side-sheet",
231
+ * resizable?: boolean,
232
+ * defaultWidth?: number,
233
+ * minWidth?: number,
234
+ * maxWidth?: number,
235
+ * widthStorageKey?: string
231
236
  * }} props
232
237
  */
233
238
  export function Modal({
@@ -242,6 +247,12 @@ export function Modal({
242
247
  onDiscardBeforeClose = null,
243
248
  activeOperationLabel = "",
244
249
  closeGuard = true,
250
+ layout = "sheet",
251
+ resizable = false,
252
+ defaultWidth = 760,
253
+ minWidth = 440,
254
+ maxWidth = 1120,
255
+ widthStorageKey = "",
245
256
  }) {
246
257
  const [visible, setVisible] = useState(false);
247
258
  const contentRef = useRef(null);
@@ -249,6 +260,13 @@ export function Modal({
249
260
  const [dragY, setDragY] = useState(0);
250
261
  const [closePromptOpen, setClosePromptOpen] = useState(false);
251
262
  const [closePromptSaving, setClosePromptSaving] = useState(false);
263
+ const isSideSheet = layout === "side-sheet";
264
+ const [sheetWidth, setSheetWidth] = useState(() => {
265
+ const fallback = Number.isFinite(Number(defaultWidth)) ? Number(defaultWidth) : 760;
266
+ if (!isSideSheet || !widthStorageKey || typeof localStorage === "undefined") return fallback;
267
+ const stored = Number(localStorage.getItem(widthStorageKey));
268
+ return Number.isFinite(stored) ? stored : fallback;
269
+ });
252
270
  const scopedUnsavedCount = Number.isFinite(Number(unsavedChanges))
253
271
  ? Math.max(0, Number(unsavedChanges))
254
272
  : 0;
@@ -346,8 +364,43 @@ export function Modal({
346
364
  }, []);
347
365
 
348
366
  const isDragHandleTarget = useCallback((target) => (
349
- Boolean(target?.closest?.(".modal-handle, .modal-header"))
350
- ), []);
367
+ !isSideSheet && Boolean(target?.closest?.(".modal-handle, .modal-header"))
368
+ ), [isSideSheet]);
369
+
370
+ const persistSheetWidth = useCallback((nextWidth) => {
371
+ if (!widthStorageKey || typeof localStorage === "undefined") return;
372
+ try {
373
+ localStorage.setItem(widthStorageKey, String(nextWidth));
374
+ } catch {
375
+ /* ignore width persistence failures */
376
+ }
377
+ }, [widthStorageKey]);
378
+
379
+ const handleSheetResizeStart = useCallback((event) => {
380
+ if (!isSideSheet || !resizable) return;
381
+ event.preventDefault();
382
+ event.stopPropagation();
383
+ const startX = event.clientX;
384
+ const startWidth = sheetWidth;
385
+ const minAllowed = Math.max(320, Number.isFinite(Number(minWidth)) ? Number(minWidth) : 440);
386
+ const maxAllowedBase = Number.isFinite(Number(maxWidth)) ? Number(maxWidth) : 1120;
387
+ const maxAllowed = typeof window !== "undefined"
388
+ ? Math.min(maxAllowedBase, Math.max(minAllowed, window.innerWidth - 48))
389
+ : maxAllowedBase;
390
+ let latestWidth = startWidth;
391
+ const onMove = (moveEvent) => {
392
+ const delta = startX - moveEvent.clientX;
393
+ latestWidth = Math.max(minAllowed, Math.min(maxAllowed, startWidth + delta));
394
+ setSheetWidth(latestWidth);
395
+ };
396
+ const onUp = () => {
397
+ window.removeEventListener("pointermove", onMove);
398
+ window.removeEventListener("pointerup", onUp);
399
+ persistSheetWidth(latestWidth);
400
+ };
401
+ window.addEventListener("pointermove", onMove);
402
+ window.addEventListener("pointerup", onUp);
403
+ }, [isSideSheet, maxWidth, minWidth, persistSheetWidth, resizable, sheetWidth]);
351
404
 
352
405
  const handleTouchStart = useCallback((e) => {
353
406
  if (!isDragHandleTarget(e.target)) return;
@@ -530,53 +583,62 @@ export function Modal({
530
583
  const dragStyle = dragY > 0
531
584
  ? `transform: translateY(${dragY}px); opacity: ${Math.max(0.2, 1 - dragY / 400)}`
532
585
  : "";
586
+ const contentStyle = [
587
+ dragStyle,
588
+ isSideSheet ? `--modal-sheet-width:${sheetWidth}px` : "",
589
+ ].filter(Boolean).join("; ");
533
590
 
534
591
  const content = html`
535
- <${Fade} in=${visible} timeout=${300}>
592
+ <div
593
+ class="modal-overlay ${visible ? "modal-overlay-visible" : ""}"
594
+ onClick=${(e) => {
595
+ if (e.target === e.currentTarget) requestClose();
596
+ }}
597
+ >
536
598
  <div
537
- class="modal-overlay ${visible ? "modal-overlay-visible" : ""}"
538
- onClick=${(e) => {
539
- if (e.target === e.currentTarget) requestClose();
540
- }}
599
+ ref=${contentRef}
600
+ class="modal-content ${isSideSheet ? "modal-side-sheet" : ""} ${contentClassName} ${visible ? "modal-content-visible" : ""} ${dragY > 0 ? "modal-dragging" : ""}"
601
+ style=${contentStyle}
602
+ onClick=${(e) => e.stopPropagation()}
603
+ onTouchStart=${handleTouchStart}
604
+ onTouchMove=${handleTouchMove}
605
+ onTouchEnd=${handleTouchEnd}
606
+ onTouchCancel=${handleTouchCancel}
607
+ onPointerDown=${handlePointerDown}
608
+ onPointerMove=${handlePointerMove}
609
+ onPointerUp=${handlePointerEnd}
610
+ onPointerCancel=${handlePointerCancel}
541
611
  >
542
- <div
543
- ref=${contentRef}
544
- class="modal-content ${contentClassName} ${visible ? "modal-content-visible" : ""} ${dragY > 0 ? "modal-dragging" : ""}"
545
- style=${dragStyle}
546
- onClick=${(e) => e.stopPropagation()}
547
- onTouchStart=${handleTouchStart}
548
- onTouchMove=${handleTouchMove}
549
- onTouchEnd=${handleTouchEnd}
550
- onTouchCancel=${handleTouchCancel}
551
- onPointerDown=${handlePointerDown}
552
- onPointerMove=${handlePointerMove}
553
- onPointerUp=${handlePointerEnd}
554
- onPointerCancel=${handlePointerCancel}
555
- >
556
- <div class="modal-header">
557
- <div class="modal-handle"></div>
558
- ${title ? html`<div class="modal-title">${title}</div>` : null}
559
- <${IconButton}
560
- className="modal-close-btn"
561
- size="small"
562
- onTouchStart=${(e) => e.stopPropagation()}
563
- onPointerDown=${(e) => e.stopPropagation()}
564
- onPointerUp=${(e) => e.stopPropagation()}
565
- onMouseDown=${(e) => e.stopPropagation()}
566
- onClick=${requestClose}
567
- aria-label="Close"
568
- sx=${{ position: "absolute", right: 8, top: 8, zIndex: 8, pointerEvents: "auto" }}
569
- >
570
- ${ICONS.close}
571
- <//>
572
- </div>
573
- <div class="modal-body" onTouchStart=${handleBodyTouchStart}>
574
- ${children}
575
- </div>
576
- ${footer ? html`<div class="modal-footer">${footer}</div>` : null}
612
+ ${isSideSheet && resizable
613
+ ? html`<div
614
+ class="modal-sheet-resizer"
615
+ onPointerDown=${handleSheetResizeStart}
616
+ title="Resize panel"
617
+ ></div>`
618
+ : null}
619
+ <div class="modal-header">
620
+ <div class="modal-handle"></div>
621
+ ${title ? html`<div class="modal-title">${title}</div>` : null}
622
+ <${IconButton}
623
+ className="modal-close-btn"
624
+ size="small"
625
+ onTouchStart=${(e) => e.stopPropagation()}
626
+ onPointerDown=${(e) => e.stopPropagation()}
627
+ onPointerUp=${(e) => e.stopPropagation()}
628
+ onMouseDown=${(e) => e.stopPropagation()}
629
+ onClick=${requestClose}
630
+ aria-label="Close"
631
+ sx=${{ position: "absolute", right: 8, top: 8, zIndex: 8, pointerEvents: "auto" }}
632
+ >
633
+ ${ICONS.close}
634
+ <//>
635
+ </div>
636
+ <div class="modal-body" onTouchStart=${handleBodyTouchStart}>
637
+ ${children}
577
638
  </div>
639
+ ${footer ? html`<div class="modal-footer">${footer}</div>` : null}
578
640
  </div>
579
- </${Fade}>
641
+ </div>
580
642
  `;
581
643
 
582
644
  const guard = closePromptOpen
@@ -13344,9 +13344,8 @@
13344
13344
  "edgeCount": 11,
13345
13345
  "recommended": false,
13346
13346
  "enabled": true,
13347
- "trigger": "trigger.task_low",
13347
+ "trigger": "trigger.task_available",
13348
13348
  "variables": {
13349
- "backlogThreshold": 3,
13350
13349
  "maxConcurrent": 2,
13351
13350
  "pollStatus": "todo",
13352
13351
  "maxBatchSize": 5,
@@ -13370,10 +13369,11 @@
13370
13369
  "nodes": [
13371
13370
  {
13372
13371
  "id": "trigger",
13373
- "type": "trigger.task_low",
13374
- "label": "Backlog Low?",
13372
+ "type": "trigger.task_available",
13373
+ "label": "Tasks Available?",
13375
13374
  "config": {
13376
- "threshold": "{{backlogThreshold}}",
13375
+ "maxParallel": "{{maxConcurrent}}",
13376
+ "pollIntervalMs": 60000,
13377
13377
  "status": "{{pollStatus}}"
13378
13378
  },
13379
13379
  "position": {
@@ -13653,9 +13653,8 @@
13653
13653
  "edgeCount": 6,
13654
13654
  "recommended": true,
13655
13655
  "enabled": true,
13656
- "trigger": "trigger.task_low",
13656
+ "trigger": "trigger.task_available",
13657
13657
  "variables": {
13658
- "backlogThreshold": 3,
13659
13658
  "maxConcurrent": 3,
13660
13659
  "pollStatus": "todo",
13661
13660
  "maxBatchSize": 10,
@@ -13678,10 +13677,11 @@
13678
13677
  "nodes": [
13679
13678
  {
13680
13679
  "id": "trigger",
13681
- "type": "trigger.task_low",
13682
- "label": "Backlog Low?",
13680
+ "type": "trigger.task_available",
13681
+ "label": "Tasks Available?",
13683
13682
  "config": {
13684
- "threshold": "{{backlogThreshold}}",
13683
+ "maxParallel": "{{maxConcurrent}}",
13684
+ "pollIntervalMs": 60000,
13685
13685
  "status": "{{pollStatus}}"
13686
13686
  },
13687
13687
  "position": {
@@ -28166,9 +28166,8 @@
28166
28166
  "category": "lifecycle",
28167
28167
  "enabled": true,
28168
28168
  "nodeCount": 11,
28169
- "trigger": "trigger.task_low",
28169
+ "trigger": "trigger.task_available",
28170
28170
  "variables": {
28171
- "backlogThreshold": 3,
28172
28171
  "maxConcurrent": 2,
28173
28172
  "pollStatus": "todo",
28174
28173
  "maxBatchSize": 5,
@@ -28179,10 +28178,11 @@
28179
28178
  "nodes": [
28180
28179
  {
28181
28180
  "id": "trigger",
28182
- "type": "trigger.task_low",
28183
- "label": "Backlog Low?",
28181
+ "type": "trigger.task_available",
28182
+ "label": "Tasks Available?",
28184
28183
  "config": {
28185
- "threshold": "{{backlogThreshold}}",
28184
+ "maxParallel": "{{maxConcurrent}}",
28185
+ "pollIntervalMs": 60000,
28186
28186
  "status": "{{pollStatus}}"
28187
28187
  },
28188
28188
  "position": {
@@ -28463,9 +28463,8 @@
28463
28463
  "category": "lifecycle",
28464
28464
  "enabled": true,
28465
28465
  "nodeCount": 7,
28466
- "trigger": "trigger.task_low",
28466
+ "trigger": "trigger.task_available",
28467
28467
  "variables": {
28468
- "backlogThreshold": 3,
28469
28468
  "maxConcurrent": 3,
28470
28469
  "pollStatus": "todo",
28471
28470
  "maxBatchSize": 10,
@@ -28475,10 +28474,11 @@
28475
28474
  "nodes": [
28476
28475
  {
28477
28476
  "id": "trigger",
28478
- "type": "trigger.task_low",
28479
- "label": "Backlog Low?",
28477
+ "type": "trigger.task_available",
28478
+ "label": "Tasks Available?",
28480
28479
  "config": {
28481
- "threshold": "{{backlogThreshold}}",
28480
+ "maxParallel": "{{maxConcurrent}}",
28481
+ "pollIntervalMs": 60000,
28482
28482
  "status": "{{pollStatus}}"
28483
28483
  },
28484
28484
  "position": {