@goodandready/dsh-cron 0.3.5 → 0.3.7
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/api-helpers.js +22 -0
- package/lib/api.js +2 -1
- package/lib/client.js +388 -305
- package/lib/index.js +4 -0
- package/lib/metrics.js +5 -0
- package/lib/routes.js +20 -1
- package/lib/validators.js +2 -1
- package/package.json +1 -1
package/lib/api-helpers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { calculate24hTimeline } from './timeline.js';
|
|
1
2
|
import { previewSchedule } from './scheduler.js';
|
|
2
3
|
import { sendJson, readBody, rejectCrossOrigin } from './http-utils.js';
|
|
3
4
|
|
|
@@ -76,3 +77,24 @@ export async function handleDryRunTask({ store, scheduler, req, res, id }) {
|
|
|
76
77
|
sendJson(res, 500, { ok: false, error: err.message });
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
|
|
81
|
+
export async function handleScheduleTimeline({ req, res, url, store, scheduler, apiToken: explicitToken }) {
|
|
82
|
+
const apiToken = explicitToken || store?.getSettings?.()?.apiToken;
|
|
83
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
84
|
+
|
|
85
|
+
if (req.method !== 'GET') {
|
|
86
|
+
sendJson(res, 405, NOT_ALLOWED);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hours = parseInt(url.searchParams.get('hours') || '24', 10);
|
|
91
|
+
const tasks = typeof store?.list === 'function' ? store.list() : [];
|
|
92
|
+
const maxConcurrent = store?.getSettings?.()?.maxConcurrent || 3;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const timeline = calculate24hTimeline({ tasks, maxConcurrent, hoursCount: hours });
|
|
96
|
+
sendJson(res, 200, { ok: true, timeline });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
sendJson(res, 500, { ok: false, error: err.message });
|
|
99
|
+
}
|
|
100
|
+
}
|
package/lib/api.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { calculate24hTimeline } from './timeline.js';
|
|
2
1
|
import { rollbackPromptVersion, sanitizePromptHistory } from './prompt-evolver.js';
|
|
3
2
|
import { sanitizeAdaptiveConfig } from './adaptive.js';
|
|
4
3
|
import { logStreamer } from './log-streamer.js';
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
handleHeartbeatPing,
|
|
36
35
|
handleSchedulePreview,
|
|
37
36
|
handleDryRunTask,
|
|
37
|
+
handleScheduleTimeline,
|
|
38
38
|
} from './api-helpers.js';
|
|
39
39
|
|
|
40
40
|
export {
|
|
@@ -45,6 +45,7 @@ export {
|
|
|
45
45
|
handleHeartbeatPing,
|
|
46
46
|
handleSchedulePreview,
|
|
47
47
|
handleDryRunTask,
|
|
48
|
+
handleScheduleTimeline,
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
const NOT_FOUND = { ok: false, error: 'Task not found' };
|
package/lib/client.js
CHANGED
|
@@ -501,233 +501,222 @@ return s;
|
|
|
501
501
|
};
|
|
502
502
|
const T = (key, vars) => translate(key, vars);
|
|
503
503
|
const STYLES = `
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
.dsh-cron-task-title { flex-wrap: wrap; row-gap: 4px; }
|
|
721
|
-
.dsh-cron-modal { padding: 14px; }
|
|
722
|
-
.dsh-cron-modal-foot { flex-direction: column-reverse; }
|
|
723
|
-
.dsh-cron-modal-foot > button { width: 100%; }
|
|
724
|
-
.dsh-cron-filter-panel { padding: 8px 10px; }
|
|
725
|
-
.dsh-cron-card { padding: 14px; }
|
|
726
|
-
/* 16px keeps iOS Safari from zooming the page when a field is focused. */
|
|
727
|
-
.dsh-cron-modal input, .dsh-cron-modal select, .dsh-cron-modal textarea,
|
|
728
|
-
.dsh-cron-card input, .dsh-cron-search-input, .dsh-cron-filter-field select { font-size: 16px; }
|
|
729
|
-
}
|
|
730
|
-
`;
|
|
504
|
+
:root {
|
|
505
|
+
--dsh-cron-success: var(--dsw-alias-state-success-primary);
|
|
506
|
+
--dsh-cron-success-bg: color-mix(in srgb, var(--dsh-cron-success) 10%, transparent);
|
|
507
|
+
--dsh-cron-danger: var(--dsw-alias-state-error-primary);
|
|
508
|
+
--dsh-cron-danger-bg: color-mix(in srgb, var(--dsh-cron-danger) 10%, transparent);
|
|
509
|
+
--dsh-cron-info: var(--dsw-alias-state-brand-primary);
|
|
510
|
+
--dsh-cron-info-bg: color-mix(in srgb, var(--dsh-cron-info) 12%, transparent);
|
|
511
|
+
--dsh-cron-accent: var(--dsw-alias-state-info-primary);
|
|
512
|
+
--dsh-cron-accent-bg: color-mix(in srgb, var(--dsh-cron-accent) 12%, transparent);
|
|
513
|
+
--dsh-cron-warning: var(--dsw-alias-state-warning-primary);
|
|
514
|
+
--dsh-cron-warning-bg: color-mix(in srgb, var(--dsh-cron-warning) 12%, transparent);
|
|
515
|
+
}
|
|
516
|
+
[data-pane=conversation], [class*=centerCol] {
|
|
517
|
+
position: relative !important;
|
|
518
|
+
}
|
|
519
|
+
[${VIEW_ATTR}] {
|
|
520
|
+
z-index: 60;
|
|
521
|
+
background: var(--dsw-alias-bg-base);
|
|
522
|
+
display: none;
|
|
523
|
+
position: absolute;
|
|
524
|
+
inset: 0;
|
|
525
|
+
}
|
|
526
|
+
html[${ACTIVE_ATTR}] [${VIEW_ATTR}] {
|
|
527
|
+
display: block !important;
|
|
528
|
+
}
|
|
529
|
+
html[${ACTIVE_ATTR}] [data-pane=conversation] > *:not([${VIEW_ATTR}]),
|
|
530
|
+
html[${ACTIVE_ATTR}] [class*=centerCol] > *:not([${VIEW_ATTR}]) {
|
|
531
|
+
display: none !important;
|
|
532
|
+
}
|
|
533
|
+
.dsh-cron-entry-clone { display: flex; align-items: center; gap: 8px; }
|
|
534
|
+
.dsh-cron-entry-icon { display: flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; }
|
|
535
|
+
.dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
|
|
536
|
+
[data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
|
|
537
|
+
.dsh-cron-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); overflow-y: auto; padding: 24px 48px 32px; z-index: 100; box-sizing: border-box; }
|
|
538
|
+
.dsh-cron-container { max-width: 960px; width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; padding: 8px 0 32px; box-sizing: border-box; }
|
|
539
|
+
.dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
|
|
540
|
+
.dsh-cron-back-btn { appearance: none; font: inherit; cursor: pointer; color: var(--dsw-alias-label-secondary); background: 0 0; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; align-items: center; gap: 6px; padding: 4px 12px; font-size: 13px; display: inline-flex; flex: none; height: 32px; box-sizing: border-box; }
|
|
541
|
+
.dsh-cron-back-btn:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent))); }
|
|
542
|
+
.dsh-cron-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
543
|
+
.dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
|
|
544
|
+
.dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary); line-height: 1.5; }
|
|
545
|
+
.dsh-cron-create-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 9999px; padding: 8px 16px; font-size: 14px; font-weight: 500; cursor: pointer; position: relative; }
|
|
546
|
+
.dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover)); }
|
|
547
|
+
.dsh-cron-dropdown { position: absolute; top: calc(100% + 6px); right: 0; width: 200px; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; padding: 6px; box-shadow: 0 10px 25px color-mix(in srgb, var(--dsw-alias-bg-mask) 50%, transparent); z-index: 200; }
|
|
548
|
+
.dsh-cron-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; background: transparent; border: none; border-radius: 8px; color: var(--dsw-alias-label-primary); font-size: 13.5px; text-align: left; cursor: pointer; }
|
|
549
|
+
.dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
550
|
+
.dsh-cron-search-bar { position: relative; margin-bottom: 0; }
|
|
551
|
+
.dsh-cron-search-input { width: 100%; height: 36px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 0 12px 0 38px; color: var(--dsw-alias-label-primary, inherit); font-size: 13px; outline: none; box-sizing: border-box; }
|
|
552
|
+
.dsh-cron-search-input:focus { outline: none; border-color: var(--dsh-cron-info); }
|
|
553
|
+
.dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary); }
|
|
554
|
+
.dsh-cron-filters { margin-top: 8px; }
|
|
555
|
+
.dsh-cron-filter-toggle { background: none; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
556
|
+
.dsh-cron-filter-toggle:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsh-cron-info); }
|
|
557
|
+
.dsh-cron-filter-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent)); }
|
|
558
|
+
.dsh-cron-filter-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary); }
|
|
559
|
+
.dsh-cron-filter-field select { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary, inherit); border-radius: 6px; padding: 5px 8px; font-size: 12.5px; }
|
|
560
|
+
.dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
561
|
+
.dsh-cron-tab { appearance: none; background: transparent; border: 1px solid var(--dsw-alias-border-l2); padding: 4px 12px; border-radius: 999px; color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; cursor: pointer; }
|
|
562
|
+
.dsh-cron-tab[data-active="true"] { background: var(--dsw-alias-bg-layer-4); color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2)); }
|
|
563
|
+
.dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 0; }
|
|
564
|
+
.dsh-cron-task-item { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; cursor: pointer; transition: background 0.15s, border-color 0.15s; }
|
|
565
|
+
.dsh-cron-task-item:hover, .dsh-cron-task-item:focus-visible { background: var(--dsw-alias-bg-layer-4); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l1)); outline: none; }
|
|
566
|
+
.dsh-cron-task-prompt-preview { font-size: 12.5px; color: var(--dsw-alias-label-secondary); margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 520px; line-height: 1.3; }
|
|
567
|
+
.dsh-cron-type-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); text-transform: uppercase; font-weight: 500; margin-left: 8px; vertical-align: middle; }
|
|
568
|
+
.dsh-cron-tag-success { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
|
|
569
|
+
.dsh-cron-tag-failure { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
|
|
570
|
+
.dsh-cron-tag-heartbeat { background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); border-color: var(--dsh-cron-accent); }
|
|
571
|
+
.dsh-cron-tag-session { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
|
|
572
|
+
.dsh-cron-tag-preflight { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
|
|
573
|
+
.dsh-cron-task-model-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent)); color: var(--dsw-alias-label-secondary); margin-left: 8px; vertical-align: middle; }
|
|
574
|
+
.dsh-cron-task-actions { display: flex; gap: 8px; }
|
|
575
|
+
.dsh-cron-icon-btn { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 6px 12px; font-size: 12.5px; font-weight: 500; color: var(--dsw-alias-label-primary); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
|
|
576
|
+
.dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2)); }
|
|
577
|
+
.dsh-cron-icon-btn.dsh-cron-danger { color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger-bg); }
|
|
578
|
+
.dsh-cron-icon-btn.dsh-cron-danger:hover { background: var(--dsh-cron-danger-bg); border-color: var(--dsh-cron-danger); }
|
|
579
|
+
.dsh-cron-task-status-btn { background: none; border: 1px solid var(--dsw-alias-border-l2); border-radius: 50%; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; color: var(--dsw-alias-label-tertiary); cursor: pointer; flex-shrink: 0; }
|
|
580
|
+
.dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
581
|
+
.dsh-cron-recs-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
|
|
582
|
+
.dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
|
|
583
|
+
.dsh-cron-rec-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; cursor: pointer; transition: background 0.15s; }
|
|
584
|
+
.dsh-cron-rec-card:hover, .dsh-cron-rec-card:focus-visible { background: var(--dsw-alias-bg-layer-4); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2)); outline: none; }
|
|
585
|
+
.dsh-cron-rec-icon { width: 36px; height: 36px; border-radius: 10px; background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary) 3%, transparent)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
586
|
+
.dsh-cron-rec-content { flex: 1; min-width: 0; }
|
|
587
|
+
.dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
|
|
588
|
+
.dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary); font-weight: normal; margin-left: 8px; }
|
|
589
|
+
.dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary); }
|
|
590
|
+
.dsh-cron-modal-overlay { position: fixed; inset: 0; background: var(--dsw-alias-bg-mask, color-mix(in srgb, #000 75%, transparent)); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; box-sizing: border-box; overflow-y: auto; }
|
|
591
|
+
.dsh-cron-modal { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; width: 100%; max-width: 540px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; padding: 24px; box-shadow: 0 20px 40px color-mix(in srgb, var(--dsw-alias-bg-mask) 60%, transparent); box-sizing: border-box; }
|
|
592
|
+
.dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
|
|
593
|
+
.dsh-cron-modal::-webkit-scrollbar { width: 6px; }
|
|
594
|
+
.dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
|
|
595
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l2); border-radius: 3px; }
|
|
596
|
+
.dsh-cron-modal::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-label-tertiary); }
|
|
597
|
+
.dsh-cron-section-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 0; margin-bottom: 10px; font: inherit; font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
598
|
+
.dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary); }
|
|
599
|
+
.dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
|
|
600
|
+
.dsh-cron-section-chevron[data-open="false"] { transform: rotate(-90deg); }
|
|
601
|
+
.dsh-cron-section-body { padding-bottom: 4px; }
|
|
602
|
+
.dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
|
|
603
|
+
.dsh-cron-form-group { margin-bottom: 16px; }
|
|
604
|
+
.dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary); margin-bottom: 6px; }
|
|
605
|
+
.dsh-cron-form-group input, .dsh-cron-form-group textarea, .dsh-cron-form-group select { width: 100%; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 8px 12px; color: var(--dsw-alias-label-primary); font-size: 13px; outline: none; box-sizing: border-box; }
|
|
606
|
+
.dsh-cron-form-group input:focus, .dsh-cron-form-group textarea:focus, .dsh-cron-form-group select:focus { outline: none; border-color: var(--dsh-cron-info); }
|
|
607
|
+
.dsh-cron-form-group select { cursor: pointer; }
|
|
608
|
+
.dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
|
|
609
|
+
.dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
610
|
+
.dsh-cron-modal-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 20px; position: sticky; bottom: -24px; background: var(--dsw-alias-bg-layer-2); border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 14px; padding-bottom: 4px; margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; z-index: 10; }
|
|
611
|
+
.dsh-cron-btn-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border: 1px solid transparent; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
|
|
612
|
+
.dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
|
|
613
|
+
.dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
614
|
+
.dsh-cron-btn-secondary { appearance: none; font: inherit; background: var(--dsw-alias-bg-layer-2, transparent); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all 0.15s ease; }
|
|
615
|
+
.dsh-cron-btn-secondary:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2)); }
|
|
616
|
+
.dsh-cron-chat-box { position: relative; background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; padding: 12px; }
|
|
617
|
+
.dsh-cron-chat-input { width: 100%; background: transparent; border: none; color: var(--dsw-alias-label-primary); font-size: 14px; min-height: 100px; resize: vertical; outline: none; box-sizing: border-box; line-height: 1.5; }
|
|
618
|
+
.dsh-cron-chat-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1); }
|
|
619
|
+
.dsh-cron-chat-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
|
|
620
|
+
.dsh-cron-send-btn { width: 34px; height: 34px; border-radius: 8px; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.1s, background 0.15s; }
|
|
621
|
+
.dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
|
|
622
|
+
.dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
623
|
+
.dsh-cron-card { background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; padding: 18px 20px; margin-bottom: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
624
|
+
.dsh-cron-card-head-btn { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 0; }
|
|
625
|
+
.dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
|
|
626
|
+
.dsh-cron-card-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary); }
|
|
627
|
+
.dsh-cron-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary); transition: transform 0.16s; display: flex; align-items: center; }
|
|
628
|
+
.dsh-cron-chev-open { transform: rotate(180deg); }
|
|
629
|
+
.dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin-top: 2px; padding-top: 14px; }
|
|
630
|
+
.dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
631
|
+
.dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary); line-height: 1.4; }
|
|
632
|
+
.dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2); margin-bottom: 18px; padding-bottom: 2px; }
|
|
633
|
+
.dsh-cron-modal-tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 14px; font-size: 13.5px; color: var(--dsw-alias-label-tertiary); cursor: pointer; font-weight: 500; transition: color 0.15s, border-color 0.15s; }
|
|
634
|
+
.dsh-cron-modal-tab:hover { color: var(--dsw-alias-label-secondary); }
|
|
635
|
+
.dsh-cron-modal-tab[data-active="true"] { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-label-primary); }
|
|
636
|
+
.dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
|
|
637
|
+
.dsh-cron-history-item { background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 12px 14px; font-size: 12.5px; }
|
|
638
|
+
.dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
|
639
|
+
.dsh-cron-history-status { font-weight: 500; font-size: 11px; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); }
|
|
640
|
+
.dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
641
|
+
.dsh-cron-history-status[data-status="validation_failed"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
|
|
642
|
+
.dsh-cron-stage-pill { font-size: 10.5px; padding: 1px 7px; border-radius: 4px; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); }
|
|
643
|
+
.dsh-cron-stage-pill[data-status="success"], .dsh-cron-stage-pill[data-status="delivered"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
|
|
644
|
+
.dsh-cron-stage-pill[data-status="failed"], .dsh-cron-stage-pill[data-status="error"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
|
|
645
|
+
.dsh-cron-stage-pill[data-status="partial"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
|
|
646
|
+
.dsh-cron-stage-pill[data-status="skipped"] { opacity: 0.6; }
|
|
647
|
+
.dsh-cron-history-status[data-status="error"], .dsh-cron-history-status[data-status="timeout"] { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
|
|
648
|
+
.dsh-cron-history-status[data-status="skipped"], .dsh-cron-history-status[data-status="missed"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
|
|
649
|
+
.dsh-cron-history-time { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
650
|
+
.dsh-cron-history-output { background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 12px; color: var(--dsw-alias-label-secondary); max-height: 140px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; margin-top: 6px; }
|
|
651
|
+
.dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary); font-size: 13.5px; }
|
|
652
|
+
.dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
|
|
653
|
+
.dsh-cron-stat-card { background: var(--dsw-alias-bg-layer-2, color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent)); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
|
|
654
|
+
.dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary); }
|
|
655
|
+
.dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 0; }
|
|
656
|
+
.dsh-cron-oneshot-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-accent); background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); font-weight: 500; }
|
|
657
|
+
.dsh-cron-config-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); font-weight: 500; }
|
|
658
|
+
.dsh-cron-cost-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-success); background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); font-weight: 500; }
|
|
659
|
+
.dsh-cron-tokens-tag { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsh-cron-info); background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); }
|
|
660
|
+
.dsh-cron-checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dsw-alias-label-secondary); cursor: pointer; user-select: none; }
|
|
661
|
+
.dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary); }
|
|
662
|
+
.dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary); margin-top: 4px; }
|
|
663
|
+
.dsh-cron-channel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px 12px; }
|
|
664
|
+
.dsh-cron-section { border-top: 1px solid var(--dsw-alias-border-l2); margin-top: 16px; padding-top: 12px; }
|
|
665
|
+
.dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary); margin-bottom: 10px; }
|
|
666
|
+
.dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
|
|
667
|
+
.dsh-cron-secret-row input { flex: 1; }
|
|
668
|
+
.dsh-cron-badge-ref { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-3, color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent)); color: var(--dsw-alias-label-tertiary); white-space: nowrap; }
|
|
669
|
+
.dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
|
|
670
|
+
.dsh-cron-status-banner[data-type="success"] { background: var(--dsh-cron-success-bg); border: 1px solid var(--dsh-cron-success); color: var(--dsh-cron-success); }
|
|
671
|
+
.dsh-cron-status-banner[data-type="error"] { background: var(--dsh-cron-danger-bg); border: 1px solid var(--dsh-cron-danger); color: var(--dsh-cron-danger); }
|
|
672
|
+
.dsh-cron-status-banner[data-type="info"] { background: var(--dsh-cron-info-bg); border: 1px solid var(--dsh-cron-info); color: var(--dsh-cron-info); }
|
|
673
|
+
@keyframes dsh-cron-pulse-ring { 0% { box-shadow: 0 0 0 0 var(--dsh-cron-success-bg); } 70% { box-shadow: 0 0 0 9px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
|
674
|
+
.dsh-cron-pulse { animation: dsh-cron-pulse-ring 1.6s infinite; border-color: var(--dsh-cron-success); color: var(--dsh-cron-success); }
|
|
675
|
+
/* #34: sidebar job list */
|
|
676
|
+
.dsh-cron-sidebar-jobs { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 6px 0; }
|
|
677
|
+
.dsh-cron-sidebar-jobs-head { display: flex; align-items: center; justify-content: space-between; width: 100%; background: none; border: none; padding: 4px 8px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-tertiary); cursor: pointer; border-radius: 6px; text-align: left; }
|
|
678
|
+
.dsh-cron-sidebar-jobs-head:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)); }
|
|
679
|
+
.dsh-cron-sidebar-jobs-head::after { content: '▾'; font-size: 10px; transition: transform 0.15s ease; }
|
|
680
|
+
.dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-head::after { transform: rotate(-90deg); }
|
|
681
|
+
.dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-list { display: none; }
|
|
682
|
+
.dsh-cron-sidebar-jobs-list { display: flex; flex-direction: column; gap: 2px; padding-left: 10px; }
|
|
683
|
+
.dsh-cron-sidebar-job { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; background: none; border: none; border-left: 2px solid transparent; padding: 3px 8px; font: inherit; cursor: pointer; border-radius: 0 6px 6px 0; text-align: left; }
|
|
684
|
+
.dsh-cron-sidebar-job:hover { background: var(--dsw-alias-interactive-bg-hover, color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent)); border-left-color: var(--dsh-cron-info); }
|
|
685
|
+
.dsh-cron-sidebar-job-title { font-size: 12px; color: var(--dsw-alias-label-secondary); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
686
|
+
.dsh-cron-sidebar-job-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
|
|
687
|
+
.dsh-cron-sidebar-empty, .dsh-cron-sidebar-more { font-size: 11px; color: var(--dsw-alias-label-tertiary); padding: 2px 8px; }
|
|
688
|
+
.dsh-cron-task-highlight { border-color: var(--dsh-cron-info) !important; box-shadow: 0 0 0 2px var(--dsh-cron-info-bg); }
|
|
689
|
+
/* #39: narrow screens. Only layout changes — no information is hidden. */
|
|
690
|
+
@media (max-width: 900px) {
|
|
691
|
+
.dsh-cron-container { padding: 8px 12px 24px; gap: 14px; }
|
|
692
|
+
.dsh-cron-form-row { grid-template-columns: 1fr; }
|
|
693
|
+
.dsh-cron-channel-grid { grid-template-columns: 1fr; }
|
|
694
|
+
.dsh-cron-filter-panel { grid-template-columns: 1fr; }
|
|
695
|
+
.dsh-cron-modal { padding: 16px; border-radius: 12px; }
|
|
696
|
+
.dsh-cron-modal-foot { bottom: -16px; margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; }
|
|
697
|
+
.dsh-cron-modal-scroll { max-height: 92vh; }
|
|
698
|
+
.dsh-cron-task-item { flex-wrap: wrap; gap: 10px; }
|
|
699
|
+
.dsh-cron-task-actions { margin-left: auto; }
|
|
700
|
+
.dsh-cron-history-list { max-height: 320px; }
|
|
701
|
+
}
|
|
702
|
+
@media (max-width: 640px) {
|
|
703
|
+
.dsh-cron-container { padding: 6px 8px 20px; }
|
|
704
|
+
.dsh-cron-top-bar { flex-wrap: wrap; gap: 8px; }
|
|
705
|
+
.dsh-cron-tabs { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
|
|
706
|
+
.dsh-cron-tab { white-space: nowrap; }
|
|
707
|
+
.dsh-cron-stats-bar { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
|
|
708
|
+
.dsh-cron-task-item { align-items: flex-start; padding: 12px; }
|
|
709
|
+
.dsh-cron-task-title { flex-wrap: wrap; row-gap: 4px; }
|
|
710
|
+
.dsh-cron-modal { padding: 14px; }
|
|
711
|
+
.dsh-cron-modal-foot { flex-direction: column-reverse; }
|
|
712
|
+
.dsh-cron-modal-foot > button { width: 100%; }
|
|
713
|
+
.dsh-cron-filter-panel { padding: 8px 10px; }
|
|
714
|
+
.dsh-cron-card { padding: 14px; }
|
|
715
|
+
/* 16px keeps iOS Safari from zooming the page when a field is focused. */
|
|
716
|
+
.dsh-cron-modal input, .dsh-cron-modal select, .dsh-cron-modal textarea,
|
|
717
|
+
.dsh-cron-card input, .dsh-cron-search-input, .dsh-cron-filter-field select { font-size: 16px; }
|
|
718
|
+
}
|
|
719
|
+
`;
|
|
731
720
|
const STYLE_ID = 'dsh-cron-styles';
|
|
732
721
|
function ensureStyles() {
|
|
733
722
|
if (typeof document === 'undefined') return null;
|
|
@@ -1062,6 +1051,41 @@ onChange: (e) => onChannelTemplate(id, e.target.value)
|
|
|
1062
1051
|
]),
|
|
1063
1052
|
];
|
|
1064
1053
|
}
|
|
1054
|
+
function parseChainTargets(raw) {
|
|
1055
|
+
if (!raw) return [];
|
|
1056
|
+
if (Array.isArray(raw)) return raw.filter((x) => typeof x === 'string' && x.trim().length > 0);
|
|
1057
|
+
if (typeof raw === 'string') return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1060
|
+
const sanitizeDependsOn = parseChainTargets;
|
|
1061
|
+
const sanitizeJoinPolicy = (raw) => (raw === 'any_success' ? 'any_success' : 'all_success');
|
|
1062
|
+
function buildTaskGraphModel(tasks = [], snapshots = [], focusTaskId = null) {
|
|
1063
|
+
const taskMap = new Map();
|
|
1064
|
+
for (const t of tasks) if (t?.id) taskMap.set(t.id, t);
|
|
1065
|
+
const snapMap = new Map();
|
|
1066
|
+
for (const s of snapshots) if (s?.stepTaskId) snapMap.set(s.stepTaskId, s);
|
|
1067
|
+
const nodes = [], edges = [];
|
|
1068
|
+
let hasFailure = false;
|
|
1069
|
+
for (const [id, task] of taskMap.entries()) {
|
|
1070
|
+
const snap = snapMap.get(id);
|
|
1071
|
+
let nodeStatus = 'pending', durationMs = 0, outputSnippet = '', retryCount = 0;
|
|
1072
|
+
if (snap) {
|
|
1073
|
+
nodeStatus = snap.status === 'completed' || snap.status === 'success' ? 'success' : 'failed';
|
|
1074
|
+
durationMs = snap.durationMs || 0;
|
|
1075
|
+
retryCount = snap.retryCount || 0;
|
|
1076
|
+
if (snap.output) outputSnippet = String(snap.output).slice(0, 200);
|
|
1077
|
+
if (nodeStatus === 'failed') hasFailure = true;
|
|
1078
|
+
} else if (task.status === 'running') {
|
|
1079
|
+
nodeStatus = 'running';
|
|
1080
|
+
}
|
|
1081
|
+
const deps = sanitizeDependsOn(task.dependsOn);
|
|
1082
|
+
nodes.push({ id, title: task.title || id, type: task.type || 'prompt', status: nodeStatus, durationMs, retryCount, outputSnippet, isFocus: Boolean(focusTaskId && id === focusTaskId), isRetryable: nodeStatus === 'failed', dependsOn: deps, joinPolicy: sanitizeJoinPolicy(task.joinPolicy) });
|
|
1083
|
+
for (const d of deps) edges.push({ from: d, to: id, type: 'dependsOn', label: task.joinPolicy === 'any_success' ? 'join:any' : 'join:all' });
|
|
1084
|
+
for (const t of parseChainTargets(task.onSuccess)) edges.push({ from: id, to: t, type: 'onSuccess', label: 'onSuccess' });
|
|
1085
|
+
for (const f of parseChainTargets(task.onFailure)) edges.push({ from: id, to: f, type: 'onFailure', label: 'onFailure' });
|
|
1086
|
+
}
|
|
1087
|
+
return { nodes, edges, hasFailure };
|
|
1088
|
+
}
|
|
1065
1089
|
function renderChainGraphModal({ isOpen, onClose, data, loading, onRetry, t }) {
|
|
1066
1090
|
if (!isOpen) return null;
|
|
1067
1091
|
const graph = data?.graph || { nodes: [], edges: [], hasFailure: false };
|
|
@@ -1353,7 +1377,7 @@ topExpensiveTasks: []
|
|
|
1353
1377
|
const copyMarkdown = () => {
|
|
1354
1378
|
if (!digestData) return;
|
|
1355
1379
|
const text = [
|
|
1356
|
-
'# ' + (T_KEY(t, 'digest.modalTitle')
|
|
1380
|
+
'# ' + (T_KEY(t, 'digest.modalTitle')),
|
|
1357
1381
|
'Period: Last ' + (periodDays || 7) + ' days',
|
|
1358
1382
|
'Total Runs: ' + digest.totalRuns + ' (' + digest.successRate + '% SR)',
|
|
1359
1383
|
'Tokens: ' + digest.totalTokens + ' (~$' + digest.totalCostUsd + ' USD)',
|
|
@@ -1379,10 +1403,10 @@ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap
|
|
|
1379
1403
|
React.createElement('span', { style: { fontSize: '20px' } }, '📊'),
|
|
1380
1404
|
React.createElement('div', null,
|
|
1381
1405
|
React.createElement('div', { style: { fontWeight: '600', fontSize: '15px', color: 'var(--dsw-alias-label-primary, #fff)' } },
|
|
1382
|
-
T_KEY(t, 'digest.modalTitle')
|
|
1406
|
+
T_KEY(t, 'digest.modalTitle')
|
|
1383
1407
|
),
|
|
1384
1408
|
React.createElement('div', { style: { fontSize: '11.5px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } },
|
|
1385
|
-
(T_KEY(t, 'digest.buttonTitle')
|
|
1409
|
+
(T_KEY(t, 'digest.buttonTitle')) + ' · ' + (periodDays || 7) + 'd'
|
|
1386
1410
|
)
|
|
1387
1411
|
)
|
|
1388
1412
|
),
|
|
@@ -1392,13 +1416,13 @@ type: 'button',
|
|
|
1392
1416
|
className: 'dsh-cron-btn-secondary',
|
|
1393
1417
|
style: { padding: '4px 10px', fontSize: '12px', fontWeight: periodDays === 7 ? '600' : '400', background: periodDays === 7 ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
|
|
1394
1418
|
onClick: () => onSelectPeriod && onSelectPeriod(7)
|
|
1395
|
-
}, T_KEY(t, 'digest.period7')
|
|
1419
|
+
}, T_KEY(t, 'digest.period7')),
|
|
1396
1420
|
React.createElement('button', {
|
|
1397
1421
|
type: 'button',
|
|
1398
1422
|
className: 'dsh-cron-btn-secondary',
|
|
1399
1423
|
style: { padding: '4px 10px', fontSize: '12px', fontWeight: periodDays === 30 ? '600' : '400', background: periodDays === 30 ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
|
|
1400
1424
|
onClick: () => onSelectPeriod && onSelectPeriod(30)
|
|
1401
|
-
}, T_KEY(t, 'digest.period30')
|
|
1425
|
+
}, T_KEY(t, 'digest.period30')),
|
|
1402
1426
|
React.createElement('button', {
|
|
1403
1427
|
type: 'button',
|
|
1404
1428
|
className: 'dsh-cron-icon-btn',
|
|
@@ -1411,33 +1435,22 @@ React.createElement('div', {
|
|
|
1411
1435
|
style: { padding: '16px 18px', overflowY: 'auto', flex: 1, display: 'flex', flexDirection: 'column', gap: '16px' }
|
|
1412
1436
|
},
|
|
1413
1437
|
isLoading ? React.createElement('div', { style: { padding: '30px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary, #888)' } }, 'Loading digest metrics...') : React.createElement(React.Fragment, null,
|
|
1414
|
-
|
|
1415
|
-
style: {
|
|
1416
|
-
},
|
|
1417
|
-
React.createElement('div', { style: {
|
|
1418
|
-
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)'
|
|
1419
|
-
|
|
1420
|
-
React.createElement('div', { style: {
|
|
1421
|
-
),
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
),
|
|
1427
|
-
React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '10px 12px' } },
|
|
1428
|
-
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, T_KEY(t, 'digest.cost') || 'Cost'),
|
|
1429
|
-
React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, '$' + Number(digest.totalCostUsd || 0).toFixed(4)),
|
|
1430
|
-
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } }, 'USD estimated')
|
|
1431
|
-
),
|
|
1432
|
-
React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '10px 12px' } },
|
|
1433
|
-
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, T_KEY(t, 'digest.fleet') || 'Fleet'),
|
|
1434
|
-
React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, digest.activeTasksCount),
|
|
1435
|
-
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } }, digest.pausedTasksCount + ' paused')
|
|
1436
|
-
)
|
|
1437
|
-
),
|
|
1438
|
+
(() => {
|
|
1439
|
+
const renderKpi = (label, val, sub, subCol) => React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '10px 12px' } },
|
|
1440
|
+
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, label),
|
|
1441
|
+
React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, val),
|
|
1442
|
+
React.createElement('div', { style: { fontSize: '11px', color: subCol || 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } }, sub)
|
|
1443
|
+
);
|
|
1444
|
+
return React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px' } },
|
|
1445
|
+
renderKpi(T_KEY(t, 'digest.executions'), digest.totalRuns, digest.successRate + '% SR', digest.successRate >= 90 ? 'var(--dsh-cron-success, #2a2)' : 'var(--dsh-cron-warning, #fa0)'),
|
|
1446
|
+
renderKpi(T_KEY(t, 'digest.tokens'), Number(digest.totalTokens || 0).toLocaleString(), 'LLM prompt+out'),
|
|
1447
|
+
renderKpi(T_KEY(t, 'digest.cost'), '$' + Number(digest.totalCostUsd || 0).toFixed(4), 'USD estimated'),
|
|
1448
|
+
renderKpi(T_KEY(t, 'digest.fleet'), digest.activeTasksCount, digest.pausedTasksCount + ' paused')
|
|
1449
|
+
);
|
|
1450
|
+
})(),
|
|
1438
1451
|
React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '12px' } },
|
|
1439
1452
|
React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsh-cron-warning, #fa0)', marginBottom: '8px' } },
|
|
1440
|
-
'⚠️ ' + (T_KEY(t, 'digest.unstableTitle')
|
|
1453
|
+
'⚠️ ' + (T_KEY(t, 'digest.unstableTitle'))
|
|
1441
1454
|
),
|
|
1442
1455
|
(digest.topUnstableTasks && digest.topUnstableTasks.length > 0) ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1443
1456
|
digest.topUnstableTasks.map((tItem, idx) => React.createElement('div', {
|
|
@@ -1453,7 +1466,7 @@ tItem.failures + ' fail (' + tItem.successRate + '% SR)'
|
|
|
1453
1466
|
),
|
|
1454
1467
|
React.createElement('div', { style: { background: 'var(--dsw-alias-bg-layer-2, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '6px', padding: '12px' } },
|
|
1455
1468
|
React.createElement('div', { style: { fontWeight: '600', fontSize: '13px', color: 'var(--dsh-cron-info, #4af)', marginBottom: '8px' } },
|
|
1456
|
-
'💰 ' + (T_KEY(t, 'digest.expensiveTitle')
|
|
1469
|
+
'💰 ' + (T_KEY(t, 'digest.expensiveTitle'))
|
|
1457
1470
|
),
|
|
1458
1471
|
(digest.topExpensiveTasks && digest.topExpensiveTasks.length > 0) ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1459
1472
|
digest.topExpensiveTasks.map((tItem, idx) => React.createElement('div', {
|
|
@@ -1481,13 +1494,13 @@ type: 'button',
|
|
|
1481
1494
|
className: 'dsh-cron-btn-secondary',
|
|
1482
1495
|
onClick: copyMarkdown,
|
|
1483
1496
|
style: { padding: '6px 14px', fontSize: '12px' }
|
|
1484
|
-
}, T_KEY(t, 'digest.copyMarkdown')
|
|
1497
|
+
}, T_KEY(t, 'digest.copyMarkdown')),
|
|
1485
1498
|
React.createElement('button', {
|
|
1486
1499
|
type: 'button',
|
|
1487
1500
|
className: 'dsh-cron-btn-primary',
|
|
1488
1501
|
onClick: onSendTelegram,
|
|
1489
1502
|
style: { padding: '6px 14px', fontSize: '12px' }
|
|
1490
|
-
}, T_KEY(t, 'digest.sendTelegram')
|
|
1503
|
+
}, T_KEY(t, 'digest.sendTelegram'))
|
|
1491
1504
|
)
|
|
1492
1505
|
)
|
|
1493
1506
|
)
|
|
@@ -1525,7 +1538,7 @@ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap
|
|
|
1525
1538
|
React.createElement('span', { style: { fontSize: '20px' } }, tab === 'timeline' ? '⏱' : '🔥'),
|
|
1526
1539
|
React.createElement('div', null,
|
|
1527
1540
|
React.createElement('div', { style: { fontWeight: '600', fontSize: '15px', color: 'var(--dsw-alias-label-primary, #fff)' } },
|
|
1528
|
-
tab === 'timeline' ? (T_KEY(t, 'timeline.modalTitle')
|
|
1541
|
+
tab === 'timeline' ? (T_KEY(t, 'timeline.modalTitle')) : (T_KEY(t, 'heatmap.modalTitle'))
|
|
1529
1542
|
),
|
|
1530
1543
|
React.createElement('div', { style: { fontSize: '11.5px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } },
|
|
1531
1544
|
tab === 'timeline'
|
|
@@ -1540,13 +1553,13 @@ type: 'button',
|
|
|
1540
1553
|
className: 'dsh-cron-btn-secondary',
|
|
1541
1554
|
style: { padding: '4px 12px', fontSize: '12px', fontWeight: tab === 'timeline' ? '600' : '400', background: tab === 'timeline' ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
|
|
1542
1555
|
onClick: () => onSelectTab && onSelectTab('timeline')
|
|
1543
|
-
}, '⏱ ' + (T_KEY(t, 'timeline.tabTimeline')
|
|
1556
|
+
}, '⏱ ' + (T_KEY(t, 'timeline.tabTimeline'))),
|
|
1544
1557
|
React.createElement('button', {
|
|
1545
1558
|
type: 'button',
|
|
1546
1559
|
className: 'dsh-cron-btn-secondary',
|
|
1547
1560
|
style: { padding: '4px 12px', fontSize: '12px', fontWeight: tab === 'heatmap' ? '600' : '400', background: tab === 'heatmap' ? 'var(--dsw-alias-bg-layer-3, #222)' : 'transparent' },
|
|
1548
1561
|
onClick: () => onSelectTab && onSelectTab('heatmap')
|
|
1549
|
-
}, '🔥 ' + (T_KEY(t, 'heatmap.tabHeatmap')
|
|
1562
|
+
}, '🔥 ' + (T_KEY(t, 'heatmap.tabHeatmap'))),
|
|
1550
1563
|
React.createElement('button', {
|
|
1551
1564
|
type: 'button',
|
|
1552
1565
|
className: 'dsh-cron-icon-btn',
|
|
@@ -1562,54 +1575,31 @@ isLoading ? React.createElement('div', { style: { padding: '40px', textAlign: 'c
|
|
|
1562
1575
|
tab === 'timeline' ? (
|
|
1563
1576
|
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
1564
1577
|
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)', display: 'flex', justifyContent: 'space-between' } },
|
|
1565
|
-
React.createElement('span', null, T_KEY(t, 'timeline.legendDesc')
|
|
1566
|
-
React.createElement('span', { style: { color: 'var(--dsh-cron-warning, #fa0)' } }, '⚠️ ' + (T_KEY(t, 'timeline.peakWarning')
|
|
1578
|
+
React.createElement('span', null, T_KEY(t, 'timeline.legendDesc')),
|
|
1579
|
+
React.createElement('span', { style: { color: 'var(--dsh-cron-warning, #fa0)' } }, '⚠️ ' + (T_KEY(t, 'timeline.peakWarning')) + ' (' + timeline.maxConcurrent + ')')
|
|
1567
1580
|
),
|
|
1568
1581
|
React.createElement('div', {
|
|
1569
1582
|
style: { display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: '8px', marginTop: '6px' }
|
|
1570
1583
|
},
|
|
1571
1584
|
(timeline.buckets || []).map((b) => {
|
|
1572
|
-
const isPeak = b.isPeak;
|
|
1573
|
-
const hasRuns = b.count > 0;
|
|
1585
|
+
const isPeak = b.isPeak, hasRuns = b.count > 0;
|
|
1574
1586
|
return React.createElement('div', {
|
|
1575
1587
|
key: b.index,
|
|
1576
1588
|
style: {
|
|
1577
|
-
background: isPeak
|
|
1578
|
-
? '
|
|
1579
|
-
:
|
|
1580
|
-
border: isPeak
|
|
1581
|
-
? '1px solid var(--dsh-cron-danger, #e33)'
|
|
1582
|
-
: (hasRuns ? '1px solid var(--dsw-alias-border-l2, #333)' : '1px dashed var(--dsw-alias-border-l2, #222)'),
|
|
1583
|
-
borderRadius: '6px',
|
|
1584
|
-
padding: '8px',
|
|
1585
|
-
minHeight: '75px',
|
|
1586
|
-
display: 'flex',
|
|
1587
|
-
flexDirection: 'column',
|
|
1588
|
-
justifyContent: 'space-between'
|
|
1589
|
+
background: isPeak ? 'color-mix(in srgb, var(--dsh-cron-danger, #e33) 12%, var(--dsw-alias-bg-layer-2, #111))' : (hasRuns ? 'var(--dsw-alias-bg-layer-2, #111)' : 'var(--dsw-alias-bg-base, #111)'),
|
|
1590
|
+
border: isPeak ? '1px solid var(--dsh-cron-danger, #e33)' : (hasRuns ? '1px solid var(--dsw-alias-border-l2, #333)' : '1px dashed var(--dsw-alias-border-l2, #222)'),
|
|
1591
|
+
borderRadius: '6px', padding: '8px', minHeight: '75px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between'
|
|
1589
1592
|
}
|
|
1590
1593
|
},
|
|
1591
1594
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '11px' } },
|
|
1592
1595
|
React.createElement('span', { style: { fontWeight: '600', color: isPeak ? 'var(--dsh-cron-danger, #e33)' : 'var(--dsw-alias-label-primary, #fff)' } }, b.label),
|
|
1593
|
-
hasRuns ? React.createElement('span', {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
),
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
title: r.title + ' (at ' + new Date(r.runAt).toLocaleTimeString() + ')',
|
|
1601
|
-
style: {
|
|
1602
|
-
fontSize: '10px',
|
|
1603
|
-
padding: '2px 4px',
|
|
1604
|
-
borderRadius: '3px',
|
|
1605
|
-
background: 'var(--dsw-alias-bg-layer-3, #222)',
|
|
1606
|
-
color: 'var(--dsw-alias-label-primary, #ddd)',
|
|
1607
|
-
overflow: 'hidden',
|
|
1608
|
-
textOverflow: 'ellipsis',
|
|
1609
|
-
whiteSpace: 'nowrap'
|
|
1610
|
-
}
|
|
1611
|
-
}, r.title)),
|
|
1612
|
-
b.count > 3 ? React.createElement('div', { style: { fontSize: '9px', color: 'var(--dsw-alias-label-secondary, #888)' } }, '+' + (b.count - 3) + ' more') : null
|
|
1596
|
+
hasRuns ? React.createElement('span', { style: { fontSize: '10px', padding: '1px 5px', borderRadius: '8px', background: isPeak ? 'var(--dsh-cron-danger, #e33)' : 'var(--dsw-alias-bg-layer-3, #222)', color: '#fff' } }, b.count) : null
|
|
1597
|
+
),
|
|
1598
|
+
React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '3px', marginTop: '4px' } },
|
|
1599
|
+
(b.runs || []).slice(0, 3).map((r, rIdx) => React.createElement('span', {
|
|
1600
|
+
key: rIdx, title: (r.title || r.taskId) + ' (' + r.time + ')',
|
|
1601
|
+
style: { fontSize: '9.5px', fontFamily: 'monospace', padding: '1px 4px', borderRadius: '3px', background: 'var(--dsw-alias-bg-base, #111)', border: '1px solid var(--dsw-alias-border-l2, #333)', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
1602
|
+
}, r.title || r.taskId))
|
|
1613
1603
|
)
|
|
1614
1604
|
);
|
|
1615
1605
|
})
|
|
@@ -1618,7 +1608,7 @@ b.count > 3 ? React.createElement('div', { style: { fontSize: '9px', color: 'var
|
|
|
1618
1608
|
) : (
|
|
1619
1609
|
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
|
|
1620
1610
|
React.createElement('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #888)', display: 'flex', justifyContent: 'space-between' } },
|
|
1621
|
-
React.createElement('span', null, T_KEY(t, 'heatmap.matrixDesc')
|
|
1611
|
+
React.createElement('span', null, T_KEY(t, 'heatmap.matrixDesc')),
|
|
1622
1612
|
React.createElement('span', null, 'Max hour spend: $' + Number(heatmap.maxCostHour || 0).toFixed(4))
|
|
1623
1613
|
),
|
|
1624
1614
|
React.createElement('div', {
|
|
@@ -1673,7 +1663,7 @@ type: 'button',
|
|
|
1673
1663
|
className: 'dsh-cron-btn-secondary',
|
|
1674
1664
|
onClick: onClose,
|
|
1675
1665
|
style: { padding: '6px 16px', fontSize: '12px' }
|
|
1676
|
-
}, T_KEY(t, 'chainGraph.close')
|
|
1666
|
+
}, T_KEY(t, 'chainGraph.close'))
|
|
1677
1667
|
)
|
|
1678
1668
|
)
|
|
1679
1669
|
);
|
|
@@ -1742,6 +1732,17 @@ const [chainGraphModalOpen, setChainGraphModalOpen] = React.useState(false);
|
|
|
1742
1732
|
const [chainGraphData, setChainGraphData] = React.useState(null);
|
|
1743
1733
|
const [chainGraphLoading, setChainGraphLoading] = React.useState(false);
|
|
1744
1734
|
const [chainGraphRunId, setChainGraphRunId] = React.useState(null);
|
|
1735
|
+
const [digestOpen, setDigestOpen] = React.useState(false);
|
|
1736
|
+
const [digestData, setDigestData] = React.useState(null);
|
|
1737
|
+
const [digestMarkdown, setDigestMarkdown] = React.useState('');
|
|
1738
|
+
const [digestLoading, setDigestLoading] = React.useState(false);
|
|
1739
|
+
const [periodDays, setPeriodDays] = React.useState(7);
|
|
1740
|
+
const [sendStatus, setSendStatus] = React.useState(null);
|
|
1741
|
+
const [timelineOpen, setTimelineOpen] = React.useState(false);
|
|
1742
|
+
const [timelineData, setTimelineData] = React.useState(null);
|
|
1743
|
+
const [heatmapData, setHeatmapData] = React.useState(null);
|
|
1744
|
+
const [timelineLoading, setTimelineLoading] = React.useState(false);
|
|
1745
|
+
const [timelineTab, setTimelineTab] = React.useState('timeline');
|
|
1745
1746
|
const [formHeartbeatIntervalSeconds, setFormHeartbeatIntervalSeconds] = React.useState(0);
|
|
1746
1747
|
const [formGracePeriodSeconds, setFormGracePeriodSeconds] = React.useState(300);
|
|
1747
1748
|
const [formPreflightType, setFormPreflightType] = React.useState('none');
|
|
@@ -2885,6 +2886,67 @@ window.__dshLiveLogsEvtSource = null;
|
|
|
2885
2886
|
}
|
|
2886
2887
|
setLiveLogsModalOpen(false);
|
|
2887
2888
|
}
|
|
2889
|
+
const openAnalyticsDigest = async (days = 7) => {
|
|
2890
|
+
setDigestOpen(true);
|
|
2891
|
+
setDigestLoading(true);
|
|
2892
|
+
setPeriodDays(days);
|
|
2893
|
+
setSendStatus(null);
|
|
2894
|
+
try {
|
|
2895
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/analytics?days=' + encodeURIComponent(days));
|
|
2896
|
+
if (data && data.ok) {
|
|
2897
|
+
setDigestData(data.digest || null);
|
|
2898
|
+
setDigestMarkdown(data.markdown || '');
|
|
2899
|
+
}
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
console.debug('[dsh-cron] analytics digest fetch failed', err);
|
|
2902
|
+
} finally {
|
|
2903
|
+
setDigestLoading(false);
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
const onSelectPeriod = async (days) => {
|
|
2907
|
+
setPeriodDays(days);
|
|
2908
|
+
await openAnalyticsDigest(days);
|
|
2909
|
+
};
|
|
2910
|
+
const onSendTelegram = async () => {
|
|
2911
|
+
setSendStatus('⏳ ' + (T_KEY(t, 'digest.sending') || 'Sending...'));
|
|
2912
|
+
try {
|
|
2913
|
+
const { res, data } = await fetchJsonWithTimeout('/dsh-cron/analytics', {
|
|
2914
|
+
method: 'POST',
|
|
2915
|
+
body: JSON.stringify({ days: periodDays })
|
|
2916
|
+
});
|
|
2917
|
+
if (data && data.ok) {
|
|
2918
|
+
setSendStatus('✅ ' + (T_KEY(t, 'digest.sent') || 'Sent!'));
|
|
2919
|
+
} else {
|
|
2920
|
+
setSendStatus('❌ ' + (data?.error || 'Failed'));
|
|
2921
|
+
}
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
setSendStatus('❌ ' + (err.message || 'Error'));
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
const openTimelineModal = async (tab = 'timeline') => {
|
|
2927
|
+
setTimelineOpen(true);
|
|
2928
|
+
setTimelineLoading(true);
|
|
2929
|
+
setTimelineTab(tab);
|
|
2930
|
+
try {
|
|
2931
|
+
const [timelineRes, heatmapRes] = await Promise.all([
|
|
2932
|
+
fetchJsonWithTimeout('/dsh-cron/schedule/timeline-24h?hours=24'),
|
|
2933
|
+
fetchJsonWithTimeout('/dsh-cron/analytics/heatmap')
|
|
2934
|
+
]);
|
|
2935
|
+
if (timelineRes?.data?.ok && timelineRes?.data?.timeline) {
|
|
2936
|
+
setTimelineData(timelineRes.data.timeline);
|
|
2937
|
+
}
|
|
2938
|
+
if (heatmapRes?.data?.ok && heatmapRes?.data?.heatmap) {
|
|
2939
|
+
setHeatmapData(heatmapRes.data.heatmap);
|
|
2940
|
+
}
|
|
2941
|
+
} catch (err) {
|
|
2942
|
+
console.debug('[dsh-cron] timeline fetch failed', err);
|
|
2943
|
+
} finally {
|
|
2944
|
+
setTimelineLoading(false);
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
const onSelectTimelineTab = (tab) => {
|
|
2948
|
+
setTimelineTab(tab);
|
|
2949
|
+
};
|
|
2888
2950
|
return React.createElement('div', { className: 'dsh-cron-overlay' },
|
|
2889
2951
|
React.createElement('div', { className: 'dsh-cron-container' },
|
|
2890
2952
|
React.createElement('div', { className: 'dsh-cron-top-bar' },
|
|
@@ -3819,21 +3881,21 @@ React.createElement('span', null, T_KEY(t, 'form.keepWorktreeLabel'))
|
|
|
3819
3881
|
formType !== 'http' && React.createElement('div', { className: 'dsh-cron-form-group' },
|
|
3820
3882
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' } },
|
|
3821
3883
|
React.createElement('label', { style: { margin: 0 } }, AGENT_FORM_TYPES.includes(formType) ? T_KEY(t, 'form.promptLabelLlm') : T_KEY(t, 'form.promptLabelScript')),
|
|
3822
|
-
(
|
|
3884
|
+
(selectedTaskMeta && selectedTaskMeta.promptHistory && selectedTaskMeta.promptHistory.length > 0) ? React.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } },
|
|
3823
3885
|
React.createElement('span', {
|
|
3824
3886
|
style: { fontSize: '11px', padding: '1px 6px', borderRadius: '4px', background: 'var(--dsw-alias-bg-layer-3, #222)', color: 'var(--dsh-cron-info, #4af)', border: '1px solid var(--dsw-alias-border-l2, #333)' }
|
|
3825
|
-
}, 'v' + (
|
|
3887
|
+
}, 'v' + (selectedTaskMeta.promptVersion || 1) + ' (' + (selectedTaskMeta.promptHistory[selectedTaskMeta.promptHistory.length - 1]?.successRate !== undefined ? Math.round(selectedTaskMeta.promptHistory[selectedTaskMeta.promptHistory.length - 1].successRate * 100) : 100) + '% SR)'),
|
|
3826
3888
|
React.createElement('select', {
|
|
3827
3889
|
style: { fontSize: '11px', padding: '1px 4px', background: 'var(--dsw-alias-bg-base, #111)', color: 'var(--dsw-alias-label-primary, #ddd)', border: '1px solid var(--dsw-alias-border-l2, #333)', borderRadius: '4px' },
|
|
3828
3890
|
onChange: (e) => {
|
|
3829
3891
|
const targetV = Number(e.target.value);
|
|
3830
|
-
const entry =
|
|
3892
|
+
const entry = selectedTaskMeta.promptHistory.find((h) => h.version === targetV);
|
|
3831
3893
|
if (entry && entry.prompt) {
|
|
3832
3894
|
setFormPrompt(entry.prompt);
|
|
3833
3895
|
}
|
|
3834
3896
|
}
|
|
3835
3897
|
},
|
|
3836
|
-
|
|
3898
|
+
selectedTaskMeta.promptHistory.map((h) => React.createElement('option', { key: h.version, value: h.version }, 'v' + h.version + ' (' + Math.round((h.successRate || 1) * 100) + '%)'))
|
|
3837
3899
|
)
|
|
3838
3900
|
) : null
|
|
3839
3901
|
),
|
|
@@ -4449,6 +4511,27 @@ onToggleAutoScroll: () => setLiveLogsAutoScroll((v) => !v),
|
|
|
4449
4511
|
completed: liveLogsCompleted,
|
|
4450
4512
|
status: liveLogsStatus,
|
|
4451
4513
|
t
|
|
4514
|
+
}),
|
|
4515
|
+
renderAnalyticsDigestModal({
|
|
4516
|
+
isOpen: digestOpen,
|
|
4517
|
+
onClose: () => setDigestOpen(false),
|
|
4518
|
+
digestData,
|
|
4519
|
+
isLoading: digestLoading,
|
|
4520
|
+
periodDays,
|
|
4521
|
+
onSelectPeriod,
|
|
4522
|
+
onSendTelegram,
|
|
4523
|
+
sendStatus,
|
|
4524
|
+
t
|
|
4525
|
+
}),
|
|
4526
|
+
renderTimelineHeatmapModal({
|
|
4527
|
+
isOpen: timelineOpen,
|
|
4528
|
+
onClose: () => setTimelineOpen(false),
|
|
4529
|
+
activeTab: timelineTab,
|
|
4530
|
+
onSelectTab: onSelectTimelineTab,
|
|
4531
|
+
timelineData,
|
|
4532
|
+
heatmapData,
|
|
4533
|
+
isLoading: timelineLoading,
|
|
4534
|
+
t
|
|
4452
4535
|
})
|
|
4453
4536
|
);
|
|
4454
4537
|
}
|
package/lib/index.js
CHANGED
|
@@ -305,6 +305,10 @@ export function apply(ctx, rawConfig) {
|
|
|
305
305
|
path: '/dsh-cron/settings',
|
|
306
306
|
handler: async (req, res) => {
|
|
307
307
|
try {
|
|
308
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
309
|
+
const apiToken = settings.apiToken;
|
|
310
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
311
|
+
|
|
308
312
|
if (req.method === 'GET') {
|
|
309
313
|
const unwrapped = {
|
|
310
314
|
...unwrapConfig(getContextConfig(ctx, rawConfig)),
|
package/lib/metrics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rejectCrossOrigin } from './http-utils.js';
|
|
1
2
|
/**
|
|
2
3
|
* Prometheus exposition of the scheduler (#53).
|
|
3
4
|
*
|
|
@@ -108,6 +109,10 @@ export function createMetricsHandler({ store, scheduler }) {
|
|
|
108
109
|
res.end('Method not allowed' + NL);
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
112
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
113
|
+
const apiToken = settings.apiToken;
|
|
114
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
115
|
+
|
|
111
116
|
const body = renderMetrics({
|
|
112
117
|
tasks: store.list({ status: 'all' }),
|
|
113
118
|
stats: store.getAggregatedStats(),
|
package/lib/routes.js
CHANGED
|
@@ -88,6 +88,9 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
88
88
|
kind: 'exact',
|
|
89
89
|
path: '/dsh-cron/models',
|
|
90
90
|
handler: async (req, res) => {
|
|
91
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
92
|
+
const apiToken = settings.apiToken;
|
|
93
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
91
94
|
await getModelsHandler(ctx, req, res, sendJson);
|
|
92
95
|
}
|
|
93
96
|
}), 'dsh-cron: /models');
|
|
@@ -108,6 +111,10 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
108
111
|
path: '/dsh-cron/analytics',
|
|
109
112
|
handler: async (req, res) => {
|
|
110
113
|
try {
|
|
114
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
115
|
+
const apiToken = settings.apiToken;
|
|
116
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
117
|
+
|
|
111
118
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
112
119
|
const path = url.pathname;
|
|
113
120
|
|
|
@@ -211,6 +218,8 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
211
218
|
return;
|
|
212
219
|
}
|
|
213
220
|
const settings = store.getSettings ? store.getSettings() : {};
|
|
221
|
+
const apiToken = settings.apiToken;
|
|
222
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
214
223
|
const categories = recipesByCategory();
|
|
215
224
|
const recipes = listVerifiedRecipes(settings);
|
|
216
225
|
sendJson(res, 200, { ok: true, categories, recipes });
|
|
@@ -230,6 +239,10 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
230
239
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
231
240
|
return;
|
|
232
241
|
}
|
|
242
|
+
const settings = store.getSettings ? store.getSettings() : {};
|
|
243
|
+
const apiToken = settings.apiToken;
|
|
244
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
245
|
+
|
|
233
246
|
const tasks = store.list({ status: 'active' });
|
|
234
247
|
const lastRuns = tasks.map((t) => t.lastRunAt || 0);
|
|
235
248
|
sendJson(res, 200, {
|
|
@@ -319,7 +332,9 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
|
|
|
319
332
|
* Handler for chain step retries (#319)
|
|
320
333
|
*/
|
|
321
334
|
export async function handleChainRoutes(store, scheduler, req, res) {
|
|
322
|
-
|
|
335
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
336
|
+
const apiToken = settings.apiToken;
|
|
337
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
323
338
|
const url = new URL(req.url, 'http://localhost');
|
|
324
339
|
|
|
325
340
|
// GET /dsh-cron/chains/:chainRunId (#334)
|
|
@@ -374,6 +389,10 @@ export async function handleRunArtifactRoutes(store, req, res) {
|
|
|
374
389
|
sendJson(res, 405, { ok: false, error: 'Method not allowed' });
|
|
375
390
|
return;
|
|
376
391
|
}
|
|
392
|
+
const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
|
|
393
|
+
const apiToken = settings.apiToken;
|
|
394
|
+
if (rejectCrossOrigin(req, res, { apiToken })) return;
|
|
395
|
+
|
|
377
396
|
const [, runId, artifactId, action] = m;
|
|
378
397
|
if (!artifactId) {
|
|
379
398
|
const artifacts = store.getArtifactsForRun(runId);
|
package/lib/validators.js
CHANGED
|
@@ -128,7 +128,8 @@ export function validateRunResult(task, runInfo, options = {}) {
|
|
|
128
128
|
// Guard against directory traversal escaping baseDir
|
|
129
129
|
const resolvedPath = path.resolve(baseDir, relPath);
|
|
130
130
|
const resolvedBase = path.resolve(baseDir);
|
|
131
|
-
|
|
131
|
+
const basePrefix = resolvedBase.endsWith(path.sep) ? resolvedBase : resolvedBase + path.sep;
|
|
132
|
+
if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(basePrefix)) {
|
|
132
133
|
const message = `File path "${relPath}" escapes allowed directory boundary`;
|
|
133
134
|
details.push({ type: 'file', passed: false, message });
|
|
134
135
|
return { ok: false, status: 'failed', error: message, details };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|