Merge pull request #2067 from PR0M3TH3AN/ai/combine-relay-health-list-16567863514667506835

Combine relay list and health check UI
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-02 09:11:02 -05:00
commit 3328402b1c
4 files changed
+133 -168

No files matched your search

+1 -29
View File
@@ -718,34 +718,7 @@
</button>
</div>
<ul id="relayList" class="grid-stack" data-orientation="vertical"></ul>
<div id="relayHealthPanel" class="card space-y-4 p-5">
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="space-y-1">
<h3 class="text-base font-semibold text-text">Relay health</h3>
<p class="text-sm text-muted">
Monitor connectivity, latency, and errors for your configured relays.
</p>
</div>
<button
id="relayHealthRefreshBtn"
type="button"
class="btn-ghost focus-ring text-xs"
>
Refresh
</button>
</div>
<div
id="relayHealthSummary"
class="rounded-md border border-border/70 bg-surface/40 p-3"
>
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="text-2xs font-semibold uppercase tracking-extra-wide text-muted-strong">
Relay status
</p>
<p id="relayHealthCounts" class="text-xs font-semibold text-text"></p>
</div>
<p id="relayHealthMessage" class="mt-1 text-xs text-muted"></p>
</div>
<div class="space-y-2 px-1">
<label
for="relayHealthTelemetryOptIn"
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
@@ -760,7 +733,6 @@
<p class="text-xs text-muted">
Telemetry is opt-in and only includes relay URLs, latency, and error counts.
</p>
<ul id="relayHealthList" class="grid-stack" data-orientation="vertical"></ul>
<p
id="relayHealthStatus"
class="text-xs text-muted"
+57 -139
View File
@@ -1253,13 +1253,7 @@ export class ProfileModalController {
this.relayInput = null;
this.addRelayButton = null;
this.restoreRelaysButton = null;
this.relayHealthPanel = null;
this.relayHealthSummary = null;
this.relayHealthCounts = null;
this.relayHealthMessage = null;
this.relayHealthList = null;
this.relayHealthStatus = null;
this.relayHealthRefreshButton = null;
this.relayHealthTelemetryToggle = null;
this.relayHealthRefreshPromise = null;
this.profileRelayList = null;
@@ -1599,20 +1593,8 @@ export class ProfileModalController {
this.addRelayButton = document.getElementById("addRelayBtn") || null;
this.restoreRelaysButton =
document.getElementById("restoreRelaysBtn") || null;
this.relayHealthPanel =
document.getElementById("relayHealthPanel") || null;
this.relayHealthSummary =
document.getElementById("relayHealthSummary") || null;
this.relayHealthCounts =
document.getElementById("relayHealthCounts") || null;
this.relayHealthMessage =
document.getElementById("relayHealthMessage") || null;
this.relayHealthList =
document.getElementById("relayHealthList") || null;
this.relayHealthStatus =
document.getElementById("relayHealthStatus") || null;
this.relayHealthRefreshButton =
document.getElementById("relayHealthRefreshBtn") || null;
this.relayHealthTelemetryToggle =
document.getElementById("relayHealthTelemetryOptIn") || null;
this.profileRelayRefreshBtn =
@@ -5423,6 +5405,10 @@ export class ProfileModalController {
.loadRelayList(activeHex)
.then(() => {
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;
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+75
View File
@@ -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()