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();
}