@mce-bt/microagents-dashboard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregator.d.ts +278 -0
- package/dist/aggregator.d.ts.map +1 -0
- package/dist/aggregator.js +477 -0
- package/dist/aggregator.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +260 -0
- package/dist/server.js.map +1 -0
- package/package.json +51 -0
- package/public/chart.umd.min.js +20 -0
- package/public/htmx.min.js +1 -0
- package/public/styles.css +1192 -0
- package/views/404.ejs +10 -0
- package/views/agent-detail.ejs +8 -0
- package/views/agents.ejs +181 -0
- package/views/browser-detail.ejs +373 -0
- package/views/browser.ejs +57 -0
- package/views/layout.ejs +47 -0
- package/views/logs.ejs +65 -0
- package/views/memory.ejs +144 -0
- package/views/partials/agent-detail-content.ejs +193 -0
- package/views/partials/agents-table.ejs +57 -0
- package/views/partials/browser-table.ejs +71 -0
- package/views/partials/logs-content.ejs +43 -0
- package/views/partials/queues-table.ejs +39 -0
- package/views/queues.ejs +63 -0
- package/views/scheduler.ejs +95 -0
- package/views/session.ejs +74 -0
- package/views/tokens.ejs +381 -0
package/views/queues.ejs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Message Queues</h2>
|
|
3
|
+
<p>RabbitMQ queue health and throughput</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%
|
|
7
|
+
const totalMessages = rabbit.queues.reduce((s, q) => s + q.messages, 0);
|
|
8
|
+
const totalConsumers = rabbit.queues.reduce((s, q) => s + q.consumers, 0);
|
|
9
|
+
const unhealthy = rabbit.queues.filter(q => q.state !== 'running').length;
|
|
10
|
+
%>
|
|
11
|
+
|
|
12
|
+
<div class="grid grid-4 mb-4">
|
|
13
|
+
<div class="stat-card">
|
|
14
|
+
<div class="label">Queues</div>
|
|
15
|
+
<div class="value accent"><%= rabbit.queues.length %></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="stat-card">
|
|
18
|
+
<div class="label">Total Messages</div>
|
|
19
|
+
<div class="value purple"><%= totalMessages.toLocaleString() %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="stat-card">
|
|
22
|
+
<div class="label">Consumers</div>
|
|
23
|
+
<div class="value green"><%= totalConsumers %></div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<div class="label">Unhealthy</div>
|
|
27
|
+
<div class="value <%= unhealthy > 0 ? 'red' : 'green' %>"><%= unhealthy %></div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<% if (rabbit.messageStats) { %>
|
|
32
|
+
<div class="grid grid-3 mb-4">
|
|
33
|
+
<div class="stat-card">
|
|
34
|
+
<div class="label">Publish Rate</div>
|
|
35
|
+
<div class="value accent"><%= rabbit.messageStats.publishRate.toFixed(1) %>/s</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="stat-card">
|
|
38
|
+
<div class="label">Deliver Rate</div>
|
|
39
|
+
<div class="value green"><%= rabbit.messageStats.deliverRate.toFixed(1) %>/s</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="stat-card">
|
|
42
|
+
<div class="label">Ack Rate</div>
|
|
43
|
+
<div class="value purple"><%= rabbit.messageStats.ackRate.toFixed(1) %>/s</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% } %>
|
|
47
|
+
|
|
48
|
+
<% if (rabbit.clusterName || rabbit.rabbitmqVersion) { %>
|
|
49
|
+
<p class="text-muted text-sm mb-4">
|
|
50
|
+
Cluster: <code><%= rabbit.clusterName || '—' %></code> ·
|
|
51
|
+
RabbitMQ v<%= rabbit.rabbitmqVersion || '?' %>
|
|
52
|
+
</p>
|
|
53
|
+
<% } %>
|
|
54
|
+
|
|
55
|
+
<div class="card">
|
|
56
|
+
<div class="card-header">
|
|
57
|
+
<h3>Queues</h3>
|
|
58
|
+
<span class="htmx-indicator"><span class="spinner"></span></span>
|
|
59
|
+
</div>
|
|
60
|
+
<div hx-get="/partial/queues" hx-trigger="every 10s" hx-indicator="closest .card" hx-swap="innerHTML">
|
|
61
|
+
<%- include('partials/queues-table', { rabbit }) %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Scheduler</h2>
|
|
3
|
+
<p>Heartbeats and cron jobs across all agents</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<% if (schedulerStats.length === 0) { %>
|
|
7
|
+
<div class="empty-state">
|
|
8
|
+
<div class="icon">⏱</div>
|
|
9
|
+
<p>No agents with scheduler data</p>
|
|
10
|
+
</div>
|
|
11
|
+
<% } else { %>
|
|
12
|
+
|
|
13
|
+
<% schedulerStats.forEach(stat => { %>
|
|
14
|
+
<div class="card mb-4">
|
|
15
|
+
<div class="card-header">
|
|
16
|
+
<h3><%= stat.agentName %> <span class="text-muted text-sm">(<%= stat.agentId %>)</span></h3>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<%
|
|
20
|
+
const sched = stat.scheduler || {};
|
|
21
|
+
const heartbeats = sched.heartbeats || [];
|
|
22
|
+
const crons = sched.crons || [];
|
|
23
|
+
const isRunning = sched.running ?? false;
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
<div class="flex gap-4 mb-4" style="flex-wrap:wrap;">
|
|
27
|
+
<div class="stat-card" style="flex:1;min-width:120px;">
|
|
28
|
+
<div class="label">Status</div>
|
|
29
|
+
<div class="value <%= isRunning ? 'green' : 'red' %>" style="font-size: 18px;">
|
|
30
|
+
<%= isRunning ? 'RUNNING' : 'STOPPED' %>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="stat-card" style="flex:1;min-width:120px;">
|
|
34
|
+
<div class="label">Heartbeats</div>
|
|
35
|
+
<div class="value accent" style="font-size: 18px;"><%= heartbeats.length %></div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="stat-card" style="flex:1;min-width:120px;">
|
|
38
|
+
<div class="label">Cron Jobs</div>
|
|
39
|
+
<div class="value purple" style="font-size: 18px;"><%= crons.length %></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<% if (heartbeats.length > 0) { %>
|
|
44
|
+
<h4 style="margin-bottom:8px;font-size:13px;color:var(--text-secondary);">Heartbeats</h4>
|
|
45
|
+
<div class="table-wrap mb-4">
|
|
46
|
+
<table>
|
|
47
|
+
<thead>
|
|
48
|
+
<tr><th>Name</th><th>Enabled</th><th>Last Run</th><th>Next Run</th><th>Runs</th><th>Skips</th><th>Tokens</th><th>Duration</th><th>Errors</th></tr>
|
|
49
|
+
</thead>
|
|
50
|
+
<tbody>
|
|
51
|
+
<% heartbeats.forEach(hb => { %>
|
|
52
|
+
<tr>
|
|
53
|
+
<td><code><%= hb.name || '—' %></code></td>
|
|
54
|
+
<td><span class="badge badge-<%= hb.enabled ? 'green' : 'red' %>"><%= hb.enabled ? 'Yes' : 'No' %></span></td>
|
|
55
|
+
<td class="text-sm"><%= hb.lastRunAt ? new Date(hb.lastRunAt).toLocaleTimeString() : '—' %></td>
|
|
56
|
+
<td class="text-sm"><%= hb.nextRunAt ? new Date(hb.nextRunAt).toLocaleTimeString() : '—' %></td>
|
|
57
|
+
<td class="mono"><%= hb.totalRuns ?? '—' %></td>
|
|
58
|
+
<td class="mono"><%= hb.totalSkips ?? 0 %></td>
|
|
59
|
+
<td class="mono"><%= hb.totalTokens != null ? hb.totalTokens.toLocaleString() : '—' %></td>
|
|
60
|
+
<td class="mono"><%= hb.lastDurationMs != null ? (hb.lastDurationMs / 1000).toFixed(1) + 's' : '—' %></td>
|
|
61
|
+
<td class="mono"><%= hb.errors ?? 0 %></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<% }) %>
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
</div>
|
|
67
|
+
<% } %>
|
|
68
|
+
|
|
69
|
+
<% if (crons.length > 0) { %>
|
|
70
|
+
<h4 style="margin-bottom:8px;font-size:13px;color:var(--text-secondary);">Cron Jobs</h4>
|
|
71
|
+
<div class="table-wrap">
|
|
72
|
+
<table>
|
|
73
|
+
<thead>
|
|
74
|
+
<tr><th>Name</th><th>Enabled</th><th>Next Run</th><th>Last Run</th><th>Runs</th><th>Duration</th><th>Errors</th></tr>
|
|
75
|
+
</thead>
|
|
76
|
+
<tbody>
|
|
77
|
+
<% crons.forEach(cron => { %>
|
|
78
|
+
<tr>
|
|
79
|
+
<td><code><%= cron.name || '—' %></code></td>
|
|
80
|
+
<td><span class="badge badge-<%= cron.enabled ? 'green' : 'red' %>"><%= cron.enabled ? 'Yes' : 'No' %></span></td>
|
|
81
|
+
<td class="text-sm"><%= cron.nextRunAt ? new Date(cron.nextRunAt).toLocaleString() : '—' %></td>
|
|
82
|
+
<td class="text-sm"><%= cron.lastRunAt ? new Date(cron.lastRunAt).toLocaleTimeString() : '—' %></td>
|
|
83
|
+
<td class="mono"><%= cron.totalRuns ?? '—' %></td>
|
|
84
|
+
<td class="mono"><%= cron.lastDurationMs != null ? (cron.lastDurationMs / 1000).toFixed(1) + 's' : '—' %></td>
|
|
85
|
+
<td class="mono"><%= cron.errors ?? 0 %></td>
|
|
86
|
+
</tr>
|
|
87
|
+
<% }) %>
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div>
|
|
91
|
+
<% } %>
|
|
92
|
+
</div>
|
|
93
|
+
<% }) %>
|
|
94
|
+
|
|
95
|
+
<% } %>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Session Detail</h2>
|
|
3
|
+
<p>
|
|
4
|
+
Agent: <a href="/agents/<%= agent.id %>" class="link"><%= agent.name %></a> ·
|
|
5
|
+
Session: <code><%= sessionId %></code>
|
|
6
|
+
</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<% if (!detail) { %>
|
|
10
|
+
<div class="empty-state">
|
|
11
|
+
<div class="icon">◎</div>
|
|
12
|
+
<p>Session not found or agent unreachable</p>
|
|
13
|
+
</div>
|
|
14
|
+
<% } else { %>
|
|
15
|
+
|
|
16
|
+
<div class="grid grid-4 mb-4">
|
|
17
|
+
<div class="stat-card">
|
|
18
|
+
<div class="label">Agent</div>
|
|
19
|
+
<div class="value accent" style="font-size: 18px;"><%= detail.session.agentId %></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="stat-card">
|
|
22
|
+
<div class="label">Started</div>
|
|
23
|
+
<div class="value" style="font-size: 16px;"><%= new Date(detail.session.startedAt).toLocaleString() %></div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<div class="label">Ended</div>
|
|
27
|
+
<div class="value" style="font-size: 16px;">
|
|
28
|
+
<%= detail.session.endedAt ? new Date(detail.session.endedAt).toLocaleString() : 'Active' %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="stat-card">
|
|
32
|
+
<div class="label">Entries</div>
|
|
33
|
+
<div class="value purple"><%= detail.entries.length %></div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if (detail.session.metadata && Object.keys(detail.session.metadata).length > 0) { %>
|
|
38
|
+
<div class="card mb-4">
|
|
39
|
+
<div class="card-header"><h3>Metadata</h3></div>
|
|
40
|
+
<pre><%= JSON.stringify(detail.session.metadata, null, 2) %></pre>
|
|
41
|
+
</div>
|
|
42
|
+
<% } %>
|
|
43
|
+
|
|
44
|
+
<div class="card">
|
|
45
|
+
<div class="card-header"><h3>Conversation Timeline</h3></div>
|
|
46
|
+
<% if (detail.entries.length === 0) { %>
|
|
47
|
+
<p class="text-muted">No entries in this session</p>
|
|
48
|
+
<% } else { %>
|
|
49
|
+
<div class="timeline">
|
|
50
|
+
<% detail.entries.forEach(entry => { %>
|
|
51
|
+
<div class="timeline-entry type-<%= entry.type %>">
|
|
52
|
+
<div class="entry-header">
|
|
53
|
+
<span class="entry-type badge
|
|
54
|
+
<% if (entry.type === 'user') { %>badge-purple
|
|
55
|
+
<% } else if (entry.type === 'assistant') { %>badge-green
|
|
56
|
+
<% } else if (entry.type === 'tool') { %>badge-yellow
|
|
57
|
+
<% } else { %>badge-muted<% } %>
|
|
58
|
+
"><%= entry.type %></span>
|
|
59
|
+
<span class="entry-time"><%= new Date(entry.timestamp).toLocaleTimeString() %></span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="entry-content"><%= entry.content %></div>
|
|
62
|
+
<% if (entry.metadata && Object.keys(entry.metadata).length > 0) { %>
|
|
63
|
+
<details class="entry-meta mt-4">
|
|
64
|
+
<summary style="cursor: pointer; color: var(--text-muted);">Metadata</summary>
|
|
65
|
+
<pre style="margin-top: 8px;"><%= JSON.stringify(entry.metadata, null, 2) %></pre>
|
|
66
|
+
</details>
|
|
67
|
+
<% } %>
|
|
68
|
+
</div>
|
|
69
|
+
<% }) %>
|
|
70
|
+
</div>
|
|
71
|
+
<% } %>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<% } %>
|
package/views/tokens.ejs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Tokens & Cost</h2>
|
|
3
|
+
<p>Token usage across all agents</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%
|
|
7
|
+
let totalPrompt = 0, totalCompletion = 0, totalTokens = 0, totalCalls = 0;
|
|
8
|
+
const rows = [];
|
|
9
|
+
const allModels = {};
|
|
10
|
+
const allCallHistory = [];
|
|
11
|
+
const agentNames = new Set();
|
|
12
|
+
const modelNames = new Set();
|
|
13
|
+
agentMetrics.forEach(({ agent, metrics }) => {
|
|
14
|
+
const c = metrics?.completion || {};
|
|
15
|
+
const p = (c.total?.promptTokens ?? c.promptTokens) || 0;
|
|
16
|
+
const comp = (c.total?.completionTokens ?? c.completionTokens) || 0;
|
|
17
|
+
const t = (c.total?.totalTokens ?? c.totalTokens) || 0;
|
|
18
|
+
const calls = c.callCount || 0;
|
|
19
|
+
totalPrompt += p;
|
|
20
|
+
totalCompletion += comp;
|
|
21
|
+
totalTokens += t;
|
|
22
|
+
totalCalls += calls;
|
|
23
|
+
agentNames.add(agent.name);
|
|
24
|
+
rows.push({ agent, promptTokens: p, completionTokens: comp, totalTokens: t, callCount: calls, byModel: c.byModel || {} });
|
|
25
|
+
|
|
26
|
+
if (c.byModel) {
|
|
27
|
+
Object.entries(c.byModel).forEach(([model, stats]) => {
|
|
28
|
+
modelNames.add(model);
|
|
29
|
+
if (!allModels[model]) allModels[model] = { promptTokens: 0, completionTokens: 0, totalTokens: 0, calls: 0, totalDuration: 0 };
|
|
30
|
+
allModels[model].promptTokens += stats.promptTokens || 0;
|
|
31
|
+
allModels[model].completionTokens += stats.completionTokens || 0;
|
|
32
|
+
allModels[model].totalTokens += stats.totalTokens || 0;
|
|
33
|
+
allModels[model].calls += stats.calls || 0;
|
|
34
|
+
allModels[model].totalDuration += (stats.avgDurationMs || 0) * (stats.calls || 0);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (metrics?.callHistory) {
|
|
39
|
+
metrics.callHistory.forEach(call => {
|
|
40
|
+
allCallHistory.push({ ...call, agentName: agent.name, agentId: agent.id });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
allCallHistory.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
46
|
+
%>
|
|
47
|
+
|
|
48
|
+
<div class="grid grid-4 mb-4">
|
|
49
|
+
<div class="stat-card">
|
|
50
|
+
<div class="label">Total Tokens</div>
|
|
51
|
+
<div class="value accent" id="stat-total"><%= totalTokens.toLocaleString() %></div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="stat-card">
|
|
54
|
+
<div class="label">Prompt Tokens</div>
|
|
55
|
+
<div class="value purple" id="stat-prompt"><%= totalPrompt.toLocaleString() %></div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="stat-card">
|
|
58
|
+
<div class="label">Completion Tokens</div>
|
|
59
|
+
<div class="value green" id="stat-completion"><%= totalCompletion.toLocaleString() %></div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="stat-card">
|
|
62
|
+
<div class="label">LLM Calls</div>
|
|
63
|
+
<div class="value" id="stat-calls"><%= totalCalls %></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Charts Row -->
|
|
68
|
+
<% if (totalTokens > 0) { %>
|
|
69
|
+
<div class="grid grid-2 mb-4">
|
|
70
|
+
<div class="card">
|
|
71
|
+
<div class="card-header"><h3>Prompt vs Completion</h3></div>
|
|
72
|
+
<div style="position:relative;height:200px;display:flex;justify-content:center;">
|
|
73
|
+
<canvas id="tokenSplitChart"></canvas>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="card">
|
|
77
|
+
<div class="card-header"><h3>Usage by Agent</h3></div>
|
|
78
|
+
<div style="position:relative;height:200px;">
|
|
79
|
+
<canvas id="agentUsageChart"></canvas>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<script>
|
|
84
|
+
(function() {
|
|
85
|
+
const chartColors = ['#4dabf7','#bc8cff','#51cf66','#ffd43b','#ff6b6b','#20c997','#ff922b','#da77f2'];
|
|
86
|
+
const chartTheme = {
|
|
87
|
+
bg: '#1e2230', titleColor: '#e1e4ea', bodyColor: '#8b95a5',
|
|
88
|
+
borderColor: '#2d3348', gridColor: 'rgba(45,51,72,0.5)', labelColor: '#8b95a5'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Doughnut: prompt vs completion
|
|
92
|
+
new Chart(document.getElementById('tokenSplitChart'), {
|
|
93
|
+
type: 'doughnut',
|
|
94
|
+
data: {
|
|
95
|
+
labels: ['Prompt', 'Completion'],
|
|
96
|
+
datasets: [{
|
|
97
|
+
data: [<%= totalPrompt %>, <%= totalCompletion %>],
|
|
98
|
+
backgroundColor: ['rgba(188,140,255,0.8)', 'rgba(81,207,102,0.8)'],
|
|
99
|
+
borderColor: ['#bc8cff', '#51cf66'],
|
|
100
|
+
borderWidth: 2,
|
|
101
|
+
hoverOffset: 6,
|
|
102
|
+
}]
|
|
103
|
+
},
|
|
104
|
+
options: {
|
|
105
|
+
responsive: true, maintainAspectRatio: false,
|
|
106
|
+
cutout: '60%',
|
|
107
|
+
plugins: {
|
|
108
|
+
legend: {
|
|
109
|
+
position: 'bottom',
|
|
110
|
+
labels: { color: chartTheme.labelColor, usePointStyle: true, pointStyle: 'rectRounded', padding: 16, font: { size: 11 } }
|
|
111
|
+
},
|
|
112
|
+
tooltip: {
|
|
113
|
+
backgroundColor: chartTheme.bg, titleColor: chartTheme.titleColor,
|
|
114
|
+
bodyColor: chartTheme.bodyColor, borderColor: chartTheme.borderColor, borderWidth: 1,
|
|
115
|
+
callbacks: { label: function(ctx) { return ctx.label + ': ' + ctx.parsed.toLocaleString() + ' tokens (' + ((ctx.parsed / <%= totalTokens %>) * 100).toFixed(1) + '%)'; } }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Horizontal bar: per agent
|
|
122
|
+
<% const sortedRows = rows.slice().sort((a, b) => b.totalTokens - a.totalTokens); %>
|
|
123
|
+
new Chart(document.getElementById('agentUsageChart'), {
|
|
124
|
+
type: 'bar',
|
|
125
|
+
data: {
|
|
126
|
+
labels: [<% sortedRows.forEach((r, i) => { %>'<%= r.agent.name %>'<% if (i < sortedRows.length - 1) { %>,<% } %><% }) %>],
|
|
127
|
+
datasets: [
|
|
128
|
+
{
|
|
129
|
+
label: 'Prompt',
|
|
130
|
+
data: [<% sortedRows.forEach((r, i) => { %><%= r.promptTokens %><% if (i < sortedRows.length - 1) { %>,<% } %><% }) %>],
|
|
131
|
+
backgroundColor: 'rgba(188,140,255,0.7)',
|
|
132
|
+
borderColor: '#bc8cff',
|
|
133
|
+
borderWidth: 1,
|
|
134
|
+
borderRadius: 3,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
label: 'Completion',
|
|
138
|
+
data: [<% sortedRows.forEach((r, i) => { %><%= r.completionTokens %><% if (i < sortedRows.length - 1) { %>,<% } %><% }) %>],
|
|
139
|
+
backgroundColor: 'rgba(81,207,102,0.7)',
|
|
140
|
+
borderColor: '#51cf66',
|
|
141
|
+
borderWidth: 1,
|
|
142
|
+
borderRadius: 3,
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
options: {
|
|
147
|
+
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
|
148
|
+
interaction: { mode: 'index', intersect: false },
|
|
149
|
+
plugins: {
|
|
150
|
+
legend: {
|
|
151
|
+
position: 'bottom',
|
|
152
|
+
labels: { color: chartTheme.labelColor, usePointStyle: true, pointStyle: 'rectRounded', padding: 16, font: { size: 11 } }
|
|
153
|
+
},
|
|
154
|
+
tooltip: {
|
|
155
|
+
backgroundColor: chartTheme.bg, titleColor: chartTheme.titleColor,
|
|
156
|
+
bodyColor: chartTheme.bodyColor, borderColor: chartTheme.borderColor, borderWidth: 1,
|
|
157
|
+
callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.x.toLocaleString() + ' tokens'; } }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
scales: {
|
|
161
|
+
x: {
|
|
162
|
+
stacked: true,
|
|
163
|
+
grid: { color: chartTheme.gridColor, drawBorder: false },
|
|
164
|
+
ticks: { color: chartTheme.labelColor, font: { size: 10 }, callback: function(v) { return v >= 1000 ? (v/1000).toFixed(0) + 'k' : v; } }
|
|
165
|
+
},
|
|
166
|
+
y: {
|
|
167
|
+
stacked: true,
|
|
168
|
+
grid: { display: false },
|
|
169
|
+
ticks: { color: chartTheme.labelColor, font: { size: 11 } }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
})();
|
|
175
|
+
</script>
|
|
176
|
+
<% } %>
|
|
177
|
+
|
|
178
|
+
<!-- Filters -->
|
|
179
|
+
<div class="card mb-4" style="padding:12px 16px;">
|
|
180
|
+
<div class="flex gap-4" style="flex-wrap:wrap;align-items:center;">
|
|
181
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-right:4px;">Time Range:</div>
|
|
182
|
+
<div class="btn-group" id="time-range-btns">
|
|
183
|
+
<button class="btn btn-sm active" data-range="all">All</button>
|
|
184
|
+
<button class="btn btn-sm" data-range="3600000">1h</button>
|
|
185
|
+
<button class="btn btn-sm" data-range="86400000">24h</button>
|
|
186
|
+
<button class="btn btn-sm" data-range="604800000">7d</button>
|
|
187
|
+
</div>
|
|
188
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-left:12px;margin-right:4px;">Agent:</div>
|
|
189
|
+
<select id="filter-agent" class="filter-select">
|
|
190
|
+
<option value="">All Agents</option>
|
|
191
|
+
<% [...agentNames].sort().forEach(n => { %>
|
|
192
|
+
<option value="<%= n %>"><%= n %></option>
|
|
193
|
+
<% }) %>
|
|
194
|
+
</select>
|
|
195
|
+
<div style="font-size:12px;color:var(--text-secondary);margin-left:12px;margin-right:4px;">Model:</div>
|
|
196
|
+
<select id="filter-model" class="filter-select">
|
|
197
|
+
<option value="">All Models</option>
|
|
198
|
+
<% [...modelNames].sort().forEach(m => { %>
|
|
199
|
+
<option value="<%= m %>"><%= m %></option>
|
|
200
|
+
<% }) %>
|
|
201
|
+
</select>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Per-Model Breakdown -->
|
|
206
|
+
<% const modelEntries = Object.entries(allModels); %>
|
|
207
|
+
<% if (modelEntries.length > 0) { %>
|
|
208
|
+
<div class="card mb-4">
|
|
209
|
+
<div class="card-header"><h3>Usage by Model</h3></div>
|
|
210
|
+
<div class="table-wrap">
|
|
211
|
+
<table>
|
|
212
|
+
<thead>
|
|
213
|
+
<tr>
|
|
214
|
+
<th>Model</th>
|
|
215
|
+
<th class="text-right">Calls</th>
|
|
216
|
+
<th class="text-right">Prompt</th>
|
|
217
|
+
<th class="text-right">Completion</th>
|
|
218
|
+
<th class="text-right">Total</th>
|
|
219
|
+
<th class="text-right">Avg Duration</th>
|
|
220
|
+
</tr>
|
|
221
|
+
</thead>
|
|
222
|
+
<tbody>
|
|
223
|
+
<% modelEntries.sort((a, b) => b[1].totalTokens - a[1].totalTokens).forEach(([model, stats]) => { %>
|
|
224
|
+
<tr>
|
|
225
|
+
<td><code><%= model %></code></td>
|
|
226
|
+
<td class="text-right mono"><%= stats.calls %></td>
|
|
227
|
+
<td class="text-right mono"><%= stats.promptTokens.toLocaleString() %></td>
|
|
228
|
+
<td class="text-right mono"><%= stats.completionTokens.toLocaleString() %></td>
|
|
229
|
+
<td class="text-right mono"><%= stats.totalTokens.toLocaleString() %></td>
|
|
230
|
+
<td class="text-right mono"><%= stats.calls > 0 ? Math.round(stats.totalDuration / stats.calls) + 'ms' : '—' %></td>
|
|
231
|
+
</tr>
|
|
232
|
+
<% }) %>
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<% } %>
|
|
238
|
+
|
|
239
|
+
<!-- Per-Agent Token Usage -->
|
|
240
|
+
<div class="card mb-4">
|
|
241
|
+
<div class="card-header"><h3>Per-Agent Token Usage</h3></div>
|
|
242
|
+
<% if (rows.length === 0) { %>
|
|
243
|
+
<div class="empty-state">
|
|
244
|
+
<div class="icon">◈</div>
|
|
245
|
+
<p>No agents online to report token usage</p>
|
|
246
|
+
</div>
|
|
247
|
+
<% } else { %>
|
|
248
|
+
<div class="table-wrap">
|
|
249
|
+
<table>
|
|
250
|
+
<thead>
|
|
251
|
+
<tr>
|
|
252
|
+
<th>Agent</th>
|
|
253
|
+
<th class="text-right">Calls</th>
|
|
254
|
+
<th class="text-right">Prompt</th>
|
|
255
|
+
<th class="text-right">Completion</th>
|
|
256
|
+
<th class="text-right">Total</th>
|
|
257
|
+
<th>Share</th>
|
|
258
|
+
</tr>
|
|
259
|
+
</thead>
|
|
260
|
+
<tbody>
|
|
261
|
+
<% rows.forEach(r => { %>
|
|
262
|
+
<tr>
|
|
263
|
+
<td>
|
|
264
|
+
<a href="/agents/<%= r.agent.id %>" class="link"><%= r.agent.name %></a>
|
|
265
|
+
</td>
|
|
266
|
+
<td class="text-right mono"><%= r.callCount %></td>
|
|
267
|
+
<td class="text-right mono"><%= r.promptTokens.toLocaleString() %></td>
|
|
268
|
+
<td class="text-right mono"><%= r.completionTokens.toLocaleString() %></td>
|
|
269
|
+
<td class="text-right mono"><%= r.totalTokens.toLocaleString() %></td>
|
|
270
|
+
<td>
|
|
271
|
+
<% const pct = totalTokens > 0 ? ((r.totalTokens / totalTokens) * 100).toFixed(1) : '0.0'; %>
|
|
272
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
273
|
+
<div style="width:80px;height:6px;background:var(--bg-tertiary);border-radius:3px;overflow:hidden;">
|
|
274
|
+
<div style="width:<%= pct %>%;height:100%;background:var(--accent);border-radius:3px;"></div>
|
|
275
|
+
</div>
|
|
276
|
+
<span class="text-muted text-sm"><%= pct %>%</span>
|
|
277
|
+
</div>
|
|
278
|
+
</td>
|
|
279
|
+
</tr>
|
|
280
|
+
<% }) %>
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
<% } %>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Recent LLM Call History -->
|
|
288
|
+
<div class="card">
|
|
289
|
+
<div class="card-header">
|
|
290
|
+
<h3>Recent LLM Calls</h3>
|
|
291
|
+
<span class="text-muted text-sm" id="call-count-label"><%= allCallHistory.length %> calls</span>
|
|
292
|
+
</div>
|
|
293
|
+
<% if (allCallHistory.length === 0) { %>
|
|
294
|
+
<div class="empty-state">
|
|
295
|
+
<div class="icon">◈</div>
|
|
296
|
+
<p>No LLM calls recorded yet. Send a message to an agent to see call history.</p>
|
|
297
|
+
</div>
|
|
298
|
+
<% } else { %>
|
|
299
|
+
<div class="table-wrap">
|
|
300
|
+
<table id="call-history-table">
|
|
301
|
+
<thead>
|
|
302
|
+
<tr>
|
|
303
|
+
<th>Time</th>
|
|
304
|
+
<th>Agent</th>
|
|
305
|
+
<th>Model</th>
|
|
306
|
+
<th>Tier</th>
|
|
307
|
+
<th class="text-right">Prompt</th>
|
|
308
|
+
<th class="text-right">Completion</th>
|
|
309
|
+
<th class="text-right">Total</th>
|
|
310
|
+
<th class="text-right">Duration</th>
|
|
311
|
+
</tr>
|
|
312
|
+
</thead>
|
|
313
|
+
<tbody>
|
|
314
|
+
<% allCallHistory.forEach(call => { %>
|
|
315
|
+
<tr data-ts="<%= new Date(call.timestamp).getTime() %>" data-agent="<%= call.agentName %>" data-model="<%= call.model %>" data-prompt="<%= call.promptTokens %>" data-completion="<%= call.completionTokens %>" data-total="<%= call.totalTokens %>">
|
|
316
|
+
<td class="text-sm mono"><%= new Date(call.timestamp).toLocaleTimeString() %></td>
|
|
317
|
+
<td>
|
|
318
|
+
<a href="/agents/<%= call.agentId %>" class="link"><%= call.agentName %></a>
|
|
319
|
+
</td>
|
|
320
|
+
<td><code><%= call.model %></code></td>
|
|
321
|
+
<td><span class="badge badge-muted"><%= call.tier %></span></td>
|
|
322
|
+
<td class="text-right mono"><%= call.promptTokens.toLocaleString() %></td>
|
|
323
|
+
<td class="text-right mono"><%= call.completionTokens.toLocaleString() %></td>
|
|
324
|
+
<td class="text-right mono"><%= call.totalTokens.toLocaleString() %></td>
|
|
325
|
+
<td class="text-right mono"><%= call.durationMs %>ms</td>
|
|
326
|
+
</tr>
|
|
327
|
+
<% }) %>
|
|
328
|
+
</tbody>
|
|
329
|
+
</table>
|
|
330
|
+
</div>
|
|
331
|
+
<% } %>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<script>
|
|
335
|
+
(function() {
|
|
336
|
+
const table = document.getElementById('call-history-table');
|
|
337
|
+
if (!table) return;
|
|
338
|
+
const allRows = Array.from(table.querySelectorAll('tbody tr'));
|
|
339
|
+
const rangeGroup = document.getElementById('time-range-btns');
|
|
340
|
+
const agentSel = document.getElementById('filter-agent');
|
|
341
|
+
const modelSel = document.getElementById('filter-model');
|
|
342
|
+
const countLabel = document.getElementById('call-count-label');
|
|
343
|
+
|
|
344
|
+
let activeRange = 'all';
|
|
345
|
+
|
|
346
|
+
function applyFilters() {
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
const agentFilter = agentSel.value;
|
|
349
|
+
const modelFilter = modelSel.value;
|
|
350
|
+
let shown = 0, fPrompt = 0, fComp = 0, fTotal = 0;
|
|
351
|
+
allRows.forEach(row => {
|
|
352
|
+
const ts = parseInt(row.dataset.ts, 10);
|
|
353
|
+
const agent = row.dataset.agent;
|
|
354
|
+
const model = row.dataset.model;
|
|
355
|
+
let visible = true;
|
|
356
|
+
if (activeRange !== 'all' && (now - ts) > parseInt(activeRange, 10)) visible = false;
|
|
357
|
+
if (agentFilter && agent !== agentFilter) visible = false;
|
|
358
|
+
if (modelFilter && model !== modelFilter) visible = false;
|
|
359
|
+
row.style.display = visible ? '' : 'none';
|
|
360
|
+
if (visible) {
|
|
361
|
+
shown++;
|
|
362
|
+
fPrompt += parseInt(row.dataset.prompt, 10) || 0;
|
|
363
|
+
fComp += parseInt(row.dataset.completion, 10) || 0;
|
|
364
|
+
fTotal += parseInt(row.dataset.total, 10) || 0;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
if (countLabel) countLabel.textContent = shown + ' calls';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
rangeGroup.addEventListener('click', function(e) {
|
|
371
|
+
const btn = e.target.closest('[data-range]');
|
|
372
|
+
if (!btn) return;
|
|
373
|
+
activeRange = btn.dataset.range;
|
|
374
|
+
rangeGroup.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
|
|
375
|
+
btn.classList.add('active');
|
|
376
|
+
applyFilters();
|
|
377
|
+
});
|
|
378
|
+
agentSel.addEventListener('change', applyFilters);
|
|
379
|
+
modelSel.addEventListener('change', applyFilters);
|
|
380
|
+
})();
|
|
381
|
+
</script>
|