CI / Build & Test (push) Successful in 2m52s
CRITICAL:
- certificate-authority.js: declare `regenerated` variable in installRootCA Windows path to prevent ReferenceError crash
HIGH:
- virtual-hosts.js/service-tunnels.js: call hs.removeAllListeners() in catch blocks to prevent stale listeners on failed Holesail instances
- https-proxy.js: destroy rawSocket in TLS error handler to prevent file descriptor exhaustion
- message-router.js (native): move setEventEmitter() to module-level init instead of re-calling on every message
- ssh-manager.js: add error handler to WS server to prevent unhandled error crashes
- init.js: store setInterval ID and clear on beforeunload to prevent interval accumulation
- logs.js: store and remove chrome.runtime.onMessage listener on beforeunload; add duplicate-call guard
- events.js: move pending++ before async sendMessage call to fix SSH/RDP-only import showing "Nothing to import"
- ssh.js: store resizeTimer on activeSshSession and clear in disconnectSsh; fix auto-reconnect race with _sshConnecting lock
- native-messaging.js: track retry timer IDs in array and cancel all on disconnect
- rdp.js: reuse single offscreen canvas per session instead of allocating per bitmap
MEDIUM:
- virtual-hosts.js/service-tunnels.js: clear existing.reconnectTimer before replacing tunnel entries
- message-router.js (native): destroy pingTunnel socket on error path; clear 15s fallback timer via finally()
- startup.js: wrap setImmediate body in try/finally to always resolve proxiesReadyPromise
- https-proxy.js: fix pre-connect upstream error handler to avoid writing raw HTTP into piped TLS stream; move HOP_BY_HOP to module-level constant
- connect-proxy.js: destroy upstreamSocket on clientSocket close; track and destroy active sockets in stop()
- ssh-manager.js: call cancelPasswordWatch on WS disconnect during password collection
- rdp-manager.js: remove dead remotePort variable; add error handlers to both WS servers
- backup-manager.js: log cleanupStaging errors and non-zero exit codes
- rdp.js: null out ws callbacks before closing in disconnectRdp; disconnect MutationObserver on beforeunload
- proxy-ca.js: prune stale entries from validationResults Map in renderValidatorTable
- refresh.js: deduplicate in-flight pings per port via Set
- messaging.js: read chrome.runtime.lastError in sendToNative callback
- servers.js (dashboard): add null check for $('serverEditId') element
LOW:
- port-allocator.js: add dedup check before pushing to tunnelPortFreeList
- servers.js (native): add error listener to server-mode Holesail instances
- virtual-hosts.js: remove dead prevReconnectDelay variable
- tab-lifecycle.js: change swarmRefCount fallback from || 1 to || 0 to prevent premature swarm destroy
- ssh.js/rdp.js: disconnect MutationObservers on beforeunload
221 lines
9.6 KiB
JavaScript
221 lines
9.6 KiB
JavaScript
// Depends on: core/utils.js ($, escapeHtml, truncate), ui/toast.js (showToast, copyToClipboard),
|
||
// ui/modal.js (openModal, closeModal, showModalError)
|
||
|
||
function _updateServerBulkBar() {
|
||
const checked = document.querySelectorAll('#swarmsTable input[type="checkbox"]:checked');
|
||
const bar = $('serverBulkBar');
|
||
const countEl = $('serverBulkCount');
|
||
if (!bar) return;
|
||
if (checked.length > 0) {
|
||
bar.style.display = 'flex';
|
||
if (countEl) countEl.textContent = checked.length + ' selected';
|
||
} else {
|
||
bar.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function updateSwarmsTable(state) {
|
||
const tbody = $('swarmsTable');
|
||
if (!tbody) return;
|
||
const servers = state.servers || [];
|
||
|
||
if (servers.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr><td colspan="6">
|
||
<div class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="6" height="6" rx="1"/><rect x="16" y="3" width="6" height="6" rx="1"/><rect x="9" y="15" width="6" height="6" rx="1"/><line x1="5" y1="9" x2="12" y2="15"/><line x1="19" y1="9" x2="12" y2="15"/></svg>
|
||
<div class="empty-state-title">No server tunnels</div>
|
||
<div class="empty-state-desc">Click "New Server" to expose a local port as an hs:// tunnel</div>
|
||
</div>
|
||
</td></tr>`;
|
||
_updateServerBulkBar();
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = servers.map(s => {
|
||
const id = s.id || s.serverId || '';
|
||
const url = s.url || s.hsUrl || '';
|
||
const safeId = id.replace(/"/g, '"');
|
||
const label = s.label || '';
|
||
return `
|
||
<tr>
|
||
<td style="width:32px;"><input type="checkbox" class="server-row-cb" data-server-id="${safeId}"></td>
|
||
<td>
|
||
${label ? `<div style="font-weight:600;color:var(--text);margin-bottom:2px;">${escapeHtml(label)}</div>` : ''}
|
||
<div class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(truncate(id, 20))}</div>
|
||
</td>
|
||
<td style="font-weight:600;color:var(--text);">${s.port ?? '—'}</td>
|
||
<td>
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<span class="mono" title="${escapeHtml(url)}" style="font-size:11px;color:var(--cyan);">${truncate(url, 30)}</span>
|
||
${url ? `<button class="btn-icon copy-btn" data-copy="${escapeHtml(url)}" title="Copy hs:// URL">
|
||
<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>
|
||
<span class="badge badge-neutral" style="margin-right:4px;">${s.udp ? 'UDP' : 'TCP'}</span>${s.secure ? `<span class="badge badge-green">secure</span>` : `<span class="badge badge-neutral">plain</span>`}
|
||
</td>
|
||
<td>
|
||
<div style="display:flex;align-items:center;gap:6px;">
|
||
<button class="btn btn-ghost btn-sm" data-edit-server="${safeId}" title="Edit server">
|
||
<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-stop-server="${safeId}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
||
Stop
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
tbody.querySelectorAll('.server-row-cb').forEach(cb => {
|
||
cb.addEventListener('change', _updateServerBulkBar);
|
||
});
|
||
|
||
tbody.querySelectorAll('[data-copy]').forEach(btn => {
|
||
btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn));
|
||
});
|
||
|
||
tbody.querySelectorAll('[data-edit-server]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const serverId = btn.dataset.editServer;
|
||
const server = (state.servers || []).find(s => (s.id || s.serverId) === serverId);
|
||
if (!server) return;
|
||
const serverEditIdEl = $('serverEditId');
|
||
if (serverEditIdEl) serverEditIdEl.value = serverId;
|
||
const labelEl = $('startServerLabel');
|
||
const portEl = $('startServerPort');
|
||
const hostEl = $('startServerHost');
|
||
const secureEl = $('startServerSecure');
|
||
if (labelEl) labelEl.value = server.label || '';
|
||
if (portEl) portEl.value = server.port ?? 3000;
|
||
if (hostEl) hostEl.value = server.host ?? '127.0.0.1';
|
||
if (secureEl) secureEl.checked = server.secure !== false;
|
||
const udpEl = document.querySelector('input[name="startServerProtocol"][value="' + (server.udp ? 'udp' : 'tcp') + '"]');
|
||
if (udpEl) udpEl.checked = true;
|
||
const titleEl = $('modal-startServer-title');
|
||
if (titleEl) titleEl.textContent = 'Edit Server Tunnel';
|
||
const submitEl = $('startServerSubmit');
|
||
if (submitEl) submitEl.textContent = 'Save Changes';
|
||
openModal('modal-startServer');
|
||
});
|
||
});
|
||
|
||
tbody.querySelectorAll('[data-stop-server]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const serverId = btn.dataset.stopServer;
|
||
const nameEl = $('stopServerName');
|
||
if (nameEl) nameEl.textContent = serverId;
|
||
$('stopServerConfirm').dataset.serverId = serverId;
|
||
openModal('modal-stopServer');
|
||
});
|
||
});
|
||
|
||
_updateServerBulkBar();
|
||
}
|
||
|
||
function setupServerEvents() {
|
||
$('serverSelectAll')?.addEventListener('change', (e) => {
|
||
document.querySelectorAll('#swarmsTable .server-row-cb').forEach(cb => { cb.checked = e.target.checked; });
|
||
_updateServerBulkBar();
|
||
});
|
||
|
||
$('btnServerBulkStop')?.addEventListener('click', () => {
|
||
const checked = Array.from(document.querySelectorAll('#swarmsTable .server-row-cb:checked'));
|
||
if (!checked.length) return;
|
||
const ids = checked.map(cb => cb.dataset.serverId);
|
||
if (!confirm('Stop ' + ids.length + ' server(s)?')) return;
|
||
let done = 0;
|
||
for (const serverId of ids) {
|
||
chrome.runtime.sendMessage(
|
||
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId } } },
|
||
() => { done++; if (done === ids.length) { showToast(ids.length + ' server(s) stopped', 'success'); refresh(); } }
|
||
);
|
||
}
|
||
});
|
||
|
||
$('startServerBtn')?.addEventListener('click', () => {
|
||
const editIdEl = $('serverEditId');
|
||
if (editIdEl) editIdEl.value = '';
|
||
const labelEl = $('startServerLabel');
|
||
if (labelEl) labelEl.value = '';
|
||
const portEl = $('startServerPort');
|
||
if (portEl) portEl.value = '3000';
|
||
const hostEl = $('startServerHost');
|
||
if (hostEl) hostEl.value = '127.0.0.1';
|
||
const secureEl = $('startServerSecure');
|
||
if (secureEl) secureEl.checked = true;
|
||
const tcpEl = $('startServerProtocolTcp');
|
||
if (tcpEl) tcpEl.checked = true;
|
||
const titleEl = $('modal-startServer-title');
|
||
if (titleEl) titleEl.textContent = 'Start Server Tunnel';
|
||
const submitEl = $('startServerSubmit');
|
||
if (submitEl) submitEl.textContent = 'Start Server';
|
||
openModal('modal-startServer');
|
||
});
|
||
|
||
$('startServerSubmit')?.addEventListener('click', () => {
|
||
const portEl = $('startServerPort');
|
||
const hostEl = $('startServerHost');
|
||
const secureEl = $('startServerSecure');
|
||
const labelEl = $('startServerLabel');
|
||
const editIdEl = $('serverEditId');
|
||
const port = parseInt(portEl?.value, 10) || 3000;
|
||
const host = (hostEl?.value || '127.0.0.1').trim();
|
||
const secure = secureEl?.checked !== false;
|
||
const udp = document.querySelector('input[name="startServerProtocol"]:checked')?.value === 'udp';
|
||
const label = (labelEl?.value || '').trim();
|
||
const editId = (editIdEl?.value || '').trim();
|
||
if (!port || port < 1 || port > 65535) { showModalError('modal-startServer', 'startServerError', 'Port must be 1–65535'); return; }
|
||
const btn = $('startServerSubmit');
|
||
if (btn) { btn.disabled = true; btn.textContent = editId ? 'Saving…' : 'Starting…'; }
|
||
|
||
const doStart = () => {
|
||
chrome.runtime.sendMessage(
|
||
{ target: 'holesail-native', action: 'send', payload: { type: 'startServer', payload: { port, host, secure, udp, label } } },
|
||
(response) => {
|
||
if (btn) { btn.disabled = false; btn.textContent = editId ? 'Save Changes' : 'Start Server'; }
|
||
if (response?.ok) {
|
||
if (editIdEl) editIdEl.value = '';
|
||
closeModal('modal-startServer');
|
||
showToast(editId ? 'Server updated' : 'Server started', 'success');
|
||
refresh();
|
||
} else {
|
||
showModalError('modal-startServer', 'startServerError', response?.error || 'Failed to start');
|
||
}
|
||
}
|
||
);
|
||
};
|
||
|
||
if (editId) {
|
||
chrome.runtime.sendMessage(
|
||
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId: editId } } },
|
||
() => doStart()
|
||
);
|
||
} else {
|
||
doStart();
|
||
}
|
||
});
|
||
|
||
$('stopServerConfirm')?.addEventListener('click', () => {
|
||
const serverId = $('stopServerConfirm').dataset.serverId;
|
||
if (!serverId) return;
|
||
const btn = $('stopServerConfirm');
|
||
btn.disabled = true; btn.textContent = 'Stopping…';
|
||
chrome.runtime.sendMessage(
|
||
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServer', payload: { serverId } } },
|
||
(response) => {
|
||
btn.disabled = false; btn.textContent = 'Stop Server';
|
||
closeModal('modal-stopServer');
|
||
if (response?.ok) showToast('Server stopped', 'success');
|
||
else showToast(response?.error || 'Failed to stop', 'error');
|
||
refresh();
|
||
}
|
||
);
|
||
});
|
||
}
|