CI / Build & Test (push) Failing after 28s
Split monolithic dashboard.js (2753 lines) into 18 focused modules
under dashboard/{core,data,ui,pages}/ with refresh.js and events.js
as orchestrators. Extracted ~900-line inline <style> into dashboard.css
and moved dashboard.html to dashboard/dashboard.html.
Split background.js (609 lines) into background/{logs,state,proxy,
native-messaging,tab-lifecycle,message-router}.js with a thin entry
point using importScripts().
Deleted dead files: wrong-domain.js (duplicate of inline script).
Updated manifest.json web_accessible_resources for new paths.
Updated docs/ARCHITECTURE.md to reflect the new file structure.
No functionality changed. No build step introduced.
148 lines
6.9 KiB
JavaScript
148 lines
6.9 KiB
JavaScript
// 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 updateConnectionsTable(state) {
|
|
const tbody = $('connectionsTable');
|
|
if (!tbody) return;
|
|
const virtualHosts = state.virtualHosts || [];
|
|
|
|
if (virtualHosts.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr><td colspan="5">
|
|
<div class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="2"/><circle cx="4" cy="6" r="2"/><circle cx="20" cy="6" r="2"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="18" r="2"/><line x1="6" y1="6" x2="10" y2="11"/><line x1="18" y1="6" x2="14" y2="11"/><line x1="6" y1="18" x2="10" y2="13"/><line x1="18" y1="18" x2="14" y2="13"/></svg>
|
|
<div class="empty-state-title">No virtual hosts</div>
|
|
<div class="empty-state-desc">Click "Add Host" to assign a hostname to an hs:// tunnel</div>
|
|
</div>
|
|
</td></tr>`;
|
|
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 `
|
|
<tr>
|
|
<td><span class="mono-chip">${escapeHtml(hostname)}</span></td>
|
|
<td>
|
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
<span class="mono" title="${escapeHtml(hsUrl)}" style="color:var(--text3);font-size:11px;">${truncate(hsUrl, 28)}</span>
|
|
<button class="btn-icon copy-btn" data-copy="${escapeHtml(hsUrl)}" title="Copy hs:// URL" style="flex-shrink:0;">
|
|
<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 class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(backend)}</td>
|
|
<td>${stateTag(v.state)}</td>
|
|
<td>
|
|
<div style="display:flex;align-items:center;gap:6px;">
|
|
<a href="${openUrl}" target="_blank" rel="noopener" class="btn btn-secondary btn-sm">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
|
Open
|
|
</a>
|
|
${needsReconnect ? `<button class="btn btn-secondary btn-sm" data-reconnect-vhost="${safeHostname}" data-hs-url="${escapeHtml(hsUrl)}" title="Reconnect tunnel">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg>
|
|
Reconnect
|
|
</button>` : ''}
|
|
<button class="btn btn-danger btn-sm" data-remove-vhost="${safeHostname}">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
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');
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupVirtualHostEvents() {
|
|
$('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();
|
|
}
|
|
);
|
|
});
|
|
}
|