feat: implement 13 features — auto-reconnect, notifications, bulk actions, latency, traffic, theme, export/import, scheduled backups, peer lookup, SSH auto-reconnect, log filters, keyboard shortcut, and readyTimeout default
CI / Build & Test (push) Successful in 2m44s

- Change readyTimeoutMs default from 0 to 30000 in both state files
- Register Alt+Shift+H keyboard shortcut via manifest _execute_action command
- Add severity filter buttons (All/Info/Warn/Error) and Download .txt to Logs page; background logs.js now tags entries with proper level field
- Fire browser notifications on tunnelError events (notifyOnTunnelError setting)
- Add exponential-backoff auto-reconnect for virtual hosts and service tunnels (tunnelAutoReconnect setting, 5s–120s backoff)
- Add backupIntervalHours setting and scheduled auto-backup timer in message-router.js
- Add TCP connect latency badges to Virtual Hosts and Service Tunnels tables via new pingTunnel native message
- Add bytesIn/bytesOut/requests counters to https-proxy.js; expose in Overview status bar via getState
- Add Export/Import connection configs (JSON, no CA key) in Settings
- Add autoReconnect flag and exponential-backoff reconnect to SSH connection cards
- Add light theme CSS variables and theme toggle in Settings (persisted to localStorage)
- Add checkbox column and bulk Stop/Remove actions to Virtual Hosts, Service Tunnels, and Servers tables
- Add Peer Lookup UI card on Overview page using existing lookup message handler
This commit is contained in:
Raven Scott
2026-02-28 23:51:14 -05:00
parent 31f30b2974
commit 849897f324
24 changed files with 726 additions and 47 deletions
+41 -1
View File
@@ -1,6 +1,19 @@
// 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;
@@ -8,13 +21,14 @@ function updateSwarmsTable(state) {
if (servers.length === 0) {
tbody.innerHTML = `
<tr><td colspan="5">
<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;
}
@@ -25,6 +39,7 @@ function updateSwarmsTable(state) {
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>
@@ -57,6 +72,10 @@ function updateSwarmsTable(state) {
</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));
});
@@ -94,9 +113,30 @@ function updateSwarmsTable(state) {
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 = '';