mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Combine relay list with relay health check
- Remove separate relay health panel from profile modal. - Merge health indicators (latency, errors) directly into the relay list items. - Consolidate refresh logic to update both list configuration and health status. - Move telemetry toggle to the bottom of the relay section. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
d018e21ad4
commit
5df489e504
4 files changed
+133
-168
No files matched your search
@@ -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
@@ -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 |
@@ -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()
|
||||
Reference in new issue
Block a user