@agenttrace-io/dashboard 0.1.9
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/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +520 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/public/404.html +70 -0
- package/public/app.js +901 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/error.html +81 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +193 -0
- package/public/style.css +1018 -0
- package/public/usage.html +284 -0
- package/public/usage.js +663 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentTrace Dashboard — Complete Vanilla JS Rewrite
|
|
3
|
+
* No frameworks. Pure HTML/CSS/JS. Follows exact design system.
|
|
4
|
+
*/
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// State
|
|
9
|
+
var state = {
|
|
10
|
+
stats: null,
|
|
11
|
+
runs: [],
|
|
12
|
+
traces: [],
|
|
13
|
+
selectedRunId: null,
|
|
14
|
+
selectedTraceId: null,
|
|
15
|
+
statusFilter: 'all',
|
|
16
|
+
dateRange: 'all',
|
|
17
|
+
searchTerm: '',
|
|
18
|
+
autoRefreshId: null,
|
|
19
|
+
lastRefresh: 0,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Utils
|
|
23
|
+
function $(id) {
|
|
24
|
+
return document.getElementById(id);
|
|
25
|
+
}
|
|
26
|
+
function el(tag, cls, text) {
|
|
27
|
+
var e = document.createElement(tag);
|
|
28
|
+
if (cls) e.className = cls;
|
|
29
|
+
if (text != null) e.textContent = text;
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
function fmtLatency(ms) {
|
|
33
|
+
if (ms == null) return '0ms';
|
|
34
|
+
if (ms < 1000) return Math.round(ms) + 'ms';
|
|
35
|
+
return (ms / 1000).toFixed(1) + 's';
|
|
36
|
+
}
|
|
37
|
+
function fmtCost(c) {
|
|
38
|
+
if (c == null) return '$0.0000';
|
|
39
|
+
return '$' + Number(c).toFixed(4);
|
|
40
|
+
}
|
|
41
|
+
function fmtPct(r) {
|
|
42
|
+
if (r == null) return '0%';
|
|
43
|
+
return (Number(r) * 100).toFixed(1) + '%';
|
|
44
|
+
}
|
|
45
|
+
function fmtNum(n) {
|
|
46
|
+
if (n == null) return '0';
|
|
47
|
+
return Number(n).toLocaleString();
|
|
48
|
+
}
|
|
49
|
+
function statusCls(s) {
|
|
50
|
+
if (!s) return '';
|
|
51
|
+
var v = String(s).toLowerCase();
|
|
52
|
+
if (v === 'success') return 'success';
|
|
53
|
+
if (v === 'failure' || v === 'error') return 'failure';
|
|
54
|
+
if (v === 'running') return 'running';
|
|
55
|
+
if (v === 'timeout') return 'timeout';
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
function relTime(ts) {
|
|
59
|
+
if (!ts) return '';
|
|
60
|
+
var d = Date.now() - new Date(ts).getTime();
|
|
61
|
+
if (d < 0) d = 0;
|
|
62
|
+
var s = Math.floor(d / 1000);
|
|
63
|
+
if (s < 60) return s + 's ago';
|
|
64
|
+
var m = Math.floor(s / 60);
|
|
65
|
+
if (m < 60) return m + 'm ago';
|
|
66
|
+
var h = Math.floor(m / 60);
|
|
67
|
+
if (h < 24) return h + 'h ago';
|
|
68
|
+
return Math.floor(h / 24) + 'd ago';
|
|
69
|
+
}
|
|
70
|
+
function withinRange(run, range) {
|
|
71
|
+
if (range === 'all') return true;
|
|
72
|
+
var t = new Date(run.startedAt || run.createdAt || 0).getTime();
|
|
73
|
+
var now = Date.now();
|
|
74
|
+
if (range === '1h') return now - t <= 3600000;
|
|
75
|
+
if (range === 'today') {
|
|
76
|
+
var start = new Date();
|
|
77
|
+
start.setHours(0, 0, 0, 0);
|
|
78
|
+
return t >= start.getTime();
|
|
79
|
+
}
|
|
80
|
+
if (range === 'week') {
|
|
81
|
+
var w = new Date();
|
|
82
|
+
w.setDate(w.getDate() - 7);
|
|
83
|
+
w.setHours(0, 0, 0, 0);
|
|
84
|
+
return t >= w.getTime();
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fetch
|
|
90
|
+
async function fetchJSON(url) {
|
|
91
|
+
var res = await fetch(url);
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
var msg = 'Request failed: ' + res.status;
|
|
94
|
+
try {
|
|
95
|
+
var j = await res.json();
|
|
96
|
+
if (j && j.error) msg = j.error;
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
throw new Error(msg);
|
|
99
|
+
}
|
|
100
|
+
return res.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Data
|
|
104
|
+
async function loadStats() {
|
|
105
|
+
state.stats = await fetchJSON('/api/stats');
|
|
106
|
+
renderStats();
|
|
107
|
+
renderBurnAndTop();
|
|
108
|
+
return state.stats;
|
|
109
|
+
}
|
|
110
|
+
async function loadRuns() {
|
|
111
|
+
var runs = await fetchJSON('/api/runs?limit=400');
|
|
112
|
+
state.runs = Array.isArray(runs) ? runs : [];
|
|
113
|
+
renderRuns();
|
|
114
|
+
return state.runs;
|
|
115
|
+
}
|
|
116
|
+
async function loadTraces(runId) {
|
|
117
|
+
if (!runId) return [];
|
|
118
|
+
var url = '/api/traces?runId=' + encodeURIComponent(runId) + '&limit=300';
|
|
119
|
+
var t = await fetchJSON(url);
|
|
120
|
+
state.traces = Array.isArray(t) ? t : [];
|
|
121
|
+
renderTraces();
|
|
122
|
+
return state.traces;
|
|
123
|
+
}
|
|
124
|
+
async function loadTraceDetail(id) {
|
|
125
|
+
try {
|
|
126
|
+
return await fetchJSON('/api/traces/' + encodeURIComponent(id));
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return (
|
|
129
|
+
state.traces.find(function (x) {
|
|
130
|
+
return x.id === id;
|
|
131
|
+
}) || null
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function refreshAll(keepSel) {
|
|
137
|
+
try {
|
|
138
|
+
await loadStats();
|
|
139
|
+
await loadRuns();
|
|
140
|
+
if (keepSel && state.selectedRunId) {
|
|
141
|
+
await loadTraces(state.selectedRunId);
|
|
142
|
+
if (state.selectedTraceId) {
|
|
143
|
+
var still = state.traces.find(function (t) {
|
|
144
|
+
return t.id === state.selectedTraceId;
|
|
145
|
+
});
|
|
146
|
+
if (still) renderTraceDetails(still);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
state.lastRefresh = Date.now();
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.warn('[AgentTrace] refresh error', e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Rendering — Stats with sparklines (mini SVG)
|
|
156
|
+
function sparklineSVG(values, w, h, color) {
|
|
157
|
+
if (!values || !values.length) values = [0, 0];
|
|
158
|
+
var min = Math.min.apply(null, values);
|
|
159
|
+
var max = Math.max.apply(null, values);
|
|
160
|
+
if (max === min) max = min + 1;
|
|
161
|
+
var pts = values
|
|
162
|
+
.map(function (v, i) {
|
|
163
|
+
var x = (i / (values.length - 1)) * w;
|
|
164
|
+
var y = h - ((v - min) / (max - min)) * h;
|
|
165
|
+
return x.toFixed(1) + ',' + y.toFixed(1);
|
|
166
|
+
})
|
|
167
|
+
.join(' ');
|
|
168
|
+
var svg =
|
|
169
|
+
'<svg width="' +
|
|
170
|
+
w +
|
|
171
|
+
'" height="' +
|
|
172
|
+
h +
|
|
173
|
+
'" viewBox="0 0 ' +
|
|
174
|
+
w +
|
|
175
|
+
' ' +
|
|
176
|
+
h +
|
|
177
|
+
'" preserveAspectRatio="none">' +
|
|
178
|
+
'<polyline points="' +
|
|
179
|
+
pts +
|
|
180
|
+
'" fill="none" stroke="' +
|
|
181
|
+
(color || '#3b82f6') +
|
|
182
|
+
'" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>' +
|
|
183
|
+
'</svg>';
|
|
184
|
+
return svg;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderStats() {
|
|
188
|
+
var s = state.stats || {};
|
|
189
|
+
var html = '';
|
|
190
|
+
var total = s.totalRuns || 0;
|
|
191
|
+
var rate = s.successRate || 0;
|
|
192
|
+
var lat = s.avgLatencyMs || 0;
|
|
193
|
+
var cost = s.totalCostUsd || 0;
|
|
194
|
+
|
|
195
|
+
// Build tiny history arrays from top-level if present, else synthesize from runs
|
|
196
|
+
var tokenHist = (s.tokenHistory || []).slice(-12);
|
|
197
|
+
var costHist = (s.costHistory || []).slice(-12);
|
|
198
|
+
if ((!tokenHist.length || !costHist.length) && state.runs.length) {
|
|
199
|
+
// synthesize from recent runs (cost and tokens)
|
|
200
|
+
var recent = state.runs.slice(0, 12).reverse();
|
|
201
|
+
tokenHist = recent.map(function (r) {
|
|
202
|
+
return (r.totalTokens && r.totalTokens.totalTokens) || 0;
|
|
203
|
+
});
|
|
204
|
+
costHist = recent.map(function (r) {
|
|
205
|
+
return r.totalCostUsd || 0;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (!tokenHist.length) tokenHist = [0, 1, 0, 2, 1, 3];
|
|
209
|
+
if (!costHist.length) costHist = [0, 0.001, 0.002, 0.0015, 0.003, 0.0025];
|
|
210
|
+
|
|
211
|
+
html +=
|
|
212
|
+
'<div class="stat-card">' +
|
|
213
|
+
'<div class="stat-value" id="total-runs">' +
|
|
214
|
+
fmtNum(total) +
|
|
215
|
+
'</div>' +
|
|
216
|
+
'<div class="stat-label">Total Runs</div>' +
|
|
217
|
+
'<div class="stat-spark">' +
|
|
218
|
+
sparklineSVG(tokenHist, 110, 26, '#3b82f6') +
|
|
219
|
+
'</div>' +
|
|
220
|
+
'</div>';
|
|
221
|
+
|
|
222
|
+
html +=
|
|
223
|
+
'<div class="stat-card">' +
|
|
224
|
+
'<div class="stat-value" id="success-rate">' +
|
|
225
|
+
fmtPct(rate) +
|
|
226
|
+
'</div>' +
|
|
227
|
+
'<div class="stat-label">Success Rate</div>' +
|
|
228
|
+
'<div class="stat-spark">' +
|
|
229
|
+
sparklineSVG(
|
|
230
|
+
state.runs.length
|
|
231
|
+
? state.runs
|
|
232
|
+
.slice(0, 12)
|
|
233
|
+
.reverse()
|
|
234
|
+
.map(function (r) {
|
|
235
|
+
return r.status === 'success' ? 1 : 0;
|
|
236
|
+
})
|
|
237
|
+
: [1, 1, 0, 1, 1, 1],
|
|
238
|
+
110,
|
|
239
|
+
26,
|
|
240
|
+
'#22c55e',
|
|
241
|
+
) +
|
|
242
|
+
'</div>' +
|
|
243
|
+
'</div>';
|
|
244
|
+
|
|
245
|
+
html +=
|
|
246
|
+
'<div class="stat-card">' +
|
|
247
|
+
'<div class="stat-value" id="avg-latency">' +
|
|
248
|
+
fmtLatency(lat) +
|
|
249
|
+
'</div>' +
|
|
250
|
+
'<div class="stat-label">Avg Latency</div>' +
|
|
251
|
+
'<div class="stat-spark">' +
|
|
252
|
+
sparklineSVG(
|
|
253
|
+
state.runs.length
|
|
254
|
+
? state.runs
|
|
255
|
+
.slice(0, 12)
|
|
256
|
+
.reverse()
|
|
257
|
+
.map(function (r) {
|
|
258
|
+
return r.totalLatencyMs || 0;
|
|
259
|
+
})
|
|
260
|
+
: [80, 120, 90, 140, 110, 95],
|
|
261
|
+
110,
|
|
262
|
+
26,
|
|
263
|
+
'#eab308',
|
|
264
|
+
) +
|
|
265
|
+
'</div>' +
|
|
266
|
+
'</div>';
|
|
267
|
+
|
|
268
|
+
html +=
|
|
269
|
+
'<div class="stat-card">' +
|
|
270
|
+
'<div class="stat-value" id="total-cost">' +
|
|
271
|
+
fmtCost(cost) +
|
|
272
|
+
'</div>' +
|
|
273
|
+
'<div class="stat-label">Total Cost (USD)</div>' +
|
|
274
|
+
'<div class="stat-spark">' +
|
|
275
|
+
sparklineSVG(costHist, 110, 26, '#3b82f6') +
|
|
276
|
+
'</div>' +
|
|
277
|
+
'</div>';
|
|
278
|
+
|
|
279
|
+
var grid = $('stats');
|
|
280
|
+
if (grid) grid.innerHTML = html;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Burn rate + Top agents (computed client side)
|
|
284
|
+
function computeBurnAndTop() {
|
|
285
|
+
var runs = state.runs || [];
|
|
286
|
+
if (!runs.length) return { tpm: 0, cph: 0, top: [] };
|
|
287
|
+
|
|
288
|
+
var now = Date.now();
|
|
289
|
+
var hourAgo = now - 3600000;
|
|
290
|
+
var recent = runs.filter(function (r) {
|
|
291
|
+
var t = new Date(r.startedAt || r.createdAt || 0).getTime();
|
|
292
|
+
return t >= hourAgo;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
var totalTokens = 0;
|
|
296
|
+
var totalCost = 0;
|
|
297
|
+
recent.forEach(function (r) {
|
|
298
|
+
totalTokens += (r.totalTokens && r.totalTokens.totalTokens) || 0;
|
|
299
|
+
totalCost += r.totalCostUsd || 0;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
var mins = Math.max(1, (now - hourAgo) / 60000);
|
|
303
|
+
var tpm = Math.round(totalTokens / mins);
|
|
304
|
+
var cph = (totalCost / (mins / 60)) * 1; // cost per hour based on last hour window
|
|
305
|
+
|
|
306
|
+
// Top agents by cost (from runs.name heuristic or metadata)
|
|
307
|
+
var byAgent = {};
|
|
308
|
+
runs.forEach(function (r) {
|
|
309
|
+
var name = (r.name || '').split('.')[0] || 'unknown';
|
|
310
|
+
byAgent[name] = (byAgent[name] || 0) + (r.totalCostUsd || 0);
|
|
311
|
+
});
|
|
312
|
+
var top = Object.entries(byAgent)
|
|
313
|
+
.sort(function (a, b) {
|
|
314
|
+
return b[1] - a[1];
|
|
315
|
+
})
|
|
316
|
+
.slice(0, 5)
|
|
317
|
+
.map(function (e) {
|
|
318
|
+
return { name: e[0], cost: e[1] };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return { tpm: tpm, cph: cph, top: top };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderBurnAndTop() {
|
|
325
|
+
var b = computeBurnAndTop();
|
|
326
|
+
var bt = $('burn-tokens');
|
|
327
|
+
var bc = $('burn-cost');
|
|
328
|
+
if (bt) bt.textContent = fmtNum(b.tpm) + ' t/min';
|
|
329
|
+
if (bc) bc.textContent = fmtCost(b.cph) + '/hr (last hour)';
|
|
330
|
+
|
|
331
|
+
var list = $('top-agents-list');
|
|
332
|
+
if (!list) return;
|
|
333
|
+
list.innerHTML = '';
|
|
334
|
+
if (!b.top.length) {
|
|
335
|
+
list.innerHTML = '<div class="empty small">No agent cost data yet</div>';
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
b.top.forEach(function (a) {
|
|
339
|
+
var row = el('div', 'row');
|
|
340
|
+
row.innerHTML =
|
|
341
|
+
'<span class="name">' + a.name + '</span><span class="cost">' + fmtCost(a.cost) + '</span>';
|
|
342
|
+
list.appendChild(row);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Runs list with relative time + mini cost bars
|
|
347
|
+
function renderRuns() {
|
|
348
|
+
var c = $('runs-list');
|
|
349
|
+
if (!c) return;
|
|
350
|
+
c.innerHTML = '';
|
|
351
|
+
|
|
352
|
+
var filtered = state.runs.filter(function (r) {
|
|
353
|
+
var okStatus = state.statusFilter === 'all' || r.status === state.statusFilter;
|
|
354
|
+
var okRange = withinRange(r, state.dateRange);
|
|
355
|
+
var okSearch =
|
|
356
|
+
!state.searchTerm ||
|
|
357
|
+
(r.name || '').toLowerCase().indexOf(state.searchTerm.toLowerCase()) !== -1;
|
|
358
|
+
return okStatus && okRange && okSearch;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
var countEl = $('runs-count');
|
|
362
|
+
if (countEl)
|
|
363
|
+
countEl.textContent = filtered.length + ' run' + (filtered.length === 1 ? '' : 's');
|
|
364
|
+
var totalEl = $('runs-total');
|
|
365
|
+
if (totalEl) totalEl.textContent = fmtNum(state.runs.length);
|
|
366
|
+
|
|
367
|
+
if (!filtered.length) {
|
|
368
|
+
var msg = state.runs.length
|
|
369
|
+
? 'No runs match your filters'
|
|
370
|
+
: 'No runs yet. Wrap your first agent!';
|
|
371
|
+
var empty = el('div', 'empty');
|
|
372
|
+
empty.innerHTML =
|
|
373
|
+
'<div class="icon">◌</div>' +
|
|
374
|
+
'<div>' +
|
|
375
|
+
msg +
|
|
376
|
+
'</div>' +
|
|
377
|
+
(state.runs.length
|
|
378
|
+
? ''
|
|
379
|
+
: '<small>Run <code>npx agenttrace-io dashboard</code> after instrumenting.</small>');
|
|
380
|
+
c.appendChild(empty);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
var maxCost = 0;
|
|
385
|
+
filtered.forEach(function (r) {
|
|
386
|
+
if ((r.totalCostUsd || 0) > maxCost) maxCost = r.totalCostUsd || 0;
|
|
387
|
+
});
|
|
388
|
+
if (maxCost <= 0) maxCost = 0.0001;
|
|
389
|
+
|
|
390
|
+
filtered.forEach(function (run) {
|
|
391
|
+
var item = el('div', 'run-item');
|
|
392
|
+
item.setAttribute('role', 'listitem');
|
|
393
|
+
item.setAttribute('tabindex', '0');
|
|
394
|
+
item.dataset.runId = run.id;
|
|
395
|
+
|
|
396
|
+
if (state.selectedRunId === run.id) item.classList.add('selected');
|
|
397
|
+
|
|
398
|
+
var header = el('div', 'run-header');
|
|
399
|
+
var badge = el('span', 'badge ' + statusCls(run.status), run.status || 'unknown');
|
|
400
|
+
var title = el('span', 'run-title', run.name || run.id);
|
|
401
|
+
header.appendChild(badge);
|
|
402
|
+
header.appendChild(title);
|
|
403
|
+
|
|
404
|
+
var meta = el('div', 'run-meta');
|
|
405
|
+
meta.appendChild(el('span', 'meta-item', (run.traceCount || 0) + ' traces'));
|
|
406
|
+
meta.appendChild(el('span', 'meta-item', fmtLatency(run.totalLatencyMs || 0)));
|
|
407
|
+
var costStr = fmtCost(run.totalCostUsd || 0);
|
|
408
|
+
var costSpan = el('span', 'meta-item', costStr);
|
|
409
|
+
// mini cost bar
|
|
410
|
+
var pct = Math.max(2, Math.min(100, Math.round(((run.totalCostUsd || 0) / maxCost) * 100)));
|
|
411
|
+
var bar = el('span', 'cost-bar');
|
|
412
|
+
bar.style.width = pct + 'px';
|
|
413
|
+
costSpan.appendChild(bar);
|
|
414
|
+
meta.appendChild(costSpan);
|
|
415
|
+
|
|
416
|
+
var rt = relTime(run.startedAt || run.createdAt);
|
|
417
|
+
if (rt) meta.appendChild(el('span', 'meta-item', rt));
|
|
418
|
+
|
|
419
|
+
if (run.errorCount > 0) meta.appendChild(el('span', 'meta-item', run.errorCount + ' errors'));
|
|
420
|
+
|
|
421
|
+
item.appendChild(header);
|
|
422
|
+
item.appendChild(meta);
|
|
423
|
+
|
|
424
|
+
// events
|
|
425
|
+
function selectThis() {
|
|
426
|
+
selectRun(run.id, item);
|
|
427
|
+
}
|
|
428
|
+
item.addEventListener('click', selectThis);
|
|
429
|
+
item.addEventListener('keydown', function (ev) {
|
|
430
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
431
|
+
ev.preventDefault();
|
|
432
|
+
selectThis();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
c.appendChild(item);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function selectRun(runId, elClicked) {
|
|
441
|
+
state.selectedRunId = runId;
|
|
442
|
+
state.selectedTraceId = null;
|
|
443
|
+
|
|
444
|
+
document.querySelectorAll('.run-item').forEach(function (it) {
|
|
445
|
+
it.classList.toggle('selected', it.dataset.runId === runId);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
var sec = $('traces-section');
|
|
449
|
+
var list = $('traces-list');
|
|
450
|
+
var nameEl = $('selected-run-name');
|
|
451
|
+
if (sec) sec.style.display = '';
|
|
452
|
+
if (list)
|
|
453
|
+
list.innerHTML = '<div class="skeleton skeleton-line" style="margin:12px 16px"></div>'.repeat(
|
|
454
|
+
3,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
var run = state.runs.find(function (r) {
|
|
458
|
+
return r.id === runId;
|
|
459
|
+
});
|
|
460
|
+
if (nameEl) nameEl.textContent = run && run.name ? run.name : runId;
|
|
461
|
+
|
|
462
|
+
var det = $('details-section');
|
|
463
|
+
if (det) det.style.display = 'none';
|
|
464
|
+
|
|
465
|
+
loadTraces(runId).catch(function () {
|
|
466
|
+
if (list) list.innerHTML = '<div class="empty">Failed to load traces</div>';
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function renderTraces() {
|
|
471
|
+
var c = $('traces-list');
|
|
472
|
+
var sec = $('traces-section');
|
|
473
|
+
var cnt = $('traces-count');
|
|
474
|
+
if (!c) return;
|
|
475
|
+
c.innerHTML = '';
|
|
476
|
+
|
|
477
|
+
if (cnt)
|
|
478
|
+
cnt.textContent = state.traces.length + ' trace' + (state.traces.length === 1 ? '' : 's');
|
|
479
|
+
|
|
480
|
+
if (!state.traces.length) {
|
|
481
|
+
c.appendChild(el('div', 'empty', 'No traces for this run'));
|
|
482
|
+
if (sec) sec.style.display = '';
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
state.traces.forEach(function (tr) {
|
|
487
|
+
var item = el('div', 'trace-item');
|
|
488
|
+
item.setAttribute('role', 'listitem');
|
|
489
|
+
item.setAttribute('tabindex', '0');
|
|
490
|
+
item.dataset.traceId = tr.id;
|
|
491
|
+
if (state.selectedTraceId === tr.id) item.classList.add('selected');
|
|
492
|
+
|
|
493
|
+
var hdr = el('div', 'trace-header');
|
|
494
|
+
hdr.appendChild(el('span', 'badge ' + statusCls(tr.status), tr.status || 'unknown'));
|
|
495
|
+
hdr.appendChild(el('span', 'trace-name', tr.name || 'trace'));
|
|
496
|
+
item.appendChild(hdr);
|
|
497
|
+
|
|
498
|
+
var m = el('div', 'trace-meta');
|
|
499
|
+
var tok = tr.tokens && tr.tokens.totalTokens ? tr.tokens.totalTokens + ' tokens' : '';
|
|
500
|
+
m.textContent =
|
|
501
|
+
fmtLatency(tr.latencyMs || 0) + ' • ' + fmtCost(tr.costUsd || 0) + (tok ? ' • ' + tok : '');
|
|
502
|
+
item.appendChild(m);
|
|
503
|
+
|
|
504
|
+
if (tr.toolCalls && tr.toolCalls.length) {
|
|
505
|
+
var tsum = el(
|
|
506
|
+
'div',
|
|
507
|
+
'',
|
|
508
|
+
tr.toolCalls.length + ' tool call' + (tr.toolCalls.length > 1 ? 's' : ''),
|
|
509
|
+
);
|
|
510
|
+
tsum.style.fontSize = '11px';
|
|
511
|
+
tsum.style.color = 'var(--text-muted)';
|
|
512
|
+
item.appendChild(tsum);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function pick() {
|
|
516
|
+
selectTrace(tr.id, item, tr);
|
|
517
|
+
}
|
|
518
|
+
item.addEventListener('click', pick);
|
|
519
|
+
item.addEventListener('keydown', function (ev) {
|
|
520
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
521
|
+
ev.preventDefault();
|
|
522
|
+
pick();
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
c.appendChild(item);
|
|
526
|
+
});
|
|
527
|
+
if (sec) sec.style.display = '';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function selectTrace(traceId, clicked, data) {
|
|
531
|
+
state.selectedTraceId = traceId;
|
|
532
|
+
document.querySelectorAll('.trace-item').forEach(function (it) {
|
|
533
|
+
it.classList.toggle('selected', it.dataset.traceId === traceId);
|
|
534
|
+
});
|
|
535
|
+
var dsec = $('details-section');
|
|
536
|
+
if (dsec) dsec.style.display = '';
|
|
537
|
+
if (data) {
|
|
538
|
+
renderTraceDetails(data);
|
|
539
|
+
} else {
|
|
540
|
+
loadTraceDetail(traceId)
|
|
541
|
+
.then(function (t) {
|
|
542
|
+
if (t) renderTraceDetails(t);
|
|
543
|
+
})
|
|
544
|
+
.catch(function () {
|
|
545
|
+
var box = $('trace-details');
|
|
546
|
+
if (box) box.innerHTML = '<div class="empty">Failed to load details</div>';
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Collapsible JSON + sections
|
|
552
|
+
function makeCollapsibleSection(title, bodyEl) {
|
|
553
|
+
var sec = el('div', 'detail-section');
|
|
554
|
+
var hdr = el('div', 'detail-section-header');
|
|
555
|
+
hdr.innerHTML = '<h4>' + title + '</h4><span class="chevron" aria-hidden="true">▾</span>';
|
|
556
|
+
var body = el('div', 'detail-section-body');
|
|
557
|
+
body.appendChild(bodyEl);
|
|
558
|
+
sec.appendChild(hdr);
|
|
559
|
+
sec.appendChild(body);
|
|
560
|
+
|
|
561
|
+
hdr.addEventListener('click', function () {
|
|
562
|
+
sec.classList.toggle('collapsed');
|
|
563
|
+
});
|
|
564
|
+
// default expanded
|
|
565
|
+
return sec;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function prettyJSON(obj) {
|
|
569
|
+
try {
|
|
570
|
+
return JSON.stringify(obj, null, 2);
|
|
571
|
+
} catch (_) {
|
|
572
|
+
return String(obj);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderTraceDetails(trace) {
|
|
577
|
+
var c = $('trace-details');
|
|
578
|
+
if (!c) return;
|
|
579
|
+
c.innerHTML = '';
|
|
580
|
+
|
|
581
|
+
// header
|
|
582
|
+
var head = el('div', 'trace-header');
|
|
583
|
+
head.appendChild(el('span', 'badge ' + statusCls(trace.status), trace.status || 'unknown'));
|
|
584
|
+
head.appendChild(el('span', 'trace-name', trace.name || trace.id));
|
|
585
|
+
c.appendChild(head);
|
|
586
|
+
|
|
587
|
+
// metrics (always visible)
|
|
588
|
+
var metrics = el('div');
|
|
589
|
+
var rows = [
|
|
590
|
+
['Latency', fmtLatency(trace.latencyMs)],
|
|
591
|
+
['Cost', fmtCost(trace.costUsd)],
|
|
592
|
+
[
|
|
593
|
+
'Tokens',
|
|
594
|
+
(trace.tokens && trace.tokens.totalTokens) +
|
|
595
|
+
' (p:' +
|
|
596
|
+
(trace.tokens && trace.tokens.promptTokens) +
|
|
597
|
+
' c:' +
|
|
598
|
+
(trace.tokens && trace.tokens.completionTokens) +
|
|
599
|
+
')',
|
|
600
|
+
],
|
|
601
|
+
['Model', (trace.tokens && trace.tokens.model) || '—'],
|
|
602
|
+
['Created', trace.createdAt ? new Date(trace.createdAt).toLocaleString() : '—'],
|
|
603
|
+
];
|
|
604
|
+
rows.forEach(function (r) {
|
|
605
|
+
var row = el('div', 'detail-row');
|
|
606
|
+
row.appendChild(el('span', 'key', r[0]));
|
|
607
|
+
row.appendChild(el('span', 'value', String(r[1])));
|
|
608
|
+
metrics.appendChild(row);
|
|
609
|
+
});
|
|
610
|
+
c.appendChild(metrics);
|
|
611
|
+
|
|
612
|
+
// Tool calls (collapsible)
|
|
613
|
+
if (trace.toolCalls && trace.toolCalls.length) {
|
|
614
|
+
var toolsWrap = el('div');
|
|
615
|
+
trace.toolCalls.forEach(function (tc) {
|
|
616
|
+
var tcEl = el('div', 'tool-call');
|
|
617
|
+
var th = el('div', 'tool-header');
|
|
618
|
+
th.appendChild(el('span', '', tc.name || 'tool'));
|
|
619
|
+
th.appendChild(
|
|
620
|
+
el('span', 'badge ' + (tc.success ? 'success' : 'failure'), tc.success ? 'ok' : 'fail'),
|
|
621
|
+
);
|
|
622
|
+
if (tc.latencyMs != null) th.appendChild(el('span', '', fmtLatency(tc.latencyMs)));
|
|
623
|
+
tcEl.appendChild(th);
|
|
624
|
+
|
|
625
|
+
if (tc.input != null) {
|
|
626
|
+
var preI = el('pre', 'json-block', prettyJSON(tc.input));
|
|
627
|
+
tcEl.appendChild(el('div', '', 'input:'));
|
|
628
|
+
tcEl.appendChild(preI);
|
|
629
|
+
}
|
|
630
|
+
if (tc.output != null) {
|
|
631
|
+
var preO = el('pre', 'json-block', prettyJSON(tc.output));
|
|
632
|
+
tcEl.appendChild(el('div', '', 'output:'));
|
|
633
|
+
tcEl.appendChild(preO);
|
|
634
|
+
}
|
|
635
|
+
if (tc.error) {
|
|
636
|
+
var er = el('div', 'value', 'Error: ' + tc.error);
|
|
637
|
+
er.style.color = 'var(--error)';
|
|
638
|
+
tcEl.appendChild(er);
|
|
639
|
+
}
|
|
640
|
+
toolsWrap.appendChild(tcEl);
|
|
641
|
+
});
|
|
642
|
+
c.appendChild(
|
|
643
|
+
makeCollapsibleSection('Tool Calls (' + trace.toolCalls.length + ')', toolsWrap),
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Error
|
|
648
|
+
if (trace.error) {
|
|
649
|
+
var erBox = el('div', 'detail-row');
|
|
650
|
+
erBox.appendChild(el('span', 'key', 'Error'));
|
|
651
|
+
var ev = el('span', 'value', trace.error);
|
|
652
|
+
ev.style.color = 'var(--error)';
|
|
653
|
+
erBox.appendChild(ev);
|
|
654
|
+
c.appendChild(erBox);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Input (collapsible)
|
|
658
|
+
var inPre = el('pre', 'json-block', prettyJSON(trace.input));
|
|
659
|
+
c.appendChild(makeCollapsibleSection('Input', inPre));
|
|
660
|
+
|
|
661
|
+
// Output (collapsible)
|
|
662
|
+
var outPre = el('pre', 'json-block', prettyJSON(trace.output));
|
|
663
|
+
c.appendChild(makeCollapsibleSection('Output', outPre));
|
|
664
|
+
|
|
665
|
+
// Tokens raw (collapsible)
|
|
666
|
+
if (trace.tokens) {
|
|
667
|
+
var tokPre = el('pre', 'json-block', prettyJSON(trace.tokens));
|
|
668
|
+
c.appendChild(makeCollapsibleSection('Token Usage', tokPre));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Filters, search, date range
|
|
673
|
+
function setupFilters() {
|
|
674
|
+
var group = $('status-filters');
|
|
675
|
+
if (group) {
|
|
676
|
+
group.addEventListener('click', function (ev) {
|
|
677
|
+
var btn = ev.target.closest('.filter-btn');
|
|
678
|
+
if (!btn) return;
|
|
679
|
+
group.querySelectorAll('.filter-btn').forEach(function (b) {
|
|
680
|
+
var act = b === btn;
|
|
681
|
+
b.classList.toggle('active', act);
|
|
682
|
+
b.setAttribute('aria-pressed', act ? 'true' : 'false');
|
|
683
|
+
});
|
|
684
|
+
state.statusFilter = btn.getAttribute('data-status') || 'all';
|
|
685
|
+
renderRuns();
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
var dr = $('date-range');
|
|
690
|
+
if (dr) {
|
|
691
|
+
dr.addEventListener('click', function (ev) {
|
|
692
|
+
var btn = ev.target.closest('.date-btn');
|
|
693
|
+
if (!btn) return;
|
|
694
|
+
dr.querySelectorAll('.date-btn').forEach(function (b) {
|
|
695
|
+
b.classList.toggle('active', b === btn);
|
|
696
|
+
});
|
|
697
|
+
state.dateRange = btn.getAttribute('data-range') || 'all';
|
|
698
|
+
renderRuns();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
var search = $('search-input');
|
|
703
|
+
if (search) {
|
|
704
|
+
var t;
|
|
705
|
+
search.addEventListener('input', function () {
|
|
706
|
+
clearTimeout(t);
|
|
707
|
+
t = setTimeout(function () {
|
|
708
|
+
state.searchTerm = search.value || '';
|
|
709
|
+
renderRuns();
|
|
710
|
+
}, 120);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Export
|
|
716
|
+
async function doExport(fmt) {
|
|
717
|
+
try {
|
|
718
|
+
var url = '/api/export?format=' + fmt;
|
|
719
|
+
var res = await fetch(url);
|
|
720
|
+
if (!res.ok) throw new Error('Export failed');
|
|
721
|
+
var blob = await res.blob();
|
|
722
|
+
var a = document.createElement('a');
|
|
723
|
+
a.href = URL.createObjectURL(blob);
|
|
724
|
+
a.download = 'agenttrace-export.' + fmt;
|
|
725
|
+
document.body.appendChild(a);
|
|
726
|
+
a.click();
|
|
727
|
+
document.body.removeChild(a);
|
|
728
|
+
setTimeout(function () {
|
|
729
|
+
URL.revokeObjectURL(a.href);
|
|
730
|
+
}, 800);
|
|
731
|
+
showToast('Exported ' + fmt.toUpperCase(), 'success');
|
|
732
|
+
} catch (e) {
|
|
733
|
+
showToast('Export failed: ' + e.message, 'error');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function setupExports() {
|
|
737
|
+
var j = $('export-json-btn');
|
|
738
|
+
var c = $('export-csv-btn');
|
|
739
|
+
if (j)
|
|
740
|
+
j.addEventListener('click', function () {
|
|
741
|
+
doExport('json');
|
|
742
|
+
});
|
|
743
|
+
if (c)
|
|
744
|
+
c.addEventListener('click', function () {
|
|
745
|
+
doExport('csv');
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Refresh button + keyboard
|
|
750
|
+
function setupRefresh() {
|
|
751
|
+
var btn = $('refresh-btn');
|
|
752
|
+
if (btn) {
|
|
753
|
+
btn.addEventListener('click', function () {
|
|
754
|
+
btn.classList.add('spinning');
|
|
755
|
+
refreshAll(true).finally(function () {
|
|
756
|
+
setTimeout(function () {
|
|
757
|
+
btn.classList.remove('spinning');
|
|
758
|
+
}, 400);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Keyboard shortcuts: j/k navigate runs, enter expand, esc close, r refresh
|
|
764
|
+
document.addEventListener('keydown', function (ev) {
|
|
765
|
+
if (ev.target && (ev.target.tagName === 'INPUT' || ev.target.tagName === 'TEXTAREA')) return;
|
|
766
|
+
|
|
767
|
+
var runs = Array.prototype.slice.call(document.querySelectorAll('.run-item'));
|
|
768
|
+
var idx = runs.findIndex(function (el) {
|
|
769
|
+
return el.classList.contains('selected');
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
if (ev.key.toLowerCase() === 'r') {
|
|
773
|
+
ev.preventDefault();
|
|
774
|
+
var rb = $('refresh-btn');
|
|
775
|
+
if (rb) rb.click();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (ev.key === 'Escape') {
|
|
779
|
+
ev.preventDefault();
|
|
780
|
+
var dsec = $('details-section');
|
|
781
|
+
if (dsec && dsec.style.display !== 'none') {
|
|
782
|
+
dsec.style.display = 'none';
|
|
783
|
+
state.selectedTraceId = null;
|
|
784
|
+
document.querySelectorAll('.trace-item').forEach(function (el) {
|
|
785
|
+
el.classList.remove('selected');
|
|
786
|
+
});
|
|
787
|
+
} else {
|
|
788
|
+
var tsec = $('traces-section');
|
|
789
|
+
if (tsec) tsec.style.display = 'none';
|
|
790
|
+
state.selectedRunId = null;
|
|
791
|
+
state.selectedTraceId = null;
|
|
792
|
+
document.querySelectorAll('.run-item').forEach(function (el) {
|
|
793
|
+
el.classList.remove('selected');
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (ev.key.toLowerCase() === 'j') {
|
|
799
|
+
ev.preventDefault();
|
|
800
|
+
var next = runs[Math.min(runs.length - 1, Math.max(0, idx + 1))];
|
|
801
|
+
if (next) next.click();
|
|
802
|
+
next && next.scrollIntoView({ block: 'nearest' });
|
|
803
|
+
}
|
|
804
|
+
if (ev.key.toLowerCase() === 'k') {
|
|
805
|
+
ev.preventDefault();
|
|
806
|
+
var prev = runs[Math.max(0, idx - 1)];
|
|
807
|
+
if (prev) prev.click();
|
|
808
|
+
prev && prev.scrollIntoView({ block: 'nearest' });
|
|
809
|
+
}
|
|
810
|
+
if (ev.key === 'Enter' && idx >= 0) {
|
|
811
|
+
// already selected; open details if traces exist
|
|
812
|
+
var tid = state.selectedTraceId;
|
|
813
|
+
if (!tid && state.traces.length) {
|
|
814
|
+
var first = state.traces[0];
|
|
815
|
+
var tEl = document.querySelector('.trace-item');
|
|
816
|
+
if (tEl && first) selectTrace(first.id, tEl, first);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function setupCloseDetails() {
|
|
823
|
+
var btn = $('close-details-btn');
|
|
824
|
+
if (btn) {
|
|
825
|
+
btn.addEventListener('click', function () {
|
|
826
|
+
var sec = $('details-section');
|
|
827
|
+
if (sec) sec.style.display = 'none';
|
|
828
|
+
state.selectedTraceId = null;
|
|
829
|
+
document.querySelectorAll('.trace-item').forEach(function (el) {
|
|
830
|
+
el.classList.remove('selected');
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function setupAutoRefresh() {
|
|
837
|
+
if (state.autoRefreshId) clearInterval(state.autoRefreshId);
|
|
838
|
+
state.autoRefreshId = setInterval(function () {
|
|
839
|
+
refreshAll(true);
|
|
840
|
+
}, 5000);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Toasts
|
|
844
|
+
function showToast(msg, type) {
|
|
845
|
+
var cont = $('toast-container');
|
|
846
|
+
if (!cont) {
|
|
847
|
+
cont = el('div', 'toast-container');
|
|
848
|
+
cont.id = 'toast-container';
|
|
849
|
+
document.body.appendChild(cont);
|
|
850
|
+
}
|
|
851
|
+
var t = el('div', 'toast ' + (type || ''));
|
|
852
|
+
t.textContent = msg;
|
|
853
|
+
cont.appendChild(t);
|
|
854
|
+
setTimeout(function () {
|
|
855
|
+
if (t && t.parentNode) t.parentNode.removeChild(t);
|
|
856
|
+
}, 2600);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Init
|
|
860
|
+
async function init() {
|
|
861
|
+
// version already in HTML
|
|
862
|
+
setupFilters();
|
|
863
|
+
setupExports();
|
|
864
|
+
setupRefresh();
|
|
865
|
+
setupCloseDetails();
|
|
866
|
+
setupAutoRefresh();
|
|
867
|
+
|
|
868
|
+
// initial skeletons already in HTML; replace on load
|
|
869
|
+
try {
|
|
870
|
+
await loadStats();
|
|
871
|
+
await loadRuns();
|
|
872
|
+
// show friendly empty if none
|
|
873
|
+
if (!state.runs.length) {
|
|
874
|
+
var list = $('runs-list');
|
|
875
|
+
if (list)
|
|
876
|
+
list.innerHTML =
|
|
877
|
+
'<div class="empty"><div class="icon">◌</div><div>No runs yet. Wrap your first agent!</div><small>Use the SDK or middleware, then refresh.</small></div>';
|
|
878
|
+
}
|
|
879
|
+
} catch (e) {
|
|
880
|
+
var list = $('runs-list');
|
|
881
|
+
if (list)
|
|
882
|
+
list.innerHTML =
|
|
883
|
+
'<div class="empty">Failed to load data. Is the dashboard server running?</div>';
|
|
884
|
+
console.error('[AgentTrace] init error', e);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// expose tiny debug hook
|
|
888
|
+
window.__agenttraceDashboard = {
|
|
889
|
+
state: state,
|
|
890
|
+
refresh: function () {
|
|
891
|
+
return refreshAll(true);
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (document.readyState === 'loading') {
|
|
897
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
898
|
+
} else {
|
|
899
|
+
init();
|
|
900
|
+
}
|
|
901
|
+
})();
|