176 lines
5.8 KiB
JavaScript
176 lines
5.8 KiB
JavaScript
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);
|