@cronvello/sdk 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/README.md +102 -15
- package/dist/cli.cjs +990 -216
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +990 -216
- package/dist/cli.js.map +1 -1
- package/dist/dev.cjs +1001 -217
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +5 -0
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +1001 -217
- package/dist/dev.js.map +1 -1
- package/dist/{dispatch-handler-BoadpsL9.d.cts → dispatch-handler-yHIFEnNG.d.cts} +17 -5
- package/dist/{dispatch-handler-BoadpsL9.d.ts → dispatch-handler-yHIFEnNG.d.ts} +17 -5
- package/dist/express.d.cts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.cjs +191 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -10
- package/dist/index.d.ts +280 -10
- package/dist/index.js +191 -26
- package/dist/index.js.map +1 -1
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/docs/dashboard.png +0 -0
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -337,7 +337,7 @@ var init_dashboard_assets = __esm({
|
|
|
337
337
|
<meta charset="utf-8" />
|
|
338
338
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
339
339
|
<meta name="robots" content="noindex" />
|
|
340
|
-
<title>Cronvello \u2014 local
|
|
340
|
+
<title>Cronvello \u2014 local engine</title>
|
|
341
341
|
<style>
|
|
342
342
|
:root {
|
|
343
343
|
--indigo: #4F46E5;
|
|
@@ -346,155 +346,415 @@ var init_dashboard_assets = __esm({
|
|
|
346
346
|
--green: #10B981;
|
|
347
347
|
--amber: #F59E0B;
|
|
348
348
|
--red: #EF4444;
|
|
349
|
-
--bg: #
|
|
349
|
+
--bg: #f7f8fa;
|
|
350
350
|
--panel: #ffffff;
|
|
351
351
|
--panel-2: #f1f5f9;
|
|
352
|
-
--
|
|
352
|
+
--panel-3: #e9edf3;
|
|
353
|
+
--border: #e3e8ef;
|
|
354
|
+
--border-strong: #cbd5e1;
|
|
353
355
|
--text: #0f172a;
|
|
354
356
|
--muted: #64748b;
|
|
355
|
-
--
|
|
357
|
+
--faint: #94a3b8;
|
|
358
|
+
--shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 3px rgba(15, 23, 42, 0.06);
|
|
359
|
+
--focus: rgba(79, 70, 229, 0.35);
|
|
356
360
|
}
|
|
357
361
|
[data-theme="dark"] {
|
|
358
|
-
--bg: #
|
|
362
|
+
--bg: #0a0f1c;
|
|
359
363
|
--panel: #111827;
|
|
360
|
-
--panel-2: #
|
|
364
|
+
--panel-2: #161f2f;
|
|
365
|
+
--panel-3: #1c2740;
|
|
361
366
|
--border: #1f2937;
|
|
362
|
-
--
|
|
367
|
+
--border-strong: #334155;
|
|
368
|
+
--text: #e6e9ef;
|
|
363
369
|
--muted: #94a3b8;
|
|
364
|
-
--
|
|
370
|
+
--faint: #64748b;
|
|
371
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.35);
|
|
372
|
+
--focus: rgba(99, 102, 241, 0.45);
|
|
365
373
|
}
|
|
366
374
|
/* Dark-mode-first: follow the OS preference unless the user has explicitly chosen light. */
|
|
367
375
|
@media (prefers-color-scheme: dark) {
|
|
368
376
|
:root:not([data-theme="light"]) {
|
|
369
|
-
--bg: #
|
|
377
|
+
--bg: #0a0f1c;
|
|
370
378
|
--panel: #111827;
|
|
371
|
-
--panel-2: #
|
|
379
|
+
--panel-2: #161f2f;
|
|
380
|
+
--panel-3: #1c2740;
|
|
372
381
|
--border: #1f2937;
|
|
373
|
-
--
|
|
382
|
+
--border-strong: #334155;
|
|
383
|
+
--text: #e6e9ef;
|
|
374
384
|
--muted: #94a3b8;
|
|
375
|
-
--
|
|
385
|
+
--faint: #64748b;
|
|
386
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.35);
|
|
387
|
+
--focus: rgba(99, 102, 241, 0.45);
|
|
376
388
|
}
|
|
377
389
|
}
|
|
390
|
+
|
|
378
391
|
* { box-sizing: border-box; }
|
|
379
392
|
html, body { margin: 0; padding: 0; }
|
|
380
393
|
body {
|
|
381
394
|
background: var(--bg);
|
|
382
395
|
color: var(--text);
|
|
383
396
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, Roboto, sans-serif;
|
|
384
|
-
font-size:
|
|
397
|
+
font-size: 13.5px;
|
|
385
398
|
line-height: 1.5;
|
|
399
|
+
-webkit-font-smoothing: antialiased;
|
|
400
|
+
}
|
|
401
|
+
code, .mono, .num {
|
|
402
|
+
font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
403
|
+
font-variant-numeric: tabular-nums;
|
|
386
404
|
}
|
|
387
|
-
|
|
388
|
-
|
|
405
|
+
.num { font-variant-numeric: tabular-nums; }
|
|
406
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
|
|
407
|
+
|
|
408
|
+
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
409
|
+
header.top {
|
|
389
410
|
display: flex; align-items: center; gap: 12px;
|
|
390
|
-
padding:
|
|
411
|
+
padding: 0 20px; height: 52px;
|
|
391
412
|
border-bottom: 1px solid var(--border);
|
|
392
413
|
background: var(--panel);
|
|
393
|
-
position: sticky; top: 0; z-index:
|
|
414
|
+
position: sticky; top: 0; z-index: 20;
|
|
394
415
|
}
|
|
395
|
-
.
|
|
396
|
-
width:
|
|
416
|
+
.mark {
|
|
417
|
+
width: 26px; height: 26px; border-radius: 7px; flex: none;
|
|
397
418
|
display: grid; place-items: center;
|
|
398
|
-
background: linear-gradient(
|
|
399
|
-
color: #fff;
|
|
419
|
+
background: linear-gradient(140deg, var(--indigo), var(--teal));
|
|
420
|
+
color: #fff;
|
|
400
421
|
}
|
|
401
|
-
.
|
|
402
|
-
.
|
|
422
|
+
.mark svg { width: 15px; height: 15px; }
|
|
423
|
+
.wordmark { font-weight: 650; letter-spacing: -0.012em; font-size: 14px; }
|
|
424
|
+
.wordmark span { color: var(--muted); font-weight: 450; margin-left: 7px; }
|
|
403
425
|
.spacer { flex: 1; }
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
426
|
+
|
|
427
|
+
.chip {
|
|
428
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
429
|
+
height: 26px; padding: 0 10px; border-radius: 7px;
|
|
430
|
+
font-size: 12px; font-weight: 500;
|
|
408
431
|
border: 1px solid var(--border); background: var(--panel-2); color: var(--muted);
|
|
432
|
+
white-space: nowrap;
|
|
433
|
+
}
|
|
434
|
+
.chip .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); flex: none; }
|
|
435
|
+
.chip.live .dot { background: var(--green); animation: pulse 2.4s ease-out infinite; }
|
|
436
|
+
.chip.down { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--border)); }
|
|
437
|
+
.chip.down .dot { background: var(--red); }
|
|
438
|
+
@keyframes pulse {
|
|
439
|
+
0% { box-shadow: 0 0 0 0 rgba(16,185,129,0.45); }
|
|
440
|
+
70% { box-shadow: 0 0 0 5px rgba(16,185,129,0); }
|
|
441
|
+
100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); }
|
|
409
442
|
}
|
|
410
|
-
|
|
411
|
-
.pill.live .dot { background: var(--green); box-shadow: 0 0 0 0 rgba(16,185,129,0.5); animation: pulse 1.8s infinite; }
|
|
412
|
-
.pill.down .dot { background: var(--red); }
|
|
413
|
-
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16,185,129,0.5); } 70% { box-shadow: 0 0 0 6px rgba(16,185,129,0); } 100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); } }
|
|
443
|
+
|
|
414
444
|
button {
|
|
415
445
|
font: inherit; cursor: pointer; color: var(--text);
|
|
416
446
|
background: var(--panel-2); border: 1px solid var(--border);
|
|
417
|
-
border-radius:
|
|
447
|
+
border-radius: 7px; padding: 5px 11px;
|
|
448
|
+
transition: background-color .12s ease, border-color .12s ease;
|
|
418
449
|
}
|
|
419
|
-
button:hover { border-color: var(--
|
|
450
|
+
button:hover:not(:disabled) { background: var(--panel-3); border-color: var(--border-strong); }
|
|
451
|
+
button:disabled { opacity: .55; cursor: default; }
|
|
420
452
|
button.primary { background: var(--indigo); color: #fff; border-color: var(--indigo); }
|
|
421
|
-
button.primary:hover { background: var(--indigo-soft); }
|
|
422
|
-
.
|
|
423
|
-
|
|
424
|
-
.
|
|
425
|
-
.
|
|
453
|
+
button.primary:hover:not(:disabled) { background: var(--indigo-soft); border-color: var(--indigo-soft); }
|
|
454
|
+
button.icon { padding: 5px 7px; line-height: 0; color: var(--muted); }
|
|
455
|
+
button.icon svg { width: 15px; height: 15px; display: block; }
|
|
456
|
+
button.ghost { background: transparent; border-color: transparent; color: var(--muted); }
|
|
457
|
+
button.ghost:hover:not(:disabled) { background: var(--panel-2); border-color: var(--border); }
|
|
458
|
+
button.tiny { font-size: 11.5px; padding: 3px 8px; border-radius: 6px; }
|
|
459
|
+
|
|
460
|
+
main { padding: 16px 20px 8px; display: flex; flex-direction: column; gap: 14px; }
|
|
461
|
+
|
|
462
|
+
/* \u2500\u2500 Stat row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
463
|
+
.stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
|
464
|
+
@media (max-width: 900px) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
465
|
+
.stat {
|
|
466
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
|
|
467
|
+
padding: 11px 13px; box-shadow: var(--shadow); min-width: 0;
|
|
468
|
+
}
|
|
469
|
+
.stat .label {
|
|
470
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
471
|
+
color: var(--faint); font-weight: 600;
|
|
472
|
+
}
|
|
473
|
+
.stat .value {
|
|
474
|
+
margin-top: 3px; font-size: 21px; font-weight: 600; letter-spacing: -0.02em;
|
|
475
|
+
font-variant-numeric: tabular-nums; line-height: 1.2;
|
|
476
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
477
|
+
}
|
|
478
|
+
.stat .sub { font-size: 11.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
479
|
+
.stat.ok .value { color: var(--green); }
|
|
480
|
+
.stat.bad .value { color: var(--red); }
|
|
481
|
+
.stat.bad.zero .value { color: var(--text); }
|
|
482
|
+
|
|
483
|
+
/* \u2500\u2500 Cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
484
|
+
.card {
|
|
485
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 11px;
|
|
486
|
+
box-shadow: var(--shadow); overflow: hidden; min-width: 0;
|
|
487
|
+
}
|
|
488
|
+
.card > .head {
|
|
489
|
+
display: flex; align-items: center; gap: 10px;
|
|
490
|
+
padding: 9px 14px; border-bottom: 1px solid var(--border);
|
|
491
|
+
background: var(--panel);
|
|
492
|
+
}
|
|
493
|
+
.card > .head h2 {
|
|
494
|
+
margin: 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
495
|
+
color: var(--muted); font-weight: 650;
|
|
496
|
+
}
|
|
497
|
+
.card > .head .hint { font-size: 11.5px; color: var(--faint); }
|
|
498
|
+
select {
|
|
499
|
+
font: inherit; font-size: 11.5px; color: var(--muted); cursor: pointer;
|
|
500
|
+
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px; padding: 3px 6px;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* \u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
504
|
+
.tl { display: flex; align-items: stretch; }
|
|
505
|
+
.tl-gutter {
|
|
506
|
+
flex: none; width: 168px; border-right: 1px solid var(--border);
|
|
507
|
+
background: var(--panel); padding-bottom: 8px;
|
|
508
|
+
}
|
|
509
|
+
@media (max-width: 700px) { .tl-gutter { width: 108px; } }
|
|
510
|
+
.tl-gutter .axis-pad { height: 22px; border-bottom: 1px solid var(--border); }
|
|
511
|
+
.tl-gutter .lane-label {
|
|
512
|
+
height: 26px; display: flex; align-items: center; padding: 0 12px;
|
|
513
|
+
font-size: 12px; font-weight: 550;
|
|
514
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
515
|
+
}
|
|
516
|
+
.tl-viewport { position: relative; flex: 1; overflow: hidden; padding-bottom: 8px; min-width: 0; }
|
|
517
|
+
.tl-track { position: relative; width: 100%; will-change: transform; }
|
|
518
|
+
.tl-axis { position: relative; height: 22px; border-bottom: 1px solid var(--border); }
|
|
519
|
+
.tl-axis .gl {
|
|
520
|
+
position: absolute; top: 0; bottom: -1000px; width: 1px;
|
|
521
|
+
background: var(--border); opacity: .75;
|
|
522
|
+
}
|
|
523
|
+
.tl-axis .gt {
|
|
524
|
+
position: absolute; top: 4px; font-size: 10.5px; color: var(--faint);
|
|
525
|
+
transform: translateX(4px); white-space: nowrap; font-variant-numeric: tabular-nums;
|
|
526
|
+
}
|
|
527
|
+
.tl-lane { position: relative; height: 26px; }
|
|
528
|
+
.tl-lane + .tl-lane { border-top: 1px dashed transparent; }
|
|
529
|
+
.tl-lane:hover { background: color-mix(in srgb, var(--panel-2) 60%, transparent); }
|
|
530
|
+
.mk { position: absolute; top: 7px; height: 12px; border-radius: 2px; }
|
|
531
|
+
.mk.run { width: 4px; margin-left: -2px; }
|
|
532
|
+
.mk.success { background: var(--green); }
|
|
533
|
+
.mk.error, .mk.timed_out { background: var(--red); }
|
|
534
|
+
.mk.skipped { background: var(--faint); }
|
|
535
|
+
.mk.retried { background: var(--amber); }
|
|
536
|
+
.mk.up {
|
|
537
|
+
width: 0; height: 12px; margin-left: -1px;
|
|
538
|
+
border-left: 1px dashed var(--border-strong); border-radius: 0; opacity: .5;
|
|
539
|
+
}
|
|
540
|
+
.mk.up.first { border-left-color: var(--indigo-soft); border-left-style: solid; opacity: 1; }
|
|
541
|
+
.lane-note {
|
|
542
|
+
position: absolute; right: 8px; top: 6px;
|
|
543
|
+
font-size: 10.5px; color: var(--faint); white-space: nowrap;
|
|
544
|
+
font-variant-numeric: tabular-nums; pointer-events: none;
|
|
545
|
+
}
|
|
546
|
+
.tl-now {
|
|
547
|
+
position: absolute; left: 50%; top: 0; bottom: 0; width: 1px;
|
|
548
|
+
background: var(--indigo); opacity: .75; pointer-events: none; z-index: 3;
|
|
549
|
+
}
|
|
550
|
+
.tl-now::before {
|
|
551
|
+
content: ""; position: absolute; top: 0; left: -3px;
|
|
552
|
+
border: 3.5px solid transparent; border-top-color: var(--indigo);
|
|
553
|
+
}
|
|
554
|
+
.tl-empty { padding: 22px 16px; color: var(--muted); text-align: center; font-size: 12.5px; }
|
|
555
|
+
.tl-legend { display: flex; gap: 14px; align-items: center; font-size: 11px; color: var(--faint); }
|
|
556
|
+
.tl-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
|
|
557
|
+
@media (max-width: 700px) { .tl-legend { display: none; } }
|
|
558
|
+
|
|
559
|
+
/* \u2500\u2500 Grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
560
|
+
.grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 14px; align-items: start; }
|
|
561
|
+
@media (max-width: 980px) { .grid { grid-template-columns: minmax(0, 1fr); } }
|
|
562
|
+
|
|
563
|
+
/* \u2500\u2500 Jobs table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
426
564
|
table { width: 100%; border-collapse: collapse; }
|
|
427
|
-
th, td { text-align: left; padding:
|
|
428
|
-
th {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
565
|
+
th, td { text-align: left; padding: 9px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
566
|
+
th {
|
|
567
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.055em;
|
|
568
|
+
color: var(--faint); font-weight: 650; padding-top: 8px; padding-bottom: 8px;
|
|
569
|
+
}
|
|
570
|
+
tbody tr:last-child td { border-bottom: none; }
|
|
571
|
+
tbody tr.job { cursor: pointer; }
|
|
572
|
+
tbody tr.job:hover { background: var(--panel-2); }
|
|
573
|
+
tbody tr.job.is-running { background: color-mix(in srgb, var(--indigo) 6%, transparent); }
|
|
432
574
|
.jobkey { font-weight: 600; }
|
|
433
|
-
.jobdesc { color: var(--muted); font-size:
|
|
434
|
-
.
|
|
435
|
-
.
|
|
436
|
-
.
|
|
437
|
-
.
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.
|
|
575
|
+
.jobdesc { color: var(--muted); font-size: 11.5px; margin-top: 1px; }
|
|
576
|
+
td.sched code { font-size: 12px; color: var(--muted); }
|
|
577
|
+
td.next { white-space: nowrap; }
|
|
578
|
+
td.act { text-align: right; width: 1%; white-space: nowrap; }
|
|
579
|
+
tbody tr.job .act button { opacity: 0; transition: opacity .12s ease; }
|
|
580
|
+
tbody tr.job:hover .act button, tbody tr.job .act button:focus-visible { opacity: 1; }
|
|
581
|
+
@media (hover: none) { tbody tr.job .act button { opacity: 1; } }
|
|
582
|
+
|
|
583
|
+
.badge {
|
|
584
|
+
display: inline-block; padding: 1.5px 8px; border-radius: 999px;
|
|
585
|
+
font-size: 10.5px; font-weight: 650; letter-spacing: 0.01em;
|
|
586
|
+
}
|
|
587
|
+
.badge.success { background: color-mix(in srgb, var(--green) 16%, transparent); color: var(--green); }
|
|
588
|
+
.badge.error, .badge.timed_out { background: color-mix(in srgb, var(--red) 16%, transparent); color: var(--red); }
|
|
589
|
+
.badge.skipped { background: color-mix(in srgb, var(--faint) 20%, transparent); color: var(--muted); }
|
|
590
|
+
.badge.running { background: color-mix(in srgb, var(--indigo) 16%, transparent); color: var(--indigo-soft); }
|
|
591
|
+
.badge.none { background: var(--panel-2); color: var(--faint); }
|
|
592
|
+
|
|
593
|
+
/* \u2500\u2500 Feed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
594
|
+
.feed { max-height: 62vh; overflow-y: auto; overscroll-behavior: contain; }
|
|
595
|
+
.ev {
|
|
596
|
+
display: flex; gap: 9px; align-items: baseline;
|
|
597
|
+
padding: 7px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px;
|
|
598
|
+
}
|
|
599
|
+
.ev:last-child { border-bottom: none; }
|
|
600
|
+
.ev .glyph { width: 13px; text-align: center; flex: none; font-size: 11px; }
|
|
601
|
+
.ev .body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
602
|
+
.ev .when { color: var(--faint); font-size: 10.5px; font-variant-numeric: tabular-nums; flex: none; }
|
|
603
|
+
.ev .k { font-weight: 600; }
|
|
446
604
|
.ev.success .glyph { color: var(--green); }
|
|
447
605
|
.ev.error .glyph, .ev.timeout .glyph { color: var(--red); }
|
|
448
606
|
.ev.retry .glyph { color: var(--amber); }
|
|
449
607
|
.ev.fire .glyph { color: var(--indigo-soft); }
|
|
450
|
-
.ev.skipped .glyph, .ev.
|
|
451
|
-
.
|
|
452
|
-
|
|
453
|
-
|
|
608
|
+
.ev.skipped .glyph, .ev.engine .glyph { color: var(--faint); }
|
|
609
|
+
.ev .detail { color: var(--muted); }
|
|
610
|
+
.paused-note {
|
|
611
|
+
padding: 6px 14px; font-size: 11.5px; color: var(--amber);
|
|
612
|
+
background: color-mix(in srgb, var(--amber) 10%, transparent);
|
|
613
|
+
border-bottom: 1px solid var(--border);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.empty { padding: 26px 16px; color: var(--muted); text-align: center; font-size: 12.5px; }
|
|
617
|
+
.empty .lead { color: var(--text); font-weight: 550; margin-bottom: 3px; }
|
|
618
|
+
|
|
619
|
+
footer {
|
|
620
|
+
padding: 14px 20px 30px; color: var(--faint); font-size: 11.5px;
|
|
621
|
+
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
622
|
+
}
|
|
623
|
+
footer .sep { opacity: .5; }
|
|
624
|
+
|
|
625
|
+
/* \u2500\u2500 Drawer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
626
|
+
.overlay {
|
|
627
|
+
position: fixed; inset: 0; background: rgba(2, 6, 23, 0.5);
|
|
628
|
+
display: none; justify-content: flex-end; z-index: 40;
|
|
629
|
+
}
|
|
454
630
|
.overlay.open { display: flex; }
|
|
455
|
-
.drawer {
|
|
456
|
-
|
|
631
|
+
.drawer {
|
|
632
|
+
width: min(560px, 94vw); background: var(--panel); border-left: 1px solid var(--border);
|
|
633
|
+
height: 100%; overflow-y: auto; display: flex; flex-direction: column;
|
|
634
|
+
}
|
|
635
|
+
.drawer .dhead {
|
|
636
|
+
display: flex; align-items: center; gap: 10px; padding: 12px 16px;
|
|
637
|
+
border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); z-index: 2;
|
|
638
|
+
}
|
|
639
|
+
.drawer .dtitle { font-weight: 650; letter-spacing: -0.01em; }
|
|
457
640
|
.drawer .pad { padding: 16px; }
|
|
458
|
-
.kv { display: flex; gap:
|
|
459
|
-
.kv span:first-child { color: var(--muted); min-width:
|
|
460
|
-
.section-title {
|
|
641
|
+
.kv { display: flex; gap: 10px; padding: 5px 0; font-size: 12.5px; }
|
|
642
|
+
.kv > span:first-child { color: var(--muted); min-width: 92px; flex: none; }
|
|
643
|
+
.section-title {
|
|
644
|
+
font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
645
|
+
color: var(--faint); font-weight: 650; margin: 20px 0 7px;
|
|
646
|
+
}
|
|
461
647
|
ul.plain { list-style: none; margin: 0; padding: 0; }
|
|
462
|
-
ul.plain li {
|
|
648
|
+
ul.plain > li {
|
|
649
|
+
padding: 7px 0; border-bottom: 1px solid var(--border);
|
|
650
|
+
display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px;
|
|
651
|
+
}
|
|
652
|
+
ul.plain > li:last-child { border-bottom: none; }
|
|
653
|
+
.runline { display: block; }
|
|
654
|
+
.runline .row1 { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
|
|
655
|
+
.runline .meta { color: var(--faint); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
656
|
+
.runline pre {
|
|
657
|
+
margin: 6px 0 0; padding: 8px 10px; border-radius: 7px;
|
|
658
|
+
background: var(--panel-2); border: 1px solid var(--border);
|
|
659
|
+
font-size: 11.5px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 180px; overflow: auto;
|
|
660
|
+
}
|
|
661
|
+
.runline pre.err { color: var(--red); border-color: color-mix(in srgb, var(--red) 30%, var(--border)); }
|
|
463
662
|
</style>
|
|
464
663
|
</head>
|
|
465
664
|
<body>
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
<div class="
|
|
665
|
+
|
|
666
|
+
<header class="top">
|
|
667
|
+
<div class="mark" aria-hidden="true">
|
|
668
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7.2V12l3.4 2"></path></svg>
|
|
669
|
+
</div>
|
|
670
|
+
<div class="wordmark">Cronvello <span>local engine</span></div>
|
|
671
|
+
<span id="engine-chip" class="chip"><span class="dot"></span><span id="engine-text">starting…</span></span>
|
|
469
672
|
<div class="spacer"></div>
|
|
470
|
-
<span id="conn" class="
|
|
471
|
-
<button id="theme-btn" title="Toggle theme"
|
|
673
|
+
<span id="conn" class="chip"><span class="dot"></span><span id="conn-text">connecting…</span></span>
|
|
674
|
+
<button id="theme-btn" class="icon" title="Toggle light / dark" aria-label="Toggle light or dark theme">
|
|
675
|
+
<svg id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z"></path></svg>
|
|
676
|
+
<svg id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" hidden><circle cx="12" cy="12" r="4"></circle><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4l-1.4 1.4"></path></svg>
|
|
677
|
+
</button>
|
|
472
678
|
</header>
|
|
473
679
|
|
|
474
|
-
<
|
|
475
|
-
<section class="
|
|
476
|
-
<
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
</
|
|
680
|
+
<main>
|
|
681
|
+
<section class="stats" aria-label="Engine summary">
|
|
682
|
+
<div class="stat"><div class="label">Jobs</div><div class="value num" id="s-jobs">—</div><div class="sub" id="s-jobs-sub"> </div></div>
|
|
683
|
+
<div class="stat"><div class="label">Runs this session</div><div class="value num" id="s-runs">—</div><div class="sub" id="s-runs-sub"> </div></div>
|
|
684
|
+
<div class="stat ok"><div class="label">Succeeded</div><div class="value num" id="s-ok">—</div><div class="sub" id="s-ok-sub"> </div></div>
|
|
685
|
+
<div class="stat bad zero"><div class="label">Failed</div><div class="value num" id="s-bad">—</div><div class="sub" id="s-bad-sub"> </div></div>
|
|
686
|
+
<div class="stat"><div class="label">Next fire</div><div class="value num" id="s-next">—</div><div class="sub" id="s-next-sub"> </div></div>
|
|
481
687
|
</section>
|
|
482
688
|
|
|
483
689
|
<section class="card">
|
|
484
|
-
<
|
|
485
|
-
|
|
690
|
+
<div class="head">
|
|
691
|
+
<h2>Timeline</h2>
|
|
692
|
+
<span class="hint" id="tl-hint"></span>
|
|
693
|
+
<div class="spacer"></div>
|
|
694
|
+
<div class="tl-legend" aria-hidden="true">
|
|
695
|
+
<span><i style="background:var(--green)"></i>ok</span>
|
|
696
|
+
<span><i style="background:var(--red)"></i>failed</span>
|
|
697
|
+
<span><i style="background:var(--faint)"></i>skipped</span>
|
|
698
|
+
<span><i style="border-left:2px dashed var(--border-strong);width:2px;border-radius:0"></i>upcoming</span>
|
|
699
|
+
</div>
|
|
700
|
+
<label class="hint" for="tl-span">Window</label>
|
|
701
|
+
<select id="tl-span" aria-label="Timeline window">
|
|
702
|
+
<option value="120000">2 min</option>
|
|
703
|
+
<option value="600000" selected>10 min</option>
|
|
704
|
+
<option value="3600000">1 hour</option>
|
|
705
|
+
<option value="21600000">6 hours</option>
|
|
706
|
+
</select>
|
|
707
|
+
</div>
|
|
708
|
+
<div class="tl" id="tl">
|
|
709
|
+
<div class="tl-gutter" id="tl-gutter"><div class="axis-pad"></div></div>
|
|
710
|
+
<div class="tl-viewport" id="tl-viewport">
|
|
711
|
+
<div class="tl-track" id="tl-track"><div class="tl-axis" id="tl-axis"></div></div>
|
|
712
|
+
<div class="tl-now" aria-hidden="true"></div>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="tl-empty" id="tl-empty" hidden>No jobs to plot.</div>
|
|
486
716
|
</section>
|
|
487
|
-
</div>
|
|
488
717
|
|
|
489
|
-
<
|
|
718
|
+
<div class="grid">
|
|
719
|
+
<section class="card">
|
|
720
|
+
<div class="head"><h2>Jobs</h2><span class="hint" id="jobs-hint"></span></div>
|
|
721
|
+
<table>
|
|
722
|
+
<thead><tr><th>Job</th><th>Schedule</th><th>Zone</th><th>Next fire</th><th>Last run</th><th></th></tr></thead>
|
|
723
|
+
<tbody id="jobs"><tr><td colspan="6" class="empty">Loading…</td></tr></tbody>
|
|
724
|
+
</table>
|
|
725
|
+
</section>
|
|
726
|
+
|
|
727
|
+
<section class="card">
|
|
728
|
+
<div class="head">
|
|
729
|
+
<h2>Live feed</h2>
|
|
730
|
+
<div class="spacer"></div>
|
|
731
|
+
<button id="feed-pause" class="tiny ghost" aria-pressed="false">Pause</button>
|
|
732
|
+
<button id="feed-clear" class="tiny ghost">Clear</button>
|
|
733
|
+
</div>
|
|
734
|
+
<div id="feed-paused" class="paused-note" hidden></div>
|
|
735
|
+
<div id="feed" class="feed" role="log" aria-label="Live run feed">
|
|
736
|
+
<div class="empty"><div class="lead">Waiting for the first run.</div><div id="feed-hint">Every fire shows up here as it happens.</div></div>
|
|
737
|
+
</div>
|
|
738
|
+
</section>
|
|
739
|
+
</div>
|
|
740
|
+
</main>
|
|
741
|
+
|
|
742
|
+
<footer>
|
|
743
|
+
<span>Local engine</span><span class="sep">·</span>
|
|
744
|
+
<span>no account</span><span class="sep">·</span>
|
|
745
|
+
<span>no cloud</span><span class="sep">·</span>
|
|
746
|
+
<span>no outbound network</span><span class="sep">·</span>
|
|
747
|
+
<span>this process is the only source of truth</span>
|
|
748
|
+
</footer>
|
|
490
749
|
|
|
491
|
-
<div id="overlay" class="overlay">
|
|
750
|
+
<div id="overlay" class="overlay" role="dialog" aria-modal="true" aria-labelledby="drawer-title" hidden>
|
|
492
751
|
<div class="drawer">
|
|
493
|
-
<
|
|
494
|
-
<div class="
|
|
752
|
+
<div class="dhead">
|
|
753
|
+
<div class="dtitle" id="drawer-title">Job</div>
|
|
495
754
|
<div class="spacer"></div>
|
|
496
|
-
<button id="drawer-
|
|
497
|
-
|
|
755
|
+
<button id="drawer-run" class="primary tiny">Run now</button>
|
|
756
|
+
<button id="drawer-close" class="tiny">Close</button>
|
|
757
|
+
</div>
|
|
498
758
|
<div class="pad" id="drawer-body"></div>
|
|
499
759
|
</div>
|
|
500
760
|
</div>
|
|
@@ -502,40 +762,64 @@ var init_dashboard_assets = __esm({
|
|
|
502
762
|
<script>
|
|
503
763
|
(function () {
|
|
504
764
|
"use strict";
|
|
505
|
-
|
|
765
|
+
|
|
766
|
+
var MAX_FEED = 250;
|
|
767
|
+
var REFRESH_MS = 10000; // reconciliation poll behind the event stream
|
|
768
|
+
var DEBOUNCE_MS = 400; // collapse a burst of events into one refetch
|
|
769
|
+
var UPCOMING = 40; // fire times requested per job
|
|
770
|
+
|
|
771
|
+
var state = { engine: {}, jobs: [], runs: [] };
|
|
506
772
|
var jobsByKey = {};
|
|
773
|
+
var rows = {}; // key -> { tr, next, last, running }
|
|
774
|
+
var lanes = {}; // key -> { lane, label }
|
|
775
|
+
var laneOrder = [];
|
|
776
|
+
var anchorNow = Date.now();
|
|
777
|
+
var spanMs = 600000;
|
|
778
|
+
var openKey = null;
|
|
779
|
+
var lastFocus = null;
|
|
780
|
+
var paused = false;
|
|
781
|
+
var pausedCount = 0;
|
|
782
|
+
var refreshTimer = null;
|
|
507
783
|
|
|
508
784
|
function el(id) { return document.getElementById(id); }
|
|
509
|
-
function
|
|
510
|
-
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
511
|
-
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
function pad(n) { return n < 10 ? "0" + n : "" + n; }
|
|
785
|
+
function pad2(n) { return n < 10 ? "0" + n : "" + n; }
|
|
515
786
|
function clockTime(ms) {
|
|
516
787
|
var d = new Date(ms);
|
|
517
|
-
return
|
|
788
|
+
return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
|
|
518
789
|
}
|
|
519
790
|
function fmtDuration(ms) {
|
|
520
791
|
if (ms == null) return "";
|
|
521
792
|
if (ms < 1000) return ms + "ms";
|
|
522
|
-
return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + "s";
|
|
793
|
+
if (ms < 60000) return (ms / 1000).toFixed(ms < 10000 ? 2 : 1) + "s";
|
|
794
|
+
var m = Math.floor(ms / 60000);
|
|
795
|
+
return m + "m " + Math.round((ms % 60000) / 1000) + "s";
|
|
523
796
|
}
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
var diff = new Date(iso).getTime() - Date.now();
|
|
527
|
-
if (diff <= 0) return "due now";
|
|
528
|
-
var s = Math.floor(diff / 1000);
|
|
797
|
+
function fmtElapsed(ms) {
|
|
798
|
+
var s = Math.max(0, Math.floor(ms / 1000));
|
|
529
799
|
var d = Math.floor(s / 86400); s -= d * 86400;
|
|
530
800
|
var h = Math.floor(s / 3600); s -= h * 3600;
|
|
531
801
|
var m = Math.floor(s / 60); s -= m * 60;
|
|
532
|
-
|
|
533
|
-
if (
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
802
|
+
if (d) return d + "d " + h + "h";
|
|
803
|
+
if (h) return h + "h " + m + "m";
|
|
804
|
+
if (m) return m + "m " + s + "s";
|
|
805
|
+
return s + "s";
|
|
806
|
+
}
|
|
807
|
+
/** A round duration, for labels rather than countdowns: "5m", not "5m 0s". */
|
|
808
|
+
function fmtSpan(ms) {
|
|
809
|
+
if (ms % 3600000 === 0) return (ms / 3600000) + "h";
|
|
810
|
+
if (ms % 60000 === 0) return (ms / 60000) + "m";
|
|
811
|
+
return Math.round(ms / 1000) + "s";
|
|
812
|
+
}
|
|
813
|
+
function countdown(iso) {
|
|
814
|
+
if (!iso) return "\\u2014";
|
|
815
|
+
var diff = new Date(iso).getTime() - Date.now();
|
|
816
|
+
if (diff <= 0) return "due now";
|
|
817
|
+
return "in " + fmtElapsed(diff);
|
|
818
|
+
}
|
|
819
|
+
function localTime(iso) {
|
|
820
|
+
try { return new Date(iso).toLocaleString(); } catch (e) { return String(iso); }
|
|
538
821
|
}
|
|
822
|
+
function setText(node, text) { if (node && node.textContent !== text) node.textContent = text; }
|
|
539
823
|
|
|
540
824
|
// \u2500\u2500 Theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
541
825
|
function applyTheme(t) {
|
|
@@ -547,174 +831,626 @@ var init_dashboard_assets = __esm({
|
|
|
547
831
|
if (t) return t;
|
|
548
832
|
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
549
833
|
}
|
|
834
|
+
// The icon offers the theme you would switch *to*, which is the only reading that stays true
|
|
835
|
+
// whether the current theme came from the OS or from a previous click.
|
|
836
|
+
function paintThemeIcon() {
|
|
837
|
+
var dark = resolvedTheme() === "dark";
|
|
838
|
+
el("icon-sun").hidden = !dark;
|
|
839
|
+
el("icon-moon").hidden = dark;
|
|
840
|
+
el("theme-btn").title = dark ? "Switch to light" : "Switch to dark";
|
|
841
|
+
}
|
|
550
842
|
try { applyTheme(localStorage.getItem("cronvello-theme")); } catch (e) {}
|
|
843
|
+
paintThemeIcon();
|
|
551
844
|
el("theme-btn").addEventListener("click", function () {
|
|
552
845
|
var next = resolvedTheme() === "dark" ? "light" : "dark";
|
|
553
846
|
applyTheme(next);
|
|
847
|
+
paintThemeIcon();
|
|
554
848
|
try { localStorage.setItem("cronvello-theme", next); } catch (e) {}
|
|
555
849
|
});
|
|
556
850
|
|
|
851
|
+
// \u2500\u2500 Badges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
852
|
+
function badge(status) {
|
|
853
|
+
var span = document.createElement("span");
|
|
854
|
+
span.className = "badge " + (status || "none");
|
|
855
|
+
span.textContent = status || "\\u2014";
|
|
856
|
+
return span;
|
|
857
|
+
}
|
|
858
|
+
function replaceChild1(cell, node) {
|
|
859
|
+
cell.textContent = "";
|
|
860
|
+
cell.appendChild(node);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// \u2500\u2500 Stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
864
|
+
function renderStats() {
|
|
865
|
+
var jobs = state.jobs, runs = state.runs;
|
|
866
|
+
var ok = 0, bad = 0, skipped = 0, retried = 0;
|
|
867
|
+
for (var i = 0; i < runs.length; i++) {
|
|
868
|
+
var s = runs[i].status;
|
|
869
|
+
if (s === "success") { ok++; if (runs[i].attempts > 1) retried++; }
|
|
870
|
+
else if (s === "skipped") skipped++;
|
|
871
|
+
else bad++;
|
|
872
|
+
}
|
|
873
|
+
var active = 0;
|
|
874
|
+
for (var j = 0; j < jobs.length; j++) if (jobs[j].running) active++;
|
|
875
|
+
|
|
876
|
+
setText(el("s-jobs"), String(jobs.length));
|
|
877
|
+
setText(el("s-jobs-sub"), active ? active + " running now" : "idle");
|
|
878
|
+
|
|
879
|
+
setText(el("s-runs"), String(runs.length));
|
|
880
|
+
var upSince = state.engine.startedAt ? "up " + fmtElapsed(Date.now() - state.engine.startedAt) : "not started";
|
|
881
|
+
setText(el("s-runs-sub"), upSince);
|
|
882
|
+
|
|
883
|
+
setText(el("s-ok"), String(ok));
|
|
884
|
+
setText(el("s-ok-sub"), retried ? retried + " after a retry" : (skipped ? skipped + " skipped (overlap)" : "\\u00a0"));
|
|
885
|
+
|
|
886
|
+
setText(el("s-bad"), String(bad));
|
|
887
|
+
el("s-bad").parentNode.className = "stat bad" + (bad ? "" : " zero");
|
|
888
|
+
setText(el("s-bad-sub"), bad ? "check the feed" : "nothing failed");
|
|
889
|
+
|
|
890
|
+
// Soonest upcoming fire across all jobs.
|
|
891
|
+
var soonest = null, soonestKey = "";
|
|
892
|
+
for (var k = 0; k < jobs.length; k++) {
|
|
893
|
+
if (!jobs[k].nextFire) continue;
|
|
894
|
+
var t = new Date(jobs[k].nextFire).getTime();
|
|
895
|
+
if (soonest === null || t < soonest) { soonest = t; soonestKey = jobs[k].key; }
|
|
896
|
+
}
|
|
897
|
+
if (soonest === null) {
|
|
898
|
+
setText(el("s-next"), "\\u2014");
|
|
899
|
+
setText(el("s-next-sub"), jobs.length ? "nothing scheduled" : "\\u00a0");
|
|
900
|
+
} else {
|
|
901
|
+
setText(el("s-next"), countdown(new Date(soonest).toISOString()).replace(/^in /, ""));
|
|
902
|
+
setText(el("s-next-sub"), soonestKey);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
557
906
|
// \u2500\u2500 Jobs table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
function
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
907
|
+
// "No jobs" and "not loaded yet" both leave the row map empty, so this flag is what separates
|
|
908
|
+
// them. Without it the first snapshot of an empty registry would sit on "Loading" forever.
|
|
909
|
+
var built = false;
|
|
910
|
+
|
|
911
|
+
function sameKeys(jobs) {
|
|
912
|
+
if (!built) return false;
|
|
913
|
+
var keys = Object.keys(rows);
|
|
914
|
+
if (keys.length !== jobs.length) return false;
|
|
915
|
+
for (var i = 0; i < jobs.length; i++) if (!rows[jobs[i].key]) return false;
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function buildRows(jobs) {
|
|
920
|
+
var tbody = el("jobs");
|
|
921
|
+
tbody.textContent = "";
|
|
922
|
+
rows = {};
|
|
923
|
+
if (!jobs.length) {
|
|
924
|
+
var tr0 = document.createElement("tr");
|
|
925
|
+
var td0 = document.createElement("td");
|
|
926
|
+
td0.colSpan = 6; td0.className = "empty";
|
|
927
|
+
td0.innerHTML = '<div class="lead">No jobs registered.</div>Add one to <code>jobs</code> in your config and restart.';
|
|
928
|
+
tr0.appendChild(td0); tbody.appendChild(tr0);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
566
931
|
for (var i = 0; i < jobs.length; i++) {
|
|
567
932
|
var j = jobs[i];
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
933
|
+
var tr = document.createElement("tr");
|
|
934
|
+
tr.className = "job";
|
|
935
|
+
tr.tabIndex = 0;
|
|
936
|
+
tr.setAttribute("role", "button");
|
|
937
|
+
tr.setAttribute("aria-label", "Details for " + j.key);
|
|
938
|
+
tr.dataset.key = j.key;
|
|
939
|
+
|
|
940
|
+
var tdName = document.createElement("td");
|
|
941
|
+
var name = document.createElement("div");
|
|
942
|
+
name.className = "jobkey"; name.textContent = j.key;
|
|
943
|
+
tdName.appendChild(name);
|
|
944
|
+
if (j.description) {
|
|
945
|
+
var desc = document.createElement("div");
|
|
946
|
+
desc.className = "jobdesc"; desc.textContent = j.description;
|
|
947
|
+
tdName.appendChild(desc);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
var tdSched = document.createElement("td");
|
|
951
|
+
tdSched.className = "sched";
|
|
952
|
+
var code = document.createElement("code"); code.textContent = j.schedule;
|
|
953
|
+
tdSched.appendChild(code);
|
|
954
|
+
|
|
955
|
+
var tdZone = document.createElement("td");
|
|
956
|
+
tdZone.style.color = "var(--muted)";
|
|
957
|
+
tdZone.textContent = j.timeZone;
|
|
958
|
+
|
|
959
|
+
var tdNext = document.createElement("td");
|
|
960
|
+
tdNext.className = "next num";
|
|
961
|
+
|
|
962
|
+
var tdLast = document.createElement("td");
|
|
963
|
+
|
|
964
|
+
var tdAct = document.createElement("td");
|
|
965
|
+
tdAct.className = "act";
|
|
966
|
+
var btn = document.createElement("button");
|
|
967
|
+
btn.className = "tiny"; btn.textContent = "Run";
|
|
968
|
+
btn.setAttribute("aria-label", "Run " + j.key + " now");
|
|
969
|
+
btn.dataset.key = j.key;
|
|
970
|
+
tdAct.appendChild(btn);
|
|
971
|
+
|
|
972
|
+
tr.appendChild(tdName); tr.appendChild(tdSched); tr.appendChild(tdZone);
|
|
973
|
+
tr.appendChild(tdNext); tr.appendChild(tdLast); tr.appendChild(tdAct);
|
|
974
|
+
tbody.appendChild(tr);
|
|
975
|
+
|
|
976
|
+
rows[j.key] = { tr: tr, next: tdNext, last: tdLast, btn: btn };
|
|
578
977
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function paintRows(jobs) {
|
|
981
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
982
|
+
var j = jobs[i], r = rows[j.key];
|
|
983
|
+
if (!r) continue;
|
|
984
|
+
r.tr.classList.toggle("is-running", !!j.running);
|
|
985
|
+
r.next.dataset.next = j.nextFire || "";
|
|
986
|
+
setText(r.next, j.nextFire ? countdown(j.nextFire) : "\\u2014");
|
|
987
|
+
var status = j.running ? "running" : (j.lastRun ? j.lastRun.status : "");
|
|
988
|
+
if (r.lastStatus !== status) {
|
|
989
|
+
r.lastStatus = status;
|
|
990
|
+
replaceChild1(r.last, badge(status));
|
|
991
|
+
}
|
|
583
992
|
}
|
|
584
993
|
}
|
|
994
|
+
|
|
585
995
|
function tickCountdowns() {
|
|
586
|
-
var
|
|
587
|
-
|
|
588
|
-
var
|
|
589
|
-
|
|
996
|
+
for (var key in rows) {
|
|
997
|
+
if (!Object.prototype.hasOwnProperty.call(rows, key)) continue;
|
|
998
|
+
var cell = rows[key].next;
|
|
999
|
+
var iso = cell.dataset.next;
|
|
1000
|
+
setText(cell, iso ? countdown(iso) : "\\u2014");
|
|
1001
|
+
}
|
|
1002
|
+
if (state.engine.startedAt) {
|
|
1003
|
+
setText(el("s-runs-sub"), "up " + fmtElapsed(Date.now() - state.engine.startedAt));
|
|
1004
|
+
}
|
|
1005
|
+
var jobs = state.jobs, soonest = null, soonestKey = "";
|
|
1006
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
1007
|
+
if (!jobs[i].nextFire) continue;
|
|
1008
|
+
var t = new Date(jobs[i].nextFire).getTime();
|
|
1009
|
+
if (soonest === null || t < soonest) { soonest = t; soonestKey = jobs[i].key; }
|
|
1010
|
+
}
|
|
1011
|
+
if (soonest !== null) {
|
|
1012
|
+
setText(el("s-next"), countdown(new Date(soonest).toISOString()).replace(/^in /, ""));
|
|
1013
|
+
setText(el("s-next-sub"), soonestKey);
|
|
590
1014
|
}
|
|
591
1015
|
}
|
|
592
1016
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1017
|
+
// \u2500\u2500 Timeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1018
|
+
//
|
|
1019
|
+
// The track covers a fixed absolute window [anchorNow - span/2, anchorNow + span/2] and every mark
|
|
1020
|
+
// is positioned once, as a percentage of that window. The passing second only shifts the whole
|
|
1021
|
+
// track, so an idle dashboard writes one transform per frame-ish instead of touching every mark.
|
|
1022
|
+
function buildLanes(jobs) {
|
|
1023
|
+
var gutter = el("tl-gutter"), track = el("tl-track");
|
|
1024
|
+
gutter.textContent = "";
|
|
1025
|
+
var pad = document.createElement("div"); pad.className = "axis-pad";
|
|
1026
|
+
gutter.appendChild(pad);
|
|
1027
|
+
|
|
1028
|
+
// Keep the axis, drop the lanes.
|
|
1029
|
+
var axis = el("tl-axis");
|
|
1030
|
+
track.textContent = "";
|
|
1031
|
+
track.appendChild(axis);
|
|
1032
|
+
|
|
1033
|
+
lanes = {}; laneOrder = [];
|
|
1034
|
+
for (var i = 0; i < jobs.length; i++) {
|
|
1035
|
+
var key = jobs[i].key;
|
|
1036
|
+
var label = document.createElement("div");
|
|
1037
|
+
label.className = "lane-label"; label.textContent = key; label.title = key;
|
|
1038
|
+
gutter.appendChild(label);
|
|
1039
|
+
|
|
1040
|
+
var lane = document.createElement("div");
|
|
1041
|
+
lane.className = "tl-lane"; lane.dataset.key = key;
|
|
1042
|
+
track.appendChild(lane);
|
|
1043
|
+
|
|
1044
|
+
lanes[key] = { lane: lane, label: label };
|
|
1045
|
+
laneOrder.push(key);
|
|
1046
|
+
}
|
|
1047
|
+
el("tl").hidden = jobs.length === 0;
|
|
1048
|
+
el("tl-empty").hidden = jobs.length !== 0;
|
|
606
1049
|
}
|
|
607
1050
|
|
|
1051
|
+
function axisStep(span) {
|
|
1052
|
+
var steps = [1000, 5000, 15000, 30000, 60000, 300000, 900000, 1800000, 3600000, 10800000, 21600000];
|
|
1053
|
+
for (var i = 0; i < steps.length; i++) if (span / steps[i] <= 10) return steps[i];
|
|
1054
|
+
return steps[steps.length - 1];
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function paintTimeline() {
|
|
1058
|
+
var jobs = state.jobs;
|
|
1059
|
+
anchorNow = Date.now();
|
|
1060
|
+
var start = anchorNow - spanMs / 2;
|
|
1061
|
+
var pct = function (t) { return ((t - start) / spanMs) * 100; };
|
|
1062
|
+
el("tl-track").style.transform = "translateX(0%)";
|
|
1063
|
+
|
|
1064
|
+
// Axis gridlines.
|
|
1065
|
+
var axis = el("tl-axis");
|
|
1066
|
+
axis.textContent = "";
|
|
1067
|
+
var step = axisStep(spanMs);
|
|
1068
|
+
var first = Math.ceil(start / step) * step;
|
|
1069
|
+
for (var t = first; t < start + spanMs; t += step) {
|
|
1070
|
+
var p = pct(t);
|
|
1071
|
+
var gl = document.createElement("div");
|
|
1072
|
+
gl.className = "gl"; gl.style.left = p + "%";
|
|
1073
|
+
axis.appendChild(gl);
|
|
1074
|
+
var gt = document.createElement("div");
|
|
1075
|
+
gt.className = "gt"; gt.style.left = p + "%"; gt.textContent = clockTime(t);
|
|
1076
|
+
axis.appendChild(gt);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Marks per lane: past runs, then upcoming fires.
|
|
1080
|
+
var runsByKey = {};
|
|
1081
|
+
for (var i = 0; i < state.runs.length; i++) {
|
|
1082
|
+
var run = state.runs[i];
|
|
1083
|
+
(runsByKey[run.key] || (runsByKey[run.key] = [])).push(run);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
for (var l = 0; l < laneOrder.length; l++) {
|
|
1087
|
+
var key = laneOrder[l];
|
|
1088
|
+
var lane = lanes[key].lane;
|
|
1089
|
+
lane.textContent = "";
|
|
1090
|
+
|
|
1091
|
+
var list = runsByKey[key] || [];
|
|
1092
|
+
for (var r = 0; r < list.length; r++) {
|
|
1093
|
+
var rec = list[r];
|
|
1094
|
+
if (rec.startedAt < start || rec.startedAt > start + spanMs) continue;
|
|
1095
|
+
var mk = document.createElement("div");
|
|
1096
|
+
mk.className = "mk run " + rec.status + (rec.status === "success" && rec.attempts > 1 ? " retried" : "");
|
|
1097
|
+
mk.style.left = pct(rec.startedAt) + "%";
|
|
1098
|
+
mk.title = key + " \\u00b7 " + rec.status + " \\u00b7 " + clockTime(rec.startedAt) +
|
|
1099
|
+
(rec.durationMs ? " \\u00b7 " + fmtDuration(rec.durationMs) : "") +
|
|
1100
|
+
(rec.attempts > 1 ? " \\u00b7 " + rec.attempts + " attempts" : "");
|
|
1101
|
+
lane.appendChild(mk);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
var ups = jobsByKey[key] && jobsByKey[key].upcoming ? jobsByKey[key].upcoming : [];
|
|
1105
|
+
var plotted = 0;
|
|
1106
|
+
for (var u = 0; u < ups.length; u++) {
|
|
1107
|
+
var at = new Date(ups[u]).getTime();
|
|
1108
|
+
if (at > start + spanMs) break;
|
|
1109
|
+
if (at < anchorNow) continue;
|
|
1110
|
+
var um = document.createElement("div");
|
|
1111
|
+
um.className = "mk up" + (plotted === 0 ? " first" : "");
|
|
1112
|
+
um.style.left = pct(at) + "%";
|
|
1113
|
+
um.title = key + " \\u00b7 next fire " + clockTime(at);
|
|
1114
|
+
lane.appendChild(um);
|
|
1115
|
+
plotted++;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// A lane whose whole schedule sits outside the window would otherwise read as "nothing here",
|
|
1119
|
+
// which is indistinguishable from a broken job. Say when it actually fires.
|
|
1120
|
+
if (!lane.childNodes.length) {
|
|
1121
|
+
var note = document.createElement("div");
|
|
1122
|
+
note.className = "lane-note";
|
|
1123
|
+
var job = jobsByKey[key];
|
|
1124
|
+
note.textContent = job && job.nextFire ? "next " + countdown(job.nextFire) : "not scheduled";
|
|
1125
|
+
lane.appendChild(note);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
el("tl-hint").textContent = fmtSpan(spanMs / 2) + " back, " + fmtSpan(spanMs / 2) + " ahead";
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function slideTimeline() {
|
|
1133
|
+
var drift = Date.now() - anchorNow;
|
|
1134
|
+
// Re-anchor rather than sliding forever, so marks that left the window get dropped.
|
|
1135
|
+
if (Math.abs(drift) > spanMs / 4) { paintTimeline(); return; }
|
|
1136
|
+
el("tl-track").style.transform = "translateX(" + (-(drift / spanMs) * 100) + "%)";
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
el("tl-span").addEventListener("change", function () {
|
|
1140
|
+
spanMs = Number(this.value) || 600000;
|
|
1141
|
+
paintTimeline();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
608
1144
|
// \u2500\u2500 Live feed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
609
|
-
var glyphs = {
|
|
1145
|
+
var glyphs = {
|
|
1146
|
+
fire: "\\u25b8", success: "\\u2713", error: "\\u2717", timeout: "\\u29d6",
|
|
1147
|
+
retry: "\\u21ba", skipped: "\\u2298", scheduled: "\\u00b7",
|
|
1148
|
+
"engine-start": "\\u25cf", "engine-stop": "\\u25a0"
|
|
1149
|
+
};
|
|
1150
|
+
|
|
610
1151
|
function describe(ev) {
|
|
1152
|
+
var frag = document.createDocumentFragment();
|
|
1153
|
+
function k(text) { var s = document.createElement("span"); s.className = "k"; s.textContent = text; return s; }
|
|
1154
|
+
function plain(text) { return document.createTextNode(text); }
|
|
1155
|
+
function dim(text) { var s = document.createElement("span"); s.className = "detail"; s.textContent = text; return s; }
|
|
1156
|
+
|
|
611
1157
|
switch (ev.type) {
|
|
612
|
-
case "fire":
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
case "
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
case "
|
|
621
|
-
|
|
1158
|
+
case "fire":
|
|
1159
|
+
frag.appendChild(k(ev.key)); frag.appendChild(plain(" fired"));
|
|
1160
|
+
if (ev.attempt > 1) frag.appendChild(dim(" (attempt " + ev.attempt + ")"));
|
|
1161
|
+
break;
|
|
1162
|
+
case "success":
|
|
1163
|
+
frag.appendChild(k(ev.key)); frag.appendChild(plain(" succeeded "));
|
|
1164
|
+
frag.appendChild(dim("in " + fmtDuration(ev.durationMs) + (ev.attempts > 1 ? ", after " + ev.attempts + " attempts" : "")));
|
|
1165
|
+
break;
|
|
1166
|
+
case "error":
|
|
1167
|
+
frag.appendChild(k(ev.key)); frag.appendChild(plain(" errored: " + ev.error));
|
|
1168
|
+
frag.appendChild(dim(ev.willRetry ? " (retrying)" : " (gave up)"));
|
|
1169
|
+
break;
|
|
1170
|
+
case "timeout":
|
|
1171
|
+
frag.appendChild(k(ev.key)); frag.appendChild(plain(" timed out "));
|
|
1172
|
+
frag.appendChild(dim("after " + fmtDuration(ev.durationMs) + (ev.willRetry ? ", retrying" : ", gave up")));
|
|
1173
|
+
break;
|
|
1174
|
+
case "retry":
|
|
1175
|
+
frag.appendChild(k(ev.key));
|
|
1176
|
+
frag.appendChild(dim(" retrying in " + fmtDuration(ev.delayMs) + " (attempt " + ev.attempt + ")"));
|
|
1177
|
+
break;
|
|
1178
|
+
case "skipped":
|
|
1179
|
+
frag.appendChild(k(ev.key)); frag.appendChild(dim(" skipped, previous run still in flight"));
|
|
1180
|
+
break;
|
|
1181
|
+
case "scheduled":
|
|
1182
|
+
// This event's timestamp is the *next fire*, not the moment it was emitted. Saying so keeps
|
|
1183
|
+
// the feed's right-hand clock strictly the arrival time and never looks out of order.
|
|
1184
|
+
frag.appendChild(k(ev.key)); frag.appendChild(dim(" scheduled for " + clockTime(ev.at)));
|
|
1185
|
+
break;
|
|
1186
|
+
case "engine-start":
|
|
1187
|
+
frag.appendChild(plain("Engine started with " + ev.jobs + " job" + (ev.jobs === 1 ? "" : "s")));
|
|
1188
|
+
break;
|
|
1189
|
+
case "engine-stop":
|
|
1190
|
+
frag.appendChild(plain("Engine stopped"));
|
|
1191
|
+
break;
|
|
1192
|
+
default:
|
|
1193
|
+
frag.appendChild(plain(ev.type));
|
|
622
1194
|
}
|
|
1195
|
+
return frag;
|
|
623
1196
|
}
|
|
1197
|
+
|
|
624
1198
|
function feedClass(type) {
|
|
625
1199
|
if (type === "engine-start" || type === "engine-stop" || type === "scheduled") return "engine";
|
|
626
1200
|
return type;
|
|
627
1201
|
}
|
|
1202
|
+
|
|
628
1203
|
function pushEvent(ev) {
|
|
629
1204
|
var feed = el("feed");
|
|
630
|
-
|
|
1205
|
+
var firstChild = feed.firstChild;
|
|
1206
|
+
if (firstChild && firstChild.className === "empty") feed.textContent = "";
|
|
1207
|
+
|
|
631
1208
|
var div = document.createElement("div");
|
|
632
1209
|
div.className = "ev " + feedClass(ev.type);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1210
|
+
|
|
1211
|
+
var g = document.createElement("div");
|
|
1212
|
+
g.className = "glyph"; g.textContent = glyphs[ev.type] || "\\u2022";
|
|
1213
|
+
var b = document.createElement("div");
|
|
1214
|
+
b.className = "body"; b.appendChild(describe(ev));
|
|
1215
|
+
var w = document.createElement("div");
|
|
1216
|
+
w.className = "when";
|
|
1217
|
+
w.textContent = clockTime(ev.type === "scheduled" ? Date.now() : (ev.at || Date.now()));
|
|
1218
|
+
|
|
1219
|
+
div.appendChild(g); div.appendChild(b); div.appendChild(w);
|
|
637
1220
|
feed.insertBefore(div, feed.firstChild);
|
|
638
1221
|
while (feed.childNodes.length > MAX_FEED) feed.removeChild(feed.lastChild);
|
|
639
|
-
|
|
640
|
-
// A terminal event changes a job's last status / running flag \u2014 refresh the table.
|
|
641
|
-
if (ev.type === "success" || ev.type === "error" || ev.type === "timeout" || ev.type === "skipped" || ev.type === "scheduled") {
|
|
642
|
-
loadJobs();
|
|
643
|
-
}
|
|
644
1222
|
}
|
|
645
1223
|
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1224
|
+
el("feed-pause").addEventListener("click", function () {
|
|
1225
|
+
paused = !paused;
|
|
1226
|
+
this.textContent = paused ? "Resume" : "Pause";
|
|
1227
|
+
this.setAttribute("aria-pressed", paused ? "true" : "false");
|
|
1228
|
+
if (!paused) { pausedCount = 0; el("feed-paused").hidden = true; }
|
|
1229
|
+
});
|
|
1230
|
+
el("feed-clear").addEventListener("click", function () {
|
|
1231
|
+
el("feed").textContent = "";
|
|
1232
|
+
var d = document.createElement("div");
|
|
1233
|
+
d.className = "empty"; d.textContent = "Cleared. New events appear here.";
|
|
1234
|
+
el("feed").appendChild(d);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// \u2500\u2500 Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1238
|
+
function setConn(stateName) {
|
|
1239
|
+
var chip = el("conn");
|
|
1240
|
+
chip.className = "chip " + stateName;
|
|
1241
|
+
setText(el("conn-text"), stateName === "live" ? "live" : (stateName === "down" ? "reconnecting" : "connecting\\u2026"));
|
|
1242
|
+
}
|
|
1243
|
+
function setEngineChip() {
|
|
1244
|
+
var chip = el("engine-chip");
|
|
1245
|
+
var running = state.engine.running;
|
|
1246
|
+
chip.className = "chip" + (running ? " live" : "");
|
|
1247
|
+
var n = state.jobs.length;
|
|
1248
|
+
setText(el("engine-text"), (running ? "running" : "stopped") + " \\u00b7 " + n + " job" + (n === 1 ? "" : "s"));
|
|
650
1249
|
}
|
|
651
1250
|
|
|
652
1251
|
function connect() {
|
|
653
1252
|
setConn("connecting");
|
|
654
1253
|
var src = new EventSource("/api/events");
|
|
655
|
-
src.onopen = function () { setConn("live"); };
|
|
1254
|
+
src.onopen = function () { setConn("live"); scheduleRefresh(0); };
|
|
656
1255
|
src.onmessage = function (m) {
|
|
657
|
-
|
|
1256
|
+
var ev;
|
|
1257
|
+
try { ev = JSON.parse(m.data); } catch (e) { return; }
|
|
1258
|
+
if (paused) {
|
|
1259
|
+
pausedCount++;
|
|
1260
|
+
el("feed-paused").hidden = false;
|
|
1261
|
+
el("feed-paused").textContent = "Paused \\u00b7 " + pausedCount + " event" + (pausedCount === 1 ? "" : "s") + " not shown";
|
|
1262
|
+
} else {
|
|
1263
|
+
pushEvent(ev);
|
|
1264
|
+
}
|
|
1265
|
+
// Terminal events change job status / history; reconcile on the next tick.
|
|
1266
|
+
if (ev.type !== "fire") scheduleRefresh(DEBOUNCE_MS);
|
|
658
1267
|
};
|
|
659
1268
|
src.onerror = function () { setConn("down"); };
|
|
660
1269
|
}
|
|
661
1270
|
|
|
662
|
-
// \u2500\u2500
|
|
1271
|
+
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1272
|
+
function scheduleRefresh(delay) {
|
|
1273
|
+
if (refreshTimer) return;
|
|
1274
|
+
refreshTimer = setTimeout(function () { refreshTimer = null; refresh(); }, delay);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function refresh() {
|
|
1278
|
+
return fetch("/api/state?upcoming=" + UPCOMING)
|
|
1279
|
+
.then(function (r) { return r.json(); })
|
|
1280
|
+
.then(function (next) {
|
|
1281
|
+
var structureChanged = !sameKeys(next.jobs);
|
|
1282
|
+
state = next;
|
|
1283
|
+
jobsByKey = {};
|
|
1284
|
+
for (var i = 0; i < next.jobs.length; i++) jobsByKey[next.jobs[i].key] = next.jobs[i];
|
|
1285
|
+
|
|
1286
|
+
if (structureChanged) { buildRows(next.jobs); buildLanes(next.jobs); built = true; }
|
|
1287
|
+
paintRows(next.jobs);
|
|
1288
|
+
paintTimeline();
|
|
1289
|
+
renderStats();
|
|
1290
|
+
setEngineChip();
|
|
1291
|
+
if (openKey) paintDrawer(openKey);
|
|
1292
|
+
})
|
|
1293
|
+
.catch(function () { /* the poll below retries */ });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// \u2500\u2500 Drawer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
663
1297
|
function openDrawer(key) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
el("
|
|
1298
|
+
if (!jobsByKey[key]) return;
|
|
1299
|
+
openKey = key;
|
|
1300
|
+
lastFocus = document.activeElement;
|
|
1301
|
+
el("overlay").hidden = false;
|
|
668
1302
|
el("overlay").classList.add("open");
|
|
1303
|
+
paintDrawer(key);
|
|
1304
|
+
el("drawer-close").focus();
|
|
1305
|
+
}
|
|
669
1306
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
'<div class="kv"><span>Schedule</span><code>' + esc(job.schedule) + "</code></div>" +
|
|
677
|
-
'<div class="kv"><span>Timezone</span><span>' + esc(job.timeZone) + "</span></div>" +
|
|
678
|
-
(job.description ? '<div class="kv"><span>Description</span><span>' + esc(job.description) + "</span></div>" : "") +
|
|
679
|
-
'<div style="margin-top:14px"><button class="primary" id="run-now">Run now</button></div>' +
|
|
680
|
-
'<div class="section-title">Next fire times</div>';
|
|
681
|
-
if (fires.length) {
|
|
682
|
-
html += '<ul class="plain">';
|
|
683
|
-
for (var i = 0; i < fires.length; i++) html += "<li><span>" + esc(new Date(fires[i]).toLocaleString()) + "</span><span class=\\"count\\">" + countdown(fires[i]) + "</span></li>";
|
|
684
|
-
html += "</ul>";
|
|
685
|
-
} else { html += '<div class="empty">No upcoming fires.</div>'; }
|
|
686
|
-
|
|
687
|
-
html += '<div class="section-title">Recent runs</div>';
|
|
688
|
-
if (runs.length) {
|
|
689
|
-
html += '<ul class="plain">';
|
|
690
|
-
for (var j = 0; j < runs.length && j < 20; j++) {
|
|
691
|
-
var run = runs[j];
|
|
692
|
-
html += "<li><span>" + statusBadge(run.status) + " <span class=\\"when\\">" + esc(new Date(run.startedAt).toLocaleString()) + "</span></span><span>" + fmtDuration(run.durationMs) + "</span></li>";
|
|
693
|
-
}
|
|
694
|
-
html += "</ul>";
|
|
695
|
-
} else { html += '<div class="empty">No runs yet.</div>'; }
|
|
1307
|
+
function closeDrawer() {
|
|
1308
|
+
openKey = null;
|
|
1309
|
+
el("overlay").classList.remove("open");
|
|
1310
|
+
el("overlay").hidden = true;
|
|
1311
|
+
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
|
1312
|
+
}
|
|
696
1313
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1314
|
+
function runLine(run) {
|
|
1315
|
+
var li = document.createElement("li");
|
|
1316
|
+
var wrap = document.createElement("div");
|
|
1317
|
+
wrap.className = "runline";
|
|
1318
|
+
|
|
1319
|
+
var row1 = document.createElement("div");
|
|
1320
|
+
row1.className = "row1";
|
|
1321
|
+
var left = document.createElement("span");
|
|
1322
|
+
left.appendChild(badge(run.status));
|
|
1323
|
+
var when = document.createElement("span");
|
|
1324
|
+
when.className = "meta";
|
|
1325
|
+
when.textContent = " " + localTime(new Date(run.startedAt).toISOString());
|
|
1326
|
+
left.appendChild(when);
|
|
1327
|
+
var right = document.createElement("span");
|
|
1328
|
+
right.className = "meta";
|
|
1329
|
+
right.textContent = fmtDuration(run.durationMs) + (run.attempts > 1 ? " \\u00b7 " + run.attempts + " attempts" : "");
|
|
1330
|
+
row1.appendChild(left); row1.appendChild(right);
|
|
1331
|
+
wrap.appendChild(row1);
|
|
1332
|
+
|
|
1333
|
+
if (run.error) {
|
|
1334
|
+
var pre = document.createElement("pre");
|
|
1335
|
+
pre.className = "err"; pre.textContent = run.error;
|
|
1336
|
+
wrap.appendChild(pre);
|
|
1337
|
+
} else if (run.result !== undefined && run.result !== null) {
|
|
1338
|
+
var pre2 = document.createElement("pre");
|
|
1339
|
+
try { pre2.textContent = JSON.stringify(run.result, null, 2); }
|
|
1340
|
+
catch (e) { pre2.textContent = String(run.result); }
|
|
1341
|
+
wrap.appendChild(pre2);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
li.appendChild(wrap);
|
|
1345
|
+
li.style.display = "block";
|
|
1346
|
+
return li;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function paintDrawer(key) {
|
|
1350
|
+
var job = jobsByKey[key];
|
|
1351
|
+
if (!job) { closeDrawer(); return; }
|
|
1352
|
+
setText(el("drawer-title"), key);
|
|
1353
|
+
|
|
1354
|
+
var body = el("drawer-body");
|
|
1355
|
+
body.textContent = "";
|
|
1356
|
+
|
|
1357
|
+
function kv(label, valueNode) {
|
|
1358
|
+
var d = document.createElement("div");
|
|
1359
|
+
d.className = "kv";
|
|
1360
|
+
var l = document.createElement("span"); l.textContent = label;
|
|
1361
|
+
d.appendChild(l); d.appendChild(valueNode);
|
|
1362
|
+
body.appendChild(d);
|
|
1363
|
+
}
|
|
1364
|
+
function textNode(t) { var s = document.createElement("span"); s.textContent = t; return s; }
|
|
1365
|
+
function codeNode(t) { var s = document.createElement("code"); s.textContent = t; return s; }
|
|
1366
|
+
|
|
1367
|
+
kv("Schedule", codeNode(job.schedule));
|
|
1368
|
+
kv("Time zone", textNode(job.timeZone));
|
|
1369
|
+
if (job.description) kv("Description", textNode(job.description));
|
|
1370
|
+
kv("Status", job.running ? badge("running") : badge(job.lastRun ? job.lastRun.status : ""));
|
|
1371
|
+
|
|
1372
|
+
var t1 = document.createElement("div");
|
|
1373
|
+
t1.className = "section-title"; t1.textContent = "Next fire times";
|
|
1374
|
+
body.appendChild(t1);
|
|
1375
|
+
|
|
1376
|
+
var ups = job.upcoming || [];
|
|
1377
|
+
if (ups.length) {
|
|
1378
|
+
var ul = document.createElement("ul");
|
|
1379
|
+
ul.className = "plain";
|
|
1380
|
+
for (var i = 0; i < ups.length && i < 8; i++) {
|
|
1381
|
+
var li = document.createElement("li");
|
|
1382
|
+
var a = document.createElement("span"); a.textContent = localTime(ups[i]);
|
|
1383
|
+
var b = document.createElement("span"); b.className = "meta num"; b.textContent = countdown(ups[i]);
|
|
1384
|
+
li.appendChild(a); li.appendChild(b);
|
|
1385
|
+
ul.appendChild(li);
|
|
1386
|
+
}
|
|
1387
|
+
body.appendChild(ul);
|
|
1388
|
+
} else {
|
|
1389
|
+
var e1 = document.createElement("div");
|
|
1390
|
+
e1.className = "empty"; e1.textContent = "No upcoming fires for this expression.";
|
|
1391
|
+
body.appendChild(e1);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
var t2 = document.createElement("div");
|
|
1395
|
+
t2.className = "section-title"; t2.textContent = "Recent runs";
|
|
1396
|
+
body.appendChild(t2);
|
|
1397
|
+
|
|
1398
|
+
var mine = [];
|
|
1399
|
+
for (var r = 0; r < state.runs.length; r++) if (state.runs[r].key === key) mine.push(state.runs[r]);
|
|
1400
|
+
if (mine.length) {
|
|
1401
|
+
var ul2 = document.createElement("ul");
|
|
1402
|
+
ul2.className = "plain";
|
|
1403
|
+
for (var m = 0; m < mine.length && m < 25; m++) ul2.appendChild(runLine(mine[m]));
|
|
1404
|
+
body.appendChild(ul2);
|
|
1405
|
+
} else {
|
|
1406
|
+
var e2 = document.createElement("div");
|
|
1407
|
+
e2.className = "empty"; e2.textContent = "No runs yet in this session.";
|
|
1408
|
+
body.appendChild(e2);
|
|
1409
|
+
}
|
|
700
1410
|
}
|
|
701
|
-
function closeDrawer() { el("overlay").classList.remove("open"); }
|
|
702
|
-
el("drawer-close").addEventListener("click", closeDrawer);
|
|
703
|
-
el("overlay").addEventListener("click", function (e) { if (e.target === el("overlay")) closeDrawer(); });
|
|
704
1411
|
|
|
705
1412
|
function runNow(key, btn) {
|
|
706
|
-
|
|
1413
|
+
var label = btn.textContent;
|
|
1414
|
+
btn.disabled = true; btn.textContent = "Running\\u2026";
|
|
707
1415
|
fetch("/api/trigger/" + encodeURIComponent(key), { method: "POST", headers: { "content-type": "application/json" } })
|
|
708
1416
|
.then(function (r) { return r.json(); })
|
|
709
|
-
.then(function () { btn.textContent = "Ran
|
|
710
|
-
.catch(function () { btn.
|
|
1417
|
+
.then(function () { btn.textContent = "Ran"; scheduleRefresh(0); })
|
|
1418
|
+
.catch(function () { btn.textContent = "Failed"; })
|
|
1419
|
+
.then(function () {
|
|
1420
|
+
setTimeout(function () { btn.disabled = false; btn.textContent = label; }, 1200);
|
|
1421
|
+
});
|
|
711
1422
|
}
|
|
712
1423
|
|
|
1424
|
+
el("drawer-close").addEventListener("click", closeDrawer);
|
|
1425
|
+
el("drawer-run").addEventListener("click", function () { if (openKey) runNow(openKey, this); });
|
|
1426
|
+
el("overlay").addEventListener("click", function (e) { if (e.target === el("overlay")) closeDrawer(); });
|
|
1427
|
+
document.addEventListener("keydown", function (e) {
|
|
1428
|
+
if (e.key === "Escape" && openKey) closeDrawer();
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// Row interactions are delegated, so rebuilding the table never leaks listeners.
|
|
1432
|
+
el("jobs").addEventListener("click", function (e) {
|
|
1433
|
+
var btn = e.target.closest ? e.target.closest("button[data-key]") : null;
|
|
1434
|
+
if (btn) { e.stopPropagation(); runNow(btn.dataset.key, btn); return; }
|
|
1435
|
+
var tr = e.target.closest ? e.target.closest("tr.job") : null;
|
|
1436
|
+
if (tr) openDrawer(tr.dataset.key);
|
|
1437
|
+
});
|
|
1438
|
+
el("jobs").addEventListener("keydown", function (e) {
|
|
1439
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
1440
|
+
var tr = e.target.closest ? e.target.closest("tr.job") : null;
|
|
1441
|
+
if (tr) { e.preventDefault(); openDrawer(tr.dataset.key); }
|
|
1442
|
+
});
|
|
1443
|
+
el("tl-gutter").addEventListener("click", function (e) {
|
|
1444
|
+
var label = e.target.closest ? e.target.closest(".lane-label") : null;
|
|
1445
|
+
if (label) openDrawer(label.textContent);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
713
1448
|
// \u2500\u2500 Boot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
714
|
-
|
|
1449
|
+
refresh();
|
|
715
1450
|
connect();
|
|
716
1451
|
setInterval(tickCountdowns, 1000);
|
|
717
|
-
setInterval(
|
|
1452
|
+
setInterval(slideTimeline, 1000);
|
|
1453
|
+
setInterval(function () { scheduleRefresh(0); }, REFRESH_MS);
|
|
718
1454
|
})();
|
|
719
1455
|
</script>
|
|
720
1456
|
</body>
|
|
@@ -773,7 +1509,38 @@ function handleRequest(engine, sseClients, req, res) {
|
|
|
773
1509
|
return openEventStream(engine, sseClients, req, res);
|
|
774
1510
|
}
|
|
775
1511
|
if (method === "GET" && path === "/api/health") {
|
|
776
|
-
return sendJson(res, 200, {
|
|
1512
|
+
return sendJson(res, 200, {
|
|
1513
|
+
ok: true,
|
|
1514
|
+
jobs: engine.jobs().length,
|
|
1515
|
+
activeRuns: engine.activeRuns,
|
|
1516
|
+
running: engine.running,
|
|
1517
|
+
startedAt: engine.startedAt
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
if (method === "GET" && path === "/api/state") {
|
|
1521
|
+
const runs = engine.runs();
|
|
1522
|
+
const lastByKey = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const run2 of runs) if (!lastByKey.has(run2.key)) lastByKey.set(run2.key, run2);
|
|
1524
|
+
const upcomingCount = clampCount(url.searchParams.get("upcoming"));
|
|
1525
|
+
return sendJson(res, 200, {
|
|
1526
|
+
engine: {
|
|
1527
|
+
running: engine.running,
|
|
1528
|
+
startedAt: engine.startedAt,
|
|
1529
|
+
activeRuns: engine.activeRuns,
|
|
1530
|
+
now: Date.now()
|
|
1531
|
+
},
|
|
1532
|
+
jobs: engine.snapshot().map((j) => ({
|
|
1533
|
+
key: j.key,
|
|
1534
|
+
schedule: j.schedule,
|
|
1535
|
+
timeZone: j.timeZone,
|
|
1536
|
+
description: j.description ?? null,
|
|
1537
|
+
nextFire: j.nextFire ? j.nextFire.toISOString() : null,
|
|
1538
|
+
running: j.running,
|
|
1539
|
+
lastRun: lastByKey.get(j.key) ?? null,
|
|
1540
|
+
upcoming: upcoming(j.schedule, j.timeZone, upcomingCount)
|
|
1541
|
+
})),
|
|
1542
|
+
runs
|
|
1543
|
+
});
|
|
777
1544
|
}
|
|
778
1545
|
if (method === "GET" && path === "/api/runs.ndjson") {
|
|
779
1546
|
res.writeHead(200, { "content-type": "application/x-ndjson; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -856,6 +1623,13 @@ function sendHtml(res, html) {
|
|
|
856
1623
|
function decodeKey(path, prefix) {
|
|
857
1624
|
return decodeURIComponent(path.slice(prefix.length));
|
|
858
1625
|
}
|
|
1626
|
+
function upcoming(schedule, timeZone, count) {
|
|
1627
|
+
try {
|
|
1628
|
+
return previewSchedule(schedule, { timeZone, count }).map((d) => d.toISOString());
|
|
1629
|
+
} catch {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
859
1633
|
function clampCount(raw) {
|
|
860
1634
|
const n = raw ? Number.parseInt(raw, 10) : 5;
|
|
861
1635
|
if (!Number.isFinite(n) || n < 1) return 5;
|