diff --git a/docs/plans/state.json b/docs/plans/state.json
index f7839a1..beecb3e 100644
--- a/docs/plans/state.json
+++ b/docs/plans/state.json
@@ -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",
diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js
index 4004f77..9d827d9 100644
--- a/src/gateway/ui/pages/dashboard.js
+++ b/src/gateway/ui/pages/dashboard.js
@@ -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) {
Loading...
+
Loading events...
@@ -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 = `
+
+
+
Sessions (Window)
+
${formatNumber(totalSessions)}
+
+
+
Messages (Window)
+
${formatNumber(totalMessages)}
+
+
+
Avg Session
+
${formatSessionDurationFromMessages(avgMessagesPerSession)}
+
+
+ `;
+
+ const topToolsHtml = topTools.length > 0
+ ? `
`
+ : '
No tool usage captured in this window
';
+
+ const topTopicsHtml = topTopics.length > 0
+ ? `
`
+ : '
No indexed topics captured in this window
';
+
+ const topSessionsHtml = topSessions.length > 0
+ ? `
+
+
+ | Session |
+ Messages |
+ Last Active |
+
+
+
+ ${topSessions.map((session) => `
+
+ | ${escapeHtml(session.sessionId)} |
+ ${formatNumber(session.messages)} |
+ ${timeAgo(session.lastActivity * 1000)} |
+
+ `).join('')}
+
+
`
+ : '
No session activity in this window
';
+
+ const dailyHtml = daily.length > 0
+ ? `
+
+
+ | Day |
+ Sessions |
+ Messages |
+
+
+
+ ${daily.slice(0, 7).map((row) => `
+
+ | ${formatDay(row.day)} |
+ ${formatNumber(row.sessions)} |
+ ${formatNumber(row.messages)} |
+
+ `).join('')}
+
+
`
+ : '
No daily activity in this window
';
+
+ el.innerHTML = `
+ ${summaryHtml}
+
+
+ Top Tools
+ ${topToolsHtml}
+
+
+ Top Topics
+ ${topTopicsHtml}
+
+
+
+
+ Top Sessions
+ ${topSessionsHtml}
+
+
+ Daily Trend (Last 7 Rows)
+ ${dailyHtml}
+
+
+ `;
+}
+
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);
}