From 357888e310c83de02172fae425dfe4e7a922b893 Mon Sep 17 00:00:00 2001 From: Jan Meinl Date: Sun, 6 Sep 2026 06:23:42 +0200 Subject: [PATCH] Fix WebSocket status label not updating and move it into the topbar The onopen/onclose handlers only toggled the CSS class on the status dot, never the separate text span, so it stayed "offline" after a reconnect. Replace the querySelector-based toggling with an updateWsIndicator() helper that updates both the dot and its label from stored element refs. The indicator now lives in the topbar (via renderTopbar's extra slot) instead of next to the competition name, with its own .ws-indicator flex wrapper so the dot and label line up vertically. --- web/competition.js | 24 +++++++++++++++++------- web/style.css | 13 ++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/web/competition.js b/web/competition.js index 61f4864..1c372b9 100644 --- a/web/competition.js +++ b/web/competition.js @@ -1313,12 +1313,26 @@ // ---- Main render ------------------------------------------------------- + function wsIndicator() { + state.wsDot = el("span", { class: "connection-status " + (state.wsOnline ? "online" : "offline") }); + state.wsText = el("span", { class: "muted" }, state.wsOnline ? t("online") : t("offline")); + return el("span", { class: "ws-indicator" }, state.wsDot, state.wsText); + } + + function updateWsIndicator() { + if (state.wsDot) { + state.wsDot.classList.toggle("online", state.wsOnline); + state.wsDot.classList.toggle("offline", !state.wsOnline); + } + if (state.wsText) state.wsText.textContent = state.wsOnline ? t("online") : t("offline"); + } + function render() { clearNode(root); const backBtn = el("button", { class: "ghost", onclick: () => { if (state.ws) { state.ws.close(); state.ws = null; } navigate("competitions"); } }, "← " + t("back")); - root.appendChild(renderTopbar(user, { extra: backBtn })); + root.appendChild(renderTopbar(user, { extra: [wsIndicator(), backBtn] })); const container = el("div", { class: "container" }); container.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.5rem" } }, @@ -1329,10 +1343,6 @@ isClosed() ? " " : null, isClosed() ? el("span", { class: "badge warn" }, t("closed")) : null, ), - el("div", { class: "row" }, - el("span", { class: "connection-status " + (state.wsOnline ? "online" : "offline") }), - el("span", { class: "muted" }, state.wsOnline ? t("online") : t("offline")), - ), )); const tabs = el("div", { class: "tabs" }); @@ -1393,8 +1403,8 @@ await loadAll(); state.ws = openCompetitionWS(competitionId, { - onopen: () => { state.wsOnline = true; const e = document.querySelector(".connection-status"); if (e) { e.classList.add("online"); e.classList.remove("offline"); } }, - onclose: () => { state.wsOnline = false; const e = document.querySelector(".connection-status"); if (e) { e.classList.remove("online"); e.classList.add("offline"); } }, + onopen: () => { state.wsOnline = true; updateWsIndicator(); }, + onclose: () => { state.wsOnline = false; updateWsIndicator(); }, onmessage: handleWSMessage, }); render(); diff --git a/web/style.css b/web/style.css index 7b8c552..6426ea6 100644 --- a/web/style.css +++ b/web/style.css @@ -387,11 +387,22 @@ tbody tr.transferred { background: #f3f6fb; } height: 8px; border-radius: 50%; background: #d1d5db; - margin-right: 0.5rem; + flex: none; } .connection-status.online { background: #10b981; } .connection-status.offline { background: #ef4444; } +.ws-indicator { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + line-height: 1; +} +/* Optically drop the dot ~1px: an all-caps label (Online/Offline) has + unused descender space, so its glyphs sit above the flex centre line. */ +.ws-indicator .connection-status { position: relative; top: 1px; } + textarea { resize: vertical; min-height: 60px; } @media (max-width: 700px) {