async function loadStats() { const res = await fetch('/admin/stats'); const stats = await res.json(); const div = document.getElementById('serverStats'); const formatTime = (s) => { const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return `${h}h ${m}m`; }; div.innerHTML = `
Uptime
${formatTime(stats.uptime)}
Users
${stats.active_users}
Rooms
${stats.active_rooms}
Messages
${stats.total_messages}
Bans
${stats.banned_count}
`; } async function loadUsers() { const res = await fetch('/admin/users'); const users = await res.json(); const div = document.getElementById('userList'); const countDisplay = document.getElementById('activeUserCount'); div.innerHTML = ''; users.forEach(user => { const item = document.createElement('div'); const label = document.createElement('span'); label.textContent = `${user.username} (${user.ip})`; const banIpBtn = document.createElement('button'); banIpBtn.textContent = 'Ban IP'; banIpBtn.style.marginLeft = '10px'; banIpBtn.onclick = () => banUser(user.username); const banUserBtn = document.createElement('button'); banUserBtn.textContent = 'Ban Username'; banUserBtn.style.marginLeft = '5px'; banUserBtn.onclick = () => banUsername(user.username); const releaseUserBtn = document.createElement('button'); releaseUserBtn.textContent = 'Release Username'; releaseUserBtn.style.marginLeft = '5px'; releaseUserBtn.onclick = () => releaseUser(user.username); item.append(label, banIpBtn, banUserBtn, releaseUserBtn); div.appendChild(item); }); countDisplay.textContent = `(${users.length})`; } async function loadRooms() { const res = await fetch('/admin/rooms'); const rooms = await res.json(); const div = document.getElementById('roomList'); const countDisplay = document.getElementById('activeRoomCount'); div.innerHTML = ''; rooms.forEach(r => { const container = document.createElement('div'); container.style.border = '1px solid #444'; container.style.padding = '12px 15px'; container.style.marginBottom = '10px'; container.style.borderRadius = '6px'; container.style.background = '#222'; container.style.display = 'block'; const titleRow = document.createElement('div'); titleRow.style.display = 'flex'; titleRow.style.justifyContent = 'space-between'; titleRow.style.alignItems = 'center'; titleRow.style.marginBottom = '8px'; const titleGroup = document.createElement('div'); const link = document.createElement('a'); link.href = r.type === 'watchparty' ? `/watchparty_chat.html?room=${encodeURIComponent(r.name)}` : `/chat.html?room=${encodeURIComponent(r.name)}`; link.target = "_blank"; link.textContent = r.name; link.style.fontWeight = 'bold'; link.style.fontSize = '1.1em'; const typeSpan = document.createElement('span'); typeSpan.textContent = ` (${r.type} | ${r.is_public ? 'Public' : 'Unlisted'})`; typeSpan.style.color = '#888'; typeSpan.style.fontSize = '0.95em'; typeSpan.style.marginLeft = '5px'; titleGroup.append(link, typeSpan); titleRow.appendChild(titleGroup); const actionsDiv = document.createElement('div'); const viewBtn = document.createElement('button'); viewBtn.textContent = 'View Transcript'; viewBtn.style.marginLeft = '5px'; viewBtn.onclick = () => viewTranscript(r.name); const clearBtn = document.createElement('button'); clearBtn.textContent = 'Clear History'; clearBtn.style.marginLeft = '5px'; clearBtn.onclick = () => clearHistory(r.name); const nukeBtn = document.createElement('button'); nukeBtn.textContent = 'Nuke'; nukeBtn.style.marginLeft = '5px'; nukeBtn.style.backgroundColor = '#d9534f'; nukeBtn.style.color = 'white'; nukeBtn.onclick = () => nukeRoom(r.name); actionsDiv.append(viewBtn, clearBtn, nukeBtn); titleRow.appendChild(actionsDiv); container.appendChild(titleRow); const metaRow = document.createElement('div'); metaRow.style.fontSize = '0.9em'; metaRow.style.color = '#aaa'; metaRow.style.marginBottom = '8px'; metaRow.style.display = 'flex'; metaRow.style.alignItems = 'center'; metaRow.style.gap = '10px'; const ownerLabel = document.createElement('span'); ownerLabel.textContent = `Owner: ${r.owner}`; const changeOwnerBtn = document.createElement('button'); changeOwnerBtn.textContent = 'Assign Owner'; changeOwnerBtn.style.padding = '2px 6px'; changeOwnerBtn.style.fontSize = '11px'; changeOwnerBtn.onclick = () => assignOwner(r.name); const reclaimOwnerBtn = document.createElement('button'); reclaimOwnerBtn.textContent = 'Reclaim Owner (Reset)'; reclaimOwnerBtn.style.padding = '2px 6px'; reclaimOwnerBtn.style.fontSize = '11px'; reclaimOwnerBtn.onclick = () => reclaimOwner(r.name); metaRow.append(ownerLabel, changeOwnerBtn, reclaimOwnerBtn); container.appendChild(metaRow); const usersRow = document.createElement('div'); usersRow.style.fontSize = '0.9em'; usersRow.style.color = '#ccc'; if (r.users.length === 0) { usersRow.textContent = 'Users: (None)'; } else { usersRow.textContent = 'Users: '; r.users.forEach(user => { const userSpan = document.createElement('span'); userSpan.style.display = 'inline-flex'; userSpan.style.alignItems = 'center'; userSpan.style.marginRight = '8px'; userSpan.style.marginBottom = '4px'; userSpan.style.padding = '2px 6px'; userSpan.style.border = '1px solid #555'; userSpan.style.borderRadius = '3px'; userSpan.style.background = '#111'; const nameSpan = document.createElement('span'); nameSpan.textContent = user; const kickBtn = document.createElement('button'); kickBtn.textContent = 'Kick'; kickBtn.style.marginLeft = '5px'; kickBtn.style.padding = '1px 4px'; kickBtn.style.fontSize = '10px'; kickBtn.style.background = 'none'; kickBtn.style.border = 'none'; kickBtn.style.color = '#ff5722'; kickBtn.style.cursor = 'pointer'; kickBtn.style.minHeight = 'auto'; kickBtn.onclick = () => adminKickUser(r.name, user); userSpan.append(nameSpan, kickBtn); usersRow.appendChild(userSpan); }); } container.appendChild(usersRow); div.appendChild(container); }); countDisplay.textContent = `(${rooms.length})`; } async function banUser(username) { if (!confirm(`Ban user ${username}?`)) return; const res = await fetch('/admin/banip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const result = await res.json(); alert(JSON.stringify(result)); loadUsers(); loadRooms(); loadBanned(); } async function banUsername(username) { if (!confirm(`Ban username ${username}?`)) return; const res = await fetch('/admin/banusername', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const result = await res.json(); alert(JSON.stringify(result)); loadUsers(); loadRooms(); loadBanned(); } async function loadBanned() { const res = await fetch('/admin/banned'); const banned = await res.json(); const div = document.getElementById('bannedList'); div.innerHTML = ''; banned.forEach(item => { const el = document.createElement('div'); el.style.display = 'flex'; el.style.alignItems = 'center'; el.style.justifyContent = 'space-between'; const label = document.createElement('span'); label.textContent = `${item.type}: ${item.value}`; const unbanBtn = document.createElement('button'); unbanBtn.textContent = 'Unban'; unbanBtn.style.marginLeft = '10px'; unbanBtn.onclick = async () => { if (confirm(`Remove ban for ${item.value}?`)) { await fetch('/admin/unban', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: item.type, value: item.value }) }); loadBanned(); loadStats(); } }; el.append(label, unbanBtn); div.appendChild(el); }); } async function bulkBanIPs() { const lines = document.getElementById('bulkInput').value .split('\n') .map(s => s.trim()) .filter(Boolean); for (const ip of lines) { const res = await fetch('/admin/banrawip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ip }) }); const result = await res.json(); if (!res.ok) { console.warn(`Failed to ban IP: ${ip} — ${result.error}`); } else { console.log(`Banned IP: ${ip}`, result); } } alert("Raw IP ban requests completed."); loadUsers(); loadRooms(); loadBanned(); } async function bulkBanUsernames() { const lines = document.getElementById('bulkInput').value.split('\n').map(s => s.trim()).filter(Boolean); for (const username of lines) { const res = await fetch('/admin/banusername', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const result = await res.json(); console.log(result); } alert("Username ban requests completed."); loadUsers(); loadRooms(); loadBanned(); } async function releaseUser(username) { if (!confirm(`Release user ${username}?`)) return; const res = await fetch('/admin/releaseuser', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); const result = await res.json(); alert(JSON.stringify(result)); loadUsers(); loadRooms(); loadBanned(); } async function broadcastSystemMessage() { const input = document.getElementById('broadcastInput'); const text = input.value.trim(); if (!text) return; if (!confirm(`Broadcast to ALL rooms: "${text}"?`)) return; const res = await fetch('/admin/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); const result = await res.json(); alert(`Broadcast sent to ${result.rooms_affected} rooms.`); input.value = ''; } async function nukeRoom(room) { if (!confirm(`Are you sure you want to NUKE room: "${room}"? This will delete all history and kick users.`)) return; await fetch('/admin/nukeroom', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room }) }); loadRooms(); } // Initial load loadUsers(); loadRooms(); loadBanned(); loadStats(); // Refresh every 5 seconds setInterval(() => { loadUsers(); loadRooms(); loadBanned(); loadStats(); }, 5000); document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll(".collapsible").forEach(header => { const countSpan = header.querySelector('span'); const labelText = header.childNodes[0].textContent.trim(); // original text // Set initial state if (countSpan) { header.childNodes[0].textContent = '▶ ' + labelText + ' '; } else { header.textContent = '▶ ' + labelText; } const content = header.nextElementSibling; if (content && content.classList.contains("collapsible-content")) { content.style.display = 'none'; header.style.cursor = "pointer"; header.addEventListener("click", () => { const isOpen = content.style.display === 'block'; content.style.display = isOpen ? 'none' : 'block'; if (countSpan) { header.childNodes[0].textContent = (isOpen ? '▶ ' : '▼ ') + labelText + ' '; } else { header.textContent = (isOpen ? '▶ ' : '▼ ') + labelText; } }); } }); }); async function viewTranscript(room) { const res = await fetch(`/admin/room/messages?room=${encodeURIComponent(room)}`); const msgs = await res.json(); let overlay = document.getElementById('transcriptOverlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'transcriptOverlay'; overlay.style.position = 'fixed'; overlay.style.top = '10%'; overlay.style.left = '10%'; overlay.style.width = '80%'; overlay.style.height = '80%'; overlay.style.background = '#1a1f2c'; overlay.style.color = 'white'; overlay.style.border = '2px solid #555'; overlay.style.borderRadius = '8px'; overlay.style.padding = '20px'; overlay.style.zIndex = '10000'; overlay.style.overflowY = 'auto'; document.body.appendChild(overlay); } overlay.style.display = 'block'; overlay.innerHTML = `

Transcript: ${room}

`; const contentDiv = document.getElementById('transcriptContent'); if (msgs.length === 0) { contentDiv.textContent = "(No messages in room)"; } else { contentDiv.textContent = msgs.map(m => { const timeStr = new Date(m.timestamp).toLocaleTimeString(); return `[${timeStr}] ${m.username}: ${m.text}`; }).join('\n'); } } async function clearHistory(room) { if (!confirm(`Clear all message history for room: "${room}"?`)) return; await fetch('/admin/room/clearmessages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room }) }); loadRooms(); loadStats(); } async function assignOwner(room) { const owner = prompt(`Enter new owner username for room: "${room}":`); if (owner === null) return; const trimmed = owner.trim(); await fetch('/admin/room/setowner', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room, owner: trimmed || null }) }); loadRooms(); } async function reclaimOwner(room) { if (!confirm(`Reclaim ownership for room: "${room}"? This will clear the owner, letting the next active user claim it.`)) return; await fetch('/admin/room/setowner', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room, owner: null }) }); loadRooms(); } async function adminKickUser(room, username) { if (!confirm(`Kick user "${username}" from room: "${room}"?`)) return; await fetch('/admin/kick', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room, target: username }) }); loadRooms(); }