diff --git a/components/profile-modal.html b/components/profile-modal.html index b610f523..4ecaef64 100644 --- a/components/profile-modal.html +++ b/components/profile-modal.html @@ -718,34 +718,7 @@
- Monitor connectivity, latency, and errors for your configured relays. -
- Relay status -
{ this.populateProfileRelays(); + void this.refreshRelayHealthPanel({ + forceRefresh: true, + reason: "relay-update", + }); }) .catch((error) => { devLogger.warn("[profileModal] Failed to refresh relay list:", error); @@ -5430,12 +5416,6 @@ export class ProfileModalController { }); } - if (this.relayHealthRefreshButton instanceof HTMLElement) { - this.relayHealthRefreshButton.addEventListener("click", () => { - void this.refreshRelayHealthPanel({ forceRefresh: true, reason: "manual" }); - }); - } - if (this.relayHealthTelemetryToggle instanceof HTMLInputElement) { this.relayHealthTelemetryToggle.addEventListener("change", () => { this.handleRelayHealthTelemetryToggle(); @@ -7455,8 +7435,8 @@ export class ProfileModalController { relays.forEach((entry) => { const item = document.createElement("li"); - item.className = - "card flex items-start justify-between gap-4 p-4"; + item.className = "card flex items-start justify-between gap-4 p-4"; + item.dataset.relayUrl = entry.url; const info = document.createElement("div"); info.className = "flex-1 min-w-0"; @@ -7475,8 +7455,13 @@ export class ProfileModalController { } statusEl.textContent = modeLabel; + const health = document.createElement("div"); + health.className = "flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-2xs text-muted empty:hidden"; + health.dataset.role = "relay-health"; + info.appendChild(urlEl); info.appendChild(statusEl); + info.appendChild(health); const actions = document.createElement("div"); actions.className = "flex items-center gap-2"; @@ -7518,126 +7503,58 @@ export class ProfileModalController { this.relayHealthStatus.textContent = text; } - updateRelayHealthSummary(snapshot = []) { - if (!this.relayHealthCounts || !this.relayHealthMessage) { + updateRelayHealthIndicators(snapshot = []) { + if (!this.relayList) { return; } - const entries = Array.isArray(snapshot) ? snapshot : []; - const total = entries.length; - const healthy = entries.filter((entry) => entry.connected).length; - const failed = Math.max(0, total - healthy); - - this.relayHealthCounts.textContent = `${healthy} healthy • ${failed} failed`; - - let message = ""; - if (!total) { - message = "No relays configured."; - } else if (failed > 0) { - message = - failed === total - ? "All relays are unreachable. Check your connection or update your relay list." - : `${failed} relay${failed === 1 ? "" : "s"} unreachable. Playback and publishing may be delayed.`; - } else { - message = "All relays reachable."; - } - - this.relayHealthMessage.className = "mt-1 text-xs text-muted"; - if (failed > 0) { - this.relayHealthMessage.classList.add("text-status-warning"); - } else if (total > 0) { - this.relayHealthMessage.classList.add("text-status-success"); - } - this.relayHealthMessage.textContent = message; - } - - renderRelayHealthSnapshot(snapshot = []) { - if (!this.relayHealthList) { - return; - } - - this.updateRelayHealthSummary(snapshot); - this.relayHealthList.innerHTML = ""; - - if (!snapshot.length) { - const emptyState = document.createElement("li"); - emptyState.className = - "card border border-dashed border-surface-strong p-4 text-center text-sm text-muted"; - emptyState.textContent = "No relays configured."; - this.relayHealthList.appendChild(emptyState); + if (!Array.isArray(snapshot)) { return; } snapshot.forEach((entry) => { - const item = document.createElement("li"); - item.className = "card flex flex-col gap-2 p-4"; - - const header = document.createElement("div"); - header.className = "flex items-center justify-between gap-3"; - - const urlEl = document.createElement("p"); - urlEl.className = "text-sm font-medium text-primary break-all"; - urlEl.textContent = entry.url; - - const connection = document.createElement("span"); - const connected = Boolean(entry.connected); - connection.className = `text-xs font-semibold ${ - connected ? "text-status-success" : "text-status-danger" - }`; - connection.textContent = connected ? "Connected" : "Unavailable"; - - header.appendChild(urlEl); - header.appendChild(connection); - - const details = document.createElement("div"); - details.className = "grid grid-cols-2 gap-3 text-xs text-muted sm:grid-cols-3"; - - const latency = document.createElement("div"); - latency.className = "flex items-center justify-between gap-2"; - const latencyLabel = document.createElement("span"); - latencyLabel.textContent = "Latency"; - const latencyValue = document.createElement("span"); - latencyValue.className = "text-text"; - latencyValue.textContent = Number.isFinite(entry.lastLatencyMs) - ? `${entry.lastLatencyMs} ms` - : "—"; - latency.appendChild(latencyLabel); - latency.appendChild(latencyValue); - - const errors = document.createElement("div"); - errors.className = "flex items-center justify-between gap-2"; - const errorsLabel = document.createElement("span"); - errorsLabel.textContent = "Errors"; - const errorsValue = document.createElement("span"); - errorsValue.className = "text-text"; - errorsValue.textContent = Number.isFinite(entry.errorCount) - ? `${entry.errorCount}` - : "0"; - errors.appendChild(errorsLabel); - errors.appendChild(errorsValue); - - const checks = document.createElement("div"); - checks.className = "flex items-center justify-between gap-2"; - const checksLabel = document.createElement("span"); - checksLabel.textContent = "Checked"; - const checksValue = document.createElement("span"); - checksValue.className = "text-text"; - if (Number.isFinite(entry.lastCheckedAt) && entry.lastCheckedAt > 0) { - checksValue.textContent = new Date(entry.lastCheckedAt).toLocaleTimeString(); - } else { - checksValue.textContent = "—"; + const url = entry.url; + const item = this.relayList.querySelector( + `li[data-relay-url="${CSS.escape(url)}"]`, + ); + if (!item) { + return; } - checks.appendChild(checksLabel); - checks.appendChild(checksValue); - details.appendChild(latency); - details.appendChild(errors); - details.appendChild(checks); + const healthContainer = item.querySelector('[data-role="relay-health"]'); + if (!healthContainer) { + return; + } - item.appendChild(header); - item.appendChild(details); + healthContainer.innerHTML = ""; - this.relayHealthList.appendChild(item); + const createBadge = (label, value, colorClass) => { + const span = document.createElement("span"); + span.className = "inline-flex items-center gap-1 bg-surface-strong/30 px-1.5 py-0.5 rounded"; + const l = document.createElement("span"); + l.textContent = label; + const v = document.createElement("span"); + v.className = colorClass || "text-text"; + v.textContent = value; + span.appendChild(l); + span.appendChild(v); + return span; + }; + + if (Number.isFinite(entry.lastLatencyMs)) { + let color = "text-status-success"; + if (entry.lastLatencyMs > 1000) color = "text-status-danger"; + else if (entry.lastLatencyMs > 300) color = "text-status-warning"; + healthContainer.appendChild( + createBadge("Ping:", `${entry.lastLatencyMs}ms`, color), + ); + } + + if (entry.errorCount > 0) { + healthContainer.appendChild( + createBadge("Errors:", `${entry.errorCount}`, "text-status-danger"), + ); + } }); } @@ -7658,7 +7575,7 @@ export class ProfileModalController { async refreshRelayHealthPanel({ forceRefresh = false, reason = "" } = {}) { const service = this.services?.relayHealthService; - if (!service || !this.relayHealthList) { + if (!service) { return []; } @@ -7667,7 +7584,7 @@ export class ProfileModalController { } const snapshot = service.getSnapshot(); - this.renderRelayHealthSnapshot(snapshot); + this.updateRelayHealthIndicators(snapshot); if (!forceRefresh) { return snapshot; @@ -7677,13 +7594,14 @@ export class ProfileModalController { return this.relayHealthRefreshPromise; } - const statusMessage = reason === "manual" ? "Refreshing relay health…" : "Checking relays…"; + const statusMessage = + reason === "manual" ? "Refreshing relay health…" : "Checking relays…"; this.updateRelayHealthStatus(statusMessage); const refreshPromise = service .refresh() .then((latest) => { - this.renderRelayHealthSnapshot(latest); + this.updateRelayHealthIndicators(latest); this.updateRelayHealthStatus("Relay health updated."); return latest; }) diff --git a/verification/relay_ui.png b/verification/relay_ui.png new file mode 100644 index 00000000..e6d9ac2b Binary files /dev/null and b/verification/relay_ui.png differ diff --git a/verification/verify_relay_ui.py b/verification/verify_relay_ui.py new file mode 100644 index 00000000..89b76a92 --- /dev/null +++ b/verification/verify_relay_ui.py @@ -0,0 +1,75 @@ +from playwright.sync_api import sync_playwright +import time + +def run(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # Set dummy profile in localStorage to simulate login + page.add_init_script(""" + localStorage.setItem("bitvid:savedProfiles", JSON.stringify([{pubkey: "0000000000000000000000000000000000000000000000000000000000000001", authType: "nip07"}])); + localStorage.setItem("bitvid:activeProfilePubkey", "0000000000000000000000000000000000000000000000000000000000000001"); + """) + + page.goto("http://localhost:8080") + + # Wait for app to load and hydrate + try: + page.wait_for_selector("#profileButton:not(.hidden)", timeout=10000) + except: + print("Profile button did not appear. Dumping console logs.") + # Maybe auth failed or slow. + # Let's try to find if app is attached to window? No. + pass + + # If profile button is visible, click it + if page.is_visible("#profileButton"): + print("Clicking profile button...") + page.click("#profileButton") + else: + print("Profile button not visible. Trying to find it anyway (maybe hidden class issue).") + # If hidden, maybe just click it? + # Or maybe I need to wait longer. + # Let's try to access the modal directly if it's already loaded? No it loads on show. + + # Wait for modal + try: + page.wait_for_selector("#profileModal", state="visible", timeout=5000) + except: + print("Profile modal did not appear.") + + # Click "Relays" nav + try: + page.click("#profileNavRelays") + except: + print("Could not click Relays nav.") + + # Wait for relay list + page.wait_for_selector("#relayList", state="visible") + + # Check if relayHealthPanel is gone + health_panel = page.query_selector("#relayHealthPanel") + if health_panel: + print("FAILURE: #relayHealthPanel should not exist!") + else: + print("SUCCESS: #relayHealthPanel is gone.") + + # Check if telemetry toggle is present (moved) + toggle = page.query_selector("#relayHealthTelemetryOptIn") + if toggle: + print("SUCCESS: Telemetry toggle found.") + else: + print("FAILURE: Telemetry toggle not found.") + + # Check for health stats placeholders in relay list items + # If there are no relays, we might see "No relays configured". + # We can add a relay via UI or script. + + # Take screenshot + page.screenshot(path="verification/relay_ui.png") + + browser.close() + +if __name__ == "__main__": + run()