CI / Build & Test (push) Successful in 3m1s
Snapshot checked row IDs before each innerHTML swap in the virtual hosts, service tunnels, and server tunnels tables so that periodic refresh cycles no longer reset multi-selection state.
238 lines
10 KiB
JavaScript
238 lines
10 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|
||
|
||
// Snapshot checked checkboxes so selection survives the innerHTML swap
|
||
const _checkedServerIds = new Set();
|
||
tbody.querySelectorAll('.server-row-cb:checked').forEach(cb => _checkedServerIds.add(cb.dataset.serverId));
|
||
|
||
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 `
|
||
<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 => {
|
||
if (_checkedServerIds.has(cb.dataset.serverId)) cb.checked = true;
|
||
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();
|
||
}
|
||
);
|
||
});
|
||
}
|