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