diff --git a/TODO.md b/TODO.md index bed8e9d..0caaa52 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,8 @@ files, so merging it also closes most compliance gaps. # Completed Steps +- 2026-08-10: per-host status line wraps below the 768px breakpoint instead of + forcing horizontal page scroll at 320px - 2026-08-09: `Dockerfile.backend` reworked to the mandated Go multistage lint-stage pattern: separate `lint` stage on the hash-pinned `golangci/golangci-lint` image, `COPY --from=lint` stage dependency, diff --git a/src/main.js b/src/main.js index 3ab6bb8..aaa3cdf 100644 --- a/src/main.js +++ b/src/main.js @@ -537,6 +537,12 @@ class SparklineRenderer { // --- UI Renderer ------------------------------------------------------------- +// The per-host status line must stay wrappable: its populated content is +// wider than the host column at a 320px viewport, and `whitespace-nowrap` +// here overflows the element and forces the whole document to scroll +// horizontally. +const STATUS_TEXT_CLASS = "status-text text-xs text-right col-span-2 mt-5"; + function hostRowHTML(host, index, showPin = true) { const pinColor = host.pinned ? "text-blue-500" @@ -562,7 +568,7 @@ function hostRowHTML(host, index, showPin = true) { --- ${host.url} -
waiting...
+
waiting...
@@ -688,6 +694,26 @@ function buildUI(state) { // --- UI Updaters ------------------------------------------------------------- +// Renders `min 1ms / med 2ms / avg 3ms / max 4ms`. Each label, value and +// trailing separator is one unbreakable unit, so wrapping only ever happens +// between stats and a wrapped line never starts with a separator. +function statusStatsHTML(stats) { + return stats + .map(([label, value], i) => { + const sep = + i < stats.length - 1 + ? ` /` + : ""; + return ( + `` + + `${label} ` + + `${value}ms` + + `${sep}` + ); + }) + .join(" "); +} + function updateHostRow(host, index) { const latencyEl = document.querySelector( `.latency-value[data-host="${index}"]`, @@ -712,28 +738,22 @@ function updateHostRow(host, index) { const min = host.minLatency(); const max = host.maxLatency(); if (host.status === "online" && avg !== null) { - statusEl.innerHTML = - `min ${min}ms` + - ` / ` + - `med ${med}ms` + - ` / ` + - `avg ${avg}ms` + - ` / ` + - `max ${max}ms`; - statusEl.className = - "status-text text-xs whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.innerHTML = statusStatsHTML([ + ["min", min], + ["med", med], + ["avg", avg], + ["max", max], + ]); + statusEl.className = STATUS_TEXT_CLASS; } else if (host.status === "offline") { statusEl.textContent = "unreachable"; - statusEl.className = - "status-text text-xs text-red-400 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-red-400`; } else if (host.status === "error") { statusEl.textContent = "timeout"; - statusEl.className = - "status-text text-xs text-orange-400 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-orange-400`; } else { statusEl.textContent = "connecting..."; - statusEl.className = - "status-text text-xs text-gray-500 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`; } SparklineRenderer.draw(canvas, host.history); @@ -1044,8 +1064,7 @@ function greyOutUI(state) { } if (statusEl) { statusEl.textContent = "paused"; - statusEl.className = - "status-text text-xs text-gray-500 whitespace-nowrap text-right col-span-2 mt-5"; + statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`; } // Grey out the status dot const row = document.querySelector(`.host-row[data-index="${i}"]`);