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,172 @@
|
||||
/**
|
||||
* Flynn Settings Page
|
||||
*
|
||||
* Read-only config view (redacted), editable hook patterns,
|
||||
* tool list, and channel overview.
|
||||
*/
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let _client = null;
|
||||
let _el = null;
|
||||
|
||||
async function loadSettings() {
|
||||
if (!_client || !_el) return;
|
||||
|
||||
let config, tools, channels;
|
||||
|
||||
try {
|
||||
[config, tools, channels] = await Promise.all([
|
||||
_client.call('config.get'),
|
||||
_client.call('tools.list'),
|
||||
_client.call('system.channels'),
|
||||
]);
|
||||
} catch (err) {
|
||||
_el.innerHTML = `
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<div class="empty-state">Failed to load settings: ${err.message}</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract hooks from config
|
||||
const hooks = config?.hooks ?? {};
|
||||
const confirmPatterns = hooks.confirm ?? [];
|
||||
const logPatterns = hooks.log ?? [];
|
||||
const silentPatterns = hooks.silent ?? [];
|
||||
|
||||
// Build config view (redacted JSON)
|
||||
const configJson = JSON.stringify(config, null, 2);
|
||||
|
||||
// Build tool list
|
||||
const toolList = tools?.tools ?? [];
|
||||
|
||||
// Build channel list
|
||||
const channelList = channels?.channels ?? [];
|
||||
|
||||
_el.innerHTML = `
|
||||
<h1 class="page-title">Settings</h1>
|
||||
|
||||
<h2 class="section-title">Hook Patterns</h2>
|
||||
<div class="settings-section">
|
||||
<div class="hook-editor">
|
||||
<div class="hook-group">
|
||||
<label>Confirm (require approval)</label>
|
||||
<textarea id="hooks-confirm" rows="3">${escapeHtml(confirmPatterns.join('\n'))}</textarea>
|
||||
</div>
|
||||
<div class="hook-group">
|
||||
<label>Log (allow + log)</label>
|
||||
<textarea id="hooks-log" rows="3">${escapeHtml(logPatterns.join('\n'))}</textarea>
|
||||
</div>
|
||||
<div class="hook-group">
|
||||
<label>Silent (allow silently)</label>
|
||||
<textarea id="hooks-silent" rows="3">${escapeHtml(silentPatterns.join('\n'))}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button id="hooks-save" class="btn btn-primary">Save Hook Patterns</button>
|
||||
<span id="hooks-status" class="text-sm text-muted" style="margin-left: 12px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Tools (${toolList.length})</h2>
|
||||
<div class="settings-section">
|
||||
${toolList.length > 0 ? `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${toolList.map(t => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(t.name)}</code></td>
|
||||
<td class="text-secondary">${escapeHtml(t.description ?? '')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : '<div class="empty-state">No tools available</div>'}
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Channels</h2>
|
||||
<div class="settings-section">
|
||||
${channelList.length > 0 ? `
|
||||
<div class="channels-grid">
|
||||
${channelList.map(ch => `
|
||||
<div class="channel-card">
|
||||
<span class="channel-dot ${ch.status}"></span>
|
||||
<span class="channel-name">${escapeHtml(ch.name)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : '<div class="text-muted text-sm">No channels registered</div>'}
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Configuration (read-only)</h2>
|
||||
<div class="settings-section">
|
||||
<div class="config-view"><code>${escapeHtml(configJson)}</code></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Bind save hooks
|
||||
_el.querySelector('#hooks-save').addEventListener('click', saveHooks);
|
||||
}
|
||||
|
||||
async function saveHooks() {
|
||||
const status = _el.querySelector('#hooks-status');
|
||||
status.textContent = 'Saving...';
|
||||
status.className = 'text-sm text-muted';
|
||||
|
||||
try {
|
||||
const confirm = _el.querySelector('#hooks-confirm').value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
const log = _el.querySelector('#hooks-log').value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
const silent = _el.querySelector('#hooks-silent').value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
const result = await _client.call('config.patch', {
|
||||
patches: {
|
||||
'hooks.confirm': confirm,
|
||||
'hooks.log': log,
|
||||
'hooks.silent': silent,
|
||||
},
|
||||
});
|
||||
|
||||
const applied = result.applied ?? [];
|
||||
const rejected = result.rejected ?? [];
|
||||
|
||||
if (rejected.length > 0) {
|
||||
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
|
||||
status.className = 'text-sm text-error';
|
||||
} else {
|
||||
status.textContent = `Saved (${applied.length} updated)`;
|
||||
status.className = 'text-sm text-success';
|
||||
}
|
||||
} catch (err) {
|
||||
status.textContent = `Error: ${err.message}`;
|
||||
status.className = 'text-sm text-error';
|
||||
}
|
||||
|
||||
// Clear status after 5s
|
||||
setTimeout(() => {
|
||||
if (status) status.textContent = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
export const SettingsPage = {
|
||||
async render(el, client) {
|
||||
_client = client;
|
||||
_el = el;
|
||||
await loadSettings();
|
||||
},
|
||||
|
||||
teardown() {
|
||||
_client = null;
|
||||
_el = null;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user