From f4517ae9533b302da7f498dda82e7e14e69c42bc Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 23 Feb 2026 01:13:04 +0700 Subject: [PATCH] Redesign summary box and add host pinning Summary now shows current min/avg/max and history-window min/max. Each host row has a pin icon that pins it to the top. Pinned hosts sort alphabetically, unpinned sort by latency. datavi.be is pinned by default. --- src/main.js | 91 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/src/main.js b/src/main.js index 926e89a..ea9632f 100644 --- a/src/main.js +++ b/src/main.js @@ -148,12 +148,13 @@ async function detectGateway() { // --- App State --------------------------------------------------------------- class HostState { - constructor(host) { + constructor(host, pinned = false) { this.name = host.name; this.url = host.url; this.history = []; // { timestamp, latency, paused } this.lastLatency = null; this.status = "pending"; // 'online' | 'offline' | 'error' | 'pending' + this.pinned = pinned; } pushSample(timestamp, result) { @@ -190,7 +191,9 @@ class HostState { class AppState { constructor(localHosts) { - this.wan = WAN_HOSTS.map((h) => new HostState(h)); + this.wan = WAN_HOSTS.map( + (h) => new HostState(h, h.name === "datavi.be"), + ); this.local = localHosts.map((h) => new HostState(h)); this.paused = false; this.tickCount = 0; @@ -218,6 +221,22 @@ class AppState { }; } + /** Min/max across all WAN host history (the full 300s window) */ + wanHistoryStats() { + let min = Infinity; + let max = -Infinity; + for (const host of this.wan) { + for (const p of host.history) { + if (p.latency !== null) { + if (p.latency < min) min = p.latency; + if (p.latency > max) max = p.latency; + } + } + } + if (min === Infinity) return { min: null, max: null }; + return { min, max }; + } + /** Overall health: true = healthy (more than half WAN reachable) */ isHealthy() { const reachable = this.wan.filter((h) => h.lastLatency !== null).length; @@ -412,9 +431,17 @@ class SparklineRenderer { // --- UI Renderer ------------------------------------------------------------- function hostRowHTML(host, index) { + const pinColor = host.pinned + ? "text-blue-400" + : "text-gray-600 hover:text-gray-400"; return `
+
@@ -468,11 +495,11 @@ function buildUI(state) {
Reachable: --/-- | - Min: --ms + Now: + --/--/--ms | - Max: --ms - | - Avg: --ms + ${CONFIG.historyDuration}s: + --/--ms | Checks: 0
@@ -548,11 +575,14 @@ function updateHostRow(host, index) { function updateSummary(state) { const stats = state.wanStats(); + const hstats = state.wanHistoryStats(); const reachableEl = document.getElementById("summary-reachable"); const minEl = document.getElementById("summary-min"); const maxEl = document.getElementById("summary-max"); const avgEl = document.getElementById("summary-avg"); + const hminEl = document.getElementById("summary-hmin"); + const hmaxEl = document.getElementById("summary-hmax"); if (!reachableEl) return; reachableEl.textContent = `${stats.reachable}/${stats.total}`; @@ -564,17 +594,31 @@ function updateSummary(state) { : "text-yellow-400"; if (stats.min !== null) { - minEl.textContent = `${stats.min}ms`; + minEl.textContent = `${stats.min}`; minEl.className = latencyClass(stats.min, "online"); + avgEl.textContent = `${stats.avg}`; + avgEl.className = latencyClass(stats.avg, "online"); maxEl.textContent = `${stats.max}ms`; maxEl.className = latencyClass(stats.max, "online"); - avgEl.textContent = `${stats.avg}ms`; - avgEl.className = latencyClass(stats.avg, "online"); } else { - for (const el of [minEl, maxEl, avgEl]) { - el.textContent = "--ms"; - el.className = "text-gray-500"; - } + minEl.textContent = "--"; + minEl.className = "text-gray-500"; + avgEl.textContent = "--"; + avgEl.className = "text-gray-500"; + maxEl.textContent = "--ms"; + maxEl.className = "text-gray-500"; + } + + if (hstats.min !== null) { + hminEl.textContent = `${hstats.min}`; + hminEl.className = latencyClass(hstats.min, "online"); + hmaxEl.textContent = `${hstats.max}ms`; + hmaxEl.className = latencyClass(hstats.max, "online"); + } else { + hminEl.textContent = "--"; + hminEl.className = "text-gray-500"; + hmaxEl.textContent = "--ms"; + hmaxEl.className = "text-gray-500"; } const checksEl = document.getElementById("summary-checks"); @@ -603,11 +647,13 @@ function updateHealthBox(state) { // --- Sorting ----------------------------------------------------------------- -// Sort WAN hosts by last latency (ascending), with datavi.be pinned at top -// and unreachable hosts sorted to the bottom. Rebuilds the WAN DOM rows. +// Sort WAN hosts: pinned hosts first (alphabetically), then unpinned hosts +// by last latency ascending, with unreachable hosts at the bottom. function sortAndRebuildWAN(state) { - const pinned = state.wan.filter((h) => h.name === "datavi.be"); - const rest = state.wan.filter((h) => h.name !== "datavi.be"); + const pinned = state.wan + .filter((h) => h.pinned) + .sort((a, b) => a.name.localeCompare(b.name)); + const rest = state.wan.filter((h) => !h.pinned); rest.sort((a, b) => { if (a.lastLatency === null && b.lastLatency === null) return 0; if (a.lastLatency === null) return 1; @@ -724,6 +770,17 @@ async function init() { .getElementById("pause-btn") .addEventListener("click", () => togglePause(state)); + // Pin button clicks — use event delegation so it survives DOM rebuilds + document.addEventListener("click", (e) => { + const btn = e.target.closest(".pin-btn"); + if (!btn) return; + const idx = parseInt(btn.dataset.pin, 10); + const host = state.wan[idx]; + if (!host) return; + host.pinned = !host.pinned; + sortAndRebuildWAN(state); + }); + function updateClocks() { const now = new Date(); const utc = now.toISOString().replace(/\.\d{3}Z$/, "Z");