@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/layout.ejs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>MicroAgents Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/public/styles.css">
|
|
8
|
+
<script src="/public/htmx.min.js"></script>
|
|
9
|
+
<script src="/public/chart.umd.min.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div class="app">
|
|
13
|
+
<aside class="sidebar">
|
|
14
|
+
<div class="sidebar-brand">
|
|
15
|
+
<h1>MicroAgents</h1>
|
|
16
|
+
<span class="subtitle">Observability Dashboard</span>
|
|
17
|
+
</div>
|
|
18
|
+
<nav>
|
|
19
|
+
<a href="/" class="<%= currentPage === 'agents' ? 'active' : '' %>">
|
|
20
|
+
<span class="icon">⬡</span> Agents
|
|
21
|
+
</a>
|
|
22
|
+
<a href="/tokens" class="<%= currentPage === 'tokens' ? 'active' : '' %>">
|
|
23
|
+
<span class="icon">◈</span> Tokens & Cost
|
|
24
|
+
</a>
|
|
25
|
+
<a href="/scheduler" class="<%= currentPage === 'scheduler' ? 'active' : '' %>">
|
|
26
|
+
<span class="icon">⏱</span> Scheduler
|
|
27
|
+
</a>
|
|
28
|
+
<a href="/memory" class="<%= currentPage === 'memory' ? 'active' : '' %>">
|
|
29
|
+
<span class="icon">◉</span> Memory
|
|
30
|
+
</a>
|
|
31
|
+
<a href="/queues" class="<%= currentPage === 'queues' ? 'active' : '' %>">
|
|
32
|
+
<span class="icon">≡</span> Queues
|
|
33
|
+
</a>
|
|
34
|
+
<a href="/browser" class="<%= currentPage === 'browser' || currentPage === 'browser-detail' ? 'active' : '' %>">
|
|
35
|
+
<span class="icon">⊞</span> Browser
|
|
36
|
+
</a>
|
|
37
|
+
<a href="/logs" class="<%= currentPage === 'logs' ? 'active' : '' %>">
|
|
38
|
+
<span class="icon">▤</span> Logs
|
|
39
|
+
</a>
|
|
40
|
+
</nav>
|
|
41
|
+
</aside>
|
|
42
|
+
<main class="main">
|
|
43
|
+
<%- body %>
|
|
44
|
+
</main>
|
|
45
|
+
</div>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
package/views/logs.ejs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Live Logs</h2>
|
|
3
|
+
<p>Real-time log output from all agents</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-header">
|
|
8
|
+
<h3>Log Stream</h3>
|
|
9
|
+
<div class="flex items-center gap-4">
|
|
10
|
+
<select id="log-agent-filter" onchange="filterLogs()" class="filter-select">
|
|
11
|
+
<option value="all">All agents</option>
|
|
12
|
+
<% agents.forEach(a => { %>
|
|
13
|
+
<option value="<%= a.id %>"><%= a.name %></option>
|
|
14
|
+
<% }) %>
|
|
15
|
+
</select>
|
|
16
|
+
<select id="log-level-filter" onchange="filterLogs()" class="filter-select">
|
|
17
|
+
<option value="all">All levels</option>
|
|
18
|
+
<option value="debug">Debug</option>
|
|
19
|
+
<option value="info">Info</option>
|
|
20
|
+
<option value="warn">Warn</option>
|
|
21
|
+
<option value="error">Error</option>
|
|
22
|
+
</select>
|
|
23
|
+
<span class="text-muted text-sm" id="log-count"><%= logs ? logs.length : 0 %> entries</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div id="log-viewer"
|
|
28
|
+
hx-get="/partial/logs" hx-trigger="every 5s" hx-swap="innerHTML">
|
|
29
|
+
<%- include('partials/logs-content', { logs }) %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
function filterLogs() {
|
|
35
|
+
const agentFilter = document.getElementById('log-agent-filter').value;
|
|
36
|
+
const levelFilter = document.getElementById('log-level-filter').value;
|
|
37
|
+
const rows = document.querySelectorAll('.log-row');
|
|
38
|
+
let visible = 0;
|
|
39
|
+
rows.forEach(row => {
|
|
40
|
+
const matchAgent = agentFilter === 'all' || row.dataset.agent === agentFilter;
|
|
41
|
+
const matchLevel = levelFilter === 'all' || row.dataset.level === levelFilter;
|
|
42
|
+
const show = matchAgent && matchLevel;
|
|
43
|
+
row.style.display = show ? '' : 'none';
|
|
44
|
+
// Also hide its detail row
|
|
45
|
+
const detail = row.nextElementSibling;
|
|
46
|
+
if (detail && detail.classList.contains('log-detail-row')) {
|
|
47
|
+
if (!show) detail.style.display = 'none';
|
|
48
|
+
}
|
|
49
|
+
if (show) visible++;
|
|
50
|
+
});
|
|
51
|
+
const label = document.getElementById('log-count');
|
|
52
|
+
if (label) label.textContent = visible + ' entries';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener('click', function(e) {
|
|
56
|
+
const row = e.target.closest('.log-row');
|
|
57
|
+
if (!row) return;
|
|
58
|
+
const detail = row.nextElementSibling;
|
|
59
|
+
if (detail && detail.classList.contains('log-detail-row')) {
|
|
60
|
+
const isHidden = detail.style.display === 'none';
|
|
61
|
+
detail.style.display = isHidden ? '' : 'none';
|
|
62
|
+
row.classList.toggle('expanded', isHidden);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
</script>
|
package/views/memory.ejs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<h2>Memory</h2>
|
|
3
|
+
<p>Long-term knowledge, episodic memory, and notes</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="grid grid-3 mb-4">
|
|
7
|
+
<div class="stat-card">
|
|
8
|
+
<div class="label">Knowledge (LTM)</div>
|
|
9
|
+
<div class="value accent"><%= stats.knowledge.toLocaleString() %></div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="stat-card">
|
|
12
|
+
<div class="label">Episodes (MTM)</div>
|
|
13
|
+
<div class="value green"><%= stats.episodes.toLocaleString() %></div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="stat-card">
|
|
16
|
+
<div class="label">Notes</div>
|
|
17
|
+
<div class="value purple"><%= stats.notes.toLocaleString() %></div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Search -->
|
|
22
|
+
<div class="card mb-4" style="padding:12px 16px;">
|
|
23
|
+
<form method="GET" action="/memory" class="flex gap-4" style="align-items:center;">
|
|
24
|
+
<div style="font-size:12px;color:var(--text-secondary);white-space:nowrap;">Search Memory:</div>
|
|
25
|
+
<input type="text" name="q" value="<%= q || '' %>" placeholder="Search knowledge & episodes..." class="filter-select" style="flex:1;padding:6px 10px;">
|
|
26
|
+
<button type="submit" class="btn">Search</button>
|
|
27
|
+
<% if (q) { %><a href="/memory" class="btn" style="text-decoration:none;">Clear</a><% } %>
|
|
28
|
+
</form>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<% if (q) { %>
|
|
32
|
+
<div class="card mb-4" style="padding:8px 16px;font-size:13px;color:var(--text-secondary);">
|
|
33
|
+
Results for "<strong style="color:var(--text-primary);"><%= q %></strong>": <%= knowledge.length %> knowledge, <%= episodes.length %> episodes
|
|
34
|
+
</div>
|
|
35
|
+
<% } %>
|
|
36
|
+
|
|
37
|
+
<!-- Knowledge Entries -->
|
|
38
|
+
<div class="card mb-4">
|
|
39
|
+
<div class="card-header">
|
|
40
|
+
<h3>Knowledge (LTM)</h3>
|
|
41
|
+
<span class="text-muted text-sm"><%= knowledge.length %> entries</span>
|
|
42
|
+
</div>
|
|
43
|
+
<% if (knowledge.length === 0) { %>
|
|
44
|
+
<div class="empty-state">
|
|
45
|
+
<div class="icon">◈</div>
|
|
46
|
+
<p><%= q ? 'No matching knowledge entries' : 'No knowledge stored yet' %></p>
|
|
47
|
+
</div>
|
|
48
|
+
<% } else { %>
|
|
49
|
+
<div class="table-wrap">
|
|
50
|
+
<table>
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
<th>Content</th>
|
|
54
|
+
<th>Category</th>
|
|
55
|
+
<th>Source</th>
|
|
56
|
+
<th>Created</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
<% knowledge.forEach(k => { %>
|
|
61
|
+
<tr>
|
|
62
|
+
<td style="max-width:500px;white-space:pre-wrap;word-break:break-word;font-size:12px;"><%= k.content || '—' %></td>
|
|
63
|
+
<td class="mono text-sm"><%= k.category || '—' %></td>
|
|
64
|
+
<td class="mono text-sm"><%= k.source || '—' %></td>
|
|
65
|
+
<td class="text-sm mono"><%= k.created_at ? new Date(k.created_at).toLocaleString() : '—' %></td>
|
|
66
|
+
</tr>
|
|
67
|
+
<% }) %>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
<% } %>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Episodes -->
|
|
75
|
+
<div class="card mb-4">
|
|
76
|
+
<div class="card-header">
|
|
77
|
+
<h3>Episodes (MTM)</h3>
|
|
78
|
+
<span class="text-muted text-sm"><%= episodes.length %> entries</span>
|
|
79
|
+
</div>
|
|
80
|
+
<% if (episodes.length === 0) { %>
|
|
81
|
+
<div class="empty-state">
|
|
82
|
+
<div class="icon">◈</div>
|
|
83
|
+
<p><%= q ? 'No matching episodes' : 'No episodes recorded yet' %></p>
|
|
84
|
+
</div>
|
|
85
|
+
<% } else { %>
|
|
86
|
+
<div class="table-wrap">
|
|
87
|
+
<table>
|
|
88
|
+
<thead>
|
|
89
|
+
<tr>
|
|
90
|
+
<th>Summary</th>
|
|
91
|
+
<th>Type</th>
|
|
92
|
+
<th>Created</th>
|
|
93
|
+
</tr>
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
<% episodes.forEach(ep => { %>
|
|
97
|
+
<tr>
|
|
98
|
+
<td style="max-width:600px;white-space:pre-wrap;word-break:break-word;font-size:12px;"><%= ep.summary || ep.content || '—' %></td>
|
|
99
|
+
<td class="mono text-sm"><%= ep.type || ep.episode_type || '—' %></td>
|
|
100
|
+
<td class="text-sm mono"><%= ep.created_at ? new Date(ep.created_at).toLocaleString() : '—' %></td>
|
|
101
|
+
</tr>
|
|
102
|
+
<% }) %>
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
<% } %>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Notes -->
|
|
110
|
+
<% if (!q) { %>
|
|
111
|
+
<div class="card mb-4">
|
|
112
|
+
<div class="card-header">
|
|
113
|
+
<h3>Notes</h3>
|
|
114
|
+
<span class="text-muted text-sm"><%= notes.length %> entries</span>
|
|
115
|
+
</div>
|
|
116
|
+
<% if (notes.length === 0) { %>
|
|
117
|
+
<div class="empty-state">
|
|
118
|
+
<div class="icon">◈</div>
|
|
119
|
+
<p>No notes created yet</p>
|
|
120
|
+
</div>
|
|
121
|
+
<% } else { %>
|
|
122
|
+
<div class="table-wrap">
|
|
123
|
+
<table>
|
|
124
|
+
<thead>
|
|
125
|
+
<tr>
|
|
126
|
+
<th>Title</th>
|
|
127
|
+
<th>Content</th>
|
|
128
|
+
<th>Created</th>
|
|
129
|
+
</tr>
|
|
130
|
+
</thead>
|
|
131
|
+
<tbody>
|
|
132
|
+
<% notes.forEach(n => { %>
|
|
133
|
+
<tr>
|
|
134
|
+
<td class="mono"><%= n.title || '—' %></td>
|
|
135
|
+
<td style="max-width:500px;white-space:pre-wrap;word-break:break-word;font-size:12px;"><%= n.content || '—' %></td>
|
|
136
|
+
<td class="text-sm mono" style="white-space:nowrap;"><%= n.created_at ? new Date(n.created_at).toLocaleString() : '—' %></td>
|
|
137
|
+
</tr>
|
|
138
|
+
<% }) %>
|
|
139
|
+
</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
</div>
|
|
142
|
+
<% } %>
|
|
143
|
+
</div>
|
|
144
|
+
<% } %>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<div class="grid grid-4 mb-4">
|
|
2
|
+
<div class="stat-card">
|
|
3
|
+
<div class="label">Status</div>
|
|
4
|
+
<div class="value <%= agent.status === 'online' ? 'green' : 'red' %>">
|
|
5
|
+
<%= agent.status %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="stat-card">
|
|
9
|
+
<div class="label">Active Sessions</div>
|
|
10
|
+
<div class="value accent"><%= metrics?.sessions?.active ?? 0 %></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="stat-card">
|
|
13
|
+
<div class="label">LLM Calls</div>
|
|
14
|
+
<div class="value purple"><%= metrics?.completion?.callCount ?? 0 %></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="stat-card">
|
|
17
|
+
<div class="label">Uptime</div>
|
|
18
|
+
<div class="value"><%= metrics?.agent?.uptime ? Math.floor(metrics.agent.uptime / 60) + 'm' : '—' %></div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Token Usage Summary -->
|
|
23
|
+
<%
|
|
24
|
+
const comp = metrics?.completion || {};
|
|
25
|
+
const total = comp.total || {};
|
|
26
|
+
const hasUsage = (total.totalTokens || comp.totalTokens || 0) > 0;
|
|
27
|
+
const byModel = comp.byModel || {};
|
|
28
|
+
const modelKeys = Object.keys(byModel);
|
|
29
|
+
%>
|
|
30
|
+
<% if (hasUsage) { %>
|
|
31
|
+
<div class="card mb-4">
|
|
32
|
+
<div class="card-header"><h3>Token Usage</h3></div>
|
|
33
|
+
<div class="grid grid-3 mb-4" style="padding: 0 16px;">
|
|
34
|
+
<div class="stat-card">
|
|
35
|
+
<div class="label">Prompt Tokens</div>
|
|
36
|
+
<div class="value purple"><%= (total.promptTokens || comp.promptTokens || 0).toLocaleString() %></div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stat-card">
|
|
39
|
+
<div class="label">Completion Tokens</div>
|
|
40
|
+
<div class="value green"><%= (total.completionTokens || comp.completionTokens || 0).toLocaleString() %></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="stat-card">
|
|
43
|
+
<div class="label">Total Tokens</div>
|
|
44
|
+
<div class="value accent"><%= (total.totalTokens || comp.totalTokens || 0).toLocaleString() %></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<% if (modelKeys.length > 0) { %>
|
|
48
|
+
<div class="table-wrap">
|
|
49
|
+
<table>
|
|
50
|
+
<thead><tr><th>Model</th><th class="text-right">Calls</th><th class="text-right">Prompt</th><th class="text-right">Completion</th><th class="text-right">Total</th><th class="text-right">Avg Duration</th></tr></thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
<% modelKeys.forEach(model => { const s = byModel[model]; %>
|
|
53
|
+
<tr>
|
|
54
|
+
<td><code><%= model %></code></td>
|
|
55
|
+
<td class="text-right mono"><%= s.calls %></td>
|
|
56
|
+
<td class="text-right mono"><%= s.promptTokens.toLocaleString() %></td>
|
|
57
|
+
<td class="text-right mono"><%= s.completionTokens.toLocaleString() %></td>
|
|
58
|
+
<td class="text-right mono"><%= s.totalTokens.toLocaleString() %></td>
|
|
59
|
+
<td class="text-right mono"><%= s.avgDurationMs %>ms</td>
|
|
60
|
+
</tr>
|
|
61
|
+
<% }) %>
|
|
62
|
+
</tbody>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
<% } %>
|
|
66
|
+
</div>
|
|
67
|
+
<% } %>
|
|
68
|
+
|
|
69
|
+
<div class="grid grid-2 mb-4">
|
|
70
|
+
<!-- Tools & Skills -->
|
|
71
|
+
<div class="card">
|
|
72
|
+
<div class="card-header"><h3>Tools & Skills</h3></div>
|
|
73
|
+
<% const tools = metrics?.tools || agent.tools || []; %>
|
|
74
|
+
<% if (tools.length === 0) { %>
|
|
75
|
+
<p class="text-muted">No tools registered</p>
|
|
76
|
+
<% } else { %>
|
|
77
|
+
<div class="table-wrap">
|
|
78
|
+
<table>
|
|
79
|
+
<thead><tr><th>Name</th><th>Visibility</th><th>Description</th></tr></thead>
|
|
80
|
+
<tbody>
|
|
81
|
+
<% tools.forEach(t => { %>
|
|
82
|
+
<tr>
|
|
83
|
+
<td><code><%= t.name %></code></td>
|
|
84
|
+
<td><span class="badge badge-muted"><%= t.visibility %></span></td>
|
|
85
|
+
<td class="text-muted truncate" style="max-width: 300px;"><%= t.description %></td>
|
|
86
|
+
</tr>
|
|
87
|
+
<% }) %>
|
|
88
|
+
</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
</div>
|
|
91
|
+
<% } %>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Endpoints -->
|
|
95
|
+
<div class="card">
|
|
96
|
+
<div class="card-header"><h3>Endpoints</h3></div>
|
|
97
|
+
<% const endpoints = agent.endpoints || []; %>
|
|
98
|
+
<% if (endpoints.length === 0) { %>
|
|
99
|
+
<p class="text-muted">No custom endpoints</p>
|
|
100
|
+
<% } else { %>
|
|
101
|
+
<div class="table-wrap">
|
|
102
|
+
<table>
|
|
103
|
+
<thead><tr><th>Method</th><th>Path</th><th>Description</th></tr></thead>
|
|
104
|
+
<tbody>
|
|
105
|
+
<% endpoints.forEach(ep => { %>
|
|
106
|
+
<tr>
|
|
107
|
+
<td><span class="badge badge-purple"><%= ep.method %></span></td>
|
|
108
|
+
<td><code><%= ep.path %></code></td>
|
|
109
|
+
<td class="text-muted"><%= ep.description || '—' %></td>
|
|
110
|
+
</tr>
|
|
111
|
+
<% }) %>
|
|
112
|
+
</tbody>
|
|
113
|
+
</table>
|
|
114
|
+
</div>
|
|
115
|
+
<% } %>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<!-- Sessions -->
|
|
120
|
+
<div class="card">
|
|
121
|
+
<div class="card-header"><h3>Sessions</h3></div>
|
|
122
|
+
<% const sessions = metrics?.sessions?.recent || []; %>
|
|
123
|
+
<% if (sessions.length === 0) { %>
|
|
124
|
+
<p class="text-muted">No sessions recorded</p>
|
|
125
|
+
<% } else { %>
|
|
126
|
+
<div class="table-wrap">
|
|
127
|
+
<table>
|
|
128
|
+
<thead>
|
|
129
|
+
<tr>
|
|
130
|
+
<th>Session ID</th>
|
|
131
|
+
<th>Started</th>
|
|
132
|
+
<th>Ended</th>
|
|
133
|
+
<th>Status</th>
|
|
134
|
+
<th></th>
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
<% sessions.forEach(s => { %>
|
|
139
|
+
<tr>
|
|
140
|
+
<td><code><%= s.id.substring(0, 12) %>…</code></td>
|
|
141
|
+
<td class="text-sm"><%= new Date(s.startedAt).toLocaleString() %></td>
|
|
142
|
+
<td class="text-sm"><%= s.endedAt ? new Date(s.endedAt).toLocaleString() : '—' %></td>
|
|
143
|
+
<td>
|
|
144
|
+
<% if (!s.endedAt) { %>
|
|
145
|
+
<span class="badge badge-green">active</span>
|
|
146
|
+
<% } else { %>
|
|
147
|
+
<span class="badge badge-muted">ended</span>
|
|
148
|
+
<% } %>
|
|
149
|
+
</td>
|
|
150
|
+
<td>
|
|
151
|
+
<a href="/agents/<%= agent.id %>/sessions/<%= s.id %>" class="link">inspect →</a>
|
|
152
|
+
</td>
|
|
153
|
+
</tr>
|
|
154
|
+
<% }) %>
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
<% } %>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Recent LLM Calls -->
|
|
162
|
+
<% const callHistory = metrics?.callHistory || []; %>
|
|
163
|
+
<% if (callHistory.length > 0) { %>
|
|
164
|
+
<div class="card mt-4">
|
|
165
|
+
<div class="card-header"><h3>Recent LLM Calls</h3></div>
|
|
166
|
+
<div class="table-wrap">
|
|
167
|
+
<table>
|
|
168
|
+
<thead><tr><th>Time</th><th>Model</th><th>Tier</th><th class="text-right">Prompt</th><th class="text-right">Completion</th><th class="text-right">Total</th><th class="text-right">Duration</th></tr></thead>
|
|
169
|
+
<tbody>
|
|
170
|
+
<% callHistory.slice(-20).reverse().forEach(call => { %>
|
|
171
|
+
<tr>
|
|
172
|
+
<td class="text-sm mono"><%= new Date(call.timestamp).toLocaleTimeString() %></td>
|
|
173
|
+
<td><code><%= call.model %></code></td>
|
|
174
|
+
<td><span class="badge badge-muted"><%= call.tier %></span></td>
|
|
175
|
+
<td class="text-right mono"><%= call.promptTokens.toLocaleString() %></td>
|
|
176
|
+
<td class="text-right mono"><%= call.completionTokens.toLocaleString() %></td>
|
|
177
|
+
<td class="text-right mono"><%= call.totalTokens.toLocaleString() %></td>
|
|
178
|
+
<td class="text-right mono"><%= call.durationMs %>ms</td>
|
|
179
|
+
</tr>
|
|
180
|
+
<% }) %>
|
|
181
|
+
</tbody>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<% } %>
|
|
186
|
+
|
|
187
|
+
<!-- Scheduler -->
|
|
188
|
+
<% if (metrics?.scheduler) { %>
|
|
189
|
+
<div class="card mt-4">
|
|
190
|
+
<div class="card-header"><h3>Scheduler</h3></div>
|
|
191
|
+
<pre><%= JSON.stringify(metrics.scheduler, null, 2) %></pre>
|
|
192
|
+
</div>
|
|
193
|
+
<% } %>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<% if (agents.length === 0) { %>
|
|
2
|
+
<div class="empty-state">
|
|
3
|
+
<div class="icon">⬡</div>
|
|
4
|
+
<p>No agents registered yet</p>
|
|
5
|
+
</div>
|
|
6
|
+
<% } else { %>
|
|
7
|
+
<div class="table-wrap">
|
|
8
|
+
<table>
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th>Name</th>
|
|
12
|
+
<th>ID</th>
|
|
13
|
+
<th>Domain</th>
|
|
14
|
+
<th>Status</th>
|
|
15
|
+
<th>Uptime</th>
|
|
16
|
+
<th>Endpoints</th>
|
|
17
|
+
<th>Tools</th>
|
|
18
|
+
<th>Last Heartbeat</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
<% agents.forEach(agent => { %>
|
|
23
|
+
<tr>
|
|
24
|
+
<td><a href="/agents/<%= agent.id %>" class="link"><%= agent.name %></a></td>
|
|
25
|
+
<td><code><%= agent.id %></code></td>
|
|
26
|
+
<td><%= agent.domain || '—' %></td>
|
|
27
|
+
<td>
|
|
28
|
+
<% if (agent.status === 'online') { %>
|
|
29
|
+
<span class="badge badge-green">online</span>
|
|
30
|
+
<% } else if (agent.status === 'offline') { %>
|
|
31
|
+
<span class="badge badge-red">offline</span>
|
|
32
|
+
<% } else { %>
|
|
33
|
+
<span class="badge badge-yellow"><%= agent.status %></span>
|
|
34
|
+
<% } %>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="mono">
|
|
37
|
+
<% if (agent.health && agent.health.uptime) { %>
|
|
38
|
+
<%= Math.floor(agent.health.uptime / 60) %>m
|
|
39
|
+
<% } else { %>
|
|
40
|
+
—
|
|
41
|
+
<% } %>
|
|
42
|
+
</td>
|
|
43
|
+
<td><%= (agent.endpoints || []).length %></td>
|
|
44
|
+
<td><%= (agent.tools || []).length + (agent.skills || []).length %></td>
|
|
45
|
+
<td class="text-muted text-sm">
|
|
46
|
+
<% if (agent.lastHeartbeat) { %>
|
|
47
|
+
<%= new Date(agent.lastHeartbeat).toLocaleTimeString() %>
|
|
48
|
+
<% } else { %>
|
|
49
|
+
—
|
|
50
|
+
<% } %>
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
<% }) %>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
<% } %>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<%
|
|
2
|
+
function timeAgo(dateStr) {
|
|
3
|
+
const now = Date.now();
|
|
4
|
+
const then = new Date(dateStr).getTime();
|
|
5
|
+
const diffMs = now - then;
|
|
6
|
+
const mins = Math.floor(diffMs / 60000);
|
|
7
|
+
if (mins < 1) return 'just now';
|
|
8
|
+
if (mins < 60) return mins + 'm ago';
|
|
9
|
+
const hours = Math.floor(mins / 60);
|
|
10
|
+
if (hours < 24) return hours + 'h ago';
|
|
11
|
+
const days = Math.floor(hours / 24);
|
|
12
|
+
if (days < 7) return days + 'd ago';
|
|
13
|
+
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatDuration(startedAt, endedAt) {
|
|
17
|
+
const started = new Date(startedAt).getTime();
|
|
18
|
+
const ended = endedAt ? new Date(endedAt).getTime() : Date.now();
|
|
19
|
+
const ms = ended - started;
|
|
20
|
+
if (ms < 1000) return '<1s';
|
|
21
|
+
const sec = Math.round(ms / 1000);
|
|
22
|
+
if (sec < 60) return sec + 's';
|
|
23
|
+
const min = Math.floor(sec / 60);
|
|
24
|
+
const rem = sec % 60;
|
|
25
|
+
return min + 'm ' + rem + 's';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const statusConfig = {
|
|
29
|
+
completed: { icon: '✓', class: 'badge-green', borderColor: 'var(--green)' },
|
|
30
|
+
running: { icon: '⟳', class: 'badge-yellow', borderColor: 'var(--yellow)' },
|
|
31
|
+
failed: { icon: '✕', class: 'badge-red', borderColor: 'var(--red)' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const actionIcons = { goto: '→', act: '⚡', extract: '◎', observe: '◉', agent: '⬡' };
|
|
35
|
+
%>
|
|
36
|
+
|
|
37
|
+
<% if (!sessions || sessions.length === 0) { %>
|
|
38
|
+
<div class="empty-state">
|
|
39
|
+
<div class="icon">⊞</div>
|
|
40
|
+
<p>No browser sessions recorded yet</p>
|
|
41
|
+
</div>
|
|
42
|
+
<% } else { %>
|
|
43
|
+
<div class="browser-sessions-list">
|
|
44
|
+
<% sessions.forEach(s => {
|
|
45
|
+
const sc = statusConfig[s.status] || statusConfig.failed;
|
|
46
|
+
%>
|
|
47
|
+
<a href="/browser/<%= s.id %>" class="browser-session-card" style="border-left-color: <%= sc.borderColor %>;">
|
|
48
|
+
<div class="bsc-top">
|
|
49
|
+
<div class="bsc-status">
|
|
50
|
+
<span class="badge <%= sc.class %>"><%= sc.icon %> <%= s.status %></span>
|
|
51
|
+
<% if (s.platform) { %>
|
|
52
|
+
<span class="bsc-platform"><%= s.platform %></span>
|
|
53
|
+
<% } %>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="bsc-meta">
|
|
56
|
+
<span class="bsc-time" title="<%= new Date(s.started_at).toISOString().replace('T', ' ').slice(0, 19) %>"><%= timeAgo(s.started_at) %></span>
|
|
57
|
+
<span class="bsc-sep">·</span>
|
|
58
|
+
<span class="bsc-duration"><%= formatDuration(s.started_at, s.ended_at) %></span>
|
|
59
|
+
<span class="bsc-sep">·</span>
|
|
60
|
+
<span class="bsc-steps"><%= s.step_count || 0 %> step<%= (s.step_count || 0) !== 1 ? 's' : '' %></span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="bsc-goal"><%= s.goal %></div>
|
|
64
|
+
<div class="bsc-bottom">
|
|
65
|
+
<span class="bsc-agent"><%= s.agent_id %></span>
|
|
66
|
+
<span class="bsc-id mono"><%= s.id.slice(0, 8) %></span>
|
|
67
|
+
</div>
|
|
68
|
+
</a>
|
|
69
|
+
<% }); %>
|
|
70
|
+
</div>
|
|
71
|
+
<% } %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<% if (!logs || logs.length === 0) { %>
|
|
2
|
+
<div class="empty-state">
|
|
3
|
+
<div class="icon">▤</div>
|
|
4
|
+
<p>No log entries yet. Send messages to agents to generate logs.</p>
|
|
5
|
+
</div>
|
|
6
|
+
<% } else { %>
|
|
7
|
+
<div class="table-wrap">
|
|
8
|
+
<table class="log-table">
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th style="width:20px;"></th>
|
|
12
|
+
<th style="width:90px;">Time</th>
|
|
13
|
+
<th style="width:60px;">Level</th>
|
|
14
|
+
<th style="width:120px;">Agent</th>
|
|
15
|
+
<th>Message</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<% logs.forEach(log => {
|
|
20
|
+
const levelMap = { '10': 'trace', '20': 'debug', '30': 'info', '40': 'warn', '50': 'error', '60': 'fatal' };
|
|
21
|
+
const level = typeof log.level === 'number' ? (levelMap[log.level] || 'info') : (log.level || 'info');
|
|
22
|
+
const d = new Date(log.time);
|
|
23
|
+
const time = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
|
|
24
|
+
const agent = log.agentName || log.agentId || 'unknown';
|
|
25
|
+
const extraKeys = Object.keys(log).filter(k => !['level','time','msg','agentId','agentName','pid','hostname','v'].includes(k));
|
|
26
|
+
%>
|
|
27
|
+
<tr class="log-row" data-agent="<%= log.agentId %>" data-level="<%= level %>" style="cursor:pointer;">
|
|
28
|
+
<td class="mono text-muted" style="font-size:10px;">▸</td>
|
|
29
|
+
<td class="mono text-sm" style="white-space:nowrap;"><%= time %></td>
|
|
30
|
+
<td><span class="badge badge-<%= level === 'error' || level === 'fatal' ? 'red' : level === 'warn' ? 'yellow' : level === 'debug' || level === 'trace' ? 'muted' : 'green' %>"><%= level.toUpperCase() %></span></td>
|
|
31
|
+
<td class="mono text-sm"><%= agent %></td>
|
|
32
|
+
<td class="text-sm" style="max-width:600px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><%= log.msg || '—' %></td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr class="log-detail-row" style="display:none;">
|
|
35
|
+
<td colspan="5" style="padding:8px 16px;background:var(--bg-tertiary);border-top:none;">
|
|
36
|
+
<pre class="log-detail-pre"><%= JSON.stringify(log, null, 2) %></pre>
|
|
37
|
+
</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<% }) %>
|
|
40
|
+
</tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</div>
|
|
43
|
+
<% } %>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<% if (rabbit.queues.length === 0) { %>
|
|
2
|
+
<div class="empty-state">
|
|
3
|
+
<div class="icon">≡</div>
|
|
4
|
+
<p>No queues found. Check RabbitMQ management API connection.</p>
|
|
5
|
+
</div>
|
|
6
|
+
<% } else { %>
|
|
7
|
+
<div class="table-wrap">
|
|
8
|
+
<table>
|
|
9
|
+
<thead>
|
|
10
|
+
<tr>
|
|
11
|
+
<th>Queue Name</th>
|
|
12
|
+
<th>State</th>
|
|
13
|
+
<th class="text-right">Messages</th>
|
|
14
|
+
<th class="text-right">Ready</th>
|
|
15
|
+
<th class="text-right">Unacked</th>
|
|
16
|
+
<th class="text-right">Consumers</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% rabbit.queues.forEach(q => { %>
|
|
21
|
+
<tr>
|
|
22
|
+
<td><code><%= q.name %></code></td>
|
|
23
|
+
<td>
|
|
24
|
+
<% if (q.state === 'running') { %>
|
|
25
|
+
<span class="badge badge-green">running</span>
|
|
26
|
+
<% } else { %>
|
|
27
|
+
<span class="badge badge-red"><%= q.state %></span>
|
|
28
|
+
<% } %>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="text-right mono"><%= q.messages.toLocaleString() %></td>
|
|
31
|
+
<td class="text-right mono"><%= q.messagesReady.toLocaleString() %></td>
|
|
32
|
+
<td class="text-right mono"><%= q.messagesUnacked.toLocaleString() %></td>
|
|
33
|
+
<td class="text-right mono"><%= q.consumers %></td>
|
|
34
|
+
</tr>
|
|
35
|
+
<% }) %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
<% } %>
|