@atolis-hq/wake 0.2.22 → 0.2.24
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.
|
@@ -85,6 +85,15 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
85
85
|
.tile { background: #1a1d23; border-radius: 10px; padding: 0.6rem 0.9rem; min-width: 120px; }
|
|
86
86
|
.tile .n { font-size: 1.3rem; font-weight: 700; }
|
|
87
87
|
.tile .l { color: #9aa2ad; font-size: 0.72rem; text-transform: uppercase; }
|
|
88
|
+
.toolbar { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
89
|
+
.toolbar label { display: inline-flex; align-items: center; gap: 0.35rem; color: #9aa2ad; font-size: 0.8rem; }
|
|
90
|
+
select { background: #1a1d23; border: 1px solid #2c313a; color: #e8e8e8; padding: 0.3rem 0.5rem; border-radius: 6px; font-size: 0.8rem; }
|
|
91
|
+
select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.15); }
|
|
92
|
+
.metric-bar { width: 160px; height: 0.5rem; background: #101216; border-radius: 999px; overflow: hidden; }
|
|
93
|
+
.metric-bar-fill { height: 100%; background: var(--accent); border-radius: 999px; }
|
|
94
|
+
.stacked-bar { display: flex; width: min(520px, 45vw); min-width: 220px; height: 0.75rem; background: #101216; border-radius: 999px; overflow: hidden; }
|
|
95
|
+
.stacked-segment { height: 100%; min-width: 1px; }
|
|
96
|
+
.metric-table-value { display: flex; align-items: center; gap: 0.55rem; }
|
|
88
97
|
.amber { color: #ffcf7f; }
|
|
89
98
|
.red { color: #ff8f7f; }
|
|
90
99
|
.ok { color: #7fe3a3; }
|
|
@@ -114,6 +123,7 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
114
123
|
<button data-view="board" class="active">Board</button>
|
|
115
124
|
<button data-view="activity">Activity</button>
|
|
116
125
|
<button data-view="runs">Runs</button>
|
|
126
|
+
<button data-view="analytics">Analytics</button>
|
|
117
127
|
<button data-view="config">Config</button>
|
|
118
128
|
<button data-view="health">Health</button>
|
|
119
129
|
</nav>
|
|
@@ -132,9 +142,32 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
132
142
|
const API = '/api/v1';
|
|
133
143
|
const CONDITIONS = ['needs-human', 'active', 'ready', 'waiting', 'stalled', 'finished'];
|
|
134
144
|
let currentView = 'board';
|
|
145
|
+
let analyticsWindow = '7d';
|
|
146
|
+
let analyticsMetric = 'runs-over-time';
|
|
147
|
+
let activeViewRequest = null;
|
|
148
|
+
let activeViewRequestId = 0;
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
const METRIC_OPTIONS = [
|
|
151
|
+
['runs-over-time', 'Runs over time'],
|
|
152
|
+
['runs-by-status-over-time', 'Runs by status over time'],
|
|
153
|
+
['runs-by-status', 'Runs by status'],
|
|
154
|
+
['runs-by-action', 'Runs by action/stage'],
|
|
155
|
+
['runs-by-repo', 'Runs by repo'],
|
|
156
|
+
['runs-by-runner', 'Runs by runner'],
|
|
157
|
+
['runs-by-model', 'Runs by model'],
|
|
158
|
+
['runs-by-tier', 'Runs by tier'],
|
|
159
|
+
['tokens-over-time', 'Tokens over time'],
|
|
160
|
+
['tokens-by-action', 'Tokens by action/stage'],
|
|
161
|
+
['tokens-by-runner', 'Tokens by runner'],
|
|
162
|
+
['tokens-by-model', 'Tokens by model'],
|
|
163
|
+
['duration-by-action', 'Duration by action/stage'],
|
|
164
|
+
['duration-over-time', 'Duration over time'],
|
|
165
|
+
['work-items-over-time', 'Work items completed over time'],
|
|
166
|
+
['work-item-durations', 'Work item e2e duration'],
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
async function getJson(path, signal) {
|
|
170
|
+
const res = await fetch(API + path, signal ? { signal } : undefined);
|
|
138
171
|
if (!res.ok) throw new Error(path + ' -> ' + res.status);
|
|
139
172
|
return res.json();
|
|
140
173
|
}
|
|
@@ -167,6 +200,14 @@ function el(tag, attrs, children) {
|
|
|
167
200
|
return node;
|
|
168
201
|
}
|
|
169
202
|
|
|
203
|
+
function isActiveRequest(requestId) {
|
|
204
|
+
return activeViewRequest && activeViewRequest.id === requestId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function loadingNode(label) {
|
|
208
|
+
return el('p', { class: 'meta', text: 'Loading ' + label + '...' });
|
|
209
|
+
}
|
|
210
|
+
|
|
170
211
|
async function renderStatusBar() {
|
|
171
212
|
try {
|
|
172
213
|
const status = await getJson('/status');
|
|
@@ -203,8 +244,9 @@ async function forceTickNow() {
|
|
|
203
244
|
}
|
|
204
245
|
}
|
|
205
246
|
|
|
206
|
-
async function renderBoard() {
|
|
207
|
-
const board = await getJson('/board');
|
|
247
|
+
async function renderBoard(context) {
|
|
248
|
+
const board = await getJson('/board', context.signal);
|
|
249
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
208
250
|
const main = document.getElementById('main');
|
|
209
251
|
main.innerHTML = '';
|
|
210
252
|
const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
|
|
@@ -388,8 +430,9 @@ function renderTranscripts(transcripts) {
|
|
|
388
430
|
return root;
|
|
389
431
|
}
|
|
390
432
|
|
|
391
|
-
async function renderActivity() {
|
|
392
|
-
const events = await getJson('/events?limit=200');
|
|
433
|
+
async function renderActivity(context) {
|
|
434
|
+
const events = await getJson('/events?limit=200', context.signal);
|
|
435
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
393
436
|
const main = document.getElementById('main');
|
|
394
437
|
main.innerHTML = '';
|
|
395
438
|
const table = el('table', {}, [
|
|
@@ -415,8 +458,239 @@ function fmtTokens(tokenUsage) {
|
|
|
415
458
|
return total >= 1000 ? (total / 1000).toFixed(1) + 'k' : String(total);
|
|
416
459
|
}
|
|
417
460
|
|
|
418
|
-
|
|
419
|
-
|
|
461
|
+
function fmtNumber(value) {
|
|
462
|
+
return Number(value || 0).toLocaleString();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function fmtDuration(ms) {
|
|
466
|
+
return ms === undefined || ms === null ? '' : fmtMs(ms);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function metricBar(value, max) {
|
|
470
|
+
const pct = max > 0 ? Math.max(2, Math.round((value / max) * 100)) : 0;
|
|
471
|
+
return el('div', { class: 'metric-bar' }, [
|
|
472
|
+
el('div', { class: 'metric-bar-fill', style: 'width:' + pct + '%' }),
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function stackedBar(segments) {
|
|
477
|
+
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
478
|
+
return el('div', { class: 'stacked-bar' }, segments.filter((segment) => segment.value > 0).map((segment) =>
|
|
479
|
+
el('div', {
|
|
480
|
+
class: 'stacked-segment',
|
|
481
|
+
title: segment.label + ': ' + fmtNumber(segment.value),
|
|
482
|
+
style: 'width:' + ((segment.value / total) * 100).toFixed(2) + '%;background:' + segment.color,
|
|
483
|
+
}),
|
|
484
|
+
));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function selectBox(value, options, onChange) {
|
|
488
|
+
return el('select', { onchange: (event) => onChange(event.target.value) }, options.map((option) =>
|
|
489
|
+
el('option', {
|
|
490
|
+
value: option[0],
|
|
491
|
+
text: option[1],
|
|
492
|
+
...(option[0] === value ? { selected: 'selected' } : {}),
|
|
493
|
+
}),
|
|
494
|
+
));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function metricValueCell(value, max, format) {
|
|
498
|
+
return el('td', {}, [
|
|
499
|
+
el('div', { class: 'metric-table-value' }, [
|
|
500
|
+
el('span', { text: format(value) }),
|
|
501
|
+
metricBar(value, max),
|
|
502
|
+
]),
|
|
503
|
+
]);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderCountTable(rows, label) {
|
|
507
|
+
if (rows.length === 0) return el('p', { class: 'meta', text: 'No data for this window.' });
|
|
508
|
+
const max = Math.max(...rows.map((row) => row.count));
|
|
509
|
+
return el('table', {}, [
|
|
510
|
+
el('tr', {}, [el('th', { text: label }), el('th', { text: 'runs' })]),
|
|
511
|
+
...rows.map((row) => el('tr', {}, [
|
|
512
|
+
el('td', { text: row.key }),
|
|
513
|
+
metricValueCell(row.count, max, fmtNumber),
|
|
514
|
+
])),
|
|
515
|
+
]);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderTokenTable(rows, label) {
|
|
519
|
+
if (rows.length === 0) return el('p', { class: 'meta', text: 'No data for this window.' });
|
|
520
|
+
const max = Math.max(...rows.map((row) => row.tokens));
|
|
521
|
+
return el('table', {}, [
|
|
522
|
+
el('tr', {}, [
|
|
523
|
+
el('th', { text: label }),
|
|
524
|
+
el('th', { text: 'tokens' }),
|
|
525
|
+
el('th', { text: 'avg/run' }),
|
|
526
|
+
el('th', { text: 'cost' }),
|
|
527
|
+
el('th', { text: 'avg cost/run' }),
|
|
528
|
+
el('th', { text: 'runs' }),
|
|
529
|
+
]),
|
|
530
|
+
...rows.map((row) => el('tr', {}, [
|
|
531
|
+
el('td', { text: row.key }),
|
|
532
|
+
metricValueCell(row.tokens, max, fmtNumber),
|
|
533
|
+
el('td', { text: fmtNumber(row.averageTokens || 0) }),
|
|
534
|
+
el('td', { text: fmtCost(row.costUsd) }),
|
|
535
|
+
el('td', { text: fmtCost(row.averageCostUsd) }),
|
|
536
|
+
el('td', { text: fmtNumber(row.count || 0) }),
|
|
537
|
+
])),
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderDurationTable(rows, label) {
|
|
542
|
+
if (rows.length === 0) return el('p', { class: 'meta', text: 'No data for this window.' });
|
|
543
|
+
const max = Math.max(...rows.map((row) => row.totalMs || row.medianMs));
|
|
544
|
+
return el('table', {}, [
|
|
545
|
+
el('tr', {}, [
|
|
546
|
+
el('th', { text: label }),
|
|
547
|
+
el('th', { text: 'total' }),
|
|
548
|
+
el('th', { text: 'median' }),
|
|
549
|
+
el('th', { text: 'average' }),
|
|
550
|
+
el('th', { text: 'runs' }),
|
|
551
|
+
]),
|
|
552
|
+
...rows.map((row) => el('tr', {}, [
|
|
553
|
+
el('td', { text: row.key || row.label }),
|
|
554
|
+
metricValueCell(row.totalMs || 0, max, fmtDuration),
|
|
555
|
+
el('td', { text: fmtDuration(row.medianMs) }),
|
|
556
|
+
el('td', { text: fmtDuration(row.averageMs) }),
|
|
557
|
+
el('td', { text: fmtNumber(row.count) }),
|
|
558
|
+
])),
|
|
559
|
+
]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function renderRunsOverTime(rows) {
|
|
563
|
+
return el('table', {}, [
|
|
564
|
+
el('tr', {}, [el('th', { text: 'bucket' }), el('th', { text: 'runs' }), el('th', { text: 'status split' })]),
|
|
565
|
+
...rows.map((row) => el('tr', {}, [
|
|
566
|
+
el('td', { text: row.label }),
|
|
567
|
+
el('td', { text: fmtNumber(row.total) }),
|
|
568
|
+
el('td', {}, [stackedBar([
|
|
569
|
+
{ label: 'completed', value: row.completed, color: '#7fe3a3' },
|
|
570
|
+
{ label: 'blocked', value: row.blocked, color: '#ffcf7f' },
|
|
571
|
+
{ label: 'awaiting approval', value: row.awaitingApproval, color: '#7fb3ff' },
|
|
572
|
+
{ label: 'failed', value: row.failed, color: '#ff8f7f' },
|
|
573
|
+
{ label: 'other', value: row.other, color: '#9aa2ad' },
|
|
574
|
+
])]),
|
|
575
|
+
])),
|
|
576
|
+
]);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderRunsByStatusOverTime(rows) {
|
|
580
|
+
return el('table', {}, [
|
|
581
|
+
el('tr', {}, [
|
|
582
|
+
el('th', { text: 'bucket' }),
|
|
583
|
+
el('th', { text: 'completed' }),
|
|
584
|
+
el('th', { text: 'blocked' }),
|
|
585
|
+
el('th', { text: 'awaiting approval' }),
|
|
586
|
+
el('th', { text: 'failed' }),
|
|
587
|
+
el('th', { text: 'other' }),
|
|
588
|
+
el('th', { text: 'total' }),
|
|
589
|
+
]),
|
|
590
|
+
...rows.map((row) => el('tr', {}, [
|
|
591
|
+
el('td', { text: row.label }),
|
|
592
|
+
el('td', { text: fmtNumber(row.completed) }),
|
|
593
|
+
el('td', { text: fmtNumber(row.blocked) }),
|
|
594
|
+
el('td', { text: fmtNumber(row.awaitingApproval) }),
|
|
595
|
+
el('td', { text: fmtNumber(row.failed) }),
|
|
596
|
+
el('td', { text: fmtNumber(row.other) }),
|
|
597
|
+
el('td', { text: fmtNumber(row.total) }),
|
|
598
|
+
])),
|
|
599
|
+
]);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function renderTokensOverTime(rows) {
|
|
603
|
+
return el('table', {}, [
|
|
604
|
+
el('tr', {}, [el('th', { text: 'bucket' }), el('th', { text: 'tokens' }), el('th', { text: 'token split' }), el('th', { text: 'cost' })]),
|
|
605
|
+
...rows.map((row) => el('tr', {}, [
|
|
606
|
+
el('td', { text: row.label }),
|
|
607
|
+
el('td', { text: fmtNumber(row.tokens) }),
|
|
608
|
+
el('td', {}, [stackedBar([
|
|
609
|
+
{ label: 'input', value: row.inputTokens, color: '#7fb3ff' },
|
|
610
|
+
{ label: 'output', value: row.outputTokens, color: '#7fe3a3' },
|
|
611
|
+
{ label: 'cache creation', value: row.cacheCreationInputTokens, color: '#c79bff' },
|
|
612
|
+
{ label: 'cache read', value: row.cacheReadInputTokens, color: '#ffcf7f' },
|
|
613
|
+
])]),
|
|
614
|
+
el('td', { text: fmtCost(row.costUsd) }),
|
|
615
|
+
])),
|
|
616
|
+
]);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderWorkItemsOverTime(rows) {
|
|
620
|
+
const max = Math.max(0, ...rows.map((row) => row.completed));
|
|
621
|
+
return el('table', {}, [
|
|
622
|
+
el('tr', {}, [el('th', { text: 'bucket' }), el('th', { text: 'completed' })]),
|
|
623
|
+
...rows.map((row) => el('tr', {}, [
|
|
624
|
+
el('td', { text: row.label }),
|
|
625
|
+
metricValueCell(row.completed, max, fmtNumber),
|
|
626
|
+
])),
|
|
627
|
+
]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function renderWorkItemDurations(rows) {
|
|
631
|
+
if (rows.length === 0) return el('p', { class: 'meta', text: 'No completed work items for this window.' });
|
|
632
|
+
const max = Math.max(...rows.map((row) => row.durationMs));
|
|
633
|
+
return el('table', {}, [
|
|
634
|
+
el('tr', {}, ['work item', 'duration', 'completed'].map((h) => el('th', { text: h }))),
|
|
635
|
+
...rows.map((row) => el('tr', {}, [
|
|
636
|
+
el('td', { text: row.repo + '#' + row.issueNumber }),
|
|
637
|
+
metricValueCell(row.durationMs, max, fmtDuration),
|
|
638
|
+
el('td', { text: row.completedAt }),
|
|
639
|
+
])),
|
|
640
|
+
]);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function renderMetricDetail(detail) {
|
|
644
|
+
if (detail.kind === 'runs-over-time') return renderRunsOverTime(detail.rows);
|
|
645
|
+
if (detail.kind === 'runs-by-status-over-time') return renderRunsByStatusOverTime(detail.rows);
|
|
646
|
+
if (detail.kind === 'run-counts') return renderCountTable(detail.rows, detail.group);
|
|
647
|
+
if (detail.kind === 'tokens-over-time') return renderTokensOverTime(detail.rows);
|
|
648
|
+
if (detail.kind === 'token-counts') return renderTokenTable(detail.rows, detail.group);
|
|
649
|
+
if (detail.kind === 'durations') return renderDurationTable(detail.rows, detail.group);
|
|
650
|
+
if (detail.kind === 'duration-over-time') return renderDurationTable(detail.rows, 'bucket');
|
|
651
|
+
if (detail.kind === 'work-items-over-time') return renderWorkItemsOverTime(detail.rows);
|
|
652
|
+
if (detail.kind === 'work-item-durations') return renderWorkItemDurations(detail.rows);
|
|
653
|
+
return el('p', { class: 'meta', text: 'Unsupported metric.' });
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function renderAnalytics(context) {
|
|
657
|
+
const metrics = await getJson('/metrics?window=' + encodeURIComponent(analyticsWindow) + '&metric=' + encodeURIComponent(analyticsMetric), context.signal);
|
|
658
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
659
|
+
const main = document.getElementById('main');
|
|
660
|
+
main.innerHTML = '';
|
|
661
|
+
main.appendChild(el('div', { class: 'toolbar' }, [
|
|
662
|
+
el('label', {}, [
|
|
663
|
+
document.createTextNode('Window'),
|
|
664
|
+
selectBox(analyticsWindow, [['1d', '1d'], ['3d', '3d'], ['5d', '5d'], ['7d', '7d']], (value) => {
|
|
665
|
+
analyticsWindow = value;
|
|
666
|
+
switchView('analytics');
|
|
667
|
+
}),
|
|
668
|
+
]),
|
|
669
|
+
el('label', {}, [
|
|
670
|
+
document.createTextNode('Metric'),
|
|
671
|
+
selectBox(analyticsMetric, METRIC_OPTIONS, (value) => {
|
|
672
|
+
analyticsMetric = value;
|
|
673
|
+
switchView('analytics');
|
|
674
|
+
}),
|
|
675
|
+
]),
|
|
676
|
+
]));
|
|
677
|
+
main.appendChild(el('div', { class: 'tiles' }, [
|
|
678
|
+
tile('Runs', fmtNumber(metrics.summary.totalRuns)),
|
|
679
|
+
tile('Completed', fmtNumber(metrics.summary.completedRuns)),
|
|
680
|
+
tile('Blocked/approval', fmtNumber(metrics.summary.blockedRuns + metrics.summary.awaitingApprovalRuns)),
|
|
681
|
+
tile('Failed', fmtNumber(metrics.summary.failedRuns)),
|
|
682
|
+
tile('Tokens', fmtNumber(metrics.summary.totalTokens)),
|
|
683
|
+
tile('Cost', fmtCost(metrics.summary.totalCostUsd)),
|
|
684
|
+
tile('Median run', fmtDuration(metrics.summary.medianRunDurationMs) || '—'),
|
|
685
|
+
tile('Work done', fmtNumber(metrics.summary.completedWorkItems)),
|
|
686
|
+
tile('Median e2e', fmtDuration(metrics.summary.medianWorkItemDurationMs) || '—'),
|
|
687
|
+
]));
|
|
688
|
+
main.appendChild(renderMetricDetail(metrics.detail));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function renderRuns(context) {
|
|
692
|
+
const runs = await getJson('/runs', context.signal);
|
|
693
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
420
694
|
const main = document.getElementById('main');
|
|
421
695
|
main.innerHTML = '';
|
|
422
696
|
const table = el('table', {}, [
|
|
@@ -431,8 +705,9 @@ async function renderRuns() {
|
|
|
431
705
|
main.appendChild(table);
|
|
432
706
|
}
|
|
433
707
|
|
|
434
|
-
async function renderConfig() {
|
|
435
|
-
const data = await getJson('/config');
|
|
708
|
+
async function renderConfig(context) {
|
|
709
|
+
const data = await getJson('/config', context.signal);
|
|
710
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
436
711
|
const main = document.getElementById('main');
|
|
437
712
|
main.innerHTML = '';
|
|
438
713
|
main.appendChild(el('h3', { text: 'Routing table' }));
|
|
@@ -449,7 +724,7 @@ async function renderConfig() {
|
|
|
449
724
|
btn.textContent = 'Unpausing…';
|
|
450
725
|
try {
|
|
451
726
|
await postJson('/runners/' + encodeURIComponent(c.runnerName) + '/unpause');
|
|
452
|
-
|
|
727
|
+
switchView('config');
|
|
453
728
|
} catch (err) {
|
|
454
729
|
btn.disabled = false;
|
|
455
730
|
btn.textContent = 'Unpause';
|
|
@@ -467,8 +742,9 @@ async function renderConfig() {
|
|
|
467
742
|
main.appendChild(el('pre', { text: JSON.stringify(data.config, null, 2) }));
|
|
468
743
|
}
|
|
469
744
|
|
|
470
|
-
async function renderHealth() {
|
|
471
|
-
const health = await getJson('/health');
|
|
745
|
+
async function renderHealth(context) {
|
|
746
|
+
const health = await getJson('/health', context.signal);
|
|
747
|
+
if (!isActiveRequest(context.requestId)) return;
|
|
472
748
|
const main = document.getElementById('main');
|
|
473
749
|
main.innerHTML = '';
|
|
474
750
|
const runnerNames = Object.keys(health.pause.runnerHealth || {});
|
|
@@ -512,14 +788,25 @@ function tile(label, value) {
|
|
|
512
788
|
return el('div', { class: 'tile' }, [el('div', { class: 'n', text: value }), el('div', { class: 'l', text: label })]);
|
|
513
789
|
}
|
|
514
790
|
|
|
515
|
-
const renderers = { board: renderBoard, activity: renderActivity, runs: renderRuns, config: renderConfig, health: renderHealth };
|
|
791
|
+
const renderers = { board: renderBoard, activity: renderActivity, runs: renderRuns, analytics: renderAnalytics, config: renderConfig, health: renderHealth };
|
|
516
792
|
|
|
517
|
-
function switchView(view) {
|
|
793
|
+
function switchView(view, options) {
|
|
794
|
+
if (activeViewRequest) activeViewRequest.controller.abort();
|
|
795
|
+
const controller = new AbortController();
|
|
796
|
+
const requestId = ++activeViewRequestId;
|
|
797
|
+
activeViewRequest = { id: requestId, controller };
|
|
518
798
|
currentView = view;
|
|
519
799
|
for (const btn of document.querySelectorAll('nav button')) {
|
|
520
800
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
521
801
|
}
|
|
522
|
-
|
|
802
|
+
const main = document.getElementById('main');
|
|
803
|
+
if (!options || options.showLoading !== false) {
|
|
804
|
+
main.replaceChildren(loadingNode(view));
|
|
805
|
+
}
|
|
806
|
+
renderers[view]({ signal: controller.signal, requestId }).catch((err) => {
|
|
807
|
+
if (err.name === 'AbortError' || !isActiveRequest(requestId)) return;
|
|
808
|
+
main.replaceChildren(el('p', { class: 'meta red', text: 'Failed to load ' + view + ': ' + err.message }));
|
|
809
|
+
});
|
|
523
810
|
}
|
|
524
811
|
|
|
525
812
|
for (const btn of document.querySelectorAll('nav button')) {
|
|
@@ -539,7 +826,7 @@ renderStatusBar();
|
|
|
539
826
|
switchView('board');
|
|
540
827
|
setInterval(() => {
|
|
541
828
|
renderStatusBar();
|
|
542
|
-
|
|
829
|
+
switchView(currentView, { showLoading: false });
|
|
543
830
|
}, 7000);
|
|
544
831
|
</script>
|
|
545
832
|
</body>
|
|
@@ -364,6 +364,520 @@ export async function buildRuns(input) {
|
|
|
364
364
|
.filter((run) => input.repo === undefined || run.repo === input.repo)
|
|
365
365
|
.sort((left, right) => right.startedAt.localeCompare(left.startedAt));
|
|
366
366
|
}
|
|
367
|
+
const metricsWindows = new Set(['1d', '3d', '5d', '7d']);
|
|
368
|
+
const metricsMetrics = new Set([
|
|
369
|
+
'runs-over-time',
|
|
370
|
+
'runs-by-status-over-time',
|
|
371
|
+
'runs-by-status',
|
|
372
|
+
'runs-by-action',
|
|
373
|
+
'runs-by-repo',
|
|
374
|
+
'runs-by-runner',
|
|
375
|
+
'runs-by-model',
|
|
376
|
+
'runs-by-tier',
|
|
377
|
+
'tokens-over-time',
|
|
378
|
+
'tokens-by-action',
|
|
379
|
+
'tokens-by-runner',
|
|
380
|
+
'tokens-by-model',
|
|
381
|
+
'duration-by-action',
|
|
382
|
+
'duration-over-time',
|
|
383
|
+
'work-items-over-time',
|
|
384
|
+
'work-item-durations',
|
|
385
|
+
]);
|
|
386
|
+
function parseMetricsWindow(value) {
|
|
387
|
+
return metricsWindows.has(value) ? value : '7d';
|
|
388
|
+
}
|
|
389
|
+
function parseMetricsMetric(value) {
|
|
390
|
+
return metricsMetrics.has(value) ? value : 'runs-over-time';
|
|
391
|
+
}
|
|
392
|
+
function utcDayStartMs(date) {
|
|
393
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
394
|
+
}
|
|
395
|
+
function monthDayLabel(date) {
|
|
396
|
+
const months = [
|
|
397
|
+
'Jan',
|
|
398
|
+
'Feb',
|
|
399
|
+
'Mar',
|
|
400
|
+
'Apr',
|
|
401
|
+
'May',
|
|
402
|
+
'Jun',
|
|
403
|
+
'Jul',
|
|
404
|
+
'Aug',
|
|
405
|
+
'Sep',
|
|
406
|
+
'Oct',
|
|
407
|
+
'Nov',
|
|
408
|
+
'Dec',
|
|
409
|
+
];
|
|
410
|
+
return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
411
|
+
}
|
|
412
|
+
function pad2(value) {
|
|
413
|
+
return String(value).padStart(2, '0');
|
|
414
|
+
}
|
|
415
|
+
function createTimeBuckets(window, now) {
|
|
416
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
417
|
+
const hourMs = 60 * 60 * 1000;
|
|
418
|
+
const todayStart = utcDayStartMs(now);
|
|
419
|
+
if (window === '1d') {
|
|
420
|
+
return Array.from({ length: 24 }, (_, hour) => {
|
|
421
|
+
const startMs = todayStart + hour * hourMs;
|
|
422
|
+
const day = new Date(startMs);
|
|
423
|
+
return {
|
|
424
|
+
bucket: `${day.toISOString().slice(0, 10)}T${pad2(hour)}`,
|
|
425
|
+
label: `${pad2(hour)}:00`,
|
|
426
|
+
startMs,
|
|
427
|
+
endMs: startMs + hourMs,
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (window === '3d') {
|
|
432
|
+
const start = todayStart - 2 * dayMs;
|
|
433
|
+
return Array.from({ length: 12 }, (_, index) => {
|
|
434
|
+
const startMs = start + index * 6 * hourMs;
|
|
435
|
+
const day = new Date(startMs);
|
|
436
|
+
const hour = day.getUTCHours();
|
|
437
|
+
return {
|
|
438
|
+
bucket: `${day.toISOString().slice(0, 10)}T${pad2(hour)}`,
|
|
439
|
+
label: `${monthDayLabel(day)} ${pad2(hour)}:00`,
|
|
440
|
+
startMs,
|
|
441
|
+
endMs: startMs + 6 * hourMs,
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const days = window === '5d' ? 5 : 7;
|
|
446
|
+
const start = todayStart - (days - 1) * dayMs;
|
|
447
|
+
return Array.from({ length: days }, (_, index) => {
|
|
448
|
+
const startMs = start + index * dayMs;
|
|
449
|
+
const day = new Date(startMs);
|
|
450
|
+
return {
|
|
451
|
+
bucket: day.toISOString().slice(0, 10),
|
|
452
|
+
label: monthDayLabel(day),
|
|
453
|
+
startMs,
|
|
454
|
+
endMs: startMs + dayMs,
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function inMetricsWindow(timestamp, buckets) {
|
|
459
|
+
const at = Date.parse(timestamp);
|
|
460
|
+
const first = buckets[0];
|
|
461
|
+
const last = buckets.at(-1);
|
|
462
|
+
return (Number.isFinite(at) &&
|
|
463
|
+
first !== undefined &&
|
|
464
|
+
last !== undefined &&
|
|
465
|
+
at >= first.startMs &&
|
|
466
|
+
at < last.endMs);
|
|
467
|
+
}
|
|
468
|
+
function findBucket(timestamp, buckets) {
|
|
469
|
+
const at = Date.parse(timestamp);
|
|
470
|
+
if (!Number.isFinite(at)) {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
return buckets.find((bucket) => at >= bucket.startMs && at < bucket.endMs);
|
|
474
|
+
}
|
|
475
|
+
async function listRunsForBuckets(stateStore, buckets) {
|
|
476
|
+
const dates = [...new Set(buckets.map((bucket) => bucket.bucket.slice(0, 10)))];
|
|
477
|
+
const runsById = new Map();
|
|
478
|
+
const recordsByDate = await Promise.all(dates.map((date) => stateStore.listRunRecordsForDate(date)));
|
|
479
|
+
for (const records of recordsByDate) {
|
|
480
|
+
for (const record of records) {
|
|
481
|
+
runsById.set(record.runId, record);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return [...runsById.values()].sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
485
|
+
}
|
|
486
|
+
function runTokenTotal(run) {
|
|
487
|
+
const usage = run.tokenUsage;
|
|
488
|
+
if (usage === undefined) {
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
return (usage.inputTokens +
|
|
492
|
+
usage.outputTokens +
|
|
493
|
+
(usage.cacheCreationInputTokens ?? 0) +
|
|
494
|
+
(usage.cacheReadInputTokens ?? 0));
|
|
495
|
+
}
|
|
496
|
+
function median(values) {
|
|
497
|
+
if (values.length === 0) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
const sorted = values.slice().sort((left, right) => left - right);
|
|
501
|
+
const middle = Math.floor(sorted.length / 2);
|
|
502
|
+
if (sorted.length % 2 === 1) {
|
|
503
|
+
return sorted[middle];
|
|
504
|
+
}
|
|
505
|
+
return ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
506
|
+
}
|
|
507
|
+
function average(values) {
|
|
508
|
+
return values.length === 0
|
|
509
|
+
? 0
|
|
510
|
+
: values.reduce((total, value) => total + value, 0) / values.length;
|
|
511
|
+
}
|
|
512
|
+
function runDurationMs(run) {
|
|
513
|
+
if (run.finishedAt === undefined) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
const started = Date.parse(run.startedAt);
|
|
517
|
+
const finished = Date.parse(run.finishedAt);
|
|
518
|
+
if (!Number.isFinite(started) || !Number.isFinite(finished) || finished < started) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return finished - started;
|
|
522
|
+
}
|
|
523
|
+
function groupCount(entries, keyFor) {
|
|
524
|
+
const counts = new Map();
|
|
525
|
+
for (const entry of entries) {
|
|
526
|
+
const key = keyFor(entry);
|
|
527
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
528
|
+
}
|
|
529
|
+
return [...counts.entries()]
|
|
530
|
+
.map(([key, count]) => ({ key, count }))
|
|
531
|
+
.sort((left, right) => right.count - left.count || left.key.localeCompare(right.key));
|
|
532
|
+
}
|
|
533
|
+
function groupTokens(runs, keyFor) {
|
|
534
|
+
const groups = new Map();
|
|
535
|
+
for (const run of runs) {
|
|
536
|
+
const key = keyFor(run);
|
|
537
|
+
const existing = groups.get(key) ?? { count: 0, tokens: 0, costUsd: 0 };
|
|
538
|
+
existing.count += 1;
|
|
539
|
+
existing.tokens += runTokenTotal(run);
|
|
540
|
+
existing.costUsd += run.tokenUsage?.costUsd ?? 0;
|
|
541
|
+
groups.set(key, existing);
|
|
542
|
+
}
|
|
543
|
+
return [...groups.entries()]
|
|
544
|
+
.map(([key, value]) => ({
|
|
545
|
+
key,
|
|
546
|
+
...value,
|
|
547
|
+
averageTokens: value.count === 0 ? 0 : value.tokens / value.count,
|
|
548
|
+
averageCostUsd: value.count === 0 ? 0 : value.costUsd / value.count,
|
|
549
|
+
}))
|
|
550
|
+
.sort((left, right) => right.tokens - left.tokens || left.key.localeCompare(right.key));
|
|
551
|
+
}
|
|
552
|
+
function groupDurations(runs, keyFor) {
|
|
553
|
+
const groups = new Map();
|
|
554
|
+
for (const run of runs) {
|
|
555
|
+
const duration = runDurationMs(run);
|
|
556
|
+
if (duration === undefined) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const key = keyFor(run);
|
|
560
|
+
groups.set(key, [...(groups.get(key) ?? []), duration]);
|
|
561
|
+
}
|
|
562
|
+
return [...groups.entries()]
|
|
563
|
+
.map(([key, values]) => ({
|
|
564
|
+
key,
|
|
565
|
+
count: values.length,
|
|
566
|
+
totalMs: values.reduce((total, value) => total + value, 0),
|
|
567
|
+
averageMs: average(values),
|
|
568
|
+
medianMs: median(values) ?? 0,
|
|
569
|
+
}))
|
|
570
|
+
.sort((left, right) => right.medianMs - left.medianMs || left.key.localeCompare(right.key));
|
|
571
|
+
}
|
|
572
|
+
function runnerKey(run) {
|
|
573
|
+
return run.routing?.runnerName ?? 'unknown';
|
|
574
|
+
}
|
|
575
|
+
function tierKey(run) {
|
|
576
|
+
return run.routing?.tier ?? 'unknown';
|
|
577
|
+
}
|
|
578
|
+
function modelKey(run, config) {
|
|
579
|
+
const runnerName = run.routing?.runnerName;
|
|
580
|
+
if (runnerName === undefined) {
|
|
581
|
+
return 'unknown';
|
|
582
|
+
}
|
|
583
|
+
const runner = config.runners[runnerName];
|
|
584
|
+
if (runner === undefined || runner.kind === 'fake') {
|
|
585
|
+
return 'unknown';
|
|
586
|
+
}
|
|
587
|
+
return runner.models[run.action] ?? runner.models.default ?? runner.model;
|
|
588
|
+
}
|
|
589
|
+
function completedAtForItem(item) {
|
|
590
|
+
const doneEntry = item.wake.stageHistory.find((entry) => entry.stage === 'done');
|
|
591
|
+
if (doneEntry !== undefined) {
|
|
592
|
+
return doneEntry.changedAt;
|
|
593
|
+
}
|
|
594
|
+
if (item.issue.state === 'closed') {
|
|
595
|
+
return item.issue.updatedAt;
|
|
596
|
+
}
|
|
597
|
+
if (isTerminalStage(item.wake.stage)) {
|
|
598
|
+
return item.wake.stageHistory.at(-1)?.changedAt ?? item.issue.updatedAt;
|
|
599
|
+
}
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
function startedAtForItem(item) {
|
|
603
|
+
return item.issue.createdAt ?? item.wake.stageHistory[0]?.changedAt;
|
|
604
|
+
}
|
|
605
|
+
function completedWorkItemDurations(items, buckets) {
|
|
606
|
+
const rows = [];
|
|
607
|
+
for (const item of items) {
|
|
608
|
+
const completedAt = completedAtForItem(item);
|
|
609
|
+
const startedAt = startedAtForItem(item);
|
|
610
|
+
if (completedAt === undefined ||
|
|
611
|
+
startedAt === undefined ||
|
|
612
|
+
!inMetricsWindow(completedAt, buckets)) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const completedMs = Date.parse(completedAt);
|
|
616
|
+
const startedMs = Date.parse(startedAt);
|
|
617
|
+
if (!Number.isFinite(completedMs) || !Number.isFinite(startedMs) || completedMs < startedMs) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
rows.push({
|
|
621
|
+
key: item.workItemKey,
|
|
622
|
+
repo: item.issue.repo,
|
|
623
|
+
issueNumber: item.issue.number,
|
|
624
|
+
durationMs: completedMs - startedMs,
|
|
625
|
+
completedAt,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return rows.sort((left, right) => right.completedAt.localeCompare(left.completedAt));
|
|
629
|
+
}
|
|
630
|
+
function buildMetricsSummary(runs, workItemDurations) {
|
|
631
|
+
const runDurations = runs
|
|
632
|
+
.map((run) => runDurationMs(run))
|
|
633
|
+
.filter((duration) => duration !== undefined);
|
|
634
|
+
return {
|
|
635
|
+
totalRuns: runs.length,
|
|
636
|
+
completedRuns: runs.filter((run) => run.status === 'completed').length,
|
|
637
|
+
blockedRuns: runs.filter((run) => run.status === 'blocked').length,
|
|
638
|
+
awaitingApprovalRuns: runs.filter((run) => run.status === 'awaiting-approval').length,
|
|
639
|
+
failedRuns: runs.filter((run) => run.status === 'failed').length,
|
|
640
|
+
totalTokens: runs.reduce((total, run) => total + runTokenTotal(run), 0),
|
|
641
|
+
totalCostUsd: runs.reduce((total, run) => total + (run.tokenUsage?.costUsd ?? 0), 0),
|
|
642
|
+
medianRunDurationMs: median(runDurations),
|
|
643
|
+
completedWorkItems: workItemDurations.length,
|
|
644
|
+
medianWorkItemDurationMs: median(workItemDurations.map((row) => row.durationMs)),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
export async function buildMetrics(input) {
|
|
648
|
+
const window = parseMetricsWindow(input.window);
|
|
649
|
+
const metric = parseMetricsMetric(input.metric);
|
|
650
|
+
const buckets = createTimeBuckets(window, input.now);
|
|
651
|
+
const [allRuns, items] = await Promise.all([
|
|
652
|
+
listRunsForBuckets(input.stateStore, buckets),
|
|
653
|
+
input.stateStore.listIssueStates(),
|
|
654
|
+
]);
|
|
655
|
+
const runs = allRuns.filter((run) => inMetricsWindow(run.startedAt, buckets));
|
|
656
|
+
const workItemDurations = completedWorkItemDurations(items, buckets);
|
|
657
|
+
const summary = buildMetricsSummary(runs, workItemDurations);
|
|
658
|
+
const runCountsForBucket = (bucket) => {
|
|
659
|
+
const bucketRuns = runs.filter((run) => findBucket(run.startedAt, buckets)?.bucket === bucket.bucket);
|
|
660
|
+
return {
|
|
661
|
+
bucket: bucket.bucket,
|
|
662
|
+
label: bucket.label,
|
|
663
|
+
total: bucketRuns.length,
|
|
664
|
+
completed: bucketRuns.filter((run) => run.status === 'completed').length,
|
|
665
|
+
blocked: bucketRuns.filter((run) => run.status === 'blocked').length,
|
|
666
|
+
awaitingApproval: bucketRuns.filter((run) => run.status === 'awaiting-approval').length,
|
|
667
|
+
failed: bucketRuns.filter((run) => run.status === 'failed').length,
|
|
668
|
+
other: bucketRuns.filter((run) => !['completed', 'blocked', 'awaiting-approval', 'failed'].includes(run.status)).length,
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
const tokenCountsForBucket = (bucket) => {
|
|
672
|
+
const bucketRuns = runs.filter((run) => findBucket(run.startedAt, buckets)?.bucket === bucket.bucket);
|
|
673
|
+
return {
|
|
674
|
+
bucket: bucket.bucket,
|
|
675
|
+
label: bucket.label,
|
|
676
|
+
tokens: bucketRuns.reduce((total, run) => total + runTokenTotal(run), 0),
|
|
677
|
+
inputTokens: bucketRuns.reduce((total, run) => total + (run.tokenUsage?.inputTokens ?? 0), 0),
|
|
678
|
+
outputTokens: bucketRuns.reduce((total, run) => total + (run.tokenUsage?.outputTokens ?? 0), 0),
|
|
679
|
+
cacheCreationInputTokens: bucketRuns.reduce((total, run) => total + (run.tokenUsage?.cacheCreationInputTokens ?? 0), 0),
|
|
680
|
+
cacheReadInputTokens: bucketRuns.reduce((total, run) => total + (run.tokenUsage?.cacheReadInputTokens ?? 0), 0),
|
|
681
|
+
costUsd: bucketRuns.reduce((total, run) => total + (run.tokenUsage?.costUsd ?? 0), 0),
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
switch (metric) {
|
|
685
|
+
case 'runs-by-status':
|
|
686
|
+
return {
|
|
687
|
+
window,
|
|
688
|
+
metric,
|
|
689
|
+
generatedAt: input.now.toISOString(),
|
|
690
|
+
summary,
|
|
691
|
+
detail: {
|
|
692
|
+
kind: 'run-counts',
|
|
693
|
+
group: 'status',
|
|
694
|
+
rows: groupCount(runs, (run) => run.status),
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
case 'runs-by-status-over-time':
|
|
698
|
+
return {
|
|
699
|
+
window,
|
|
700
|
+
metric,
|
|
701
|
+
generatedAt: input.now.toISOString(),
|
|
702
|
+
summary,
|
|
703
|
+
detail: {
|
|
704
|
+
kind: 'runs-by-status-over-time',
|
|
705
|
+
rows: buckets.map(runCountsForBucket),
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
case 'runs-by-action':
|
|
709
|
+
return {
|
|
710
|
+
window,
|
|
711
|
+
metric,
|
|
712
|
+
generatedAt: input.now.toISOString(),
|
|
713
|
+
summary,
|
|
714
|
+
detail: {
|
|
715
|
+
kind: 'run-counts',
|
|
716
|
+
group: 'action',
|
|
717
|
+
rows: groupCount(runs, (run) => run.action),
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
case 'runs-by-repo':
|
|
721
|
+
return {
|
|
722
|
+
window,
|
|
723
|
+
metric,
|
|
724
|
+
generatedAt: input.now.toISOString(),
|
|
725
|
+
summary,
|
|
726
|
+
detail: {
|
|
727
|
+
kind: 'run-counts',
|
|
728
|
+
group: 'repo',
|
|
729
|
+
rows: groupCount(runs, (run) => run.repo),
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
case 'runs-by-runner':
|
|
733
|
+
return {
|
|
734
|
+
window,
|
|
735
|
+
metric,
|
|
736
|
+
generatedAt: input.now.toISOString(),
|
|
737
|
+
summary,
|
|
738
|
+
detail: {
|
|
739
|
+
kind: 'run-counts',
|
|
740
|
+
group: 'runner',
|
|
741
|
+
rows: groupCount(runs, runnerKey),
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
case 'runs-by-model':
|
|
745
|
+
return {
|
|
746
|
+
window,
|
|
747
|
+
metric,
|
|
748
|
+
generatedAt: input.now.toISOString(),
|
|
749
|
+
summary,
|
|
750
|
+
detail: {
|
|
751
|
+
kind: 'run-counts',
|
|
752
|
+
group: 'model',
|
|
753
|
+
rows: groupCount(runs, (run) => modelKey(run, input.config)),
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
case 'runs-by-tier':
|
|
757
|
+
return {
|
|
758
|
+
window,
|
|
759
|
+
metric,
|
|
760
|
+
generatedAt: input.now.toISOString(),
|
|
761
|
+
summary,
|
|
762
|
+
detail: {
|
|
763
|
+
kind: 'run-counts',
|
|
764
|
+
group: 'tier',
|
|
765
|
+
rows: groupCount(runs, tierKey),
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
case 'tokens-over-time':
|
|
769
|
+
return {
|
|
770
|
+
window,
|
|
771
|
+
metric,
|
|
772
|
+
generatedAt: input.now.toISOString(),
|
|
773
|
+
summary,
|
|
774
|
+
detail: { kind: 'tokens-over-time', rows: buckets.map(tokenCountsForBucket) },
|
|
775
|
+
};
|
|
776
|
+
case 'tokens-by-action':
|
|
777
|
+
return {
|
|
778
|
+
window,
|
|
779
|
+
metric,
|
|
780
|
+
generatedAt: input.now.toISOString(),
|
|
781
|
+
summary,
|
|
782
|
+
detail: {
|
|
783
|
+
kind: 'token-counts',
|
|
784
|
+
group: 'action',
|
|
785
|
+
rows: groupTokens(runs, (run) => run.action),
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
case 'tokens-by-runner':
|
|
789
|
+
return {
|
|
790
|
+
window,
|
|
791
|
+
metric,
|
|
792
|
+
generatedAt: input.now.toISOString(),
|
|
793
|
+
summary,
|
|
794
|
+
detail: {
|
|
795
|
+
kind: 'token-counts',
|
|
796
|
+
group: 'runner',
|
|
797
|
+
rows: groupTokens(runs, runnerKey),
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
case 'tokens-by-model':
|
|
801
|
+
return {
|
|
802
|
+
window,
|
|
803
|
+
metric,
|
|
804
|
+
generatedAt: input.now.toISOString(),
|
|
805
|
+
summary,
|
|
806
|
+
detail: {
|
|
807
|
+
kind: 'token-counts',
|
|
808
|
+
group: 'model',
|
|
809
|
+
rows: groupTokens(runs, (run) => modelKey(run, input.config)),
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
case 'duration-by-action':
|
|
813
|
+
return {
|
|
814
|
+
window,
|
|
815
|
+
metric,
|
|
816
|
+
generatedAt: input.now.toISOString(),
|
|
817
|
+
summary,
|
|
818
|
+
detail: {
|
|
819
|
+
kind: 'durations',
|
|
820
|
+
group: 'action',
|
|
821
|
+
rows: groupDurations(runs, (run) => run.action),
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
case 'duration-over-time':
|
|
825
|
+
return {
|
|
826
|
+
window,
|
|
827
|
+
metric,
|
|
828
|
+
generatedAt: input.now.toISOString(),
|
|
829
|
+
summary,
|
|
830
|
+
detail: {
|
|
831
|
+
kind: 'duration-over-time',
|
|
832
|
+
rows: buckets.map((bucket) => {
|
|
833
|
+
const values = runs
|
|
834
|
+
.filter((run) => findBucket(run.startedAt, buckets)?.bucket === bucket.bucket)
|
|
835
|
+
.map((run) => runDurationMs(run))
|
|
836
|
+
.filter((duration) => duration !== undefined);
|
|
837
|
+
return {
|
|
838
|
+
bucket: bucket.bucket,
|
|
839
|
+
label: bucket.label,
|
|
840
|
+
count: values.length,
|
|
841
|
+
totalMs: values.reduce((total, value) => total + value, 0),
|
|
842
|
+
averageMs: average(values),
|
|
843
|
+
medianMs: median(values) ?? 0,
|
|
844
|
+
};
|
|
845
|
+
}),
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
case 'work-items-over-time':
|
|
849
|
+
return {
|
|
850
|
+
window,
|
|
851
|
+
metric,
|
|
852
|
+
generatedAt: input.now.toISOString(),
|
|
853
|
+
summary,
|
|
854
|
+
detail: {
|
|
855
|
+
kind: 'work-items-over-time',
|
|
856
|
+
rows: buckets.map((bucket) => ({
|
|
857
|
+
bucket: bucket.bucket,
|
|
858
|
+
label: bucket.label,
|
|
859
|
+
completed: workItemDurations.filter((row) => findBucket(row.completedAt, buckets)?.bucket === bucket.bucket).length,
|
|
860
|
+
})),
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
case 'work-item-durations':
|
|
864
|
+
return {
|
|
865
|
+
window,
|
|
866
|
+
metric,
|
|
867
|
+
generatedAt: input.now.toISOString(),
|
|
868
|
+
summary,
|
|
869
|
+
detail: { kind: 'work-item-durations', rows: workItemDurations },
|
|
870
|
+
};
|
|
871
|
+
case 'runs-over-time':
|
|
872
|
+
return {
|
|
873
|
+
window,
|
|
874
|
+
metric,
|
|
875
|
+
generatedAt: input.now.toISOString(),
|
|
876
|
+
summary,
|
|
877
|
+
detail: { kind: 'runs-over-time', rows: buckets.map(runCountsForBucket) },
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
367
881
|
const secretKeyPattern = /token|secret|key|password/i;
|
|
368
882
|
function redact(value, keyHint = '') {
|
|
369
883
|
if (secretKeyPattern.test(keyHint) && typeof value === 'string') {
|
|
@@ -5,7 +5,7 @@ import { configuredTicketSource } from '../../domain/sources.js';
|
|
|
5
5
|
import { createEventEnvelope } from '../../lib/event-log.js';
|
|
6
6
|
import { writeJsonFile } from '../../lib/json-file.js';
|
|
7
7
|
import { indexHtml } from './ui-assets.js';
|
|
8
|
-
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildItemTranscripts, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
8
|
+
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildItemTranscripts, buildMetrics, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
9
9
|
function sendJson(res, status, body) {
|
|
10
10
|
const payload = JSON.stringify(body, null, 2);
|
|
11
11
|
res.writeHead(status, {
|
|
@@ -218,6 +218,16 @@ async function handleRequest(req, res, options, now, projectionUpdater) {
|
|
|
218
218
|
}));
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
|
+
if (resource === 'metrics' && segments.length === 1) {
|
|
222
|
+
sendJson(res, 200, await buildMetrics({
|
|
223
|
+
stateStore,
|
|
224
|
+
config,
|
|
225
|
+
now: now(),
|
|
226
|
+
window: url.searchParams.get('window') ?? undefined,
|
|
227
|
+
metric: url.searchParams.get('metric') ?? undefined,
|
|
228
|
+
}));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
221
231
|
if (resource === 'events' && segments.length === 1) {
|
|
222
232
|
const limitParam = url.searchParams.get('limit');
|
|
223
233
|
sendJson(res, 200, await buildEventsFeed({
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolis-hq/wake",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Local autonomous agent control plane for software development",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
25
|
+
"node": ">=24"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@eslint/js": "^10.0.1",
|
|
57
|
-
"@types/node": "^24.
|
|
58
|
-
"eslint": "^10.
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
58
|
+
"eslint": "^10.8.0",
|
|
59
59
|
"globals": "^17.7.0",
|
|
60
|
-
"prettier": "^3.9.
|
|
61
|
-
"tsx": "^4.
|
|
62
|
-
"typescript": "^
|
|
63
|
-
"typescript-eslint": "^8.
|
|
64
|
-
"vitest": "^
|
|
60
|
+
"prettier": "^3.9.6",
|
|
61
|
+
"tsx": "^4.23.1",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"typescript-eslint": "^8.65.0",
|
|
64
|
+
"vitest": "^4.1.10"
|
|
65
65
|
}
|
|
66
66
|
}
|