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.
189 lines
9.2 KiB
JavaScript
189 lines
9.2 KiB
JavaScript
// 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, '"');
|
||
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 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();
|
||
}
|
||
);
|
||
});
|
||
}
|