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.
This commit is contained in:
Jeffrey Paul 2026-02-23 01:13:04 +07:00
parent a3bd3d06d1
commit f4517ae953

View File

@ -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 `
<div class="host-row bg-gray-800/50 rounded-lg p-4 border border-gray-700/50" data-index="${index}">
<div class="flex items-center gap-4">
<button class="pin-btn flex-shrink-0 ${pinColor} transition-colors" data-pin="${index}" title="Pin to top">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A6 6 0 0 1 5 6.708V2.277a3 3 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354"/>
</svg>
</button>
<div class="w-72 flex-shrink-0">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full" style="background-color: ${latencyHex(null)}"></div>
@ -468,11 +495,11 @@ function buildUI(state) {
<div id="summary" class="mt-2 p-3 bg-gray-800/70 rounded-lg border border-gray-700/50 font-mono text-sm">
<span class="text-gray-400">Reachable:</span> <span id="summary-reachable" class="text-white">--/--</span>
<span class="text-gray-600 mx-3">|</span>
<span class="text-gray-400">Min:</span> <span id="summary-min" class="text-green-400">--ms</span>
<span class="text-gray-400">Now:</span>
<span id="summary-min" class="text-green-400">--</span>/<span id="summary-avg" class="text-yellow-400">--</span>/<span id="summary-max" class="text-red-400">--ms</span>
<span class="text-gray-600 mx-3">|</span>
<span class="text-gray-400">Max:</span> <span id="summary-max" class="text-red-400">--ms</span>
<span class="text-gray-600 mx-3">|</span>
<span class="text-gray-400">Avg:</span> <span id="summary-avg" class="text-yellow-400">--ms</span>
<span class="text-gray-400">${CONFIG.historyDuration}s:</span>
<span id="summary-hmin" class="text-green-400">--</span>/<span id="summary-hmax" class="text-red-400">--ms</span>
<span class="text-gray-600 mx-3">|</span>
<span class="text-gray-400">Checks:</span> <span id="summary-checks" class="text-gray-300">0</span>
</div>
@ -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");