@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.
@@ -0,0 +1,663 @@
1
+ /**
2
+ * AgentTrace Usage Dashboard — Complete Vanilla Rewrite
3
+ * Live SSE + polling. Cost projections, per-agent, Top Tools, responsive.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ var state = {
9
+ active: [],
10
+ todayStats: null,
11
+ recent: [],
12
+ topAgents: [],
13
+ feed: [],
14
+ sseConnected: false,
15
+ autoRefreshId: null,
16
+ lastRefresh: null,
17
+ es: null,
18
+ eventCount: 0,
19
+ };
20
+
21
+ function $(id) {
22
+ return document.getElementById(id);
23
+ }
24
+ function el(tag, cls, text) {
25
+ var e = document.createElement(tag);
26
+ if (cls) e.className = cls;
27
+ if (text != null) e.textContent = text;
28
+ return e;
29
+ }
30
+ function escapeHtml(s) {
31
+ return String(s).replace(/[&<>"']/g, function (c) {
32
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
33
+ });
34
+ }
35
+ function fmtCost(c) {
36
+ if (c == null) return '$0.0000';
37
+ return '$' + Number(c).toFixed(4);
38
+ }
39
+ function fmtNum(n) {
40
+ if (n == null) return '0';
41
+ return Number(n).toLocaleString();
42
+ }
43
+ function fmtTokens(n) {
44
+ if (n == null) return '0';
45
+ var v = Number(n);
46
+ if (v >= 1000000) return (v / 1000000).toFixed(1) + 'M';
47
+ if (v >= 1000) return (v / 1000).toFixed(1) + 'k';
48
+ return v.toLocaleString();
49
+ }
50
+ function fmtTime(ts) {
51
+ if (!ts) return '';
52
+ var d = new Date(ts);
53
+ return isNaN(d.getTime())
54
+ ? ''
55
+ : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
56
+ }
57
+ function fmtDay(d) {
58
+ if (typeof d === 'string') return d.slice(5);
59
+ return d.toISOString().slice(5, 10);
60
+ }
61
+ function timeAgo(ts) {
62
+ if (!ts) return 'never';
63
+ var secs = Math.max(0, Math.floor((Date.now() - ts) / 1000));
64
+ if (secs < 5) return 'just now';
65
+ if (secs < 60) return secs + 's ago';
66
+ var m = Math.floor(secs / 60);
67
+ if (m < 60) return m + 'm ago';
68
+ return Math.floor(m / 60) + 'h ago';
69
+ }
70
+ function getTodayStart() {
71
+ var d = new Date();
72
+ d.setHours(0, 0, 0, 0);
73
+ return d.getTime();
74
+ }
75
+ function get7DaysAgo() {
76
+ var d = new Date();
77
+ d.setHours(0, 0, 0, 0);
78
+ d.setDate(d.getDate() - 6);
79
+ return d.getTime();
80
+ }
81
+
82
+ async function fetchJSON(url) {
83
+ var res = await fetch(url);
84
+ if (!res.ok) throw new Error('Request failed: ' + res.status);
85
+ return res.json();
86
+ }
87
+
88
+ async function loadActive() {
89
+ state.active = await fetchJSON('/api/usage/active');
90
+ }
91
+ async function loadTodayStats() {
92
+ var from = getTodayStart();
93
+ state.todayStats = await fetchJSON('/api/usage/stats?fromDate=' + from);
94
+ }
95
+ async function loadRecent() {
96
+ state.recent = await fetchJSON('/api/usage?limit=50');
97
+ }
98
+ async function loadGlobalTop() {
99
+ var s = await fetchJSON('/api/usage/stats');
100
+ state.topAgents = s && s.topAgents ? s.topAgents : [];
101
+ }
102
+ async function load7d() {
103
+ var from = get7DaysAgo();
104
+ return (await fetchJSON('/api/usage?fromDate=' + from + '&limit=2000')) || [];
105
+ }
106
+
107
+ async function refreshAll(alsoLine) {
108
+ try {
109
+ await Promise.all([loadActive(), loadTodayStats(), loadRecent(), loadGlobalTop()]);
110
+ state.lastRefresh = Date.now();
111
+ renderSummary();
112
+ renderBar();
113
+ renderTopAgents();
114
+ renderRecent();
115
+ renderPerAgent();
116
+ renderTopTools();
117
+ renderProjections();
118
+ updateLast();
119
+ if (alsoLine) {
120
+ var recs = await load7d();
121
+ renderLine(recs);
122
+ }
123
+ } catch (e) {
124
+ console.warn('[Usage] refresh', e);
125
+ }
126
+ }
127
+
128
+ function updateLast() {
129
+ var el = $('last-updated');
130
+ if (el && state.lastRefresh) el.textContent = 'Updated ' + timeAgo(state.lastRefresh);
131
+ }
132
+
133
+ function renderSummary() {
134
+ var now = Date.now();
135
+ var TH = 30 * 60 * 1000;
136
+ var act = (state.active || []).filter(function (a) {
137
+ return now - (Date.parse(a.lastActive || 0) || 0) <= TH;
138
+ }).length;
139
+ var t = state.todayStats || { totalActions: 0, totalCostUsd: 0, totalTokens: 0 };
140
+
141
+ var aEl = $('active-agents');
142
+ if (aEl) aEl.textContent = String(act);
143
+ var actEl = $('actions-today');
144
+ if (actEl) actEl.textContent = fmtNum(t.totalActions || 0);
145
+ var cEl = $('cost-today');
146
+ if (cEl) cEl.textContent = fmtCost(t.totalCostUsd || 0);
147
+ var tokEl = $('tokens-today');
148
+ if (tokEl) tokEl.textContent = fmtTokens(t.totalTokens || 0);
149
+ }
150
+
151
+ // Projections from today's rate
152
+ function renderProjections() {
153
+ var t = state.todayStats || { totalCostUsd: 0 };
154
+ var rateEl = $('spend-rate');
155
+ var dEl = $('proj-daily');
156
+ var mEl = $('proj-monthly');
157
+
158
+ var now = Date.now();
159
+ var dayStart = getTodayStart();
160
+ var elapsed = Math.max(1, now - dayStart);
161
+ var frac = Math.min(1, elapsed / 86400000);
162
+ var hourly = (t.totalCostUsd || 0) / (elapsed / 3600000);
163
+ var daily = (t.totalCostUsd || 0) / frac;
164
+ var monthly = daily * 30;
165
+
166
+ if (rateEl) rateEl.textContent = isFinite(hourly) ? fmtCost(hourly) + '/hr' : '—';
167
+ if (dEl) dEl.textContent = isFinite(daily) ? fmtCost(daily) : '—';
168
+ if (mEl) mEl.textContent = isFinite(monthly) ? fmtCost(monthly) : '—';
169
+ }
170
+
171
+ // Bar chart (actions by type)
172
+ function renderBar() {
173
+ var c = $('bar-chart');
174
+ if (!c) return;
175
+ c.innerHTML = '';
176
+ var by = (state.todayStats && state.todayStats.actionsByType) || {};
177
+ var entries = Object.keys(by).map(function (k) {
178
+ return { type: k, count: by[k] || 0 };
179
+ });
180
+ entries.sort(function (a, b) {
181
+ return b.count - a.count;
182
+ });
183
+ entries = entries.slice(0, 8);
184
+ if (!entries.length) {
185
+ c.appendChild(el('div', 'empty small', 'No actions recorded today'));
186
+ return;
187
+ }
188
+ var max = 0;
189
+ entries.forEach(function (e) {
190
+ if (e.count > max) max = e.count;
191
+ });
192
+ if (max === 0) max = 1;
193
+
194
+ var wrap = el('div', 'bar-chart');
195
+ var palette = [
196
+ '#3b82f6',
197
+ '#22c55e',
198
+ '#eab308',
199
+ '#f59e0b',
200
+ '#a78bfa',
201
+ '#60a5fa',
202
+ '#34d399',
203
+ '#facc15',
204
+ ];
205
+ entries.forEach(function (e, i) {
206
+ var pct = Math.max(8, Math.round((e.count / max) * 100));
207
+ var b = el('div', 'bar');
208
+ b.style.height = pct + '%';
209
+ b.style.width = Math.max(20, Math.floor(120 / entries.length)) + 'px';
210
+ b.style.background = palette[i % palette.length];
211
+ b.title = e.type + ': ' + e.count;
212
+ b.appendChild(el('div', 'bar-value', String(e.count)));
213
+ b.appendChild(el('div', 'bar-label', e.type));
214
+ wrap.appendChild(b);
215
+ });
216
+ c.appendChild(wrap);
217
+ }
218
+
219
+ // Line chart (SVG)
220
+ function renderLine(recs) {
221
+ var c = $('line-chart');
222
+ if (!c) return;
223
+ c.innerHTML = '';
224
+
225
+ var byDay = {};
226
+ (recs || []).forEach(function (r) {
227
+ var d = new Date(r.createdAt || Date.now());
228
+ var k =
229
+ d.getFullYear() +
230
+ '-' +
231
+ String(d.getMonth() + 1).padStart(2, '0') +
232
+ '-' +
233
+ String(d.getDate()).padStart(2, '0');
234
+ byDay[k] = (byDay[k] || 0) + (r.costUsd || 0);
235
+ });
236
+
237
+ var days = [];
238
+ var base = new Date();
239
+ base.setHours(0, 0, 0, 0);
240
+ for (var i = 6; i >= 0; i--) {
241
+ var dt = new Date(base);
242
+ dt.setDate(base.getDate() - i);
243
+ var k =
244
+ dt.getFullYear() +
245
+ '-' +
246
+ String(dt.getMonth() + 1).padStart(2, '0') +
247
+ '-' +
248
+ String(dt.getDate()).padStart(2, '0');
249
+ days.push({ key: k, label: fmtDay(dt), cost: byDay[k] || 0 });
250
+ }
251
+
252
+ var maxC = 0;
253
+ days.forEach(function (d) {
254
+ if (d.cost > maxC) maxC = d.cost;
255
+ });
256
+ if (maxC <= 0) maxC = 0.0001;
257
+
258
+ var W = 520,
259
+ H = 140,
260
+ L = 36,
261
+ R = 8,
262
+ T = 10,
263
+ B = 20;
264
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
265
+ svg.setAttribute('class', 'line-chart');
266
+ svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
267
+ svg.setAttribute('width', '100%');
268
+ svg.setAttribute('height', '100%');
269
+
270
+ // grid
271
+ for (var g = 0; g <= 3; g++) {
272
+ var gy = T + (g * (H - T - B)) / 3;
273
+ var ln = document.createElementNS('http://www.w3.org/2000/svg', 'line');
274
+ ln.setAttribute('x1', L);
275
+ ln.setAttribute('y1', gy);
276
+ ln.setAttribute('x2', W - R);
277
+ ln.setAttribute('y2', gy);
278
+ ln.setAttribute('stroke', '#242429');
279
+ ln.setAttribute('stroke-width', '1');
280
+ svg.appendChild(ln);
281
+ }
282
+
283
+ var ax = document.createElementNS('http://www.w3.org/2000/svg', 'line');
284
+ ax.setAttribute('x1', L);
285
+ ax.setAttribute('y1', H - B);
286
+ ax.setAttribute('x2', W - R);
287
+ ax.setAttribute('y2', H - B);
288
+ ax.setAttribute('stroke', '#242429');
289
+ ax.setAttribute('stroke-width', '1');
290
+ svg.appendChild(ax);
291
+
292
+ var n = days.length;
293
+ var pw = W - L - R;
294
+ var ph = H - T - B;
295
+ var pts = [];
296
+ for (var j = 0; j < n; j++) {
297
+ var x = L + (n === 1 ? pw / 2 : (j * pw) / (n - 1));
298
+ var y = H - B - (days[j].cost / maxC) * ph;
299
+ pts.push({ x: x, y: y, val: days[j].cost, label: days[j].label });
300
+ }
301
+
302
+ if (pts.length > 1) {
303
+ var area = 'M ' + pts[0].x + ' ' + (H - B);
304
+ pts.forEach(function (p) {
305
+ area += ' L ' + p.x + ' ' + p.y;
306
+ });
307
+ area += ' L ' + pts[pts.length - 1].x + ' ' + (H - B) + ' Z';
308
+ var ap = document.createElementNS('http://www.w3.org/2000/svg', 'path');
309
+ ap.setAttribute('d', area);
310
+ ap.setAttribute('fill', 'rgba(59,130,246,0.08)');
311
+ ap.setAttribute('stroke', 'none');
312
+ svg.appendChild(ap);
313
+
314
+ var poly = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
315
+ poly.setAttribute(
316
+ 'points',
317
+ pts
318
+ .map(function (p) {
319
+ return p.x + ',' + p.y;
320
+ })
321
+ .join(' '),
322
+ );
323
+ poly.setAttribute('fill', 'none');
324
+ poly.setAttribute('stroke', '#3b82f6');
325
+ poly.setAttribute('stroke-width', '2');
326
+ poly.setAttribute('stroke-linejoin', 'round');
327
+ poly.setAttribute('stroke-linecap', 'round');
328
+ svg.appendChild(poly);
329
+ }
330
+
331
+ pts.forEach(function (p) {
332
+ var cir = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
333
+ cir.setAttribute('cx', p.x);
334
+ cir.setAttribute('cy', p.y);
335
+ cir.setAttribute('r', '2.5');
336
+ cir.setAttribute('fill', '#3b82f6');
337
+ cir.setAttribute('stroke', '#0a0a0c');
338
+ cir.setAttribute('stroke-width', '1.5');
339
+ svg.appendChild(cir);
340
+
341
+ var tx = document.createElementNS('http://www.w3.org/2000/svg', 'text');
342
+ tx.setAttribute('x', p.x);
343
+ tx.setAttribute('y', H - 4);
344
+ tx.setAttribute('text-anchor', 'middle');
345
+ tx.textContent = p.label;
346
+ svg.appendChild(tx);
347
+
348
+ if (p.val > 0) {
349
+ var ty = document.createElementNS('http://www.w3.org/2000/svg', 'text');
350
+ ty.setAttribute('x', p.x);
351
+ ty.setAttribute('y', Math.max(T + 8, p.y - 6));
352
+ ty.setAttribute('text-anchor', 'middle');
353
+ ty.setAttribute('fill', '#e8e8eb');
354
+ ty.textContent = '$' + p.val.toFixed(3);
355
+ svg.appendChild(ty);
356
+ }
357
+ });
358
+
359
+ var yl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
360
+ yl.setAttribute('x', 4);
361
+ yl.setAttribute('y', T + 10);
362
+ yl.setAttribute('fill', '#9a9aa0');
363
+ yl.textContent = '$' + maxC.toFixed(2);
364
+ svg.appendChild(yl);
365
+
366
+ c.appendChild(svg);
367
+ }
368
+
369
+ function renderTopAgents() {
370
+ var tb = $('top-agents-table');
371
+ if (!tb) return;
372
+ var tbod = tb.querySelector('tbody');
373
+ if (!tbod) return;
374
+ tbod.innerHTML = '';
375
+ var rows = state.topAgents || [];
376
+ var cnt = $('top-agents-count');
377
+ if (cnt) cnt.textContent = rows.length + '';
378
+ if (!rows.length) {
379
+ tbod.innerHTML = '<tr><td colspan="4" class="empty small">No usage data yet</td></tr>';
380
+ return;
381
+ }
382
+ rows.slice(0, 8).forEach(function (a) {
383
+ var tr = el('tr');
384
+ tr.innerHTML =
385
+ '<td class="agent">' +
386
+ escapeHtml(a.agentName || '') +
387
+ '</td><td class="num">' +
388
+ fmtNum(a.actions || 0) +
389
+ '</td><td class="num">' +
390
+ fmtTokens(a.tokens || 0) +
391
+ '</td><td class="num cost">' +
392
+ fmtCost(a.costUsd || 0) +
393
+ '</td>';
394
+ tbod.appendChild(tr);
395
+ });
396
+ }
397
+
398
+ function renderRecent() {
399
+ var tb = $('recent-actions-table');
400
+ if (!tb) return;
401
+ var tbod = tb.querySelector('tbody');
402
+ if (!tbod) return;
403
+ tbod.innerHTML = '';
404
+ var rows = state.recent || [];
405
+ var cnt = $('recent-count');
406
+ if (cnt) cnt.textContent = rows.length + '';
407
+ if (!rows.length) {
408
+ tbod.innerHTML = '<tr><td colspan="4" class="empty small">No actions recorded yet</td></tr>';
409
+ return;
410
+ }
411
+ rows.slice(0, 15).forEach(function (r) {
412
+ var tr = el('tr');
413
+ var act = (r.action || '') + (r.target ? ':' + r.target : '');
414
+ tr.innerHTML =
415
+ '<td>' +
416
+ fmtTime(r.createdAt) +
417
+ '</td><td class="agent">' +
418
+ escapeHtml(r.agentName || '') +
419
+ '</td><td>' +
420
+ escapeHtml(act) +
421
+ '</td><td class="num cost">' +
422
+ fmtCost(r.costUsd || 0) +
423
+ '</td>';
424
+ tbod.appendChild(tr);
425
+ });
426
+ }
427
+
428
+ function renderPerAgent() {
429
+ var tb = $('per-agent-table');
430
+ if (!tb) return;
431
+ var tbod = tb.querySelector('tbody');
432
+ if (!tbod) return;
433
+ tbod.innerHTML = '';
434
+ var rows = (state.todayStats && state.todayStats.topAgents) || state.topAgents || [];
435
+ if (!rows.length) {
436
+ tbod.innerHTML =
437
+ '<tr><td colspan="4" class="empty small">No per-agent data for today</td></tr>';
438
+ return;
439
+ }
440
+ rows.slice(0, 10).forEach(function (a) {
441
+ var tr = el('tr');
442
+ tr.innerHTML =
443
+ '<td class="agent">' +
444
+ escapeHtml(a.agentName || '') +
445
+ '</td><td class="num">' +
446
+ fmtNum(a.actions || 0) +
447
+ '</td><td class="num">' +
448
+ fmtTokens(a.tokens || 0) +
449
+ '</td><td class="num cost">' +
450
+ fmtCost(a.costUsd || 0) +
451
+ '</td>';
452
+ tbod.appendChild(tr);
453
+ });
454
+ }
455
+
456
+ function renderTopTools() {
457
+ var tb = $('top-tools-table');
458
+ if (!tb) return;
459
+ var tbod = tb.querySelector('tbody');
460
+ if (!tbod) return;
461
+ tbod.innerHTML = '';
462
+ var tools = (state.todayStats && state.todayStats.topTools) || [];
463
+ var cnt = $('top-tools-count');
464
+ if (cnt) cnt.textContent = tools.length + '';
465
+ if (!tools.length) {
466
+ tbod.innerHTML =
467
+ '<tr><td colspan="3" class="empty small">No tool calls recorded today</td></tr>';
468
+ return;
469
+ }
470
+ tools.slice(0, 10).forEach(function (t) {
471
+ var tr = el('tr');
472
+ tr.innerHTML =
473
+ '<td>' +
474
+ escapeHtml(t.name || '') +
475
+ '</td><td class="num">' +
476
+ fmtNum(t.count || 0) +
477
+ '</td><td class="num">' +
478
+ (t.avgLatencyMs || 0) +
479
+ 'ms</td>';
480
+ tbod.appendChild(tr);
481
+ });
482
+ }
483
+
484
+ // Live feed
485
+ function addToFeed(rec) {
486
+ if (!rec) return;
487
+ state.feed.unshift(rec);
488
+ if (state.feed.length > 50) state.feed.pop();
489
+ state.eventCount++;
490
+
491
+ var feed = $('activity-feed');
492
+ if (!feed) return;
493
+ var empty = feed.querySelector('.empty');
494
+ if (empty) empty.remove();
495
+
496
+ var item = el('div', 'activity-item');
497
+ item.style.animation = 'fadeSlideIn 0.2s ease';
498
+
499
+ var tm = el('span', 'activity-time', fmtTime(rec.createdAt));
500
+ var ag = el('span', 'activity-agent', rec.agentName || 'agent');
501
+ ag.title = rec.agentName || '';
502
+ var actStr = (rec.action || '') + (rec.target ? ':' + rec.target : '');
503
+ var ac = el('span', 'activity-action', actStr);
504
+ ac.title = actStr;
505
+ var cs = el('span', 'activity-cost', fmtCost(rec.costUsd));
506
+
507
+ var dot = el('span');
508
+ dot.style.cssText =
509
+ 'display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px;vertical-align:middle;';
510
+ dot.style.background =
511
+ rec.status === 'failure'
512
+ ? 'var(--error)'
513
+ : rec.status === 'timeout'
514
+ ? '#f59e0b'
515
+ : 'var(--success)';
516
+
517
+ item.appendChild(tm);
518
+ item.appendChild(dot);
519
+ item.appendChild(ag);
520
+ item.appendChild(ac);
521
+ item.appendChild(cs);
522
+
523
+ if (feed.firstChild) feed.insertBefore(item, feed.firstChild);
524
+ else feed.appendChild(item);
525
+
526
+ while (feed.children.length > 50) feed.removeChild(feed.lastChild);
527
+
528
+ var fc = $('feed-count');
529
+ if (fc) fc.textContent = state.feed.length + ' events';
530
+ }
531
+
532
+ function setupSSE() {
533
+ var dot = $('live-dot');
534
+ var sseSt = $('sse-status');
535
+
536
+ function setConn(ok) {
537
+ state.sseConnected = !!ok;
538
+ if (dot) {
539
+ dot.classList.toggle('off', !ok);
540
+ dot.title = ok ? 'Live — connected' : 'Live — disconnected';
541
+ }
542
+ if (sseSt) {
543
+ sseSt.classList.toggle('sse-on', ok);
544
+ sseSt.classList.toggle('sse-off', !ok);
545
+ sseSt.title = ok ? 'SSE connected' : 'SSE disconnected (polling)';
546
+ }
547
+ }
548
+ setConn(false);
549
+
550
+ try {
551
+ var es = new EventSource('/api/usage/stream');
552
+ state.es = es;
553
+ es.addEventListener('connected', function () {
554
+ setConn(true);
555
+ });
556
+ es.addEventListener('usage', function (ev) {
557
+ try {
558
+ var rec = JSON.parse(ev.data);
559
+ addToFeed(rec);
560
+ state.recent.unshift(rec);
561
+ if (state.recent.length > 50) state.recent.pop();
562
+ renderRecent();
563
+ if (state.todayStats) {
564
+ var ds = getTodayStart();
565
+ if (rec.createdAt >= ds) {
566
+ state.todayStats.totalActions = (state.todayStats.totalActions || 0) + 1;
567
+ state.todayStats.totalTokens =
568
+ (state.todayStats.totalTokens || 0) + (rec.tokensUsed || 0);
569
+ state.todayStats.totalCostUsd =
570
+ (state.todayStats.totalCostUsd || 0) + (rec.costUsd || 0);
571
+ renderSummary();
572
+ renderProjections();
573
+ }
574
+ }
575
+ } catch (_) {}
576
+ });
577
+ es.onerror = function () {
578
+ setConn(false);
579
+ };
580
+ es.onopen = function () {
581
+ setConn(true);
582
+ };
583
+ } catch (e) {
584
+ setConn(false);
585
+ }
586
+ }
587
+
588
+ function setupAuto() {
589
+ if (state.autoRefreshId) clearInterval(state.autoRefreshId);
590
+ state.autoRefreshId = setInterval(function () {
591
+ refreshAll(false).catch(function () {});
592
+ updateLast();
593
+ }, 5000);
594
+ }
595
+
596
+ function setupRefreshBtn() {
597
+ var btn = $('refresh-btn');
598
+ if (btn) {
599
+ btn.addEventListener('click', function () {
600
+ btn.classList.add('spinning');
601
+ refreshAll(true)
602
+ .catch(function () {})
603
+ .finally(function () {
604
+ setTimeout(function () {
605
+ btn.classList.remove('spinning');
606
+ }, 600);
607
+ });
608
+ });
609
+ }
610
+ }
611
+
612
+ function showToast(msg, type) {
613
+ var cont = $('toast-container');
614
+ if (!cont) {
615
+ cont = el('div', 'toast-container');
616
+ cont.id = 'toast-container';
617
+ document.body.appendChild(cont);
618
+ }
619
+ var t = el('div', 'toast ' + (type || ''));
620
+ t.textContent = msg;
621
+ cont.appendChild(t);
622
+ setTimeout(function () {
623
+ if (t && t.parentNode) t.parentNode.removeChild(t);
624
+ }, 2400);
625
+ }
626
+
627
+ function init() {
628
+ setupRefreshBtn();
629
+ setupAuto();
630
+ setupSSE();
631
+
632
+ refreshAll(true)
633
+ .then(function () {
634
+ var feed = $('activity-feed');
635
+ if (feed && state.feed.length === 0 && state.recent.length) {
636
+ var seed = state.recent.slice(0, 8).reverse();
637
+ seed.forEach(function (r) {
638
+ addToFeed(r);
639
+ });
640
+ }
641
+ })
642
+ .catch(function (e) {
643
+ var f = $('activity-feed');
644
+ if (f)
645
+ f.innerHTML =
646
+ '<div class="empty">Failed to load usage data. Is the server running?</div>';
647
+ console.error('[Usage] init', e);
648
+ });
649
+
650
+ window.__agenttraceUsage = {
651
+ state: state,
652
+ refresh: function () {
653
+ return refreshAll(true);
654
+ },
655
+ };
656
+ }
657
+
658
+ if (document.readyState === 'loading') {
659
+ document.addEventListener('DOMContentLoaded', init);
660
+ } else {
661
+ init();
662
+ }
663
+ })();