const MAX_HISTORY = 50;
let servers = [];
let activeTabs = {};
let tabCounter = 0;
let currentTheme = 'terminal';
const themes = {
terminal: 'styles/terminal.css',
dark: 'styles/dark.css',
light: 'styles/light.css'
};
document.addEventListener('DOMContentLoaded', async () => {
servers = await window.rcon.getServers();
setupEventListeners();
setupThemeButtons();
renderServerList();
servers.forEach(s => openServer(s.id));
});
let inputModalCallback = null;
function setupEventListeners() {
document.getElementById('btn-add-server').addEventListener('click', showAddServerModal);
document.getElementById('btn-servers').addEventListener('click', showServersModal);
document.getElementById('btn-cancel-server').addEventListener('click', hideAddServerModal);
document.getElementById('btn-close-servers').addEventListener('click', hideServersModal);
document.getElementById('btn-close-about').addEventListener('click', hideAboutModal);
document.getElementById('btn-about').addEventListener('click', showAboutModal);
document.getElementById('btn-close-reference').addEventListener('click', hideReferenceModal);
document.getElementById('btn-reference').addEventListener('click', showReferenceModal);
document.getElementById('form-server').addEventListener('submit', saveServer);
document.getElementById('btn-cancel-input').addEventListener('click', hideInputModal);
document.getElementById('btn-submit-input').addEventListener('click', submitInputModal);
document.getElementById('modal-input-field').addEventListener('keydown', (e) => {
if (e.key === 'Enter') submitInputModal();
if (e.key === 'Escape') hideInputModal();
});
}
function setupThemeButtons() {
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
setTheme(btn.dataset.theme);
});
});
setTheme(currentTheme);
}
function setTheme(theme) {
currentTheme = theme;
document.getElementById('theme-stylesheet').href = themes[theme];
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === theme);
});
}
function showAddServerModal(server = null) {
const modal = document.getElementById('modal-add-server');
document.getElementById('server-id').value = server?.id || '';
document.getElementById('server-name').value = server?.name || '';
document.getElementById('server-address').value = server?.address || '';
document.getElementById('server-port').value = server?.port || 27960;
document.getElementById('server-password').value = server?.password || '';
modal.classList.add('active');
}
function hideAddServerModal() {
document.getElementById('modal-add-server').classList.remove('active');
}
function showServersModal() {
renderServerList();
document.getElementById('modal-servers').classList.add('active');
}
function hideServersModal() {
document.getElementById('modal-servers').classList.remove('active');
}
function showAboutModal() {
document.getElementById('modal-about').classList.add('active');
}
function hideAboutModal() {
document.getElementById('modal-about').classList.remove('active');
}
function showReferenceModal() {
document.getElementById('modal-reference').classList.add('active');
}
function hideReferenceModal() {
document.getElementById('modal-reference').classList.remove('active');
}
function showInputModal(title, placeholder, callback) {
inputModalCallback = callback;
document.getElementById('modal-input-title').textContent = title;
document.getElementById('modal-input-field').placeholder = placeholder;
document.getElementById('modal-input-field').value = '';
document.getElementById('modal-input').classList.add('active');
document.getElementById('modal-input-field').focus();
}
function hideInputModal() {
document.getElementById('modal-input').classList.remove('active');
inputModalCallback = null;
}
function submitInputModal() {
const value = document.getElementById('modal-input-field').value;
document.getElementById('modal-input').classList.remove('active');
if (inputModalCallback) {
const cb = inputModalCallback;
inputModalCallback = null;
cb(value);
}
}
async function saveServer(e) {
e.preventDefault();
const server = {
id: document.getElementById('server-id').value || undefined,
name: document.getElementById('server-name').value,
address: document.getElementById('server-address').value,
port: parseInt(document.getElementById('server-port').value),
password: document.getElementById('server-password').value
};
servers = await window.rcon.saveServer(server);
hideAddServerModal();
renderServerList();
const saved = servers.find(s => s.name === server.name && s.address === server.address);
if (saved && !activeTabs[saved.id]) {
openServer(saved.id);
}
}
async function deleteServer(id) {
if (confirm('Delete this server?')) {
servers = await window.rcon.deleteServer(id);
closeTab(id);
renderServerList();
}
}
function renderServerList() {
const list = document.getElementById('server-list');
if (servers.length === 0) {
list.innerHTML = '
No servers configured.
';
return;
}
list.innerHTML = servers.map(s => `
${escapeHtml(s.name)}
${escapeHtml(s.address)}:${s.port}
`).join('');
}
function openServer(id) {
const server = servers.find(s => s.id === id);
if (!server) return;
if (!activeTabs[id]) {
activeTabs[id] = createTab(server);
tabCounter++;
}
switchToTab(id);
hideServersModal();
}
function createTab(server) {
const id = server.id;
const tab = {
id,
name: server.name,
server,
history: [],
historyIndex: -1
};
const tabEl = document.createElement('div');
tabEl.className = 'tab';
tabEl.id = `tab-${id}`;
tabEl.innerHTML = `
${escapeHtml(server.name)}
×
`;
tabEl.addEventListener('click', () => switchToTab(id));
const contentEl = document.createElement('div');
contentEl.className = 'tab-content';
contentEl.id = `content-${id}`;
contentEl.innerHTML = `
`;
document.getElementById('tabs-bar').appendChild(tabEl);
document.getElementById('server-content').appendChild(contentEl);
const placeholder = document.getElementById('tabs-placeholder');
if (placeholder) placeholder.style.display = 'none';
tabEl.querySelector('.tab-close').addEventListener('click', (e) => {
e.stopPropagation();
closeTab(id);
});
contentEl.querySelector('.cmd-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendCommand(id);
} else if (e.key === 'ArrowUp') {
navigateHistory(id, -1);
} else if (e.key === 'ArrowDown') {
navigateHistory(id, 1);
}
});
contentEl.querySelector('.cmd-send').addEventListener('click', () => sendCommand(id));
contentEl.querySelectorAll('.qa-btn[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => handleQuickAction(id, btn.dataset.cmd));
});
contentEl.querySelector('.qa-clear').addEventListener('click', () => {
const output = document.getElementById(`output-${id}`);
if (output) output.innerHTML = '';
});
return tab;
}
function switchToTab(id) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
const tabEl = document.getElementById(`tab-${id}`);
const contentEl = document.getElementById(`content-${id}`);
if (tabEl && contentEl) {
tabEl.classList.add('active');
contentEl.classList.add('active');
contentEl.querySelector('.cmd-input')?.focus();
}
}
function closeTab(id) {
const tabEl = document.getElementById(`tab-${id}`);
const contentEl = document.getElementById(`content-${id}`);
if (tabEl) tabEl.remove();
if (contentEl) contentEl.remove();
delete activeTabs[id];
const remaining = Object.keys(activeTabs);
if (remaining.length > 0) {
switchToTab(remaining[remaining.length - 1]);
} else {
document.getElementById('server-content').innerHTML = `
Q3A RCON Dashboard
Select or add a server to begin.
`;
const placeholder = document.getElementById('tabs-placeholder');
if (placeholder) placeholder.style.display = '';
}
}
async function sendCommand(tabId) {
const input = document.getElementById(`cmd-${tabId}`);
const output = document.getElementById(`output-${tabId}`);
const command = input.value.trim();
if (!command || !activeTabs[tabId]) return;
const tab = activeTabs[tabId];
tab.history.push(command);
if (tab.history.length > MAX_HISTORY) tab.history.shift();
tab.historyIndex = tab.history.length;
input.value = '';
appendOutput(tabId, `> ${command}`, 'command');
const result = await window.rcon.send({
address: tab.server.address,
port: tab.server.port,
password: tab.server.password,
command
});
if (result.success) {
appendOutput(tabId, result.response || '(no response)', 'response');
} else {
appendOutput(tabId, `Error: ${result.error}`, 'error');
}
}
async function handleQuickAction(tabId, action) {
const tab = activeTabs[tabId];
if (!tab) return;
let command = action;
let promptTitle = null;
let promptPlaceholder = null;
switch (action) {
case 'kick':
promptTitle = 'Kick Player';
promptPlaceholder = 'Player name or slot number';
command = 'kick ';
break;
case 'say':
promptTitle = 'Broadcast Message';
promptPlaceholder = 'Message to send to all players';
command = 'say ';
break;
case 'map':
promptTitle = 'Change Map';
promptPlaceholder = 'Map name (e.g., q3dm1)';
command = 'map ';
break;
case 'banaddr':
promptTitle = 'Ban IP Address';
promptPlaceholder = 'IP or IP/subnet (e.g., 192.168.1.100)';
command = 'banaddr ';
break;
case 'dumpuser':
promptTitle = 'Dump User Info';
promptPlaceholder = 'Player name or slot number';
command = 'dumpuser ';
break;
default:
break;
}
if (promptTitle) {
showInputModal(promptTitle, promptPlaceholder, (value) => {
if (!value) return;
const fullCommand = command + value;
document.getElementById(`cmd-${tabId}`).value = fullCommand;
sendCommand(tabId);
});
return;
}
document.getElementById(`cmd-${tabId}`).value = command;
await sendCommand(tabId);
}
function navigateHistory(tabId, direction) {
const tab = activeTabs[tabId];
if (!tab || tab.history.length === 0) return;
tab.historyIndex += direction;
if (tab.historyIndex < 0) tab.historyIndex = 0;
if (tab.historyIndex >= tab.history.length) {
tab.historyIndex = tab.history.length;
document.getElementById(`cmd-${tabId}`).value = '';
return;
}
document.getElementById(`cmd-${tabId}`).value = tab.history[tab.historyIndex];
}
function appendOutput(tabId, text, type = '') {
const output = document.getElementById(`output-${tabId}`);
if (!output) return;
const lines = text.split('\n');
lines.forEach(line => {
const div = document.createElement('div');
div.className = `output-line ${type}`;
div.textContent = line;
output.appendChild(div);
});
output.scrollTop = output.scrollHeight;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
window.servers = servers;
window.openServer = openServer;
window.deleteServer = deleteServer;
window.showAddServerModal = showAddServerModal;