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.
This commit is contained in:
2026-09-06 06:23:42 +02:00
parent ae035c3fd4
commit 357888e310
2 changed files with 29 additions and 8 deletions
+17 -7
View File
@@ -1313,12 +1313,26 @@
// ---- Main render ------------------------------------------------------- // ---- 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() { function render() {
clearNode(root); clearNode(root);
const backBtn = el("button", { class: "ghost", const backBtn = el("button", { class: "ghost",
onclick: () => { if (state.ws) { state.ws.close(); state.ws = null; } navigate("competitions"); } }, onclick: () => { if (state.ws) { state.ws.close(); state.ws = null; } navigate("competitions"); } },
"← " + t("back")); "← " + t("back"));
root.appendChild(renderTopbar(user, { extra: backBtn })); root.appendChild(renderTopbar(user, { extra: [wsIndicator(), backBtn] }));
const container = el("div", { class: "container" }); const container = el("div", { class: "container" });
container.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.5rem" } }, container.appendChild(el("div", { class: "row", style: { justifyContent: "space-between", marginBottom: "0.5rem" } },
@@ -1329,10 +1343,6 @@
isClosed() ? " " : null, isClosed() ? " " : null,
isClosed() ? el("span", { class: "badge warn" }, t("closed")) : 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" }); const tabs = el("div", { class: "tabs" });
@@ -1393,8 +1403,8 @@
await loadAll(); await loadAll();
state.ws = openCompetitionWS(competitionId, { state.ws = openCompetitionWS(competitionId, {
onopen: () => { state.wsOnline = true; const e = document.querySelector(".connection-status"); if (e) { e.classList.add("online"); e.classList.remove("offline"); } }, onopen: () => { state.wsOnline = true; updateWsIndicator(); },
onclose: () => { state.wsOnline = false; const e = document.querySelector(".connection-status"); if (e) { e.classList.remove("online"); e.classList.add("offline"); } }, onclose: () => { state.wsOnline = false; updateWsIndicator(); },
onmessage: handleWSMessage, onmessage: handleWSMessage,
}); });
render(); render();
+12 -1
View File
@@ -387,11 +387,22 @@ tbody tr.transferred { background: #f3f6fb; }
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #d1d5db; background: #d1d5db;
margin-right: 0.5rem; flex: none;
} }
.connection-status.online { background: #10b981; } .connection-status.online { background: #10b981; }
.connection-status.offline { background: #ef4444; } .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; } textarea { resize: vertical; min-height: 60px; }
@media (max-width: 700px) { @media (max-width: 700px) {