document.addEventListener('DOMContentLoaded', () => {
const addForm = document.getElementById('add-form');
const instancesList = document.getElementById('instances-list');
const refreshBtn = document.getElementById('refresh-btn');
const portInput = document.getElementById('port');
const ufwCmd = document.getElementById('ufw-cmd');
const iptablesCmd = document.getElementById('iptables-cmd');
let config = { firewall_host_ip: null };
const updateFirewallCmds = () => {
const port = portInput.value || '4646';
const hostIp = config.firewall_host_ip || window.location.hostname;
ufwCmd.textContent = `sudo ufw allow from ${hostIp} to any port ${port} proto tcp`;
iptablesCmd.textContent = `sudo iptables -I INPUT -p tcp -s ${hostIp} --dport ${port} -j ACCEPT`;
};
const fetchConfig = async () => {
try {
const res = await fetch('/api/config');
if (res.ok) {
config = await res.json();
updateFirewallCmds();
}
} catch (e) {
console.error('Failed to fetch config:', e);
}
};
portInput.addEventListener('input', updateFirewallCmds);
// Initial fetch of config and instances
fetchConfig();
updateFirewallCmds();
const fetchInstances = async () => {
try {
const res = await fetch('/api/instances');
if (res.status === 401) {
return; // Let browser handle auth
}
const data = await res.json();
renderInstances(data);
} catch (e) {
console.error('Failed to fetch instances:', e);
}
};
const renderInstances = (instances) => {
if (instances.length === 0) {
instancesList.innerHTML = '
No instances configured yet.
';
return;
}
instancesList.innerHTML = instances.map(inst => {
let statusClass = 'status-unknown';
if (inst.last_status === 'Sent') statusClass = 'status-sent';
else if (inst.last_status === 'Offline') statusClass = 'status-offline';
else if (inst.last_status.startsWith('Failed')) statusClass = 'status-offline';
const timeAgo = inst.last_checked ? Math.round((Date.now()/1000 - inst.last_checked)) + 's ago' : 'Never';
return `
${escapeHtml(inst.name)}
${escapeHtml(inst.ip)}:${inst.port}
${escapeHtml(inst.last_status)}
Last checked: ${timeAgo}
`;
}).join('');
};
addForm.addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('name').value;
const ip = document.getElementById('ip').value;
const port = parseInt(document.getElementById('port').value, 10);
const secret = document.getElementById('secret').value;
try {
const res = await fetch('/api/instances', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, ip, port, secret })
});
if (res.ok) {
addForm.reset();
document.getElementById('port').value = "4646";
fetchInstances();
} else {
alert('Failed to add instance');
}
} catch (e) {
console.error('Error adding instance', e);
}
});
refreshBtn.addEventListener('click', fetchInstances);
window.deleteInstance = async (id) => {
if (!confirm('Are you sure you want to delete this instance?')) return;
try {
const res = await fetch(`/api/instances/${id}`, { method: 'DELETE' });
if (res.ok) {
fetchInstances();
}
} catch (e) {
console.error('Error deleting instance', e);
}
};
window.copyToClipboard = async (elementId) => {
const text = document.getElementById(elementId).textContent;
const btn = event.currentTarget;
try {
await navigator.clipboard.writeText(text);
const originalHtml = btn.innerHTML;
btn.classList.add('copied');
btn.innerHTML = '';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = originalHtml;
}, 2000);
} catch (err) {
console.error('Failed to copy: ', err);
}
};
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
fetchInstances();
setInterval(fetchInstances, 5000);
});