617 lines
24 KiB
JavaScript
617 lines
24 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- State ---
|
|
let currentGuildId = null;
|
|
let guilds = [];
|
|
let currentFeatures = [];
|
|
let currentConfigs = [];
|
|
|
|
// --- Config Mapping ---
|
|
// Mapping config keys to services and validating them if needed
|
|
// --- Config Mapping ---
|
|
// Mapping config keys to services and validating them if needed
|
|
const SERVICE_CONFIG_MAP = {
|
|
'home_debt': {
|
|
keys: ['CHANNEL_HOME_DEBT_ID'],
|
|
validation: 'channel_list'
|
|
},
|
|
'noi_tu': {
|
|
keys: ['CHANNEL_NOI_TU_IDS'],
|
|
validation: 'channel_list'
|
|
},
|
|
'football': {
|
|
keys: ['CHANNEL_FOOTBALL_IDS', 'FOOTBALL_API_KEY', 'FOOTBALL_LEAGUES', 'FOOTBALL_TEAMS'],
|
|
validation: null, // Removed global validation
|
|
meta: {
|
|
'CHANNEL_FOOTBALL_IDS': { validation: 'channel_list' }, // Moved specific validation here
|
|
'FOOTBALL_LEAGUES': { type: 'multi-select', options: ['Premier League', 'La Liga', 'Serie A', 'Bundesliga', 'Ligue 1', 'UEFA Champions League', 'V-League'] },
|
|
'FOOTBALL_TEAMS': { type: 'async-select', placeholder: 'Search team...' }
|
|
}
|
|
},
|
|
'score': {
|
|
keys: [],
|
|
validation: null
|
|
}
|
|
};
|
|
|
|
// --- Elements ---
|
|
const serverList = document.getElementById('serverList');
|
|
const selectedGuildName = document.getElementById('selectedGuildName');
|
|
const contentArea = document.getElementById('contentArea');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
const adminIdsInput = document.getElementById('adminIdsInput');
|
|
const saveAdminsBtn = document.getElementById('saveAdminsBtn');
|
|
const adminValidationMsg = document.getElementById('adminValidationMsg');
|
|
const servicesContainer = document.getElementById('servicesContainer');
|
|
const serverInfoContainer = document.getElementById('serverInfoContainer');
|
|
const configTableBody = document.querySelector('#configTable tbody');
|
|
const configForm = document.getElementById('configForm');
|
|
|
|
// --- Init ---
|
|
init();
|
|
|
|
async function init() {
|
|
await fetchGuilds();
|
|
setupTabs();
|
|
setupEventListeners();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Admin Save
|
|
saveAdminsBtn.addEventListener('click', saveAdmins);
|
|
|
|
// Custom Config Save
|
|
configForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(configForm);
|
|
await saveConfig(
|
|
formData.get('key'),
|
|
formData.get('value'),
|
|
formData.get('description')
|
|
);
|
|
configForm.reset();
|
|
refreshData();
|
|
});
|
|
}
|
|
|
|
function setupTabs() {
|
|
const tabs = document.querySelectorAll('.tab-btn');
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
// UI Toggle
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
|
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.tab).classList.add('active');
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- API and Logic ---
|
|
|
|
function showLoading(show) {
|
|
if (show) {
|
|
loadingOverlay.classList.remove('hidden');
|
|
contentArea.style.opacity = '0.5';
|
|
contentArea.style.pointerEvents = 'none';
|
|
} else {
|
|
loadingOverlay.classList.add('hidden');
|
|
contentArea.style.opacity = '1';
|
|
contentArea.style.pointerEvents = 'auto';
|
|
}
|
|
}
|
|
|
|
async function fetchGuilds() {
|
|
try {
|
|
const res = await fetch('/api/guilds');
|
|
guilds = await res.json();
|
|
renderSidebar();
|
|
|
|
// Auto-select first
|
|
if (guilds.length > 0) {
|
|
selectGuild(guilds[0].id);
|
|
} else {
|
|
serverList.innerHTML = '<li style="padding:15px">No guilds found. Invite bot to server first.</li>';
|
|
selectedGuildName.textContent = 'No Servers Available';
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
serverList.innerHTML = '<li style="padding:15px; color:var(--danger-color);">Failed to load servers</li>';
|
|
}
|
|
}
|
|
|
|
// Modified refreshData to accept silent mode
|
|
async function refreshData(silent = false) {
|
|
if (!currentGuildId) return;
|
|
|
|
if (!silent) showLoading(true);
|
|
|
|
try {
|
|
// Parallel fetch
|
|
const [featuresRes, configsRes, detailsRes] = await Promise.all([
|
|
fetch(`/api/guilds/${currentGuildId}/features`),
|
|
fetch(`/api/guilds/${currentGuildId}/config`),
|
|
fetch(`/api/guilds/${currentGuildId}/details`)
|
|
]);
|
|
|
|
currentFeatures = await featuresRes.json();
|
|
currentConfigs = await configsRes.json();
|
|
const details = await detailsRes.json();
|
|
|
|
renderServerInfo(details);
|
|
renderAdminSection();
|
|
renderServicesTab();
|
|
renderConfigTab();
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Failed to load guild data.");
|
|
} finally {
|
|
if (!silent) showLoading(false);
|
|
}
|
|
}
|
|
|
|
async function saveConfig(key, value, description) {
|
|
try {
|
|
const res = await fetch(`/api/guilds/${currentGuildId}/config`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ key, value, description, guild_id: currentGuildId })
|
|
});
|
|
if (!res.ok) throw new Error('Failed to save');
|
|
return true;
|
|
} catch (e) {
|
|
alert(e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// --- Rendering ---
|
|
|
|
function renderSidebar() {
|
|
serverList.innerHTML = '';
|
|
guilds.forEach(guild => {
|
|
const li = document.createElement('li');
|
|
li.className = `server-item ${String(guild.id) === currentGuildId ? 'active' : ''}`;
|
|
li.textContent = guild.name;
|
|
li.onclick = () => selectGuild(guild.id);
|
|
serverList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
function selectGuild(id) {
|
|
currentGuildId = String(id);
|
|
const guild = guilds.find(g => String(g.id) === currentGuildId);
|
|
selectedGuildName.textContent = guild ? guild.name : 'Unknown Guild';
|
|
contentArea.classList.remove('hidden');
|
|
|
|
// Re-render sidebar to update active state
|
|
renderSidebar();
|
|
|
|
// Load Data
|
|
refreshData(); // Not silent (full load)
|
|
}
|
|
|
|
// --- Tab 1: Server Info ---
|
|
function renderServerInfo(details) {
|
|
if (!details.found) {
|
|
serverInfoContainer.innerHTML = '<p>Guild details not found in Bot cache.</p>';
|
|
return;
|
|
}
|
|
|
|
const iconHtml = details.icon_url
|
|
? `<img src="${details.icon_url}" style="width:64px; height:64px; border-radius:50%; margin-bottom:10px;">`
|
|
: `<div style="width:64px; height:64px; background:#444; border-radius:50%; margin:0 auto 10px; display:flex; align-items:center; justify-content:center;">?</div>`;
|
|
|
|
serverInfoContainer.innerHTML = `
|
|
<div class="info-item">
|
|
${iconHtml}
|
|
<div style="font-weight:bold">${details.name}</div>
|
|
<div class="text-muted">ID: ${details.id}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<label>Members</label>
|
|
<span>${details.member_count}</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label>Owner ID</label>
|
|
<span>${details.owner}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// --- Tab 2: Config & Admin ---
|
|
function renderAdminSection() {
|
|
const adminConfig = currentConfigs.find(c => c.key === 'ADMIN_IDS');
|
|
adminIdsInput.value = adminConfig ? adminConfig.value : '';
|
|
adminValidationMsg.textContent = '';
|
|
}
|
|
|
|
async function saveAdmins() {
|
|
const raw = adminIdsInput.value;
|
|
const ids = raw.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
|
// Inline Loading State
|
|
const originalText = saveAdminsBtn.textContent;
|
|
saveAdminsBtn.disabled = true;
|
|
saveAdminsBtn.innerHTML = '<span class="spinner-sm"></span> Verifying...';
|
|
adminValidationMsg.textContent = 'Checking IDs...';
|
|
|
|
let invalidIds = [];
|
|
|
|
// Note: For "System/Global" (ID 0), we can't really validate members unless we pick a random guild or check global cache?
|
|
// Actually, bot.get_user() is safer for global, but endpoints use guild.get_member().
|
|
// Let's skip validation for ID 0 for now or assume it passes.
|
|
if (currentGuildId !== "0") {
|
|
for (const uid of ids) {
|
|
try {
|
|
const res = await fetch(`/api/guilds/${currentGuildId}/members/${uid}`);
|
|
if (!res.ok) invalidIds.push(uid);
|
|
} catch (e) {
|
|
invalidIds.push(uid);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (invalidIds.length > 0) {
|
|
adminValidationMsg.innerHTML = `<span style="color:var(--danger-color)">Invalid User IDs: ${invalidIds.join(', ')}</span>`;
|
|
saveAdminsBtn.disabled = false;
|
|
saveAdminsBtn.innerHTML = originalText;
|
|
return;
|
|
}
|
|
|
|
// All valid
|
|
await saveConfig('ADMIN_IDS', ids.join(','), 'Admin List');
|
|
adminValidationMsg.innerHTML = '<span style="color:var(--accent-color)">✅ Saved successfully!</span>';
|
|
saveAdminsBtn.disabled = false;
|
|
saveAdminsBtn.innerHTML = originalText;
|
|
refreshData(true); // Silent refresh
|
|
}
|
|
|
|
// --- Tab 3: Services ---
|
|
function renderServicesTab() {
|
|
servicesContainer.innerHTML = '';
|
|
const knownServices = ['home_debt', 'noi_tu', 'score', 'football'];
|
|
|
|
knownServices.forEach(serviceName => {
|
|
const feature = currentFeatures.find(f => f.feature_name === serviceName);
|
|
const isEnabled = feature ? feature.is_enabled : false;
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'service-card';
|
|
|
|
// Header
|
|
const header = document.createElement('div');
|
|
header.className = 'service-header';
|
|
header.innerHTML = `<h3>${formatName(serviceName)}</h3>`;
|
|
|
|
// Toggle
|
|
const label = document.createElement('label');
|
|
label.className = 'switch';
|
|
const input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.checked = isEnabled;
|
|
input.onchange = () => toggleService(serviceName, input.checked);
|
|
|
|
const span = document.createElement('span');
|
|
span.className = 'slider';
|
|
|
|
label.appendChild(input);
|
|
label.appendChild(span);
|
|
header.appendChild(label);
|
|
|
|
// Body with Configs
|
|
const body = document.createElement('div');
|
|
body.className = `service-body ${isEnabled ? '' : 'disabled'}`;
|
|
|
|
const configMeta = SERVICE_CONFIG_MAP[serviceName];
|
|
const relevantKeys = configMeta ? configMeta.keys : [];
|
|
|
|
if (relevantKeys.length > 0) {
|
|
relevantKeys.forEach(key => {
|
|
const config = currentConfigs.find(c => c.key === key) || { value: '' };
|
|
const formGroup = document.createElement('div');
|
|
formGroup.className = 'form-group';
|
|
|
|
const meta = configMeta.meta && configMeta.meta[key] ? configMeta.meta[key] : {};
|
|
const inputType = meta.type || 'text';
|
|
|
|
formGroup.innerHTML = `<label>${key}</label>`;
|
|
|
|
// Input Container
|
|
const inputContainer = document.createElement('div');
|
|
inputContainer.style.position = 'relative'; // For dropdowns
|
|
|
|
if (inputType === 'multi-select') {
|
|
// Options Select
|
|
const container = document.createElement('div');
|
|
|
|
// Available Options (Filtered by what's not selected?)
|
|
// Actually simpler: Just a select box to Add, and a list of Tags for current values
|
|
const currentValues = config.value ? config.value.split(',').map(s => s.trim()).filter(s => s) : [];
|
|
|
|
const tagsDiv = document.createElement('div');
|
|
tagsDiv.className = 'tags-container';
|
|
tagsDiv.id = `tags-${key}`;
|
|
renderTags(tagsDiv, currentValues, key);
|
|
|
|
const select = document.createElement('select');
|
|
select.style.width = '100%';
|
|
select.style.marginBottom = '5px';
|
|
select.innerHTML = '<option value="">+ Add League...</option>';
|
|
meta.options.forEach(opt => {
|
|
if (!currentValues.includes(opt)) {
|
|
const o = document.createElement('option');
|
|
o.value = opt;
|
|
o.textContent = opt;
|
|
select.appendChild(o);
|
|
}
|
|
});
|
|
|
|
select.onchange = () => {
|
|
if (select.value) {
|
|
addTag(key, select.value);
|
|
}
|
|
};
|
|
|
|
container.appendChild(tagsDiv);
|
|
container.appendChild(select);
|
|
inputContainer.appendChild(container);
|
|
|
|
// Hidden input to store actual csv value for saving logic (optional, or we rebuild it)
|
|
const hidden = document.createElement('input');
|
|
hidden.type = 'hidden';
|
|
hidden.id = `input-${key}`;
|
|
hidden.value = config.value;
|
|
inputContainer.appendChild(hidden);
|
|
|
|
} else if (inputType === 'async-select') {
|
|
// Search & Add
|
|
const container = document.createElement('div');
|
|
|
|
const currentValues = config.value ? config.value.split(',').map(s => s.trim()).filter(s => s) : [];
|
|
|
|
const tagsDiv = document.createElement('div');
|
|
tagsDiv.className = 'tags-container';
|
|
tagsDiv.id = `tags-${key}`;
|
|
renderTags(tagsDiv, currentValues, key);
|
|
|
|
const searchInput = document.createElement('input');
|
|
searchInput.type = 'text';
|
|
searchInput.placeholder = meta.placeholder || 'Type to search...';
|
|
searchInput.style.marginBottom = '0';
|
|
|
|
const resultsDiv = document.createElement('div');
|
|
resultsDiv.className = 'dropdown-results';
|
|
|
|
let debounceTimer;
|
|
searchInput.oninput = (e) => {
|
|
const val = e.target.value;
|
|
clearTimeout(debounceTimer);
|
|
if (val.length < 2) {
|
|
resultsDiv.classList.remove('show');
|
|
return;
|
|
}
|
|
debounceTimer = setTimeout(async () => {
|
|
const res = await fetch(`/api/guilds/${currentGuildId}/football/teams?query=${encodeURIComponent(val)}`);
|
|
const data = await res.json();
|
|
|
|
resultsDiv.innerHTML = '';
|
|
if (data.length > 0) {
|
|
data.forEach(item => {
|
|
const div = document.createElement('div');
|
|
div.className = 'dropdown-item';
|
|
div.textContent = item.name; // Could use flag/logo if available
|
|
div.onclick = () => {
|
|
addTag(key, item.name); // Storing Name for now as per schema
|
|
searchInput.value = '';
|
|
resultsDiv.classList.remove('show');
|
|
};
|
|
resultsDiv.appendChild(div);
|
|
});
|
|
resultsDiv.classList.add('show');
|
|
} else {
|
|
resultsDiv.classList.remove('show');
|
|
}
|
|
}, 500); // 500ms debounce
|
|
};
|
|
|
|
// Hide dropdown on click outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!container.contains(e.target)) resultsDiv.classList.remove('show');
|
|
});
|
|
|
|
container.appendChild(tagsDiv);
|
|
container.appendChild(searchInput);
|
|
container.appendChild(resultsDiv);
|
|
inputContainer.appendChild(container);
|
|
|
|
const hidden = document.createElement('input');
|
|
hidden.type = 'hidden';
|
|
hidden.id = `input-${key}`;
|
|
hidden.value = config.value;
|
|
inputContainer.appendChild(hidden);
|
|
|
|
} else {
|
|
// Standard Input
|
|
inputContainer.innerHTML = `
|
|
<div style="display:flex; gap:10px;">
|
|
<input type="text" value="${config.value}" id="input-${key}" style="flex:1">
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
formGroup.appendChild(inputContainer);
|
|
|
|
// Save Button (Common)
|
|
const saveRow = document.createElement('div');
|
|
saveRow.style.marginTop = '5px';
|
|
// Determine validation type: check meta first, then service-level fallback
|
|
const validationType = (meta && meta.validation) ? meta.validation : (configMeta.validation || '');
|
|
|
|
saveRow.innerHTML = `
|
|
<button class="btn secondary" id="btn-${key}" onclick="window.saveServiceConfig('${key}', '${validationType}')">Save</button>
|
|
<small id="msg-${key}" style="margin-left:10px"></small>
|
|
`;
|
|
formGroup.appendChild(saveRow);
|
|
|
|
body.appendChild(formGroup);
|
|
});
|
|
} else {
|
|
body.innerHTML = '<p class="text-muted">No specific configurations for this service.</p>';
|
|
}
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(body);
|
|
servicesContainer.appendChild(card);
|
|
});
|
|
}
|
|
|
|
// --- Helpers for Tags ---
|
|
function renderTags(container, values, key) {
|
|
container.innerHTML = '';
|
|
values.forEach(val => {
|
|
const t = document.createElement('span');
|
|
t.className = 'tag';
|
|
t.innerHTML = `${val} <span class="remove" onclick="removeTag('${key}', '${val}')">×</span>`;
|
|
container.appendChild(t);
|
|
});
|
|
}
|
|
|
|
window.addTag = (key, value) => {
|
|
const input = document.getElementById(`input-${key}`);
|
|
let current = input.value ? input.value.split(',').map(s => s.trim()).filter(s => s) : [];
|
|
if (!current.includes(value)) {
|
|
current.push(value);
|
|
input.value = current.join(',');
|
|
// Re-render
|
|
const tagsDiv = document.getElementById(`tags-${key}`);
|
|
renderTags(tagsDiv, current, key);
|
|
|
|
// Auto-save? user usually expects "Add" then "Save". Let's stick to explicit Save button logic for consistency.
|
|
// Or trigger a "dirty" state.
|
|
}
|
|
};
|
|
|
|
window.removeTag = (key, value) => {
|
|
const input = document.getElementById(`input-${key}`);
|
|
let current = input.value ? input.value.split(',').map(s => s.trim()).filter(s => s) : [];
|
|
current = current.filter(v => v !== value);
|
|
input.value = current.join(',');
|
|
|
|
const tagsDiv = document.getElementById(`tags-${key}`);
|
|
renderTags(tagsDiv, current, key);
|
|
};
|
|
|
|
async function toggleService(name, enabled) {
|
|
try {
|
|
const res = await fetch(`/api/guilds/${currentGuildId}/features`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ feature_name: name, is_enabled: enabled })
|
|
});
|
|
if (res.ok) refreshData(true); // Silent refresh
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
// Helper for inline onclick
|
|
window.saveServiceConfig = async (key, validationType) => {
|
|
const input = document.getElementById(`input-${key}`);
|
|
const btn = document.getElementById(`btn-${key}`);
|
|
const msg = document.getElementById(`msg-${key}`);
|
|
if (!input) return;
|
|
|
|
const val = input.value.trim();
|
|
|
|
// Inline Loading
|
|
const originalText = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-sm"></span>';
|
|
|
|
msg.textContent = 'Validating...';
|
|
msg.style.color = 'var(--text-muted)';
|
|
|
|
let isValid = true;
|
|
let errStr = '';
|
|
|
|
if (val.length > 0 && validationType === 'channel') {
|
|
isValid = await validateChannel(val);
|
|
if (!isValid) errStr = "Invalid Channel ID (not visible)";
|
|
} else if (val.length > 0 && validationType === 'channel_list') {
|
|
const ids = val.split(',').map(s => s.trim());
|
|
for (let id of ids) {
|
|
if (id.length > 0 && !(await validateChannel(id))) {
|
|
isValid = false;
|
|
errStr = `Invalid Channel ID: ${id}`;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isValid) {
|
|
msg.textContent = errStr;
|
|
msg.style.color = 'var(--danger-color)';
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
return;
|
|
}
|
|
|
|
await saveConfig(key, val, 'Service Config');
|
|
msg.textContent = 'Saved!';
|
|
msg.style.color = 'var(--accent-color)';
|
|
setTimeout(() => { msg.textContent = ''; }, 2000);
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = originalText; // Restore text
|
|
|
|
refreshData(true); // Silent
|
|
};
|
|
|
|
async function validateChannel(channelId) {
|
|
try {
|
|
const res = await fetch(`/api/guilds/${currentGuildId}/channels/${channelId}`);
|
|
return res.ok;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
// --- Config Tab (Custom) ---
|
|
function renderConfigTab() {
|
|
configTableBody.innerHTML = '';
|
|
|
|
// Exclude known service keys and admins from the generic table
|
|
const serviceKeys = Object.values(SERVICE_CONFIG_MAP).flatMap(m => m.keys);
|
|
const otherConfigs = currentConfigs.filter(c =>
|
|
!serviceKeys.includes(c.key) && c.key !== 'ADMIN_IDS'
|
|
);
|
|
|
|
if (otherConfigs.length === 0) {
|
|
configTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center">No custom configurations.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
otherConfigs.forEach(c => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${c.key}</td>
|
|
<td>${c.value}</td>
|
|
<td>${c.description || ''}</td>
|
|
<td>
|
|
<button class="btn danger action-btn" onclick="deleteConfig('${c.key}')">Delete</button>
|
|
</td>
|
|
`;
|
|
configTableBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
window.deleteConfig = async (key) => {
|
|
if (!confirm('Delete config?')) return;
|
|
try {
|
|
await fetch(`/api/guilds/${currentGuildId}/config/${key}`, { method: 'DELETE' });
|
|
refreshData(true); // Silent
|
|
} catch (e) { alert(e); }
|
|
};
|
|
|
|
function formatName(str) {
|
|
return str.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
}
|
|
});
|