/**
* Server Tunnels page — renders the server table, manages bulk selection,
* and wires up start/stop/edit modal events.
* Depends on: core/utils.js ($, escapeHtml, truncate), ui/toast.js (showToast, copyToClipboard),
* ui/modal.js (openModal, closeModal, showModalError)
*/
function _updateServerBulkBar() {
const checked = document.querySelectorAll('#swarmsTable input[type="checkbox"]:checked');
const bar = $('serverBulkBar');
const countEl = $('serverBulkCount');
if (!bar) return;
if (checked.length > 0) {
bar.style.display = 'flex';
if (countEl) countEl.textContent = checked.length + ' selected';
} else {
bar.style.display = 'none';
}
}
/**
* Re-render the server tunnels table with the latest state.
* @param {object} state - Full extension state from `fetchState()`.
*/
function updateSwarmsTable(state) {
const tbody = $('swarmsTable');
if (!tbody) return;
const servers = state.servers || [];
if (servers.length === 0) {
tbody.innerHTML = `
No server tunnels
Click "New Server" to expose a local port as an hs:// tunnel
|
`;
_updateServerBulkBar();
return;
}
tbody.innerHTML = servers.map(s => {
const id = s.id || s.serverId || '';
const url = s.url || s.hsUrl || '';
const safeId = id.replace(/"/g, '"');
const label = s.label || '';
return `
|
${label ? ` ${escapeHtml(label)} ` : ''}
${escapeHtml(truncate(id, 20))}
|
${s.port ?? '—'} |
${truncate(url, 30)}
${url ? ` ` : ''}
|
${s.udp ? 'UDP' : 'TCP'}${s.secure ? `secure` : `plain`}
|
|
`;
}).join('');
tbody.querySelectorAll('.server-row-cb').forEach(cb => {
cb.addEventListener('change', _updateServerBulkBar);
});
tbody.querySelectorAll('[data-copy]').forEach(btn => {
btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn));
});
tbody.querySelectorAll('[data-edit-server]').forEach(btn => {
btn.addEventListener('click', () => {
const serverId = btn.dataset.editServer;
const server = (state.servers || []).find(s => (s.id || s.serverId) === serverId);
if (!server) return;
const serverEditIdEl = $('serverEditId');
if (serverEditIdEl) serverEditIdEl.value = serverId;
const labelEl = $('startServerLabel');
const portEl = $('startServerPort');
const hostEl = $('startServerHost');
const secureEl = $('startServerSecure');
if (labelEl) labelEl.value = server.label || '';
if (portEl) portEl.value = server.port ?? 3000;
if (hostEl) hostEl.value = server.host ?? '127.0.0.1';
if (secureEl) secureEl.checked = server.secure !== false;
const udpEl = document.querySelector('input[name="startServerProtocol"][value="' + (server.udp ? 'udp' : 'tcp') + '"]');
if (udpEl) udpEl.checked = true;
const titleEl = $('modal-startServer-title');
if (titleEl) titleEl.textContent = 'Edit Server Tunnel';
const submitEl = $('startServerSubmit');
if (submitEl) submitEl.textContent = 'Save Changes';
openModal('modal-startServer');
});
});
tbody.querySelectorAll('[data-stop-server]').forEach(btn => {
btn.addEventListener('click', () => {
const serverId = btn.dataset.stopServer;
const nameEl = $('stopServerName');
if (nameEl) nameEl.textContent = serverId;
$('stopServerConfirm').dataset.serverId = serverId;
openModal('modal-stopServer');
});
});
_updateServerBulkBar();
}
/**
* Attach all event listeners for the Server Tunnels page.
* Called once during dashboard initialisation.
*/
function setupServerEvents() {
$('serverSelectAll')?.addEventListener('change', (e) => {
document.querySelectorAll('#swarmsTable .server-row-cb').forEach(cb => { cb.checked = e.target.checked; });
_updateServerBulkBar();
});
$('btnServerBulkStop')?.addEventListener('click', () => {
const checked = Array.from(document.querySelectorAll('#swarmsTable .server-row-cb:checked'));
if (!checked.length) return;
const ids = checked.map(cb => cb.dataset.serverId);
if (!confirm('Stop ' + ids.length + ' server(s)?')) return;
let done = 0;
for (const serverId of ids) {
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId } } },
() => { done++; if (done === ids.length) { showToast(ids.length + ' server(s) stopped', 'success'); refresh(); } }
);
}
});
$('startServerBtn')?.addEventListener('click', () => {
const editIdEl = $('serverEditId');
if (editIdEl) editIdEl.value = '';
const labelEl = $('startServerLabel');
if (labelEl) labelEl.value = '';
const portEl = $('startServerPort');
if (portEl) portEl.value = '3000';
const hostEl = $('startServerHost');
if (hostEl) hostEl.value = '127.0.0.1';
const secureEl = $('startServerSecure');
if (secureEl) secureEl.checked = true;
const tcpEl = $('startServerProtocolTcp');
if (tcpEl) tcpEl.checked = true;
const titleEl = $('modal-startServer-title');
if (titleEl) titleEl.textContent = 'Start Server Tunnel';
const submitEl = $('startServerSubmit');
if (submitEl) submitEl.textContent = 'Start Server';
openModal('modal-startServer');
});
$('startServerSubmit')?.addEventListener('click', () => {
const portEl = $('startServerPort');
const hostEl = $('startServerHost');
const secureEl = $('startServerSecure');
const labelEl = $('startServerLabel');
const editIdEl = $('serverEditId');
const port = parseInt(portEl?.value, 10) || 3000;
const host = (hostEl?.value || '127.0.0.1').trim();
const secure = secureEl?.checked !== false;
const udp = document.querySelector('input[name="startServerProtocol"]:checked')?.value === 'udp';
const label = (labelEl?.value || '').trim();
const editId = (editIdEl?.value || '').trim();
if (!port || port < 1 || port > 65535) { showModalError('modal-startServer', 'startServerError', 'Port must be 1–65535'); return; }
const btn = $('startServerSubmit');
if (btn) { btn.disabled = true; btn.textContent = editId ? 'Saving…' : 'Starting…'; }
const doStart = () => {
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'startServer', payload: { port, host, secure, udp, label } } },
(response) => {
if (btn) { btn.disabled = false; btn.textContent = editId ? 'Save Changes' : 'Start Server'; }
if (response?.ok) {
if (editIdEl) editIdEl.value = '';
closeModal('modal-startServer');
showToast(editId ? 'Server updated' : 'Server started', 'success');
refresh();
} else {
showModalError('modal-startServer', 'startServerError', response?.error || 'Failed to start');
}
}
);
};
if (editId) {
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId: editId } } },
() => doStart()
);
} else {
doStart();
}
});
$('stopServerConfirm')?.addEventListener('click', () => {
const serverId = $('stopServerConfirm').dataset.serverId;
if (!serverId) return;
const btn = $('stopServerConfirm');
btn.disabled = true; btn.textContent = 'Stopping…';
chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId } } },
(response) => {
btn.disabled = false; btn.textContent = 'Stop Server';
closeModal('modal-stopServer');
if (response?.ok) showToast('Server stopped', 'success');
else showToast(response?.error || 'Failed to stop', 'error');
refresh();
}
);
});
}