@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.
@@ -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
- :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
-
534
- .dsh-cron-entry-clone { display: flex; align-items: center; gap: 8px; }
535
- .dsh-cron-entry-icon { display: flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; }
536
- .dsh-cron-entry-label { text-overflow: ellipsis; overflow: hidden; }
537
- [data-dsh-frame][data-sidebar-collapsed] .dsh-cron-entry-label { display: none; }
538
-
539
- .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; }
540
- .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; }
541
-
542
- .dsh-cron-top-bar { display: flex; align-items: center; gap: 12px; }
543
- .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; }
544
- .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))); }
545
-
546
- .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); }
547
- .dsh-cron-title { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
548
- .dsh-cron-subtitle { font-size: 14px; color: var(--dsw-alias-label-secondary); line-height: 1.5; }
549
- .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; }
550
- .dsh-cron-create-btn:hover { background: var(--dsw-alias-bg-layer-hover, var(--dsw-alias-interactive-bg-hover)); }
551
- .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; }
552
- .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; }
553
- .dsh-cron-dropdown-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
554
- .dsh-cron-search-bar { position: relative; margin-bottom: 0; }
555
- .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; }
556
- .dsh-cron-search-input:focus { outline: none; border-color: var(--dsh-cron-info); }
557
- .dsh-cron-search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary); }
558
- .dsh-cron-filters { margin-top: 8px; }
559
- .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; }
560
- .dsh-cron-filter-toggle:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsh-cron-info); }
561
- .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)); }
562
- .dsh-cron-filter-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary); }
563
- .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; }
564
- .dsh-cron-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
565
- .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; }
566
- .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)); }
567
- .dsh-cron-task-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 0; }
568
- .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; }
569
- .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; }
570
- .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; }
571
- .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; }
572
- .dsh-cron-tag-success { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
573
- .dsh-cron-tag-failure { background: var(--dsh-cron-danger-bg); color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger); }
574
- .dsh-cron-tag-heartbeat { background: var(--dsh-cron-accent-bg); color: var(--dsh-cron-accent); border-color: var(--dsh-cron-accent); }
575
- .dsh-cron-tag-session { background: var(--dsh-cron-info-bg); color: var(--dsh-cron-info); border-color: var(--dsh-cron-info); }
576
- .dsh-cron-tag-preflight { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
577
-
578
- .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; }
579
- .dsh-cron-task-actions { display: flex; gap: 8px; }
580
- .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; }
581
- .dsh-cron-icon-btn:hover { background: var(--dsw-alias-bg-layer-4); border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2)); }
582
- .dsh-cron-icon-btn.dsh-cron-danger { color: var(--dsh-cron-danger); border-color: var(--dsh-cron-danger-bg); }
583
- .dsh-cron-icon-btn.dsh-cron-danger:hover { background: var(--dsh-cron-danger-bg); border-color: var(--dsh-cron-danger); }
584
- .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; }
585
- .dsh-cron-task-status-btn[data-active="true"] { color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
586
- .dsh-cron-recs-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
587
- .dsh-cron-recs-list { display: flex; flex-direction: column; gap: 10px; }
588
- .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; }
589
- .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; }
590
- .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; }
591
- .dsh-cron-rec-content { flex: 1; min-width: 0; }
592
- .dsh-cron-rec-title { font-size: 14.5px; font-weight: 500; margin-bottom: 4px; }
593
- .dsh-cron-rec-time { font-size: 13px; color: var(--dsw-alias-label-secondary); font-weight: normal; margin-left: 8px; }
594
- .dsh-cron-rec-desc { font-size: 13px; color: var(--dsw-alias-label-tertiary); }
595
-
596
- .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; }
597
- .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; }
598
- .dsh-cron-modal-scroll { max-width: 640px; max-height: min(90vh, calc(100vh - 36px)); overflow-y: auto; }
599
- .dsh-cron-modal::-webkit-scrollbar { width: 6px; }
600
- .dsh-cron-modal::-webkit-scrollbar-track { background: transparent; }
601
- .dsh-cron-modal::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l2); border-radius: 3px; }
602
- .dsh-cron-modal::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-label-tertiary); }
603
- .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; }
604
- .dsh-cron-section-head:hover { color: var(--dsw-alias-label-primary); }
605
- .dsh-cron-section-chevron { transition: transform 0.15s ease; display: inline-block; }
606
- .dsh-cron-section-chevron[data-open="false"] { transform: rotate(-90deg); }
607
- .dsh-cron-section-body { padding-bottom: 4px; }
608
- .dsh-cron-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 18px; }
609
- .dsh-cron-form-group { margin-bottom: 16px; }
610
- .dsh-cron-form-group label { display: block; font-size: 13px; color: var(--dsw-alias-label-secondary); margin-bottom: 6px; }
611
- .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; }
612
- .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); }
613
- .dsh-cron-form-group select { cursor: pointer; }
614
- .dsh-cron-form-group textarea { min-height: 80px; resize: vertical; }
615
- .dsh-cron-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
616
- .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; }
617
- .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; }
618
- .dsh-cron-btn-primary:hover:not(:disabled) { opacity: 0.88; }
619
- .dsh-cron-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
620
- .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; }
621
- .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)); }
622
- .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; }
623
- .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; }
624
- .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); }
625
- .dsh-cron-chat-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
626
- .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; }
627
- .dsh-cron-send-btn:hover:not(:disabled) { transform: translateY(-1px); }
628
- .dsh-cron-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
629
-
630
- .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; }
631
- .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; }
632
- .dsh-cron-card-head-btn .dsh-cron-card-title { flex: 1; }
633
- .dsh-cron-card-title { font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary); }
634
- .dsh-cron-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary); transition: transform 0.16s; display: flex; align-items: center; }
635
- .dsh-cron-chev-open { transform: rotate(180deg); }
636
- .dsh-cron-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin-top: 2px; padding-top: 14px; }
637
- .dsh-cron-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
638
- .dsh-cron-card-desc { font-size: 13px; color: var(--dsw-alias-label-secondary); line-height: 1.4; }
639
-
640
- .dsh-cron-modal-nav { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l2); margin-bottom: 18px; padding-bottom: 2px; }
641
- .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; }
642
- .dsh-cron-modal-tab:hover { color: var(--dsw-alias-label-secondary); }
643
- .dsh-cron-modal-tab[data-active="true"] { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-label-primary); }
644
- .dsh-cron-history-list { display: flex; flex-direction: column; gap: 10px; max-height: 420px; overflow-y: auto; padding-right: 4px; }
645
- .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; }
646
- .dsh-cron-history-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
647
- .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); }
648
- .dsh-cron-history-status[data-status="success"] { background: var(--dsh-cron-success-bg); color: var(--dsh-cron-success); border-color: var(--dsh-cron-success); }
649
- .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); }
650
- .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); }
651
- .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); }
652
- .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); }
653
- .dsh-cron-stage-pill[data-status="partial"] { background: var(--dsh-cron-warning-bg); color: var(--dsh-cron-warning); border-color: var(--dsh-cron-warning); }
654
- .dsh-cron-stage-pill[data-status="skipped"] { opacity: 0.6; }
655
- .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); }
656
- .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); }
657
- .dsh-cron-history-time { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
658
- .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; }
659
- .dsh-cron-history-empty { text-align: center; padding: 36px 0; color: var(--dsw-alias-label-tertiary); font-size: 13.5px; }
660
-
661
- .dsh-cron-stats-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 0; }
662
- .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; }
663
- .dsh-cron-stat-val { font-size: 18px; font-weight: 700; color: var(--dsw-alias-label-primary); }
664
- .dsh-cron-stat-lbl { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: 0; }
665
- .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; }
666
- .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; }
667
- .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; }
668
- .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); }
669
- .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; }
670
- .dsh-cron-checkbox-row input { width: 15px; height: 15px; margin: 0; cursor: pointer; accent-color: var(--dsw-alias-label-primary); }
671
- .dsh-cron-hint { font-size: 12px; color: var(--dsw-alias-label-tertiary); margin-top: 4px; }
672
- .dsh-cron-channel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px 12px; }
673
- .dsh-cron-section { border-top: 1px solid var(--dsw-alias-border-l2); margin-top: 16px; padding-top: 12px; }
674
- .dsh-cron-section-title { font-weight: 500; font-size: 12.5px; color: var(--dsw-alias-label-secondary); margin-bottom: 10px; }
675
- .dsh-cron-secret-row { display: flex; align-items: center; gap: 8px; }
676
- .dsh-cron-secret-row input { flex: 1; }
677
- .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; }
678
- .dsh-cron-status-banner { padding: 10px 14px; border-radius: 8px; font-size: 13px; margin-top: 12px; }
679
- .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); }
680
- .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); }
681
- .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); }
682
- @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; } }
683
- .dsh-cron-pulse { animation: dsh-cron-pulse-ring 1.6s infinite; border-color: var(--dsh-cron-success); color: var(--dsh-cron-success); }
684
-
685
- /* #34: sidebar job list */
686
- .dsh-cron-sidebar-jobs { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 6px 0; }
687
- .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; }
688
- .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)); }
689
- .dsh-cron-sidebar-jobs-head::after { content: '▾'; font-size: 10px; transition: transform 0.15s ease; }
690
- .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-head::after { transform: rotate(-90deg); }
691
- .dsh-cron-sidebar-jobs[data-open="false"] .dsh-cron-sidebar-jobs-list { display: none; }
692
- .dsh-cron-sidebar-jobs-list { display: flex; flex-direction: column; gap: 2px; padding-left: 10px; }
693
- .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; }
694
- .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); }
695
- .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; }
696
- .dsh-cron-sidebar-job-meta { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
697
- .dsh-cron-sidebar-empty, .dsh-cron-sidebar-more { font-size: 11px; color: var(--dsw-alias-label-tertiary); padding: 2px 8px; }
698
- .dsh-cron-task-highlight { border-color: var(--dsh-cron-info) !important; box-shadow: 0 0 0 2px var(--dsh-cron-info-bg); }
699
-
700
- /* #39: narrow screens. Only layout changes — no information is hidden. */
701
- @media (max-width: 900px) {
702
- .dsh-cron-container { padding: 8px 12px 24px; gap: 14px; }
703
- .dsh-cron-form-row { grid-template-columns: 1fr; }
704
- .dsh-cron-channel-grid { grid-template-columns: 1fr; }
705
- .dsh-cron-filter-panel { grid-template-columns: 1fr; }
706
- .dsh-cron-modal { padding: 16px; border-radius: 12px; }
707
- .dsh-cron-modal-foot { bottom: -16px; margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; }
708
- .dsh-cron-modal-scroll { max-height: 92vh; }
709
- .dsh-cron-task-item { flex-wrap: wrap; gap: 10px; }
710
- .dsh-cron-task-actions { margin-left: auto; }
711
- .dsh-cron-history-list { max-height: 320px; }
712
- }
713
- @media (max-width: 640px) {
714
- .dsh-cron-container { padding: 6px 8px 20px; }
715
- .dsh-cron-top-bar { flex-wrap: wrap; gap: 8px; }
716
- .dsh-cron-tabs { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
717
- .dsh-cron-tab { white-space: nowrap; }
718
- .dsh-cron-stats-bar { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
719
- .dsh-cron-task-item { align-items: flex-start; padding: 12px; }
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') || 'DSH Cron Automation Digest'),
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') || 'Automation & Cost Digest'
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') || 'Analytics digest') + ' · ' + (periodDays || 7) + 'd'
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') || '7 Days'),
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') || '30 Days'),
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
- React.createElement('div', {
1415
- style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px' }
1416
- },
1417
- 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' } },
1418
- React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, T_KEY(t, 'digest.executions') || 'Executions'),
1419
- React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, digest.totalRuns),
1420
- React.createElement('div', { style: { fontSize: '11px', color: digest.successRate >= 90 ? 'var(--dsh-cron-success, #2a2)' : 'var(--dsh-cron-warning, #fa0)', marginTop: '2px' } }, digest.successRate + '% SR')
1421
- ),
1422
- 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' } },
1423
- React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)' } }, T_KEY(t, 'digest.tokens') || 'Tokens'),
1424
- React.createElement('div', { style: { fontSize: '18px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #fff)', marginTop: '4px' } }, Number(digest.totalTokens || 0).toLocaleString()),
1425
- React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #888)', marginTop: '2px' } }, 'LLM prompt+out')
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') || 'Top Unstable Automations')
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') || 'Top Resource Consumers')
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') || '📋 Copy Markdown'),
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') || '✈️ Send Telegram')
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') || '24-Hour Schedule Timeline') : (T_KEY(t, 'heatmap.modalTitle') || 'Cost & Token Burn Heatmap')
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') || 'Timeline')),
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') || 'Heatmap')),
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') || 'Projected run distribution across 24 hourly buckets:'),
1566
- React.createElement('span', { style: { color: 'var(--dsh-cron-warning, #fa0)' } }, '⚠️ ' + (T_KEY(t, 'timeline.peakWarning') || 'Peak hours exceed max concurrent limit') + ' (' + timeline.maxConcurrent + ')')
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
- ? 'color-mix(in srgb, var(--dsh-cron-danger, #e33) 12%, var(--dsw-alias-bg-layer-2, #111))'
1579
- : (hasRuns ? 'var(--dsw-alias-bg-layer-2, #111)' : 'var(--dsw-alias-bg-base, #111)'),
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
- style: { fontSize: '10px', padding: '1px 5px', borderRadius: '8px', background: isPeak ? 'var(--dsh-cron-danger, #e33)' : 'var(--dsw-alias-bg-layer-3, #222)', color: '#fff' }
1595
- }, b.count) : null
1596
- ),
1597
- React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '3px', marginTop: '6px' } },
1598
- (b.runs || []).slice(0, 3).map((r, rIdx) => React.createElement('div', {
1599
- key: rIdx,
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') || 'Weekly hour-of-day cost concentration (7 days × 24 hours):'),
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') || '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
- (editingTask && editingTask.promptHistory && editingTask.promptHistory.length > 0) ? React.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } },
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' + (editingTask.promptVersion || 1) + ' (' + (editingTask.promptHistory[editingTask.promptHistory.length - 1]?.successRate !== undefined ? Math.round(editingTask.promptHistory[editingTask.promptHistory.length - 1].successRate * 100) : 100) + '% SR)'),
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 = editingTask.promptHistory.find((h) => h.version === targetV);
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
- editingTask.promptHistory.map((h) => React.createElement('option', { key: h.version, value: h.version }, 'v' + h.version + ' (' + Math.round((h.successRate || 1) * 100) + '%)'))
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
- if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
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
- if (!resolvedPath.startsWith(resolvedBase)) {
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.5",
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",