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:
parent
a3bd3d06d1
commit
f4517ae953
91
src/main.js
91
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 `
|
||||
<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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user