/** * 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 ? ` ${toolList.map(t => ` `).join('')}
Name Description
${escapeHtml(t.name)} ${escapeHtml(t.description ?? '')}
` : '
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; }, };