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:
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user