feat: add web UI dashboard SPA with dashboard, chat, sessions, and settings pages

- Add SPA shell with hash-based router, sidebar navigation, and WebSocket RPC client
- Add dashboard page with system health cards, channel status, and auto-refresh
- Add chat page with session selector, streaming tool events, and markdown rendering
- Add sessions page with list, history viewer, and delete functionality
- Add settings page with hook pattern editor, tool list, and config viewer
- Add backend handlers: sessions.delete, sessions.switch, system.channels, system.usage
- Wire channelRegistry into gateway server for channel status reporting
- Extend static file server with .mjs, .png, .ico, .woff2 content types
This commit is contained in:
William Valentin
2026-02-07 10:07:45 -08:00
parent f7d889e35e
commit 22230a3e3f
14 changed files with 1836 additions and 207 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* Flynn Dashboard Page
*
* Shows system health cards, channel status, and usage stats.
* Auto-refreshes every 10 seconds.
*/
let _timer = null;
function formatUptime(seconds) {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const parts = [];
if (d > 0) parts.push(`${d}d`);
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
parts.push(`${s}s`);
return parts.join(' ');
}
async function loadDashboard(el, client) {
let health, channels, usage;
try {
[health, channels, usage] = await Promise.all([
client.call('system.health'),
client.call('system.channels'),
client.call('system.usage'),
]);
} catch (err) {
el.innerHTML = `<div class="empty-state">Failed to load dashboard: ${err.message}</div>`;
return;
}
// Build stats grid
const stats = [
{ label: 'Status', value: health.status?.toUpperCase() ?? 'UNKNOWN', cls: health.status === 'ok' ? 'ok' : 'error' },
{ label: 'Version', value: health.version ?? '-', cls: '' },
{ label: 'Uptime', value: formatUptime(health.uptime ?? 0), cls: '' },
{ label: 'Connections', value: String(health.connections ?? 0), cls: '' },
{ label: 'Sessions', value: String(health.sessions ?? 0), cls: '' },
{ label: 'Tools', value: String(health.tools ?? 0), cls: '' },
];
const statsHtml = stats.map(s =>
`<div class="stat-card">
<div class="stat-label">${s.label}</div>
<div class="stat-value ${s.cls}">${s.value}</div>
</div>`
).join('');
// Build channels grid
const channelList = channels?.channels ?? [];
let channelsHtml = '';
if (channelList.length > 0) {
channelsHtml = channelList.map(ch =>
`<div class="channel-card">
<span class="channel-dot ${ch.status}"></span>
<span class="channel-name">${ch.name}</span>
</div>`
).join('');
} else {
channelsHtml = '<div class="text-muted text-sm">No channels registered</div>';
}
// Build usage section
const usageItems = [
{ label: 'Total Sessions', value: String(usage?.totalSessions ?? 0) },
{ label: 'Active Connections', value: String(usage?.activeConnections ?? 0) },
{ label: 'Available Tools', value: String(usage?.tools ?? 0) },
{ label: 'Uptime', value: formatUptime(usage?.uptime ?? 0) },
];
const usageHtml = usageItems.map(u =>
`<div class="stat-card">
<div class="stat-label">${u.label}</div>
<div class="stat-value">${u.value}</div>
</div>`
).join('');
el.innerHTML = `
<h1 class="page-title">Dashboard</h1>
<h2 class="section-title">System Health</h2>
<div class="stats-grid">${statsHtml}</div>
<h2 class="section-title">Channels</h2>
<div class="channels-grid">${channelsHtml}</div>
<h2 class="section-title">Usage</h2>
<div class="stats-grid">${usageHtml}</div>
`;
}
export const DashboardPage = {
async render(el, client) {
await loadDashboard(el, client);
// Auto-refresh every 10 seconds
_timer = setInterval(() => {
loadDashboard(el, client).catch(() => {});
}, 10000);
},
teardown() {
if (_timer) {
clearInterval(_timer);
_timer = null;
}
},
};