// Depends on: core/utils.js ($, escapeHtml, truncate), ui/state-tag.js (stateTag), // ui/toast.js (showToast, copyToClipboard), ui/modal.js (openModal), // data/hostname-validator.js (isValidVhostHostname) function _updateVhostBulkBar() { const checked = document.querySelectorAll('#connectionsTable input[type="checkbox"]:checked'); const bar = $('vhostBulkBar'); const countEl = $('vhostBulkCount'); if (!bar) return; if (checked.length > 0) { bar.style.display = 'flex'; if (countEl) countEl.textContent = checked.length + ' selected'; } else { bar.style.display = 'none'; } } function updateConnectionsTable(state) { const tbody = $('connectionsTable'); if (!tbody) return; const virtualHosts = state.virtualHosts || []; if (virtualHosts.length === 0) { tbody.innerHTML = `
No virtual hosts
Click "Add Host" to assign a hostname to an hs:// tunnel
`; _updateVhostBulkBar(); return; } tbody.innerHTML = virtualHosts.map(v => { const hostname = v.hostname || v.id || ''; const hsUrl = v.hsUrl || ''; const backend = (v.localHost && v.localPort != null) ? v.localHost + ':' + v.localPort : '—'; const openUrl = `https://${hostname}`; const safeHostname = hostname.replace(/"/g, '"'); const needsReconnect = v.state === 'error' || v.state === 'closed'; return ` ${escapeHtml(hostname)}
${truncate(hsUrl, 28)}
${escapeHtml(backend)} ${stateTag(v.state)} ${v.localPort != null ? `…ms` : ''}
Open ${needsReconnect ? `` : ''}
`; }).join(''); tbody.querySelectorAll('.vhost-row-cb').forEach(cb => { cb.addEventListener('change', _updateVhostBulkBar); }); tbody.querySelectorAll('[data-copy]').forEach(btn => { btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn)); }); tbody.querySelectorAll('[data-reconnect-vhost]').forEach(btn => { btn.addEventListener('click', () => { const hostname = btn.dataset.reconnectVhost; const hsUrl = btn.dataset.hsUrl; btn.disabled = true; btn.textContent = 'Reconnecting…'; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl } } }, (response) => { if (response?.ok) { showToast('Tunnel reconnecting…', 'success'); } else { showToast(response?.error || 'Reconnect failed', 'error'); } refresh(); } ); }); }); tbody.querySelectorAll('[data-remove-vhost]').forEach(btn => { btn.addEventListener('click', () => { const hostname = btn.dataset.removeVhost; const nameEl = $('removeVhostName'); if (nameEl) nameEl.textContent = hostname; $('removeVhostConfirm').dataset.hostname = hostname; openModal('modal-removeVhost'); }); }); _updateVhostBulkBar(); } function setupVirtualHostEvents() { $('vhostSelectAll')?.addEventListener('change', (e) => { document.querySelectorAll('#connectionsTable .vhost-row-cb').forEach(cb => { cb.checked = e.target.checked; }); _updateVhostBulkBar(); }); $('btnVhostBulkRemove')?.addEventListener('click', () => { const checked = Array.from(document.querySelectorAll('#connectionsTable .vhost-row-cb:checked')); if (!checked.length) return; const hostnames = checked.map(cb => cb.dataset.hostname); if (!confirm('Remove ' + hostnames.length + ' virtual host(s)?')) return; let done = 0; for (const hostname of hostnames) { chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'removeVirtualHost', payload: { hostname } } }, () => { done++; if (done === hostnames.length) { showToast(hostnames.length + ' host(s) removed', 'success'); refresh(); } } ); } }); $('addVhostBtn')?.addEventListener('click', () => openModal('modal-addVhost')); $('addVhostSubmit')?.addEventListener('click', () => { const hostnameEl = $('addVhostHostname'); const hsUrlEl = $('addVhostHsUrl'); let hostname = (hostnameEl?.value || '').trim(); hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim(); const hsUrl = (hsUrlEl?.value || '').trim(); const hostnameValidation = isValidVhostHostname(hostname); if (!hostnameValidation.ok) { showModalError('modal-addVhost', 'addVhostError', hostnameValidation.error); return; } if (!hsUrl || !hsUrl.startsWith('hs://')) { showModalError('modal-addVhost', 'addVhostError', 'Enter a valid hs:// URL'); return; } const btn = $('addVhostSubmit'); if (btn) { btn.disabled = true; btn.textContent = 'Adding…'; } chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl } } }, (response) => { if (btn) { btn.disabled = false; btn.textContent = 'Add Host'; } if (response?.ok) { if (hostnameEl) hostnameEl.value = ''; if (hsUrlEl) hsUrlEl.value = ''; closeModal('modal-addVhost'); showToast('Virtual host added', 'success'); refresh(); } else { showModalError('modal-addVhost', 'addVhostError', response?.error || 'Failed to add'); } } ); }); $('removeVhostConfirm')?.addEventListener('click', () => { const hostname = $('removeVhostConfirm').dataset.hostname; if (!hostname) return; const btn = $('removeVhostConfirm'); btn.disabled = true; btn.textContent = 'Removing…'; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'removeVirtualHost', payload: { hostname } } }, (response) => { btn.disabled = false; btn.textContent = 'Remove'; closeModal('modal-removeVhost'); if (response?.ok) { showToast('Virtual host removed', 'success'); } else { showToast(response?.error || 'Failed to remove', 'error'); } refresh(); } ); }); }