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
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:
@@ -1,6 +1,19 @@
|
||||
// Depends on: core/utils.js ($, escapeHtml, truncate), ui/state-tag.js (stateTag),
|
||||
// ui/toast.js (showToast, copyToClipboard), ui/modal.js (openModal, closeModal, showModalError)
|
||||
|
||||
function _updateSvcBulkBar() {
|
||||
const checked = document.querySelectorAll('#serviceTunnelsTable input[type="checkbox"]:checked');
|
||||
const bar = $('svcBulkBar');
|
||||
const countEl = $('svcBulkCount');
|
||||
if (!bar) return;
|
||||
if (checked.length > 0) {
|
||||
bar.style.display = 'flex';
|
||||
if (countEl) countEl.textContent = checked.length + ' selected';
|
||||
} else {
|
||||
bar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateServiceTunnelsTable(state) {
|
||||
const tbody = $('serviceTunnelsTable');
|
||||
if (!tbody) return;
|
||||
@@ -11,13 +24,14 @@ function updateServiceTunnelsTable(state) {
|
||||
|
||||
if (tunnels.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"><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>`;
|
||||
_updateSvcBulkBar();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,6 +43,7 @@ function updateServiceTunnelsTable(state) {
|
||||
const safeId = id.replace(/"/g, '"');
|
||||
return `
|
||||
<tr>
|
||||
<td style="width:32px;"><input type="checkbox" class="svc-row-cb" data-tunnel-id="${safeId}"></td>
|
||||
<td style="font-weight:600;color:var(--text);">${escapeHtml(label)}</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
@@ -48,7 +63,10 @@ function updateServiceTunnelsTable(state) {
|
||||
</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td>${stateTag(t.state)}</td>
|
||||
<td>
|
||||
${stateTag(t.state)}
|
||||
${t.localPort != null ? `<span class="latency-badge" data-ping-host="127.0.0.1" data-ping-port="${t.localPort}" style="margin-left:4px;font-size:10px;color:var(--text4);">…ms</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
${(t.state === 'error' || t.state === 'closed') ? `
|
||||
@@ -69,6 +87,10 @@ function updateServiceTunnelsTable(state) {
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
tbody.querySelectorAll('.svc-row-cb').forEach(cb => {
|
||||
cb.addEventListener('change', _updateSvcBulkBar);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('[data-copy]').forEach(btn => {
|
||||
btn.addEventListener('click', () => copyToClipboard(btn.dataset.copy, btn));
|
||||
});
|
||||
@@ -123,9 +145,30 @@ function updateServiceTunnelsTable(state) {
|
||||
openModal('modal-removeServiceTunnel');
|
||||
});
|
||||
});
|
||||
|
||||
_updateSvcBulkBar();
|
||||
}
|
||||
|
||||
function setupServiceTunnelEvents() {
|
||||
$('svcSelectAll')?.addEventListener('change', (e) => {
|
||||
document.querySelectorAll('#serviceTunnelsTable .svc-row-cb').forEach(cb => { cb.checked = e.target.checked; });
|
||||
_updateSvcBulkBar();
|
||||
});
|
||||
|
||||
$('btnSvcBulkRemove')?.addEventListener('click', () => {
|
||||
const checked = Array.from(document.querySelectorAll('#serviceTunnelsTable .svc-row-cb:checked'));
|
||||
if (!checked.length) return;
|
||||
const ids = checked.map(cb => cb.dataset.tunnelId);
|
||||
if (!confirm('Remove ' + ids.length + ' service tunnel(s)?')) return;
|
||||
let done = 0;
|
||||
for (const tunnelId of ids) {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'stopServiceTunnel', payload: { tunnelId } } },
|
||||
() => { done++; if (done === ids.length) { showToast(ids.length + ' tunnel(s) removed', 'success'); refresh(); } }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('addServiceTunnelBtn')?.addEventListener('click', () => {
|
||||
$('serviceTunnelEditId').value = '';
|
||||
$('serviceTunnelLabel').value = '';
|
||||
|
||||
Reference in New Issue
Block a user