// ========================================== // Chat Switchboard – Settings Handlers // ========================================== // Settings save, provider CRUD, avatar, command palette, // user model roles. // ── Settings ───────────────────────────────── async function loadSettings() { try { const remote = await API.getSettings(); if (remote && typeof remote === 'object') { if (remote.model) App.settings.model = remote.model; if (remote.system_prompt !== undefined) App.settings.systemPrompt = remote.system_prompt; if (remote.max_tokens) App.settings.maxTokens = remote.max_tokens; if (remote.temperature !== undefined) App.settings.temperature = remote.temperature; } } catch (e) { console.warn('Settings load failed:', e.message); } } async function saveSettings() { try { await API.updateSettings({ model: App.settings.model, system_prompt: App.settings.systemPrompt, max_tokens: App.settings.maxTokens, temperature: App.settings.temperature, }); } catch (e) { console.warn('Settings sync failed:', e.message); } } // ── Avatar Preview ────────────────────────── function updateAvatarPreview(dataURI) { const preview = document.getElementById('avatarPreview'); const letter = document.getElementById('avatarPreviewLetter'); const removeBtn = document.getElementById('avatarRemoveBtn'); const existingImg = preview.querySelector('img'); if (dataURI) { letter.style.display = 'none'; if (existingImg) { existingImg.src = dataURI; } else { const img = document.createElement('img'); img.src = dataURI; img.alt = 'Avatar'; preview.insertBefore(img, letter); } removeBtn.style.display = ''; } else { if (existingImg) existingImg.remove(); const name = API.user?.display_name || API.user?.username || '?'; letter.textContent = name[0].toUpperCase(); letter.style.display = ''; removeBtn.style.display = 'none'; } } // ── Settings Handlers ──────────────────────── async function handleSaveSettings() { App.settings.model = document.getElementById('settingsModel').value || App.settings.model; App.settings.systemPrompt = document.getElementById('settingsSystemPrompt').value.trim(); App.settings.maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 0; // 0 = auto App.settings.temperature = parseFloat(document.getElementById('settingsTemp').value) || 0.7; App.settings.showThinking = document.getElementById('settingsThinking').checked; saveSettings(); // ── Save profile fields (display name, email) ── const displayName = document.getElementById('profileDisplayName').value.trim(); const email = document.getElementById('profileEmail').value.trim(); const profileUpdates = {}; if (displayName !== (API.user?.display_name || '')) profileUpdates.display_name = displayName; if (email !== (API.user?.email || '')) profileUpdates.email = email; if (Object.keys(profileUpdates).length > 0) { try { const updated = await API.updateProfile(profileUpdates); // Sync local user object so sidebar/avatar reflect the change if (updated) { if (updated.display_name !== undefined) API.user.display_name = updated.display_name; if (updated.email !== undefined) API.user.email = updated.email; API.saveTokens(); UI.updateUser(); } } catch (e) { console.warn('Profile save failed:', e.message); UI.toast('Profile update failed: ' + e.message, 'error'); } } UI.updateModelSelector(); UI.updateCapabilityBadges(); UI.closeSettings(); UI.toast('Settings saved', 'success'); } async function handleChangePassword() { const cur = document.getElementById('profileCurrentPw').value; const pw = document.getElementById('profileNewPw').value; if (!cur || !pw) return UI.toast('Fill in both fields', 'warning'); if (pw.length < 8) return UI.toast('Min 8 characters', 'warning'); try { await API.changePassword(cur, pw); UI.toast('Password updated', 'success'); document.getElementById('profileChangePwForm').style.display = 'none'; document.getElementById('profileCurrentPw').value = ''; document.getElementById('profileNewPw').value = ''; } catch (e) { UI.toast(e.message, 'error'); } } // ── User BYOK Provider — primitive instances (initialized in _initSettingsListeners) ── var _userProvForm = null; var _userProvList = null; function _initUserProviderPrimitives() { const formEl = document.getElementById('providerAddForm'); const listEl = document.getElementById('providerList'); if (!formEl || !listEl) return; _userProvForm = renderProviderForm(formEl, { prefix: 'userProv', showPrivate: false, showDefaultModel: true, onSubmit: async (vals, isEdit) => { try { if (isEdit) { if (!vals.name || !vals.endpoint) return UI.toast('Fill in required fields', 'warning'); const patch = { name: vals.name, provider: vals.provider, endpoint: vals.endpoint, model_default: vals.model_default }; if (vals.api_key) patch.api_key = vals.api_key; await API.updateConfig(vals._editId, patch); UI.toast('Provider updated', 'success'); } else { if (!vals.name || !vals.endpoint || !vals.api_key) return UI.toast('Fill in required fields', 'warning'); const result = await API.createConfig(vals.name, vals.provider, vals.endpoint, vals.api_key, vals.model_default); if (result.warning) UI.toast(`Provider added — ${result.warning}`, 'warning'); else { const count = result.models_fetched || 0; UI.toast(`Provider added — ${count} model${count !== 1 ? 's' : ''} synced`, 'success'); } } formEl.style.display = 'none'; _userProvForm.setCreateMode(); _userProvList.refresh(); fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { formEl.style.display = 'none'; _userProvForm.setCreateMode(); }, }); _userProvList = renderProviderList(listEl, { apiFetch: () => API.listConfigs(), showRefresh: true, emptyMsg: 'No providers configured.', onEdit: async (prov) => { try { const cfg = await API.getConfig(prov.id); _userProvForm.setEditMode(cfg.id, cfg); formEl.style.display = ''; } catch (e) { UI.toast(e.message, 'error'); } }, onDelete: async (prov) => { if (!await showConfirm(`Remove provider "${prov.name}"?`)) return; try { await API.deleteConfig(prov.id); UI.toast('Provider removed', 'success'); _userProvList.refresh(); fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onRefresh: async (prov) => { try { UI.toast(`Fetching models for ${prov.name}...`, 'info'); const result = await API.fetchProviderModels(prov.id); const total = result.total || 0; UI.toast(`${prov.name}: ${total} model${total !== 1 ? 's' : ''} synced`, 'success'); fetchModels(); } catch (e) { UI.toast(`Failed to fetch models: ${e.message}`, 'error'); } }, }); } async function handleSaveAdminSettings() { try { // Admin system prompt → global_settings (JSON value) const sysPromptContent = document.getElementById('adminSystemPrompt').value.trim(); await API.adminUpdateSetting('system_prompt', { value: { content: sysPromptContent } }); // Policies — send as string "true"/"false" so backend routes to platform_policies const reg = document.getElementById('adminRegToggle').checked; await API.adminUpdateSetting('allow_registration', { value: reg ? 'true' : 'false' }); // Registration default state → default_user_active policy const regState = document.getElementById('adminRegDefaultState').value; await API.adminUpdateSetting('default_user_active', { value: regState === 'active' ? 'true' : 'false' }); // User BYOK providers → allow_user_byok policy const userProviders = document.getElementById('adminUserProvidersToggle').checked; await API.adminUpdateSetting('allow_user_byok', { value: userProviders ? 'true' : 'false' }); // User presets → allow_user_personas policy const userPresets = document.getElementById('adminUserPresetsToggle').checked; await API.adminUpdateSetting('allow_user_personas', { value: userPresets ? 'true' : 'false' }); // Default model → default_model policy const defaultModel = document.getElementById('adminDefaultModel').value; await API.adminUpdateSetting('default_model', { value: defaultModel }); // KB direct access → kb_direct_access policy (v0.17.0) const kbDirect = document.getElementById('adminKBDirectAccessToggle')?.checked; if (kbDirect != null) { await API.adminUpdateSetting('kb_direct_access', { value: kbDirect ? 'true' : 'false' }); } // Banner → global_settings (JSON value) const banner = { enabled: document.getElementById('adminBannerEnabled').checked, text: document.getElementById('adminBannerText').value, position: document.getElementById('adminBannerPosition').value, bg: document.getElementById('adminBannerBg').value, fg: document.getElementById('adminBannerFg').value, }; await API.adminUpdateSetting('banner', { value: banner }); // Web Search config → global_settings (JSON value) const searchConfig = { provider: document.getElementById('adminSearchProvider').value, endpoint: document.getElementById('adminSearchEndpoint').value.trim(), api_key: document.getElementById('adminSearchApiKey').value.trim(), max_results: parseInt(document.getElementById('adminSearchMaxResults').value) || 5, }; await API.adminUpdateSetting('search_config', { value: searchConfig }); // Auto-Compaction config → global_settings const compactionEnabled = document.getElementById('adminCompactionEnabled')?.checked || false; const compactionThreshold = parseInt(document.getElementById('adminCompactionThreshold')?.value) || 70; const compactionCooldown = parseInt(document.getElementById('adminCompactionCooldown')?.value) || 30; await API.adminUpdateSetting('auto_compaction', { value: { enabled: compactionEnabled, threshold: compactionThreshold, cooldown: compactionCooldown, }}); // Scanner reads these individual keys each tick await API.adminUpdateSetting('auto_compaction_enabled', { value: compactionEnabled }); await API.adminUpdateSetting('auto_compaction_threshold', { value: compactionThreshold / 100 }); await API.adminUpdateSetting('auto_compaction_cooldown_minutes', { value: compactionCooldown }); // Memory extraction (v0.18.0) const memExtractionEnabled = document.getElementById('adminMemoryExtractionEnabled')?.checked || false; const memAutoApprove = document.getElementById('adminMemoryAutoApprove')?.checked || false; await API.adminUpdateSetting('memory_extraction', { value: { enabled: memExtractionEnabled, auto_approve: memAutoApprove, }}); await API.adminUpdateSetting('memory_extraction_enabled', { value: memExtractionEnabled }); // Email / SMTP config (v0.20.0 Phase 3) if (typeof NotifPrefs !== 'undefined' && NotifPrefs._getAdminSmtp) { const smtpConfig = NotifPrefs._getAdminSmtp(); await API.adminUpdateSetting('notifications', { value: smtpConfig }); } UI.toast('Settings saved', 'success'); // Live-apply: refresh policies and dependent UI await initBanners(); UI.checkUserProvidersAllowed(); UI.checkUserPresetsAllowed(); fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } } // ── User Model Role — primitive instance (initialized in _initSettingsListeners) ── var _userRoleConfig = null; function _initUserRolePrimitive() { const el = document.getElementById('userRolesConfig'); if (!el) return; _userRoleConfig = renderRoleConfig(el, { scope: 'personal', showFallback: false, modelIdField: 'baseModelId', modelNameField: 'name', providerIdField: 'configId', noneLabel: '— use org default —', fetchProviders: async () => { const configs = await API.listConfigs(); const list = configs.configs || configs.data || []; return list.filter(c => c.scope === 'personal'); }, fetchModels: async () => { // Refresh App.models to ensure personal provider models are current. // Without this, newly added providers may not appear in the dropdown. await fetchModels(); return App.models || []; }, fetchRoles: async () => { const settings = await API.getSettings(); return settings.model_roles || {}; }, onSave: async (roleId, config) => { if (!config.primary) throw new Error('Select both a provider and model'); const settings = await API.getSettings(); const roles = settings.model_roles || {}; roles[roleId] = config; await API.updateSettings({ model_roles: roles }); UI.toast(`${roleId} role override saved`, 'success'); }, onClear: async (roleId) => { const settings = await API.getSettings(); const roles = settings.model_roles || {}; delete roles[roleId]; await API.updateSettings({ model_roles: Object.keys(roles).length > 0 ? roles : null }); UI.toast(`${roleId} role override removed — using org default`, 'success'); }, }); } // ── Command Palette ────────────────────────── const _cmdCommands = [ // Navigation { id: 'new-chat', label: 'New Chat', group: 'Navigation', hint: '', icon: '', action: () => newChat() }, { id: 'search-chats', label: 'Search Chats', group: 'Navigation', hint: '⌘⇧S', icon: '', action: () => { const sb = document.getElementById('sidebar'); if (sb.classList.contains('collapsed')) UI.toggleSidebar(); document.getElementById('chatSearchInput')?.focus(); } }, { id: 'toggle-sidebar', label: 'Toggle Sidebar', group: 'Navigation', hint: '', icon: '', action: () => UI.toggleSidebar() }, // Tools { id: 'notes', label: 'Open Notes', group: 'Tools', hint: '', icon: '', action: () => openNotes() }, { id: 'export-md', label: 'Export Chat (Markdown)', group: 'Tools', hint: '', icon: '', action: () => UI.exportChat('md') }, { id: 'export-json', label: 'Export Chat (JSON)', group: 'Tools', hint: '', icon: '', action: () => UI.exportChat('json') }, // Settings { id: 'settings', label: 'Settings', group: 'Settings', hint: '', icon: '', action: () => UI.openSettings() }, { id: 'admin', label: 'Admin Panel', group: 'Settings', hint: '', icon: '', action: () => UI.openAdmin(), visible: () => API.user?.role === 'admin' }, { id: 'debug', label: 'Debug Log', group: 'Settings', hint: '', icon: '', action: () => openDebugModal() }, // Account { id: 'sign-out', label: 'Sign Out', group: 'Account', hint: '', icon: '', action: () => handleLogout() }, ]; let _cmdActiveIndex = 0; function toggleCmdPalette() { const el = document.getElementById('cmdPalette'); if (el.classList.contains('active')) { closeCmdPalette(); return; } openCmdPalette(); } function openCmdPalette() { const el = document.getElementById('cmdPalette'); const input = document.getElementById('cmdInput'); el.classList.add('active'); input.value = ''; _cmdActiveIndex = 0; _renderCmdResults(''); input.focus(); // Wire up input events (use named handler to avoid duplicates) input.oninput = () => { _cmdActiveIndex = 0; _renderCmdResults(input.value); }; input.onkeydown = _handleCmdKey; // Close on overlay click el.onclick = (e) => { if (e.target === el) closeCmdPalette(); }; } function closeCmdPalette() { const el = document.getElementById('cmdPalette'); el.classList.remove('active'); document.getElementById('cmdInput').oninput = null; document.getElementById('cmdInput').onkeydown = null; } function _handleCmdKey(e) { const results = document.getElementById('cmdResults'); const items = results.querySelectorAll('.cmd-item'); if (!items.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); _cmdActiveIndex = (_cmdActiveIndex + 1) % items.length; _highlightCmdItem(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); _cmdActiveIndex = (_cmdActiveIndex - 1 + items.length) % items.length; _highlightCmdItem(items); } else if (e.key === 'Enter') { e.preventDefault(); items[_cmdActiveIndex]?.click(); } } function _highlightCmdItem(items) { items.forEach((it, i) => it.classList.toggle('active', i === _cmdActiveIndex)); items[_cmdActiveIndex]?.scrollIntoView({ block: 'nearest' }); } function _getVisibleCommands() { return _cmdCommands.filter(c => !c.visible || c.visible()); } function _renderCmdResults(query) { const el = document.getElementById('cmdResults'); const q = query.trim().toLowerCase(); let commands = _getVisibleCommands(); // Add recent chats as commands when searching if (q) { const chatMatches = App.chats .filter(c => (c.title || '').toLowerCase().includes(q)) .slice(0, 5) .map(c => ({ id: 'chat-' + c.id, label: c.title || 'Untitled', group: 'Chats', hint: _relativeTime(c.updatedAt), icon: '', action: () => selectChat(c.id), })); commands = [...chatMatches, ...commands]; } // Filter by query if (q) { commands = commands.filter(c => c.label.toLowerCase().includes(q)); } if (commands.length === 0) { el.innerHTML = '
No matching commands
'; return; } // Clamp active index _cmdActiveIndex = Math.min(_cmdActiveIndex, commands.length - 1); // Render grouped let html = ''; let lastGroup = ''; commands.forEach((c, i) => { if (c.group !== lastGroup) { lastGroup = c.group; html += `
${c.group}
`; } html += `
${c.icon} ${c.label} ${c.hint ? `${c.hint}` : ''}
`; }); el.innerHTML = html; // Attach click handlers el.querySelectorAll('.cmd-item').forEach((item, i) => { item.addEventListener('click', () => { closeCmdPalette(); commands[i].action(); }); item.addEventListener('mouseenter', () => { _cmdActiveIndex = i; _highlightCmdItem(el.querySelectorAll('.cmd-item')); }); }); } // ── Settings Listeners (extracted from initListeners) ── function _initSettingsListeners() { // Settings modal document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings); document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings); document.querySelectorAll('.settings-tab').forEach(tab => { tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab)); }); document.getElementById('settingsModel').addEventListener('change', function() { const model = App.findModel(this.value); const caps = model?.capabilities || {}; const hint = document.getElementById('settingsMaxHint'); if (hint) { hint.textContent = caps.max_output_tokens > 0 ? `(model max: ${(caps.max_output_tokens / 1000).toFixed(0)}K)` : ''; } }); document.getElementById('profileChangePwBtn').addEventListener('click', () => { document.getElementById('profileChangePwForm').style.display = ''; }); document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword); // Avatar upload document.getElementById('avatarUploadBtn').addEventListener('click', () => { document.getElementById('avatarFileInput').click(); }); document.getElementById('avatarFileInput').addEventListener('change', async function() { const file = this.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async () => { try { const data = await API.uploadAvatar(reader.result); if (data.avatar) { API.user.avatar = data.avatar; API.saveTokens(); updateAvatarPreview(data.avatar); UI.updateUser(); UI.toast('Avatar updated'); } } catch (e) { UI.toast(e.message || 'Upload failed', 'error'); } }; reader.readAsDataURL(file); this.value = ''; }); document.getElementById('avatarRemoveBtn').addEventListener('click', async () => { try { await API.deleteAvatar(); API.user.avatar = null; API.saveTokens(); updateAvatarPreview(null); UI.updateUser(); UI.toast('Avatar removed'); } catch (e) { UI.toast(e.message || 'Remove failed', 'error'); } }); // User BYOK provider primitives _initUserProviderPrimitives(); _initUserRolePrimitive(); document.getElementById('providerShowAddBtn').addEventListener('click', () => { const formEl = document.getElementById('providerAddForm'); if (_userProvForm) _userProvForm.setCreateMode(); formEl.style.display = ''; }); // Settings — Team management (team admin self-service) document.getElementById('settingsTeamAddMemberBtn')?.addEventListener('click', async () => { document.getElementById('settingsTeamAddMember').style.display = ''; const sel = document.getElementById('settingsTeamMemberUser'); sel.innerHTML = ''; try { const resp = await API.teamListMembers(UI._managingTeamId); const existingIds = new Set((resp.data || []).map(m => m.user_id)); sel.innerHTML = ''; } catch (e) { sel.innerHTML = ''; } }); document.getElementById('settingsTeamCancelMember')?.addEventListener('click', () => { document.getElementById('settingsTeamAddMember').style.display = 'none'; }); var _teamPresetForm = null; document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => { const container = document.getElementById('settingsTeamAddPreset'); container.style.display = ''; if (!_teamPresetForm) { _teamPresetForm = renderPresetForm(container, { prefix: 'teamPreset', showAvatar: true, showProviderConfig: false, showKBPicker: true, kbScope: 'team', kbTeamId: UI._managingTeamId, onSubmit: async (vals) => { const teamId = UI._managingTeamId; if (!vals.name) return UI.toast('Name required', 'error'); if (!vals.base_model_id) return UI.toast('Select a base model', 'error'); try { const result = await API.teamCreatePreset(teamId, vals); const presetId = result?.id; if (presetId && vals._pendingAvatar) { try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); } catch (e) { console.warn('Team preset avatar upload failed:', e.message); } } if (presetId && vals._kbValues) { try { await API.teamSetPresetKBs(teamId, presetId, vals._kbValues); } catch (e) { console.warn('Team preset KB binding failed:', e.message); } } container.style.display = 'none'; _teamPresetForm.clearForm(); UI.toast('Team preset created'); await UI.loadTeamManagePresets(teamId); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { container.style.display = 'none'; _teamPresetForm.clearForm(); } }); } else { _teamPresetForm.clearForm(); } UI.loadTeamPresetModelDropdown(UI._managingTeamId); }); // Team — providers (primitive-based, initialized lazily via UI.loadTeamManageProviders) document.getElementById('settingsTeamAddProviderBtn')?.addEventListener('click', () => { const formEl = document.getElementById('settingsTeamProviderForm'); if (!formEl) return; if (UI._teamProvForm) UI._teamProvForm.setCreateMode(); formEl.style.display = formEl.style.display === 'none' ? '' : 'none'; }); // User — personal presets var _userPresetForm = null; document.getElementById('userAddPresetBtn')?.addEventListener('click', () => { const container = document.getElementById('userAddPresetForm'); container.style.display = container.style.display === 'none' ? '' : 'none'; if (!_userPresetForm) { _userPresetForm = renderPresetForm(container, { prefix: 'userPreset', showAvatar: true, showProviderConfig: false, showKBPicker: true, kbScope: 'personal', onSubmit: async (vals) => { if (!vals.name) return UI.toast('Name required', 'error'); if (!vals.base_model_id) return UI.toast('Select a base model', 'error'); try { const result = await API.createUserPreset(vals); const presetId = result?.id; if (presetId && vals._pendingAvatar) { try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); } catch (e) { console.warn('User preset avatar upload failed:', e.message); } } if (presetId && vals._kbValues) { try { await API.setUserPresetKBs(presetId, vals._kbValues); } catch (e) { console.warn('User preset KB binding failed:', e.message); } } container.style.display = 'none'; _userPresetForm.clearForm(); UI.toast('Preset created'); await UI.loadUserPresets(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { container.style.display = 'none'; _userPresetForm.clearForm(); } }); } const sel = _userPresetForm.getModelSelect(); if (sel) { sel.innerHTML = ''; App.models.filter(m => !m.isPreset).forEach(m => { const opt = document.createElement('option'); opt.value = m.baseModelId || m.id; opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : ''); sel.appendChild(opt); }); } }); // Admin — banner controls document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => { document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none'; }); document.getElementById('adminBannerPreset')?.addEventListener('change', (e) => { const preset = UI._bannerPresets[e.target.value]; if (preset) { document.getElementById('adminBannerText').value = preset.text || ''; document.getElementById('adminBannerBg').value = preset.bg || '#007a33'; document.getElementById('adminBannerBgHex').value = preset.bg || '#007a33'; document.getElementById('adminBannerFg').value = preset.fg || '#ffffff'; document.getElementById('adminBannerFgHex').value = preset.fg || '#ffffff'; UI.updateBannerPreview(); } }); ['adminBannerText', 'adminBannerBg', 'adminBannerFg'].forEach(id => { document.getElementById(id)?.addEventListener('input', UI.updateBannerPreview); }); document.getElementById('adminBannerBg')?.addEventListener('input', (e) => { document.getElementById('adminBannerBgHex').value = e.target.value; }); document.getElementById('adminBannerFg')?.addEventListener('input', (e) => { document.getElementById('adminBannerFgHex').value = e.target.value; }); document.getElementById('adminBannerBgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerBg').value = e.target.value; UI.updateBannerPreview(); }}); document.getElementById('adminBannerFgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerFg').value = e.target.value; UI.updateBannerPreview(); }}); // Admin — auto-compaction controls document.getElementById('adminCompactionEnabled')?.addEventListener('change', (e) => { document.getElementById('compactionConfigFields').style.display = e.target.checked ? '' : 'none'; }); // Admin — search provider controls document.getElementById('adminSearchProvider')?.addEventListener('change', (e) => { document.getElementById('searxngConfigFields').style.display = e.target.value === 'searxng' ? '' : 'none'; }); // Admin — audit log document.getElementById('auditRefreshBtn')?.addEventListener('click', () => UI.loadAuditLog(1)); document.getElementById('auditFilterAction')?.addEventListener('change', () => UI.loadAuditLog(1)); document.getElementById('auditFilterResource')?.addEventListener('change', () => UI.loadAuditLog(1)); document.getElementById('auditPrevBtn')?.addEventListener('click', () => { if (UI._auditPage > 1) UI.loadAuditLog(UI._auditPage - 1); }); document.getElementById('auditNextBtn')?.addEventListener('click', () => UI.loadAuditLog(UI._auditPage + 1)); // Admin — usage document.getElementById('usageRefreshBtn')?.addEventListener('click', () => UI.loadAdminUsage()); document.getElementById('usagePeriod')?.addEventListener('change', () => UI.loadAdminUsage()); document.getElementById('usageGroupBy')?.addEventListener('change', () => UI.loadAdminUsage()); }