feat(gateway): add dashboard session analytics panel
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user