feat(gateway): add dashboard session analytics panel

This commit is contained in:
William Valentin
2026-02-16 14:14:16 -08:00
parent 93621bbe6e
commit 3203c1f3fe
2 changed files with 145 additions and 2 deletions
+11
View File
@@ -50,6 +50,17 @@
],
"test_status": "pnpm test:run src/automation/presets.test.ts src/automation/cron.test.ts src/automation/heartbeat.test.ts src/backup/scheduler.test.ts src/backup/status.test.ts src/tools/builtin/minio-share.test.ts src/tools/policy.test.ts src/config/schema.test.ts src/daemon/channels.test.ts src/gateway/handlers/services.test.ts src/session/store.test.ts src/tools/executor.test.ts src/gateway/handlers/handlers.test.ts + pnpm typecheck passing"
},
"dashboard-session-analytics-panel": {
"status": "completed",
"date": "2026-02-16",
"updated": "2026-02-16",
"summary": "Extended the web UI dashboard with a Session Analytics panel backed by `system.sessionAnalytics`, including window summary cards, top tools/topics, top sessions, and daily trend tables refreshed alongside health/services.",
"files_modified": [
"src/gateway/ui/pages/dashboard.js",
"docs/plans/state.json"
],
"test_status": "pnpm eslint src/gateway/ui/pages/dashboard.js + pnpm typecheck passing; full pnpm lint currently fails due pre-existing unrelated repo lint errors"
},
"backup-session-summary-audit-trail": {
"status": "completed",
"date": "2026-02-16",
+134 -2
View File
@@ -33,6 +33,21 @@ function formatTime(timestamp) {
return d.toLocaleTimeString('en-GB', { hour12: false });
}
function formatDay(day) {
const parsed = new Date(`${day}T00:00:00`);
if (Number.isNaN(parsed.getTime())) {return day;}
return parsed.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
}
function formatNumber(value) {
return (value ?? 0).toLocaleString();
}
function formatSessionDurationFromMessages(avgMessagesPerSession) {
if (!avgMessagesPerSession || avgMessagesPerSession <= 0) {return '—';}
return `${avgMessagesPerSession.toFixed(1)} msgs/session`;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
@@ -55,6 +70,11 @@ function renderSkeleton(el) {
<div class="text-muted text-sm">Loading...</div>
</div>
<h2 class="section-title">Session Analytics</h2>
<div id="ops-session-analytics">
<div class="text-muted text-sm">Loading...</div>
</div>
<h2 class="section-title">Event Stream</h2>
<div class="event-stream" id="ops-events">
<div class="event-row event-level-info">Loading events...</div>
@@ -217,6 +237,115 @@ function updateActiveRequests(requestsData) {
`;
}
function updateSessionAnalytics(analyticsData) {
const el = document.getElementById('ops-session-analytics');
if (!el) {return;}
const daily = analyticsData?.daily ?? [];
const topSessions = analyticsData?.topSessions ?? [];
const topTools = analyticsData?.topTools ?? [];
const topTopics = analyticsData?.topTopics ?? [];
const totalSessions = analyticsData?.totalSessions ?? 0;
const totalMessages = analyticsData?.totalMessages ?? 0;
const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;
const summaryHtml = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Sessions (Window)</div>
<div class="stat-value">${formatNumber(totalSessions)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Messages (Window)</div>
<div class="stat-value">${formatNumber(totalMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Session</div>
<div class="stat-value">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
</div>
</div>
`;
const topToolsHtml = topTools.length > 0
? `<ul class="tool-list">${topTools.map((tool) =>
`<li><span class="text-accent">${escapeHtml(tool.toolName)}</span> <span class="text-muted">(${formatNumber(tool.executions)})</span></li>`,
).join('')}</ul>`
: '<div class="text-muted text-sm">No tool usage captured in this window</div>';
const topTopicsHtml = topTopics.length > 0
? `<ul class="tool-list">${topTopics.map((topic) =>
`<li><span class="text-accent">${escapeHtml(topic.topic)}</span> <span class="text-muted">(${formatNumber(topic.occurrences)})</span></li>`,
).join('')}</ul>`
: '<div class="text-muted text-sm">No indexed topics captured in this window</div>';
const topSessionsHtml = topSessions.length > 0
? `<table>
<thead>
<tr>
<th>Session</th>
<th>Messages</th>
<th>Last Active</th>
</tr>
</thead>
<tbody>
${topSessions.map((session) => `
<tr>
<td>${escapeHtml(session.sessionId)}</td>
<td>${formatNumber(session.messages)}</td>
<td>${timeAgo(session.lastActivity * 1000)}</td>
</tr>
`).join('')}
</tbody>
</table>`
: '<div class="text-muted text-sm">No session activity in this window</div>';
const dailyHtml = daily.length > 0
? `<table>
<thead>
<tr>
<th>Day</th>
<th>Sessions</th>
<th>Messages</th>
</tr>
</thead>
<tbody>
${daily.slice(0, 7).map((row) => `
<tr>
<td>${formatDay(row.day)}</td>
<td>${formatNumber(row.sessions)}</td>
<td>${formatNumber(row.messages)}</td>
</tr>
`).join('')}
</tbody>
</table>`
: '<div class="text-muted text-sm">No daily activity in this window</div>';
el.innerHTML = `
${summaryHtml}
<div class="services-grid">
<div class="service-card">
<span class="service-name">Top Tools</span>
${topToolsHtml}
</div>
<div class="service-card">
<span class="service-name">Top Topics</span>
${topTopicsHtml}
</div>
</div>
<div class="services-grid">
<div class="service-card">
<span class="service-name">Top Sessions</span>
${topSessionsHtml}
</div>
<div class="service-card">
<span class="service-name">Daily Trend (Last 7 Rows)</span>
${dailyHtml}
</div>
</div>
`;
}
function _updateChannels(channelsData) {
const el = document.getElementById('ops-channels');
if (!el) {return;}
@@ -285,11 +414,12 @@ async function fetchFast(client) {
async function fetchSlow(client) {
try {
const [health, services] = await Promise.all([
const [health, services, sessionAnalytics] = await Promise.all([
client.call('system.health'),
client.call('system.services'),
client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
]);
return { health, services };
return { health, services, sessionAnalytics };
} catch {
return null;
}
@@ -320,6 +450,7 @@ async function loadDashboard(el, client) {
}
if (slow) {
updateServices(slow.services);
updateSessionAnalytics(slow.sessionAnalytics);
}
// Fast refresh: 3 seconds for metrics, events, requests
@@ -341,6 +472,7 @@ async function loadDashboard(el, client) {
_lastHealth = data.health;
updateCounters(_lastMetrics, data.health);
updateServices(data.services);
updateSessionAnalytics(data.sessionAnalytics);
}
}, 10000);
}