197 lines
6.1 KiB
JavaScript
197 lines
6.1 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<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 services list
|
|
const serviceList = services?.services ?? [];
|
|
|
|
_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">Services</h2>
|
|
<div class="settings-section">
|
|
${serviceList.length > 0 ? `
|
|
<div class="services-grid">
|
|
${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 `
|
|
<div class="service-card service-${statusClass}">
|
|
<span class="service-type-icon">${typeIcon}</span>
|
|
<span class="service-name">${escapeHtml(svc.name)}${itemCount}</span>
|
|
<span class="service-status">${escapeHtml(svc.status)}</span>
|
|
<span class="service-description text-muted text-xs">${escapeHtml(svc.description ?? '')}</span>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
` : '<div class="text-muted text-sm">No services found</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 ?? [];
|
|
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;
|
|
},
|
|
};
|