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}"]`);