(function () { 'use strict'; let savedConfig = {}; let pendingChanges = {}; function setSavedConfig(config) { savedConfig = config; } function populateFields(config) { document.querySelectorAll('[data-key]').forEach(el => { const key = el.dataset.key; const value = config[key] || ''; if (el.type === 'checkbox') { el.checked = value === 'true' || value === true; } else if (el.type === 'color') { const num = parseInt(value) || 0; el.value = '#' + num.toString(16).padStart(6, '0'); } else { el.value = value; } el.addEventListener('change', () => handleFieldChange(el, key)); el.addEventListener('input', () => { if (el.type === 'text' || el.type === 'number' || el.type === 'password' || el.tagName === 'TEXTAREA') { handleFieldChange(el, key); } }); }); } function handleFieldChange(el, key) { let value; if (el.type === 'checkbox') { value = el.checked ? 'true' : 'false'; } else if (el.type === 'color') { value = '0x' + el.value.slice(1).toUpperCase(); } else { value = el.value; } markChanged(key, value); el.classList.toggle('changed', key in pendingChanges); } function initSmartSelects(config) { document.querySelectorAll('[data-smart]').forEach(el => { const key = el.dataset.key; const type = el.dataset.smart; const value = config[key] || ''; if (type === 'channel') DiscordFields.renderChannelSelect(el, value); else if (type === 'category') DiscordFields.renderCategorySelect(el, value); else if (type === 'role') DiscordFields.renderRoleSelect(el, value); else if (type === 'member') DiscordFields.renderMemberSelect(el, value); else if (type === 'multi-member') DiscordFields.renderMultiMemberSelect(el, value); }); } function markChanged(key, value) { if (String(value) === String(savedConfig[key] || '')) { delete pendingChanges[key]; } else { pendingChanges[key] = value; } updateSaveBar(); } function isChanged(key) { return key in pendingChanges; } function updateSaveBar() { const bar = document.getElementById('save-bar'); const count = Object.keys(pendingChanges).length; bar.classList.toggle('visible', count > 0); document.getElementById('change-count').textContent = `${count} unsaved change${count !== 1 ? 's' : ''}`; } function setupSaveBar() { updateSaveBar(); } function clearFieldErrors() { document.querySelectorAll('.field.field-error').forEach(f => f.classList.remove('field-error')); document.querySelectorAll('.field-error-message').forEach(el => el.remove()); } function normalizeErrorEntry(e) { if (e && typeof e === 'object' && 'key' in e) { return { key: e.key, message: e.error || 'Invalid value' }; } // Tolerate legacy string shape "KEY: message" from older bot builds. const str = String(e); const idx = str.indexOf(':'); if (idx > 0) return { key: str.slice(0, idx).trim(), message: str.slice(idx + 1).trim() }; return { key: str, message: 'Invalid value' }; } function applyFieldErrors(errors) { let firstField = null; for (const raw of errors) { const { key, message } = normalizeErrorEntry(raw); const selector = `[data-key="${(window.CSS && CSS.escape) ? CSS.escape(key) : key}"]`; const input = document.querySelector(selector); if (!input) continue; const field = input.closest('.field'); if (!field) continue; field.classList.add('field-error'); const msgEl = document.createElement('div'); msgEl.className = 'field-error-message'; msgEl.textContent = message; msgEl.setAttribute('role', 'alert'); field.appendChild(msgEl); if (!firstField) firstField = field; } if (firstField) firstField.scrollIntoView({ behavior: 'smooth', block: 'center' }); } async function saveConfig(mode) { const buttons = document.querySelectorAll('#save-bar button'); buttons.forEach(b => b.disabled = true); clearFieldErrors(); try { if (mode === 'restart' && !confirm('Save changes and restart the bot now?')) { return; } const res = await fetch('/api/config', { method: 'POST', credentials: 'same-origin', headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify(pendingChanges) }); const data = await res.json(); if (data.applied) { for (const key of data.applied) savedConfig[key] = pendingChanges[key]; for (const key of data.applied) delete pendingChanges[key]; updateSaveBar(); document.querySelectorAll('.changed').forEach(el => { const key = el.dataset && el.dataset.key; if (!key || !(key in pendingChanges)) el.classList.remove('changed'); }); if (data.applied.length > 0) { Util.showToast(`${data.applied.length} setting${data.applied.length === 1 ? '' : 's'} saved.`, 'success'); } } const hasErrors = data.errors && data.errors.length > 0; if (hasErrors) { applyFieldErrors(data.errors); const keys = data.errors.map(e => normalizeErrorEntry(e).key).join(', '); Util.showToast(`${data.errors.length} setting${data.errors.length === 1 ? '' : 's'} failed: ${keys}`, 'error'); } if (mode === 'restart' && !hasErrors) { await fetch('/api/restart', { method: 'POST', credentials: 'same-origin', headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ mode: 'immediate' }) }); Util.showToast('Restart initiated.', 'warning'); } else if (mode === 'restart' && hasErrors) { Util.showToast('Restart cancelled — some settings failed validation.', 'warning'); } } catch (e) { Util.showToast('Failed to save. Bot may be unreachable.', 'error'); } finally { buttons.forEach(b => b.disabled = false); } } window.Fields = { setSavedConfig, populateFields, handleFieldChange, initSmartSelects, markChanged, isChanged, updateSaveBar, setupSaveBar, saveConfig }; })();