/**
* 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;
let services;
try {
[config, tools, services] = await Promise.all([
_client.call('config.get'),
_client.call('tools.list'),
_client.call('system.services'),
]);
} catch (err) {
_el.innerHTML = `
Settings
Failed to load settings: ${err.message}
`;
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 services list
const serviceList = services?.services ?? [];
_el.innerHTML = `
Settings
Hook Patterns
Tools (${toolList.length})
${toolList.length > 0 ? `
| Name |
Description |
${toolList.map(t => `
${escapeHtml(t.name)} |
${escapeHtml(t.description ?? '')} |
`).join('')}
` : '
No tools available
'}
Services
${serviceList.length > 0 ? `
${serviceList.map(svc => {
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
const statusClass = svc.status === 'connected'
? 'connected'
: svc.status === 'configured'
? 'configured'
: svc.status === 'error'
? 'error'
: svc.status === 'not_configured'
? 'not-configured'
: 'disconnected';
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
return `
${typeIcon}
${escapeHtml(svc.name)}${itemCount}
${escapeHtml(svc.status)}
${escapeHtml(svc.description ?? '')}
`;
}).join('')}
` : '
No services found
'}
Configuration (read-only)
${escapeHtml(configJson)}
`;
// 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 ?? [];
const persisted = result.persisted === true;
const persistError = result.persistError;
if (rejected.length > 0) {
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
status.className = 'text-sm text-error';
} else if (persistError) {
status.textContent = `Save failed: ${persistError}`;
status.className = 'text-sm text-error';
} else if (!persisted) {
status.textContent = `Saved in runtime only (${applied.length} updated)`;
status.className = 'text-sm text-muted';
} 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;
},
};