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
+32 -6
View File
@@ -19,15 +19,38 @@ function debugLog(...args) {
if (process.stderr) process.stderr.write(msg + '\n');
}
const virtualHosts = new Map(); // hostname -> { hsUrl, holesail, localHost, localPort, state, createdAt }
const virtualHosts = new Map(); // hostname -> { hsUrl, holesail, localHost, localPort, state, createdAt, reconnectTimer, reconnectDelay }
let _saveState = null;
let _emit = null;
let _getReadyTimeoutMs = null;
let _getAutoReconnect = null;
function init(saveStateFn, emitFn, getReadyTimeoutMsFn) {
const RECONNECT_BASE_MS = 5000;
const RECONNECT_MAX_MS = 120000;
function init(saveStateFn, emitFn, getReadyTimeoutMsFn, getAutoReconnectFn) {
_saveState = saveStateFn;
_emit = emitFn;
_getReadyTimeoutMs = getReadyTimeoutMsFn;
_getAutoReconnect = getAutoReconnectFn;
}
function _scheduleVhostReconnect(hostname) {
const v = virtualHosts.get(hostname);
if (!v) return;
if (v.reconnectTimer) { clearTimeout(v.reconnectTimer); v.reconnectTimer = null; }
const autoReconnect = _getAutoReconnect ? _getAutoReconnect() : false;
if (!autoReconnect) return;
const delay = v.reconnectDelay || RECONNECT_BASE_MS;
v.reconnectDelay = Math.min(delay * 2, RECONNECT_MAX_MS);
if (process.stderr) process.stderr.write('[holesail-manager] vhost ' + hostname + ' reconnecting in ' + Math.round(delay / 1000) + 's\n');
v.reconnectTimer = setTimeout(async () => {
v.reconnectTimer = null;
const cur = virtualHosts.get(hostname);
if (!cur || cur.state === 'ready') return;
if (process.stderr) process.stderr.write('[holesail-manager] vhost ' + hostname + ' auto-reconnect attempt\n');
await setVirtualHost({ hostname, hsUrl: cur.hsUrl });
}, delay);
}
function readyWithTimeout(hs, label) {
@@ -61,17 +84,18 @@ async function setVirtualHost(payload) {
hs.on('error', (err) => {
if (process.stderr) process.stderr.write('[holesail-manager] tunnel error ' + hostname + ': ' + (err && err.message) + '\n');
const v = virtualHosts.get(hostname);
if (v && v.holesail === hs) v.state = 'error';
if (v && v.holesail === hs) { v.state = 'error'; _scheduleVhostReconnect(hostname); }
if (_emit) _emit('tunnelError', { hostname, error: err && err.message });
});
hs.on('close', () => {
const v = virtualHosts.get(hostname);
if (v && v.holesail === hs) v.state = 'closed';
if (v && v.holesail === hs) { v.state = 'closed'; _scheduleVhostReconnect(hostname); }
if (_emit) _emit('tunnelClosed', { hostname });
});
}
await readyWithTimeout(hs, 'vhost:' + hostname);
virtualHosts.set(hostname, { hsUrl, holesail: hs, localHost: TUNNEL_HOST, localPort, state: 'ready', createdAt: (existing && existing.createdAt) || Date.now() });
const prevReconnectDelay = existing ? existing.reconnectDelay : undefined;
virtualHosts.set(hostname, { hsUrl, holesail: hs, localHost: TUNNEL_HOST, localPort, state: 'ready', createdAt: (existing && existing.createdAt) || Date.now(), reconnectTimer: null, reconnectDelay: RECONNECT_BASE_MS });
if (_emit) _emit('tunnelReady', { hostname, hsUrl, localHost: TUNNEL_HOST, localPort });
if (_saveState) _saveState();
debugLog('setVirtualHost: ok hostname=', hostname, 'localPort=', localPort);
@@ -91,6 +115,7 @@ async function removeVirtualHost(payload) {
debugLog('removeVirtualHost: hostname=', hostname);
const entry = virtualHosts.get(hostname);
if (!entry) return { ok: false, error: 'Virtual host not found' };
if (entry.reconnectTimer) { clearTimeout(entry.reconnectTimer); entry.reconnectTimer = null; }
if (entry.localPort) releaseTunnelPort(entry.localPort);
if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} }
virtualHosts.delete(hostname);
@@ -129,9 +154,10 @@ function getVirtualHostMap() {
async function cleanupVirtualHosts() {
for (const [, v] of virtualHosts) {
if (v.reconnectTimer) { clearTimeout(v.reconnectTimer); v.reconnectTimer = null; }
if (v.holesail) { try { await v.holesail.close(); } catch (_) {} }
}
virtualHosts.clear();
}
module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts };
module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS };