@chrysb/alphaclaw 0.6.2-beta.2 → 0.6.2-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/public/css/cron.css +9 -1
- package/lib/public/js/components/cron-tab/cron-helpers.js +71 -3
- package/lib/public/js/components/cron-tab/cron-job-detail.js +161 -127
- package/lib/public/js/components/cron-tab/cron-job-usage.js +11 -18
- package/lib/public/js/components/cron-tab/cron-overview.js +14 -275
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +296 -0
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +155 -96
- package/lib/public/js/components/cron-tab/index.js +9 -4
- package/lib/public/js/components/cron-tab/use-cron-tab.js +118 -24
- package/lib/public/js/lib/api.js +25 -0
- package/lib/server/cron-service.js +57 -4
- package/lib/server/onboarding/import/import-applier.js +15 -3
- package/lib/server/routes/cron.js +21 -0
- package/package.json +2 -2
|
@@ -3,20 +3,15 @@ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
buildCronOptimizationWarnings,
|
|
6
|
-
formatCost,
|
|
7
6
|
formatRelativeMs,
|
|
8
7
|
formatTokenCount,
|
|
9
8
|
getNextScheduledRunAcrossJobs,
|
|
10
9
|
} from "./cron-helpers.js";
|
|
11
10
|
import { CronCalendar } from "./cron-calendar.js";
|
|
12
11
|
import { CronRunsTrendCard } from "./cron-runs-trend-card.js";
|
|
13
|
-
import {
|
|
12
|
+
import { CronRunHistoryPanel } from "./cron-run-history-panel.js";
|
|
14
13
|
import { SummaryStatCard } from "../summary-stat-card.js";
|
|
15
14
|
import { ErrorWarningLineIcon } from "../icons.js";
|
|
16
|
-
import {
|
|
17
|
-
formatDurationCompactMs,
|
|
18
|
-
formatLocaleDateTimeWithTodayTime,
|
|
19
|
-
} from "../../lib/format.js";
|
|
20
15
|
|
|
21
16
|
const html = htm.bind(h);
|
|
22
17
|
const kRecentRunFetchLimit = 100;
|
|
@@ -60,48 +55,6 @@ const formatWarningsAttentionText = (warnings = []) => {
|
|
|
60
55
|
return `${parts.join(" and ")} may need your attention`;
|
|
61
56
|
};
|
|
62
57
|
|
|
63
|
-
const runStatusClassName = (status = "") => {
|
|
64
|
-
const normalized = String(status || "")
|
|
65
|
-
.trim()
|
|
66
|
-
.toLowerCase();
|
|
67
|
-
if (normalized === "ok") return "text-green-300";
|
|
68
|
-
if (normalized === "error") return "text-red-300";
|
|
69
|
-
if (normalized === "skipped") return "text-yellow-300";
|
|
70
|
-
return "text-gray-400";
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const formatRecentRunTimestamp = (timestampMs) =>
|
|
74
|
-
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
75
|
-
fallback: "—",
|
|
76
|
-
valueIsEpochMs: true,
|
|
77
|
-
}).replace(
|
|
78
|
-
/\s([AP])M\b/g,
|
|
79
|
-
(_, marker) => `${String(marker || "").toLowerCase()}m`,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const getRunEstimatedCost = (runEntry = {}) => {
|
|
83
|
-
const usage = runEntry?.usage || {};
|
|
84
|
-
const candidates = [
|
|
85
|
-
usage?.estimatedCost,
|
|
86
|
-
usage?.estimated_cost,
|
|
87
|
-
usage?.totalCost,
|
|
88
|
-
usage?.total_cost,
|
|
89
|
-
usage?.costUsd,
|
|
90
|
-
usage?.cost,
|
|
91
|
-
runEntry?.estimatedCost,
|
|
92
|
-
runEntry?.estimated_cost,
|
|
93
|
-
runEntry?.totalCost,
|
|
94
|
-
runEntry?.total_cost,
|
|
95
|
-
runEntry?.costUsd,
|
|
96
|
-
runEntry?.cost,
|
|
97
|
-
];
|
|
98
|
-
for (const candidate of candidates) {
|
|
99
|
-
const numericValue = Number(candidate);
|
|
100
|
-
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
58
|
const flattenRecentRuns = ({ bulkRunsByJobId = {}, jobs = [] } = {}) => {
|
|
106
59
|
const jobNameById = jobs.reduce((accumulator, job) => {
|
|
107
60
|
const jobId = String(job?.id || "");
|
|
@@ -233,7 +186,7 @@ export const CronOverview = ({
|
|
|
233
186
|
const enabledCount = jobs.filter((job) => job.enabled !== false).length;
|
|
234
187
|
const disabledCount = jobs.length - enabledCount;
|
|
235
188
|
const nextRunMs = getNextScheduledRunAcrossJobs(jobs);
|
|
236
|
-
const warnings = buildCronOptimizationWarnings(jobs);
|
|
189
|
+
const warnings = buildCronOptimizationWarnings(jobs, bulkRunsByJobId);
|
|
237
190
|
const recentRuns = useMemo(
|
|
238
191
|
() => flattenRecentRuns({ bulkRunsByJobId, jobs }),
|
|
239
192
|
[bulkRunsByJobId, jobs],
|
|
@@ -367,232 +320,18 @@ export const CronOverview = ({
|
|
|
367
320
|
onBucketFilterChange=${setSelectedTrendBucketFilter}
|
|
368
321
|
/>
|
|
369
322
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
options=${kRunStatusFilterOptions}
|
|
383
|
-
value=${recentRunStatusFilter}
|
|
384
|
-
onChange=${setRecentRunStatusFilter}
|
|
385
|
-
/>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
${selectedTrendBucketFilter
|
|
389
|
-
? html`
|
|
390
|
-
<div class="flex items-center">
|
|
391
|
-
<span class="inline-flex items-center gap-1.5 text-xs pl-2.5 pr-2 py-1 rounded-full border border-border text-gray-300 bg-black/20">
|
|
392
|
-
Filtered to ${selectedTrendBucketFilter.label}
|
|
393
|
-
<button
|
|
394
|
-
type="button"
|
|
395
|
-
class="text-gray-500 hover:text-gray-200 leading-none"
|
|
396
|
-
onclick=${() => setSelectedTrendBucketFilter(null)}
|
|
397
|
-
aria-label="Clear trend filter"
|
|
398
|
-
>
|
|
399
|
-
×
|
|
400
|
-
</button>
|
|
401
|
-
</span>
|
|
402
|
-
</div>
|
|
403
|
-
`
|
|
404
|
-
: null}
|
|
405
|
-
${recentRunRows.length === 0
|
|
406
|
-
? html`<div class="text-sm text-gray-500">No runs found.</div>`
|
|
407
|
-
: html`
|
|
408
|
-
<div class="ac-history-list">
|
|
409
|
-
${recentRunRows.map((row, rowIndex) => {
|
|
410
|
-
if (row.type === "collapsed-group") {
|
|
411
|
-
const statusSummary = Object.entries(
|
|
412
|
-
row.statusCounts || {},
|
|
413
|
-
)
|
|
414
|
-
.map(([status, count]) => `${status}: ${count}`)
|
|
415
|
-
.join(" • ");
|
|
416
|
-
const timeRangeLabel = `[${formatRecentRunTimestamp(row.oldestTs)} - ${formatRecentRunTimestamp(row.newestTs)}]`;
|
|
417
|
-
return html`
|
|
418
|
-
<details
|
|
419
|
-
key=${`collapsed:${rowIndex}:${row.jobId}`}
|
|
420
|
-
class="ac-history-item"
|
|
421
|
-
>
|
|
422
|
-
<summary class="ac-history-summary">
|
|
423
|
-
<div class="ac-history-summary-row">
|
|
424
|
-
<span
|
|
425
|
-
class="inline-flex items-center gap-2 min-w-0"
|
|
426
|
-
>
|
|
427
|
-
<span
|
|
428
|
-
class="ac-history-toggle shrink-0"
|
|
429
|
-
aria-hidden="true"
|
|
430
|
-
>▸</span
|
|
431
|
-
>
|
|
432
|
-
<span class="truncate text-xs text-gray-300">
|
|
433
|
-
${row.jobName} -
|
|
434
|
-
${formatTokenCount(row.count)} runs -
|
|
435
|
-
${timeRangeLabel}
|
|
436
|
-
</span>
|
|
437
|
-
</span>
|
|
438
|
-
</div>
|
|
439
|
-
</summary>
|
|
440
|
-
<div class="ac-history-body space-y-2 text-xs">
|
|
441
|
-
<div class="text-gray-500">
|
|
442
|
-
${formatTokenCount(row.count)} consecutive runs
|
|
443
|
-
collapsed (${timeRangeLabel})
|
|
444
|
-
</div>
|
|
445
|
-
<div class="text-gray-500">
|
|
446
|
-
Statuses: ${statusSummary}
|
|
447
|
-
</div>
|
|
448
|
-
<div>
|
|
449
|
-
<button
|
|
450
|
-
type="button"
|
|
451
|
-
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
452
|
-
onclick=${() => onSelectJob(row.jobId)}
|
|
453
|
-
>
|
|
454
|
-
Open ${row.jobName}
|
|
455
|
-
</button>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
</details>
|
|
459
|
-
`;
|
|
460
|
-
}
|
|
461
|
-
const runEntry = row.entry || {};
|
|
462
|
-
const runStatus = String(runEntry?.status || "unknown");
|
|
463
|
-
const runUsage = runEntry?.usage || {};
|
|
464
|
-
const runInputTokens = Number(
|
|
465
|
-
runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0,
|
|
466
|
-
);
|
|
467
|
-
const runOutputTokens = Number(
|
|
468
|
-
runUsage?.output_tokens ?? runUsage?.outputTokens ?? 0,
|
|
469
|
-
);
|
|
470
|
-
const runTokens = Number(
|
|
471
|
-
runUsage?.total_tokens ?? runUsage?.totalTokens ?? 0,
|
|
472
|
-
);
|
|
473
|
-
const runEstimatedCost = getRunEstimatedCost(runEntry);
|
|
474
|
-
const runTitle = String(runEntry?.jobName || "").trim();
|
|
475
|
-
const hasRunTitle = runTitle.length > 0;
|
|
476
|
-
return html`
|
|
477
|
-
<details
|
|
478
|
-
key=${`entry:${rowIndex}:${runEntry.ts}:${runEntry.jobId || ""}`}
|
|
479
|
-
class="ac-history-item"
|
|
480
|
-
>
|
|
481
|
-
<summary class="ac-history-summary">
|
|
482
|
-
<div class="ac-history-summary-row">
|
|
483
|
-
<span
|
|
484
|
-
class="inline-flex items-center gap-2 min-w-0"
|
|
485
|
-
>
|
|
486
|
-
<span
|
|
487
|
-
class="ac-history-toggle shrink-0"
|
|
488
|
-
aria-hidden="true"
|
|
489
|
-
>▸</span
|
|
490
|
-
>
|
|
491
|
-
${hasRunTitle
|
|
492
|
-
? html`
|
|
493
|
-
<span
|
|
494
|
-
class="inline-flex items-center gap-2 min-w-0"
|
|
495
|
-
>
|
|
496
|
-
<span class="truncate text-xs text-gray-300">
|
|
497
|
-
${runTitle}
|
|
498
|
-
</span>
|
|
499
|
-
<span class="text-xs text-gray-500 shrink-0">
|
|
500
|
-
${formatRecentRunTimestamp(runEntry.ts)}
|
|
501
|
-
</span>
|
|
502
|
-
</span>
|
|
503
|
-
`
|
|
504
|
-
: html`
|
|
505
|
-
<span class="truncate text-xs text-gray-300">
|
|
506
|
-
${runEntry.jobId} -
|
|
507
|
-
${formatRecentRunTimestamp(runEntry.ts)}
|
|
508
|
-
</span>
|
|
509
|
-
`}
|
|
510
|
-
</span>
|
|
511
|
-
<span
|
|
512
|
-
class="inline-flex items-center gap-3 shrink-0 text-xs"
|
|
513
|
-
>
|
|
514
|
-
<span class=${runStatusClassName(runStatus)}
|
|
515
|
-
>${runStatus}</span
|
|
516
|
-
>
|
|
517
|
-
<span class="text-gray-400"
|
|
518
|
-
>${formatDurationCompactMs(
|
|
519
|
-
runEntry.durationMs,
|
|
520
|
-
)}</span
|
|
521
|
-
>
|
|
522
|
-
<span class="text-gray-400"
|
|
523
|
-
>${formatTokenCount(runTokens)} tk</span
|
|
524
|
-
>
|
|
525
|
-
<span class="text-gray-500"
|
|
526
|
-
>${runEstimatedCost == null
|
|
527
|
-
? "—"
|
|
528
|
-
: `~${formatCost(runEstimatedCost)}`}</span
|
|
529
|
-
>
|
|
530
|
-
</span>
|
|
531
|
-
</div>
|
|
532
|
-
</summary>
|
|
533
|
-
<div class="ac-history-body space-y-2 text-xs">
|
|
534
|
-
${runEntry.summary
|
|
535
|
-
? html`<div>
|
|
536
|
-
<span class="text-gray-500">Summary:</span>
|
|
537
|
-
${runEntry.summary}
|
|
538
|
-
</div>`
|
|
539
|
-
: null}
|
|
540
|
-
${runEntry.error
|
|
541
|
-
? html`<div class="text-red-300">
|
|
542
|
-
<span class="text-gray-500">Error:</span>
|
|
543
|
-
${runEntry.error}
|
|
544
|
-
</div>`
|
|
545
|
-
: null}
|
|
546
|
-
<div class="ac-surface-inset rounded-lg p-2.5 space-y-1.5">
|
|
547
|
-
<div class="text-gray-500">
|
|
548
|
-
Model:
|
|
549
|
-
<span class="text-gray-300 font-mono"
|
|
550
|
-
>${runEntry.model || "—"}</span
|
|
551
|
-
>
|
|
552
|
-
${runEntry.sessionKey
|
|
553
|
-
? html` | Session:
|
|
554
|
-
<span class="text-gray-300 font-mono"
|
|
555
|
-
>${runEntry.sessionKey}</span
|
|
556
|
-
>`
|
|
557
|
-
: null}
|
|
558
|
-
</div>
|
|
559
|
-
<div class="text-gray-500">
|
|
560
|
-
Usage:
|
|
561
|
-
<span class="text-gray-300"
|
|
562
|
-
>${formatTokenCount(runInputTokens)} in</span
|
|
563
|
-
>
|
|
564
|
-
<span class="text-gray-600">•</span>
|
|
565
|
-
<span class="text-gray-300"
|
|
566
|
-
>${formatTokenCount(runOutputTokens)} out</span
|
|
567
|
-
>
|
|
568
|
-
<span class="text-gray-600">•</span>
|
|
569
|
-
<span class="text-gray-300"
|
|
570
|
-
>${formatTokenCount(runTokens)} tk</span
|
|
571
|
-
>
|
|
572
|
-
<span class="text-gray-600">•</span>
|
|
573
|
-
<span class="text-gray-300"
|
|
574
|
-
>${runEstimatedCost == null
|
|
575
|
-
? "—"
|
|
576
|
-
: `~${formatCost(runEstimatedCost)}`}</span
|
|
577
|
-
>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
<div>
|
|
581
|
-
<button
|
|
582
|
-
type="button"
|
|
583
|
-
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
584
|
-
onclick=${() => onSelectJob(runEntry.jobId)}
|
|
585
|
-
>
|
|
586
|
-
Open ${runEntry.jobName || runEntry.jobId}
|
|
587
|
-
</button>
|
|
588
|
-
</div>
|
|
589
|
-
</div>
|
|
590
|
-
</details>
|
|
591
|
-
`;
|
|
592
|
-
})}
|
|
593
|
-
</div>
|
|
594
|
-
`}
|
|
595
|
-
</section>
|
|
323
|
+
<${CronRunHistoryPanel}
|
|
324
|
+
entryCountLabel=${`${formatTokenCount(filteredRecentRuns.length)} entries`}
|
|
325
|
+
primaryFilterOptions=${kRunStatusFilterOptions}
|
|
326
|
+
primaryFilterValue=${recentRunStatusFilter}
|
|
327
|
+
onChangePrimaryFilter=${setRecentRunStatusFilter}
|
|
328
|
+
activeFilterLabel=${selectedTrendBucketFilter?.label || ""}
|
|
329
|
+
onClearActiveFilter=${() => setSelectedTrendBucketFilter(null)}
|
|
330
|
+
rows=${recentRunRows}
|
|
331
|
+
variant="overview"
|
|
332
|
+
onSelectJob=${onSelectJob}
|
|
333
|
+
showOpenJobButton=${true}
|
|
334
|
+
/>
|
|
596
335
|
</div>
|
|
597
336
|
</div>
|
|
598
337
|
`;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
4
|
+
import {
|
|
5
|
+
formatDurationCompactMs,
|
|
6
|
+
formatLocaleDateTimeWithTodayTime,
|
|
7
|
+
} from "../../lib/format.js";
|
|
8
|
+
import { formatCost, formatTokenCount } from "./cron-helpers.js";
|
|
9
|
+
|
|
10
|
+
const html = htm.bind(h);
|
|
11
|
+
const runStatusClassName = (status = "") => {
|
|
12
|
+
const normalized = String(status || "")
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
if (normalized === "ok") return "text-green-300";
|
|
16
|
+
if (normalized === "error") return "text-red-300";
|
|
17
|
+
if (normalized === "skipped") return "text-yellow-300";
|
|
18
|
+
return "text-gray-400";
|
|
19
|
+
};
|
|
20
|
+
const runDeliveryLabel = (run) => String(run?.deliveryStatus || "not-requested");
|
|
21
|
+
const getRunEstimatedCost = (runEntry = {}) => {
|
|
22
|
+
const parsed = Number(runEntry?.estimatedCost);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
24
|
+
};
|
|
25
|
+
const formatOverviewTimestamp = (timestampMs) =>
|
|
26
|
+
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
27
|
+
fallback: "—",
|
|
28
|
+
valueIsEpochMs: true,
|
|
29
|
+
}).replace(/\s([AP])M\b/g, (_, marker) => `${String(marker || "").toLowerCase()}m`);
|
|
30
|
+
const formatDetailTimestamp = (timestampMs) =>
|
|
31
|
+
formatLocaleDateTimeWithTodayTime(timestampMs, {
|
|
32
|
+
fallback: "—",
|
|
33
|
+
valueIsEpochMs: true,
|
|
34
|
+
});
|
|
35
|
+
const formatRowTimestamp = (timestampMs, variant = "overview") =>
|
|
36
|
+
variant === "detail"
|
|
37
|
+
? formatDetailTimestamp(timestampMs)
|
|
38
|
+
: formatOverviewTimestamp(timestampMs);
|
|
39
|
+
const renderCollapsedGroupRow = ({
|
|
40
|
+
row,
|
|
41
|
+
rowIndex,
|
|
42
|
+
onSelectJob = () => {},
|
|
43
|
+
}) => {
|
|
44
|
+
const statusSummary = Object.entries(row.statusCounts || {})
|
|
45
|
+
.map(([status, count]) => `${status}: ${count}`)
|
|
46
|
+
.join(" • ");
|
|
47
|
+
const timeRangeLabel = `[${formatOverviewTimestamp(row.oldestTs)} - ${formatOverviewTimestamp(row.newestTs)}]`;
|
|
48
|
+
return html`
|
|
49
|
+
<details
|
|
50
|
+
key=${`collapsed:${rowIndex}:${row.jobId}`}
|
|
51
|
+
class="ac-history-item"
|
|
52
|
+
>
|
|
53
|
+
<summary class="ac-history-summary">
|
|
54
|
+
<div class="ac-history-summary-row">
|
|
55
|
+
<span
|
|
56
|
+
class="inline-flex items-center gap-2 min-w-0"
|
|
57
|
+
>
|
|
58
|
+
<span
|
|
59
|
+
class="ac-history-toggle shrink-0"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>▸</span
|
|
62
|
+
>
|
|
63
|
+
<span class="truncate text-xs text-gray-300">
|
|
64
|
+
${row.jobName} -
|
|
65
|
+
${formatTokenCount(row.count)} runs -
|
|
66
|
+
${timeRangeLabel}
|
|
67
|
+
</span>
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</summary>
|
|
71
|
+
<div class="ac-history-body space-y-2 text-xs">
|
|
72
|
+
<div class="text-gray-500">
|
|
73
|
+
${formatTokenCount(row.count)} consecutive runs
|
|
74
|
+
collapsed (${timeRangeLabel})
|
|
75
|
+
</div>
|
|
76
|
+
<div class="text-gray-500">
|
|
77
|
+
Statuses: ${statusSummary}
|
|
78
|
+
</div>
|
|
79
|
+
${row?.jobId
|
|
80
|
+
? html`
|
|
81
|
+
<div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
85
|
+
onclick=${() => onSelectJob(row.jobId)}
|
|
86
|
+
>
|
|
87
|
+
Open ${row.jobName || row.jobId}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
`
|
|
91
|
+
: null}
|
|
92
|
+
</div>
|
|
93
|
+
</details>
|
|
94
|
+
`;
|
|
95
|
+
};
|
|
96
|
+
const renderEntryRow = ({
|
|
97
|
+
row,
|
|
98
|
+
rowIndex,
|
|
99
|
+
variant = "overview",
|
|
100
|
+
onSelectJob = () => {},
|
|
101
|
+
showOpenJobButton = false,
|
|
102
|
+
}) => {
|
|
103
|
+
const runEntry = row?.entry || row || {};
|
|
104
|
+
const runUsage = runEntry?.usage || {};
|
|
105
|
+
const runStatus = String(runEntry?.status || "unknown");
|
|
106
|
+
const runInputTokens = Number(runUsage?.input_tokens ?? runUsage?.inputTokens ?? 0);
|
|
107
|
+
const runOutputTokens = Number(runUsage?.output_tokens ?? runUsage?.outputTokens ?? 0);
|
|
108
|
+
const runTokens = Number(runUsage?.total_tokens ?? runUsage?.totalTokens ?? 0);
|
|
109
|
+
const runEstimatedCost = getRunEstimatedCost(runEntry);
|
|
110
|
+
const runTitle = String(runEntry?.jobName || "").trim();
|
|
111
|
+
const hasRunTitle = runTitle.length > 0;
|
|
112
|
+
const isDetail = variant === "detail";
|
|
113
|
+
return html`
|
|
114
|
+
<details
|
|
115
|
+
key=${`entry:${rowIndex}:${runEntry.ts}:${runEntry.jobId || ""}`}
|
|
116
|
+
class="ac-history-item"
|
|
117
|
+
>
|
|
118
|
+
<summary class="ac-history-summary">
|
|
119
|
+
<div class="ac-history-summary-row">
|
|
120
|
+
<span
|
|
121
|
+
class="inline-flex items-center gap-2 min-w-0"
|
|
122
|
+
>
|
|
123
|
+
<span
|
|
124
|
+
class="ac-history-toggle shrink-0"
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
>▸</span
|
|
127
|
+
>
|
|
128
|
+
${isDetail
|
|
129
|
+
? html`
|
|
130
|
+
<span class="truncate text-xs text-gray-300">
|
|
131
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
132
|
+
</span>
|
|
133
|
+
`
|
|
134
|
+
: hasRunTitle
|
|
135
|
+
? html`
|
|
136
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
137
|
+
<span class="truncate text-xs text-gray-300">${runTitle}</span>
|
|
138
|
+
<span class="text-xs text-gray-500 shrink-0">
|
|
139
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
140
|
+
</span>
|
|
141
|
+
</span>
|
|
142
|
+
`
|
|
143
|
+
: html`
|
|
144
|
+
<span class="truncate text-xs text-gray-300">
|
|
145
|
+
${runEntry.jobId} -
|
|
146
|
+
${formatRowTimestamp(runEntry.ts, variant)}
|
|
147
|
+
</span>
|
|
148
|
+
`}
|
|
149
|
+
</span>
|
|
150
|
+
<span
|
|
151
|
+
class="inline-flex items-center gap-3 shrink-0 text-xs"
|
|
152
|
+
>
|
|
153
|
+
<span class=${runStatusClassName(runStatus)}>${runStatus}</span>
|
|
154
|
+
<span class="text-gray-400">${formatDurationCompactMs(runEntry.durationMs)}</span>
|
|
155
|
+
<span class="text-gray-400">${formatTokenCount(runTokens)} tk</span>
|
|
156
|
+
${isDetail
|
|
157
|
+
? html`<span class="text-gray-500">${runDeliveryLabel(runEntry)}</span>`
|
|
158
|
+
: html`
|
|
159
|
+
<span class="text-gray-500"
|
|
160
|
+
>${runEstimatedCost == null ? "—" : `~${formatCost(runEstimatedCost)}`}</span
|
|
161
|
+
>
|
|
162
|
+
`}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
</summary>
|
|
166
|
+
<div class="ac-history-body space-y-2 text-xs">
|
|
167
|
+
${runEntry.summary
|
|
168
|
+
? html`<div><span class="text-gray-500">Summary:</span> ${runEntry.summary}</div>`
|
|
169
|
+
: null}
|
|
170
|
+
${runEntry.error
|
|
171
|
+
? html`<div class="text-red-300"><span class="text-gray-500">Error:</span> ${runEntry.error}</div>`
|
|
172
|
+
: null}
|
|
173
|
+
<div class="ac-surface-inset rounded-lg p-2.5 space-y-1.5">
|
|
174
|
+
<div class="text-gray-500">
|
|
175
|
+
Model:
|
|
176
|
+
<span class="text-gray-300 font-mono">${runEntry.model || "—"}</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="text-gray-500">
|
|
179
|
+
Session:
|
|
180
|
+
<span class="text-gray-300 font-mono">${runEntry.sessionKey || "—"}</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="text-gray-500">
|
|
183
|
+
Tokens in:
|
|
184
|
+
<span class="text-gray-300">${formatTokenCount(runInputTokens)}</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="text-gray-500">
|
|
187
|
+
Tokens out:
|
|
188
|
+
<span class="text-gray-300">${formatTokenCount(runOutputTokens)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="text-gray-500">
|
|
191
|
+
Total tokens:
|
|
192
|
+
<span class="text-gray-300">${formatTokenCount(runTokens)}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="text-gray-500">
|
|
195
|
+
Total cost:
|
|
196
|
+
<span class="text-gray-300">
|
|
197
|
+
${runEstimatedCost == null ? "—" : `~${formatCost(runEstimatedCost)}`}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
${showOpenJobButton && runEntry?.jobId
|
|
202
|
+
? html`
|
|
203
|
+
<div>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200"
|
|
207
|
+
onclick=${() => onSelectJob(runEntry.jobId)}
|
|
208
|
+
>
|
|
209
|
+
Open ${runEntry.jobName || runEntry.jobId}
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
`
|
|
213
|
+
: null}
|
|
214
|
+
</div>
|
|
215
|
+
</details>
|
|
216
|
+
`;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const CronRunHistoryPanel = ({
|
|
220
|
+
entryCountLabel = "",
|
|
221
|
+
primaryFilterOptions = [],
|
|
222
|
+
primaryFilterValue = "all",
|
|
223
|
+
onChangePrimaryFilter = () => {},
|
|
224
|
+
secondaryFilterOptions = [],
|
|
225
|
+
secondaryFilterValue = "all",
|
|
226
|
+
onChangeSecondaryFilter = () => {},
|
|
227
|
+
activeFilterLabel = "",
|
|
228
|
+
onClearActiveFilter = () => {},
|
|
229
|
+
rows = [],
|
|
230
|
+
emptyText = "No runs found.",
|
|
231
|
+
variant = "overview",
|
|
232
|
+
onSelectJob = () => {},
|
|
233
|
+
showOpenJobButton = false,
|
|
234
|
+
footer = null,
|
|
235
|
+
}) => html`
|
|
236
|
+
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
237
|
+
<div class="flex items-start justify-between gap-3">
|
|
238
|
+
<div class="inline-flex items-center gap-3">
|
|
239
|
+
<h3 class="card-label card-label-bright">Run history</h3>
|
|
240
|
+
<div class="text-xs text-gray-500">${entryCountLabel}</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="shrink-0 inline-flex items-center gap-2">
|
|
243
|
+
<${SegmentedControl}
|
|
244
|
+
options=${primaryFilterOptions}
|
|
245
|
+
value=${primaryFilterValue}
|
|
246
|
+
onChange=${onChangePrimaryFilter}
|
|
247
|
+
/>
|
|
248
|
+
${Array.isArray(secondaryFilterOptions) && secondaryFilterOptions.length > 0
|
|
249
|
+
? html`
|
|
250
|
+
<${SegmentedControl}
|
|
251
|
+
options=${secondaryFilterOptions}
|
|
252
|
+
value=${secondaryFilterValue}
|
|
253
|
+
onChange=${onChangeSecondaryFilter}
|
|
254
|
+
/>
|
|
255
|
+
`
|
|
256
|
+
: null}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
${activeFilterLabel
|
|
260
|
+
? html`
|
|
261
|
+
<div class="flex items-center">
|
|
262
|
+
<span
|
|
263
|
+
class="inline-flex items-center gap-1.5 text-xs pl-2.5 pr-2 py-1 rounded-full border border-border text-gray-300 bg-black/20"
|
|
264
|
+
>
|
|
265
|
+
Filtered to ${activeFilterLabel}
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
class="text-gray-500 hover:text-gray-200 leading-none"
|
|
269
|
+
onclick=${onClearActiveFilter}
|
|
270
|
+
aria-label="Clear trend filter"
|
|
271
|
+
>
|
|
272
|
+
×
|
|
273
|
+
</button>
|
|
274
|
+
</span>
|
|
275
|
+
</div>
|
|
276
|
+
`
|
|
277
|
+
: null}
|
|
278
|
+
${rows.length === 0
|
|
279
|
+
? html`<div class="text-sm text-gray-500">${emptyText}</div>`
|
|
280
|
+
: html`
|
|
281
|
+
<div class="ac-history-list">
|
|
282
|
+
${rows.map((row, rowIndex) =>
|
|
283
|
+
row?.type === "collapsed-group"
|
|
284
|
+
? renderCollapsedGroupRow({ row, rowIndex, onSelectJob })
|
|
285
|
+
: renderEntryRow({
|
|
286
|
+
row,
|
|
287
|
+
rowIndex,
|
|
288
|
+
variant,
|
|
289
|
+
onSelectJob,
|
|
290
|
+
showOpenJobButton,
|
|
291
|
+
}))}
|
|
292
|
+
</div>
|
|
293
|
+
`}
|
|
294
|
+
${footer}
|
|
295
|
+
</section>
|
|
296
|
+
`;
|