Files
holesail-browser/extension/dashboard/pages/servers.js
T
Raven Scott f1e98a7edd
CI / Build & Test (push) Successful in 2m54s
docs: add CONTRIBUTING.md, CHANGELOG.md, and JSDoc to entire codebase
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all
npm scripts, how to add new native host message types, code style, and
debugging guidance.

Add CHANGELOG.md at the project root documenting all features and fixes
across the 1.0.0 release.

Add JSDoc (@param, @returns) to all previously undocumented exported
functions across 35 JS files:
- native-host/holesail-manager/ (index, virtual-hosts, service-tunnels,
  servers, port-allocator)
- native-host top-level managers (startup, connect-proxy, https-proxy,
  certificate-authority, ssh-manager, rdp-manager)
- extension/background/ (logs, native-messaging, proxy, message-router)
- extension/dashboard/core/ (utils, navigation, init)
- extension/dashboard/ui/ (modal, toast, state-tag)
- extension/dashboard/pages/ (all 10 page files)
- extension/dashboard/refresh.js, events.js
- extension/dashboard/data/hostname-validator.js
- scripts/ (build-host, run-install)
2026-03-01 00:40:53 -05:00

233 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<tr><td colspan="6">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="6" height="6" rx="1"/><rect x="16" y="3" width="6" height="6" rx="1"/><rect x="9" y="15" width="6" height="6" rx="1"/><line x1="5" y1="9" x2="12" y2="15"/><line x1="19" y1="9" x2="12" y2="15"/></svg>
<div class="empty-state-title">No server tunnels</div>
<div class="empty-state-desc">Click "New Server" to expose a local port as an hs:// tunnel</div>
</div>
</td></tr>`;
_updateServerBulkBar();
return;
}
tbody.innerHTML = servers.map(s => {
const id = s.id || s.serverId || '';
const url = s.url || s.hsUrl || '';
const safeId = id.replace(/"/g, '&quot;');
const label = s.label || '';
return `
<tr>
<td style="width:32px;"><input type="checkbox" class="server-row-cb" data-server-id="${safeId}"></td>
<td>
${label ? `<div style="font-weight:600;color:var(--text);margin-bottom:2px;">${escapeHtml(label)}</div>` : ''}
<div class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(truncate(id, 20))}</div>
</td>
<td style="font-weight:600;color:var(--text);">${s.port ?? '—'}</td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<span class="mono" title="${escapeHtml(url)}" style="font-size:11px;color:var(--cyan);">${truncate(url, 30)}</span>
${url ? `<button class="btn-icon copy-btn" data-copy="${escapeHtml(url)}" title="Copy hs:// URL">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span class="copy-tooltip">Copied!</span>
</button>` : ''}
</div>
</td>
<td>
<span class="badge badge-neutral" style="margin-right:4px;">${s.udp ? 'UDP' : 'TCP'}</span>${s.secure ? `<span class="badge badge-green">secure</span>` : `<span class="badge badge-neutral">plain</span>`}
</td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<button class="btn btn-ghost btn-sm" data-edit-server="${safeId}" title="Edit server">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edit
</button>
<button class="btn btn-danger btn-sm" data-stop-server="${safeId}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
Stop
</button>
</div>
</td>
</tr>`;
}).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 165535'); 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();
}
);
});
}