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:
@@ -4,6 +4,7 @@ function setupLogsEvents() {
|
||||
let logs = [];
|
||||
let autoScroll = true;
|
||||
let logFilter = '';
|
||||
let logLevel = 'all'; // 'all' | 'info' | 'warn' | 'error'
|
||||
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'registerDashboard' },
|
||||
@@ -19,31 +20,34 @@ function setupLogsEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
function getLogClass(msg) {
|
||||
const m = (msg || '').toLowerCase();
|
||||
if (m.includes('error') || m.includes('fail') || m.includes('err:')) return 'is-error';
|
||||
if (m.includes('warn') || m.includes('warning')) return 'is-warn';
|
||||
function getLogClass(level) {
|
||||
if (level === 'error') return 'is-error';
|
||||
if (level === 'warn') return 'is-warn';
|
||||
return '';
|
||||
}
|
||||
|
||||
function updateLogsDisplay() {
|
||||
const container = $('logsContainer');
|
||||
if (!container) return;
|
||||
const filtered = logFilter
|
||||
? logs.filter(e => (e.message || '').toLowerCase().includes(logFilter.toLowerCase()))
|
||||
: logs;
|
||||
let filtered = logs;
|
||||
if (logFilter) {
|
||||
filtered = filtered.filter(e => (e.message || '').toLowerCase().includes(logFilter.toLowerCase()));
|
||||
}
|
||||
if (logLevel !== 'all') {
|
||||
filtered = filtered.filter(e => (e.level || 'info') === logLevel);
|
||||
}
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
||||
<div class="empty-state-title">${logFilter ? 'No matching logs' : 'No logs yet'}</div>
|
||||
<div class="empty-state-desc">${logFilter ? 'Try a different filter' : 'Logs will appear here as the extension runs'}</div>
|
||||
<div class="empty-state-title">${(logFilter || logLevel !== 'all') ? 'No matching logs' : 'No logs yet'}</div>
|
||||
<div class="empty-state-desc">${(logFilter || logLevel !== 'all') ? 'Try a different filter' : 'Logs will appear here as the extension runs'}</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = filtered.map(entry => {
|
||||
const time = new Date(entry.timestamp).toLocaleTimeString();
|
||||
const cls = getLogClass(entry.message);
|
||||
const cls = getLogClass(entry.level || 'info');
|
||||
return `<div class="log-entry">
|
||||
<span class="log-time">${time}</span>
|
||||
<span class="log-msg ${cls}">${escapeHtml(entry.message)}</span>
|
||||
@@ -68,6 +72,33 @@ function setupLogsEvents() {
|
||||
updateLogsDisplay();
|
||||
});
|
||||
|
||||
// Severity filter buttons
|
||||
document.querySelectorAll('.log-level-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.log-level-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
logLevel = btn.dataset.level;
|
||||
updateLogsDisplay();
|
||||
});
|
||||
});
|
||||
|
||||
// Download logs as .txt
|
||||
$('btnDownloadLogs')?.addEventListener('click', () => {
|
||||
if (!logs.length) { showToast('No logs to download', 'default'); return; }
|
||||
const lines = logs.map(e => {
|
||||
const time = new Date(e.timestamp).toISOString();
|
||||
return `[${time}] ${e.message}`;
|
||||
}).join('\n');
|
||||
const blob = new Blob([lines], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'holesail-logs-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.txt';
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||||
showToast('Logs downloaded', 'success');
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
chrome.runtime.sendMessage({ target: 'holesail-native', action: 'unregisterDashboard' }, () => {});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user