efactor(extension): modularize dashboard, background, and docs
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.
This commit is contained in:
Raven Scott
2026-02-28 23:02:52 -05:00
parent f5c349a4a3
commit 5c4f0b4aba
35 changed files with 4276 additions and 4240 deletions
@@ -0,0 +1,188 @@
// 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 = `
<tr><td colspan="5">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<div class="empty-state-title">No service tunnels</div>
<div class="empty-state-desc">Click "Add Tunnel" to connect a remote TCP/UDP service via Holesail</div>
</div>
</td></tr>`;
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, '&quot;');
return `
<tr>
<td style="font-weight:600;color:var(--text);">${escapeHtml(label)}</td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<span class="mono" title="${escapeHtml(hsUrl)}" style="font-size:11px;color:var(--text3);">${truncate(hsUrl, 28)}</span>
${hsUrl ? `<button class="btn-icon copy-btn" data-copy="${escapeHtml(hsUrl)}" title="Copy hs:// key">
<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>
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-weight:600;color:var(--cyan);font-family:'JetBrains Mono',monospace;font-size:13px;">${escapeHtml(localAddr)}</span>
${t.localPort != null ? `<button class="btn-icon copy-btn" data-copy="${escapeHtml(localAddr)}" title="Copy local address">
<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>${stateTag(t.state)}</td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
${(t.state === 'error' || t.state === 'closed') ? `
<button class="btn btn-ghost btn-sm" data-reconnect-svc="${safeId}" data-hsurl="${escapeHtml(hsUrl)}" data-label="${escapeHtml(label)}" data-localport="${t.localPort != null ? t.localPort : ''}">
<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-2.12-9.36L23 10"/></svg>
Reconnect
</button>` : ''}
<button class="btn btn-ghost btn-sm" data-edit-service-tunnel="${safeId}" title="Edit tunnel">
<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-remove-service-tunnel="${safeId}">
<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-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 165535'); 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();
}
);
});
}