let savedConfig = {}; let pendingChanges = {}; async function init() { document.getElementById('loading').classList.remove('hidden'); try { const [config] = await Promise.all([ fetch('/api/config').then(r => r.json()), DiscordFields.fetchGuildData() ]); savedConfig = config; document.getElementById('bot-status-dot').className = 'dot online'; document.getElementById('bot-status-text').textContent = 'Connected'; populateFields(config); initSmartSelects(config); } catch (e) { document.getElementById('bot-status-dot').className = 'dot offline'; document.getElementById('bot-status-text').textContent = 'Unreachable'; } document.getElementById('loading').classList.add('hidden'); setupSectionToggles(); setupSaveBar(); } 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') { // Convert 0xRRGGBB to #RRGGBB 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 setupSectionToggles() { document.querySelectorAll('.section-header').forEach(header => { header.addEventListener('click', () => { header.closest('.section').classList.toggle('collapsed'); }); }); // Sidebar navigation document.querySelectorAll('.sidebar a').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const target = document.getElementById(link.getAttribute('href').slice(1)); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.classList.remove('collapsed'); } document.querySelectorAll('.sidebar a').forEach(l => l.classList.remove('active')); link.classList.add('active'); }); }); } function markChanged(key, value) { if (String(value) === String(savedConfig[key] || '')) { delete pendingChanges[key]; } else { pendingChanges[key] = value; } updateSaveBar(); } function setupSaveBar() { updateSaveBar(); } 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' : ''}`; } async function saveConfig(mode) { try { const res = await fetch('/api/config', { method: 'POST', headers: { '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]; pendingChanges = {}; updateSaveBar(); document.querySelectorAll('.changed').forEach(el => el.classList.remove('changed')); showToast(`${data.applied.length} settings saved.`, 'success'); } if (data.errors && data.errors.length > 0) { showToast(`Errors: ${data.errors.join(', ')}`, 'error'); } if (mode === 'restart') { await fetch('/api/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: 'immediate' }) }); showToast('Restart initiated.', 'warning'); } } catch (e) { showToast('Failed to save. Bot may be unreachable.', 'error'); } } function openScheduleModal() { const modal = document.getElementById('schedule-modal'); modal.classList.remove('hidden'); const dt = document.getElementById('schedule-datetime'); const min = new Date(Date.now() + 60000).toISOString().slice(0, 16); dt.min = min; dt.value = min; } async function confirmScheduledRestart() { const dt = document.getElementById('schedule-datetime').value; if (!dt) return; await fetch('/api/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: 'scheduled', scheduledFor: new Date(dt).toISOString() }) }); document.getElementById('schedule-modal').classList.add('hidden'); showToast(`Restart scheduled for ${new Date(dt).toLocaleString()}`, 'warning'); } function showToast(message, type = 'success') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; document.getElementById('toast-container').appendChild(toast); setTimeout(() => toast.remove(), 3500); } document.addEventListener('DOMContentLoaded', init);