fix(dashboard): smooth latency badge updates, add ping settings
CI / Build & Test (push) Successful in 2m52s
CI / Build & Test (push) Successful in 2m52s
- Snapshot and restore latency badge values across innerHTML re-renders so badges never flash back to "…ms" on the 2-second state refresh cycle - Add CSS transition (color 0.4s ease) and in-flight opacity dim (.pinging) to .latency-badge for smooth color changes - Decouple latency pinging from the state refresh cycle — pings now run on their own independent interval instead of being triggered by refresh() - Add two new settings under a "Latency Ping" section in Settings: - Enable Latency Ping toggle (latencyPingEnabled, default: true) - Ping Interval in seconds (latencyPingIntervalMs, default: 5s) - Restart ping interval immediately on settings save so changes take effect without a page reload
This commit is contained in:
@@ -35,8 +35,29 @@ async function init() {
|
||||
setupRdpEvents();
|
||||
setupBackupEvents();
|
||||
await refresh();
|
||||
|
||||
// Main state-refresh interval (fixed 2 s)
|
||||
const _refreshInterval = setInterval(refresh, 2000);
|
||||
window.addEventListener('beforeunload', () => clearInterval(_refreshInterval), { once: true });
|
||||
|
||||
// Latency-ping interval — driven by settings.latencyPingIntervalMs.
|
||||
// Re-created whenever settings change so the interval stays in sync.
|
||||
let _pingInterval = null;
|
||||
function _restartPingInterval() {
|
||||
if (_pingInterval) clearInterval(_pingInterval);
|
||||
_pingInterval = null;
|
||||
if (settings.latencyPingEnabled === false) return;
|
||||
const ms = (settings.latencyPingIntervalMs > 0 ? settings.latencyPingIntervalMs : 5000);
|
||||
_pingInterval = setInterval(_pingLatencyBadges, ms);
|
||||
}
|
||||
_restartPingInterval();
|
||||
|
||||
// Expose restart so settings.js can call it after saving
|
||||
window._restartPingInterval = _restartPingInterval;
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
clearInterval(_refreshInterval);
|
||||
if (_pingInterval) clearInterval(_pingInterval);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@@ -11,7 +11,9 @@ const SETTINGS_DEFAULTS = {
|
||||
disableOnFileUrls: false,
|
||||
backupRetention: 5,
|
||||
backupIntervalHours: 0,
|
||||
tunnelAutoReconnect: false
|
||||
tunnelAutoReconnect: false,
|
||||
latencyPingEnabled: true,
|
||||
latencyPingIntervalMs: 5000
|
||||
};
|
||||
|
||||
let currentState = null;
|
||||
|
||||
@@ -335,6 +335,17 @@
|
||||
.badge-red { background: var(--red-dim); color: var(--red); }
|
||||
.badge-neutral { background: var(--elevated); color: var(--text3); }
|
||||
|
||||
/* ── Latency badges ─────────────────────────────── */
|
||||
.latency-badge {
|
||||
transition: color 0.4s ease, opacity 0.3s ease;
|
||||
display: inline-block;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
.latency-badge.pinging {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* ── Overview list cards ────────────────────────── */
|
||||
.ov-list-card {
|
||||
display: flex;
|
||||
|
||||
@@ -604,6 +604,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Latency Ping</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Enable Latency Ping</div>
|
||||
<div class="setting-desc">Periodically ping tunnel ports to display live latency badges on virtual hosts and service tunnels</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="toggle" id="toggleLatencyPing" data-setting="latencyPingEnabled"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Ping Interval (seconds)</div>
|
||||
<div class="setting-desc">How often to ping tunnel ports for latency. Minimum 1 s, default 5 s.</div>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<input type="number" id="latencyPingIntervalMs" min="1" max="60" step="0.5" class="input" style="width:90px;" placeholder="5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-heading">Proxy & API</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
|
||||
@@ -30,6 +30,12 @@ function updateServiceTunnelsTable(state) {
|
||||
const countEl = $('serviceTunnelCount');
|
||||
if (countEl) countEl.textContent = tunnels.length;
|
||||
|
||||
// Snapshot latency badge values before re-render so they survive the innerHTML swap
|
||||
const _latencySnapshot = {};
|
||||
tbody.querySelectorAll('.latency-badge[data-ping-port]').forEach(b => {
|
||||
_latencySnapshot[b.dataset.pingHost + ':' + b.dataset.pingPort] = { text: b.textContent, color: b.style.color };
|
||||
});
|
||||
|
||||
if (tunnels.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="6">
|
||||
@@ -95,6 +101,15 @@ function updateServiceTunnelsTable(state) {
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Restore latency badge values immediately so there's no flash back to "…ms"
|
||||
tbody.querySelectorAll('.latency-badge[data-ping-port]').forEach(b => {
|
||||
const snap = _latencySnapshot[b.dataset.pingHost + ':' + b.dataset.pingPort];
|
||||
if (snap && snap.text && snap.text !== '…ms') {
|
||||
b.textContent = snap.text;
|
||||
b.style.color = snap.color;
|
||||
}
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.svc-row-cb').forEach(cb => {
|
||||
cb.addEventListener('change', _updateSvcBulkBar);
|
||||
});
|
||||
|
||||
@@ -15,14 +15,17 @@ function updateSettingsUI() {
|
||||
$('toggleDebug')?.classList.toggle('active', settings.debug === true);
|
||||
$('toggleDisableFileUrls')?.classList.toggle('active', settings.disableOnFileUrls === true);
|
||||
$('toggleTheme')?.classList.toggle('active', document.documentElement.getAttribute('data-theme') === 'light');
|
||||
$('toggleLatencyPing')?.classList.toggle('active', settings.latencyPingEnabled !== false);
|
||||
const proxyPortEl = $('proxyPort');
|
||||
const readyTimeoutMsEl = $('readyTimeoutMs');
|
||||
const backupRetentionEl = $('backupRetention');
|
||||
const backupIntervalEl = $('backupIntervalHours');
|
||||
const latencyPingIntervalEl = $('latencyPingIntervalMs');
|
||||
if (proxyPortEl) proxyPortEl.value = settings.proxyPort ?? SETTINGS_DEFAULTS.proxyPort;
|
||||
if (readyTimeoutMsEl) readyTimeoutMsEl.value = settings.readyTimeoutMs ?? SETTINGS_DEFAULTS.readyTimeoutMs;
|
||||
if (backupRetentionEl) backupRetentionEl.value = settings.backupRetention ?? SETTINGS_DEFAULTS.backupRetention;
|
||||
if (backupIntervalEl) backupIntervalEl.value = settings.backupIntervalHours ?? SETTINGS_DEFAULTS.backupIntervalHours;
|
||||
if (latencyPingIntervalEl) latencyPingIntervalEl.value = (settings.latencyPingIntervalMs ?? SETTINGS_DEFAULTS.latencyPingIntervalMs) / 1000;
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
@@ -31,16 +34,23 @@ function saveSettings() {
|
||||
settings.tunnelAutoReconnect = $('toggleTunnelAutoReconnect')?.classList.contains('active') ?? SETTINGS_DEFAULTS.tunnelAutoReconnect;
|
||||
settings.debug = $('toggleDebug')?.classList.contains('active') ?? SETTINGS_DEFAULTS.debug;
|
||||
settings.disableOnFileUrls = $('toggleDisableFileUrls')?.classList.contains('active') ?? SETTINGS_DEFAULTS.disableOnFileUrls;
|
||||
settings.latencyPingEnabled = $('toggleLatencyPing')?.classList.contains('active') ?? SETTINGS_DEFAULTS.latencyPingEnabled;
|
||||
settings.proxyPort = parseInt($('proxyPort')?.value, 10) || SETTINGS_DEFAULTS.proxyPort;
|
||||
settings.readyTimeoutMs = parseInt($('readyTimeoutMs')?.value, 10) || SETTINGS_DEFAULTS.readyTimeoutMs;
|
||||
settings.backupRetention = Math.max(1, parseInt($('backupRetention')?.value, 10) || SETTINGS_DEFAULTS.backupRetention);
|
||||
settings.backupIntervalHours = Math.max(0, parseInt($('backupIntervalHours')?.value, 10) || 0);
|
||||
const pingSecs = parseFloat($('latencyPingIntervalMs')?.value);
|
||||
settings.latencyPingIntervalMs = isNaN(pingSecs) || pingSecs <= 0
|
||||
? SETTINGS_DEFAULTS.latencyPingIntervalMs
|
||||
: Math.round(pingSecs * 1000);
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'updateSettings', payload: { ...settings } } },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError) { showToast('Settings save failed: ' + chrome.runtime.lastError.message, 'error'); return; }
|
||||
if (response && response.ok) {
|
||||
if (response.settings) settings = { ...SETTINGS_DEFAULTS, ...response.settings };
|
||||
// Restart the ping interval so the new interval / enabled state takes effect immediately
|
||||
if (typeof window._restartPingInterval === 'function') window._restartPingInterval();
|
||||
if (response.requiresRestart) {
|
||||
showToast('Settings saved — restart the native host for proxy port changes to take effect', 'warning');
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,12 @@ function updateConnectionsTable(state) {
|
||||
if (!tbody) return;
|
||||
const virtualHosts = state.virtualHosts || [];
|
||||
|
||||
// Snapshot latency badge values before re-render so they survive the innerHTML swap
|
||||
const _latencySnapshot = {};
|
||||
tbody.querySelectorAll('.latency-badge[data-ping-port]').forEach(b => {
|
||||
_latencySnapshot[b.dataset.pingHost + ':' + b.dataset.pingPort] = { text: b.textContent, color: b.style.color };
|
||||
});
|
||||
|
||||
if (virtualHosts.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="6">
|
||||
@@ -85,6 +91,15 @@ function updateConnectionsTable(state) {
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Restore latency badge values immediately so there's no flash back to "…ms"
|
||||
tbody.querySelectorAll('.latency-badge[data-ping-port]').forEach(b => {
|
||||
const snap = _latencySnapshot[b.dataset.pingHost + ':' + b.dataset.pingPort];
|
||||
if (snap && snap.text && snap.text !== '…ms') {
|
||||
b.textContent = snap.text;
|
||||
b.style.color = snap.color;
|
||||
}
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.vhost-row-cb').forEach(cb => {
|
||||
cb.addEventListener('change', _updateVhostBulkBar);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,15 @@
|
||||
|
||||
const _pingInFlight = new Set();
|
||||
|
||||
/**
|
||||
* Ping all latency badges visible in the DOM.
|
||||
* Updates badge text/color in-place with a CSS transition so the table never
|
||||
* reflows. Skips badges that already have an in-flight request for the same
|
||||
* host:port key. Respects the `latencyPingEnabled` and `latencyPingIntervalMs`
|
||||
* settings — callers should check `latencyPingEnabled` before calling this.
|
||||
*/
|
||||
function _pingLatencyBadges() {
|
||||
if (settings.latencyPingEnabled === false) return;
|
||||
const badges = document.querySelectorAll('.latency-badge[data-ping-port]');
|
||||
badges.forEach(badge => {
|
||||
const port = parseInt(badge.dataset.pingPort, 10);
|
||||
@@ -26,20 +34,25 @@ function _pingLatencyBadges() {
|
||||
const key = host + ':' + port;
|
||||
if (_pingInFlight.has(key)) return;
|
||||
_pingInFlight.add(key);
|
||||
// Dim badge while request is in-flight — avoids a hard jump on update
|
||||
badge.classList.add('pinging');
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'pingTunnel', payload: { host, port } } },
|
||||
(response) => {
|
||||
_pingInFlight.delete(key);
|
||||
void chrome.runtime.lastError;
|
||||
if (!badge.isConnected) return;
|
||||
badge.classList.remove('pinging');
|
||||
if (response && response.ok) {
|
||||
const ms = response.latencyMs;
|
||||
const color = ms < 50 ? 'var(--green)' : ms < 200 ? 'var(--amber)' : 'var(--red)';
|
||||
// Only update if the value actually changed to avoid unnecessary repaints
|
||||
const next = ms + 'ms';
|
||||
if (badge.textContent !== next) badge.textContent = next;
|
||||
badge.style.color = color;
|
||||
badge.textContent = ms + 'ms';
|
||||
} else {
|
||||
if (badge.textContent !== '—') badge.textContent = '—';
|
||||
badge.style.color = 'var(--text4)';
|
||||
badge.textContent = '—';
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -93,6 +106,4 @@ async function refresh() {
|
||||
const sshCountEl = $('sshCount');
|
||||
if (sshCountEl) sshCountEl.textContent = sshConnections.length;
|
||||
refreshBackups();
|
||||
// Ping latency badges after a short delay so the DOM has been updated
|
||||
setTimeout(_pingLatencyBadges, 200);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user