// Depends on: core/utils.js ($, escapeHtml, truncate), ui/state-tag.js (stateTag), // ui/toast.js (showToast, copyToClipboard), ui/modal.js (openModal, closeModal, showModalError) function updateServiceTunnelsTable(state) { const tbody = $('serviceTunnelsTable'); if (!tbody) return; const tunnels = state.serviceTunnels || []; const countEl = $('serviceTunnelCount'); if (countEl) countEl.textContent = tunnels.length; if (tunnels.length === 0) { tbody.innerHTML = `
No service tunnels
Click "Add Tunnel" to connect a remote TCP/UDP service via Holesail
`; return; } tbody.innerHTML = tunnels.map(t => { const id = t.id || ''; const label = t.label || id; const hsUrl = t.hsUrl || ''; const localAddr = t.localPort != null ? `127.0.0.1:${t.localPort}` : '—'; const safeId = id.replace(/"/g, '"'); return ` ${escapeHtml(label)}
${truncate(hsUrl, 28)} ${hsUrl ? `` : ''}
${escapeHtml(localAddr)} ${t.localPort != null ? `` : ''}
${stateTag(t.state)}
${(t.state === 'error' || t.state === 'closed') ? ` ` : ''}
`; }).join(''); tbody.querySelectorAll('[data-copy]').forEach(btn => { btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn)); }); tbody.querySelectorAll('[data-reconnect-svc]').forEach(btn => { btn.addEventListener('click', () => { const tunnelId = btn.dataset.reconnectSvc; const hsUrl = btn.dataset.hsurl; const label = btn.dataset.label; const localPort = parseInt(btn.dataset.localport, 10); btn.disabled = true; btn.textContent = 'Reconnecting…'; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'updateServiceTunnel', payload: { tunnelId, hsUrl, label, localPort } } }, (response) => { if (chrome.runtime.lastError) { showToast('Reconnect failed: ' + chrome.runtime.lastError.message, 'error'); return; } if (response?.ok) { showToast('Service tunnel reconnecting…', 'success'); } else { showToast(response?.error || 'Reconnect failed', 'error'); } refresh(); } ); }); }); tbody.querySelectorAll('[data-edit-service-tunnel]').forEach(btn => { btn.addEventListener('click', () => { const tunnelId = btn.dataset.editServiceTunnel; const tunnel = (state.serviceTunnels || []).find(t => t.id === tunnelId); if (!tunnel) return; $('serviceTunnelEditId').value = tunnelId; $('serviceTunnelLabel').value = tunnel.label || ''; $('serviceTunnelHsUrl').value = tunnel.hsUrl || ''; $('serviceTunnelLocalPort').value = tunnel.localPort != null ? tunnel.localPort : ''; const titleEl = $('modal-addServiceTunnel-title'); if (titleEl) titleEl.textContent = 'Edit Service Tunnel'; const submitEl = $('serviceTunnelSubmit'); if (submitEl) submitEl.textContent = 'Save'; openModal('modal-addServiceTunnel'); }); }); tbody.querySelectorAll('[data-remove-service-tunnel]').forEach(btn => { btn.addEventListener('click', () => { const tunnelId = btn.dataset.removeServiceTunnel; const tunnel = (state.serviceTunnels || []).find(t => t.id === tunnelId); const nameEl = $('removeServiceTunnelName'); if (nameEl) nameEl.textContent = (tunnel && tunnel.label) || tunnelId; $('removeServiceTunnelConfirm').dataset.tunnelId = tunnelId; openModal('modal-removeServiceTunnel'); }); }); } function setupServiceTunnelEvents() { $('addServiceTunnelBtn')?.addEventListener('click', () => { $('serviceTunnelEditId').value = ''; $('serviceTunnelLabel').value = ''; $('serviceTunnelHsUrl').value = ''; $('serviceTunnelLocalPort').value = ''; const titleEl = $('modal-addServiceTunnel-title'); if (titleEl) titleEl.textContent = 'Add Service Tunnel'; const submitEl = $('serviceTunnelSubmit'); if (submitEl) submitEl.textContent = 'Connect'; openModal('modal-addServiceTunnel'); }); $('serviceTunnelSubmit')?.addEventListener('click', () => { const label = ($('serviceTunnelLabel')?.value || '').trim(); const hsUrl = ($('serviceTunnelHsUrl')?.value || '').trim(); const localPort = parseInt($('serviceTunnelLocalPort')?.value, 10); const editId = ($('serviceTunnelEditId')?.value || '').trim(); if (!label) { showModalError('modal-addServiceTunnel', 'serviceTunnelError', 'Label is required'); return; } if (!hsUrl || !hsUrl.startsWith('hs://')) { showModalError('modal-addServiceTunnel', 'serviceTunnelError', 'Enter a valid hs:// key'); return; } if (!localPort || localPort < 1 || localPort > 65535) { showModalError('modal-addServiceTunnel', 'serviceTunnelError', 'Local port must be 1–65535'); return; } const btn = $('serviceTunnelSubmit'); if (btn) { btn.disabled = true; btn.textContent = 'Connecting…'; } const type = editId ? 'updateServiceTunnel' : 'startServiceTunnel'; const payload = editId ? { tunnelId: editId, label, hsUrl, localPort } : { label, hsUrl, localPort }; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type, payload } }, (response) => { if (btn) { btn.disabled = false; btn.textContent = editId ? 'Save' : 'Connect'; } if (response?.ok) { closeModal('modal-addServiceTunnel'); showToast(editId ? 'Tunnel updated' : 'Service tunnel connected', 'success'); refresh(); } else { showModalError('modal-addServiceTunnel', 'serviceTunnelError', response?.error || 'Failed to connect'); } } ); }); $('removeServiceTunnelConfirm')?.addEventListener('click', () => { const tunnelId = $('removeServiceTunnelConfirm').dataset.tunnelId; if (!tunnelId) return; const btn = $('removeServiceTunnelConfirm'); btn.disabled = true; btn.textContent = 'Removing…'; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'stopServiceTunnel', payload: { tunnelId } } }, (response) => { btn.disabled = false; btn.textContent = 'Remove'; closeModal('modal-removeServiceTunnel'); if (response?.ok) showToast('Service tunnel removed', 'success'); else showToast(response?.error || 'Failed to remove', 'error'); refresh(); } ); }); }