mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add relay health panel and telemetry opt-in
This commit is contained in:
1 parent
6545fb201b
commit
6116b6a7db
6 files changed
+527
-6
No files matched your search
@@ -664,6 +664,44 @@
|
||||
</div>
|
||||
</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>
|
||||
<label
|
||||
for="relayHealthTelemetryOptIn"
|
||||
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
|
||||
>
|
||||
<input
|
||||
id="relayHealthTelemetryOptIn"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
/>
|
||||
Share relay health telemetry
|
||||
</label>
|
||||
<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"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
|
||||
+10
-6
@@ -116,6 +116,10 @@ import {
|
||||
decryptDmInWorker,
|
||||
isDmDecryptWorkerSupported,
|
||||
} from "./dmDecryptWorkerClient.js";
|
||||
import {
|
||||
sanitizeDecryptError,
|
||||
summarizeDmEventForLog,
|
||||
} from "./dmDecryptDiagnostics.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { LRUCache } from "../utils/lruCache.js";
|
||||
import { updateConversationFromMessage, writeMessages } from "../storage/dmDb.js";
|
||||
@@ -4177,8 +4181,8 @@ export class NostrClient {
|
||||
result = await decryptDM(event, context);
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] DM decryptor threw unexpectedly.", {
|
||||
error,
|
||||
eventId,
|
||||
error: sanitizeDecryptError(error),
|
||||
event: summarizeDmEventForLog(event),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -4262,8 +4266,8 @@ export class NostrClient {
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to decrypt DM event during list.", {
|
||||
error,
|
||||
id: event?.id || null,
|
||||
error: sanitizeDecryptError(error),
|
||||
event: summarizeDmEventForLog(event),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4349,8 +4353,8 @@ export class NostrClient {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to decrypt DM event from subscription.",
|
||||
{
|
||||
error,
|
||||
id: eventId,
|
||||
error: sanitizeDecryptError(error),
|
||||
event: summarizeDmEventForLog(event),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export function summarizeDmEventForLog(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return {
|
||||
kind: null,
|
||||
createdAt: null,
|
||||
hasContent: false,
|
||||
contentLength: 0,
|
||||
tagCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const createdAt = Number.isFinite(event.created_at)
|
||||
? Math.floor(event.created_at)
|
||||
: null;
|
||||
|
||||
return {
|
||||
kind: Number.isFinite(event.kind) ? event.kind : null,
|
||||
createdAt,
|
||||
hasContent: typeof event.content === "string" && event.content.length > 0,
|
||||
contentLength: typeof event.content === "string" ? event.content.length : 0,
|
||||
tagCount: Array.isArray(event.tags) ? event.tags.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeDecryptError(error) {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof error.message === "string"
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: "";
|
||||
|
||||
return {
|
||||
name: typeof error.name === "string" ? error.name : "",
|
||||
code: typeof error.code === "string" ? error.code : "",
|
||||
message,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
|
||||
const TELEMETRY_STORAGE_KEY = "bitvid:relay-health-telemetry-opt-in";
|
||||
|
||||
function resolveLogger(logger) {
|
||||
if (logger && logger.dev && logger.user) {
|
||||
return logger;
|
||||
}
|
||||
return { dev: devLogger, user: userLogger };
|
||||
}
|
||||
|
||||
function nowMs() {
|
||||
return typeof performance !== "undefined" && performance.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
}
|
||||
|
||||
function readTelemetryOptIn() {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(TELEMETRY_STORAGE_KEY);
|
||||
return stored === "true";
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"[relayHealth] Failed to read telemetry preference from storage.",
|
||||
error,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeTelemetryOptIn(nextValue) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
TELEMETRY_STORAGE_KEY,
|
||||
nextValue ? "true" : "false",
|
||||
);
|
||||
} catch (error) {
|
||||
userLogger.warn(
|
||||
"[relayHealth] Failed to persist telemetry preference.",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RelayHealthService {
|
||||
constructor({ relayManager, nostrClient, logger, telemetryEmitter } = {}) {
|
||||
this.relayManager = relayManager || null;
|
||||
this.nostrClient = nostrClient || null;
|
||||
this.logger = resolveLogger(logger);
|
||||
this.telemetryEmitter =
|
||||
typeof telemetryEmitter === "function" ? telemetryEmitter : null;
|
||||
this.relayStates = new Map();
|
||||
this.attachedRelays = new Set();
|
||||
this.cachedTelemetryOptIn = readTelemetryOptIn();
|
||||
}
|
||||
|
||||
getTelemetryOptIn() {
|
||||
return this.cachedTelemetryOptIn;
|
||||
}
|
||||
|
||||
setTelemetryOptIn(value) {
|
||||
const normalized = Boolean(value);
|
||||
this.cachedTelemetryOptIn = normalized;
|
||||
writeTelemetryOptIn(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
getRelayUrls() {
|
||||
if (!this.relayManager || typeof this.relayManager.getEntries !== "function") {
|
||||
return [];
|
||||
}
|
||||
const entries = this.relayManager.getEntries();
|
||||
return entries
|
||||
.map((entry) => (typeof entry?.url === "string" ? entry.url.trim() : ""))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
ensureRelayState(relayUrl) {
|
||||
if (!this.relayStates.has(relayUrl)) {
|
||||
this.relayStates.set(relayUrl, {
|
||||
url: relayUrl,
|
||||
connected: false,
|
||||
lastLatencyMs: null,
|
||||
errorCount: 0,
|
||||
lastCheckedAt: null,
|
||||
lastErrorAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
return this.relayStates.get(relayUrl);
|
||||
}
|
||||
|
||||
attachRelayListeners(relayUrl, relay) {
|
||||
if (!relay || typeof relay.on !== "function") {
|
||||
return;
|
||||
}
|
||||
if (this.attachedRelays.has(relayUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
relay.on("connect", () => {
|
||||
this.recordRelaySuccess(relayUrl, null);
|
||||
});
|
||||
relay.on("disconnect", () => {
|
||||
this.recordRelayFailure(relayUrl, new Error("relay-disconnect"));
|
||||
});
|
||||
relay.on("error", (error) => {
|
||||
this.recordRelayFailure(relayUrl, error);
|
||||
});
|
||||
this.attachedRelays.add(relayUrl);
|
||||
} catch (error) {
|
||||
this.logger.dev.warn(
|
||||
"[relayHealth] Failed to attach relay listeners.",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
recordRelaySuccess(relayUrl, latencyMs) {
|
||||
const state = this.ensureRelayState(relayUrl);
|
||||
state.connected = true;
|
||||
state.lastCheckedAt = Date.now();
|
||||
if (Number.isFinite(latencyMs)) {
|
||||
state.lastLatencyMs = Math.max(0, Math.round(latencyMs));
|
||||
}
|
||||
}
|
||||
|
||||
recordRelayFailure(relayUrl, error) {
|
||||
const state = this.ensureRelayState(relayUrl);
|
||||
state.connected = false;
|
||||
state.errorCount += 1;
|
||||
state.lastCheckedAt = Date.now();
|
||||
state.lastErrorAt = Date.now();
|
||||
|
||||
this.logger.dev.warn("[relayHealth] Relay check failed.", {
|
||||
relayUrl,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
async checkRelay(relayUrl) {
|
||||
if (!relayUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.nostrClient) {
|
||||
this.recordRelayFailure(relayUrl, new Error("nostr-client-unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof this.nostrClient.ensurePool === "function") {
|
||||
await this.nostrClient.ensurePool();
|
||||
}
|
||||
} catch (error) {
|
||||
this.recordRelayFailure(relayUrl, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.nostrClient.pool || typeof this.nostrClient.pool.ensureRelay !== "function") {
|
||||
this.recordRelayFailure(relayUrl, new Error("relay-pool-unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
const start = nowMs();
|
||||
try {
|
||||
const relay = await this.nostrClient.pool.ensureRelay(relayUrl);
|
||||
if (!relay) {
|
||||
throw new Error("relay-unavailable");
|
||||
}
|
||||
|
||||
this.attachRelayListeners(relayUrl, relay);
|
||||
this.recordRelaySuccess(relayUrl, nowMs() - start);
|
||||
} catch (error) {
|
||||
this.recordRelayFailure(relayUrl, error);
|
||||
}
|
||||
}
|
||||
|
||||
getSnapshot(relayUrls = null) {
|
||||
const urls = Array.isArray(relayUrls) ? relayUrls : this.getRelayUrls();
|
||||
return urls.map((url) => ({ ...this.ensureRelayState(url) }));
|
||||
}
|
||||
|
||||
async refresh(relayUrls = null) {
|
||||
const urls = Array.isArray(relayUrls) ? relayUrls : this.getRelayUrls();
|
||||
if (!urls.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await Promise.allSettled(urls.map((url) => this.checkRelay(url)));
|
||||
const snapshot = this.getSnapshot(urls);
|
||||
this.emitTelemetry(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
emitTelemetry(snapshot) {
|
||||
if (!this.telemetryEmitter || !this.getTelemetryOptIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
sampledAt: Date.now(),
|
||||
relays: Array.isArray(snapshot)
|
||||
? snapshot.map((entry) => ({
|
||||
url: entry.url,
|
||||
connected: Boolean(entry.connected),
|
||||
latencyMs:
|
||||
Number.isFinite(entry.lastLatencyMs) ? entry.lastLatencyMs : null,
|
||||
errorCount: Number.isFinite(entry.errorCount)
|
||||
? entry.errorCount
|
||||
: 0,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
|
||||
try {
|
||||
this.telemetryEmitter("relay_health_snapshot", payload);
|
||||
} catch (error) {
|
||||
this.logger.user.warn(
|
||||
"[relayHealth] Failed to emit relay health telemetry.",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RelayHealthService;
|
||||
@@ -17,6 +17,7 @@ import nostrService from "../services/nostrService.js";
|
||||
import storageService from "../services/storageService.js";
|
||||
import watchHistoryService from "../watchHistoryService.js";
|
||||
import r2Service from "../services/r2Service.js";
|
||||
import RelayHealthService from "../services/relayHealthService.js";
|
||||
import { createFeedEngine } from "../feedEngine/index.js";
|
||||
import { URL_FIRST_ENABLED } from "../constants.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "../config.js";
|
||||
@@ -426,6 +427,12 @@ export default class ApplicationBootstrap {
|
||||
const modalContainer = doc?.getElementById("modalContainer") || null;
|
||||
try {
|
||||
if (modalContainer) {
|
||||
const relayHealthService = new RelayHealthService({
|
||||
relayManager,
|
||||
nostrClient,
|
||||
telemetryEmitter: (eventName, payload) =>
|
||||
app.emitTelemetryEvent(eventName, payload),
|
||||
});
|
||||
const profileModalServices = {
|
||||
normalizeHexPubkey: (value) => app.normalizeHexPubkey(value),
|
||||
safeEncodeNpub: (pubkey) => app.safeEncodeNpub(pubkey),
|
||||
@@ -480,6 +487,7 @@ export default class ApplicationBootstrap {
|
||||
app.describeHashtagPreferencesError(error, {
|
||||
fallbackMessage,
|
||||
}),
|
||||
relayHealthService,
|
||||
log: (...args) => app.log(...args),
|
||||
closeAllMoreMenus: (options) => app.closeAllMoreMenus(options),
|
||||
clipboard:
|
||||
|
||||
@@ -1241,6 +1241,12 @@ export class ProfileModalController {
|
||||
this.relayInput = null;
|
||||
this.addRelayButton = null;
|
||||
this.restoreRelaysButton = null;
|
||||
this.relayHealthPanel = null;
|
||||
this.relayHealthList = null;
|
||||
this.relayHealthStatus = null;
|
||||
this.relayHealthRefreshButton = null;
|
||||
this.relayHealthTelemetryToggle = null;
|
||||
this.relayHealthRefreshPromise = null;
|
||||
this.profileRelayList = null;
|
||||
this.profileRelayInput = null;
|
||||
this.profileAddRelayBtn = null;
|
||||
@@ -1521,6 +1527,16 @@ export class ProfileModalController {
|
||||
this.addRelayButton = document.getElementById("addRelayBtn") || null;
|
||||
this.restoreRelaysButton =
|
||||
document.getElementById("restoreRelaysBtn") || null;
|
||||
this.relayHealthPanel =
|
||||
document.getElementById("relayHealthPanel") || 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.subscriptionList =
|
||||
document.getElementById("subscriptionsList") || null;
|
||||
@@ -4188,6 +4204,18 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addBlockedButton instanceof HTMLElement) {
|
||||
this.addBlockedButton.addEventListener("click", () => {
|
||||
void this.handleAddBlockedCreator();
|
||||
@@ -5828,6 +5856,9 @@ export class ProfileModalController {
|
||||
|
||||
if (target === "history") {
|
||||
void this.populateProfileWatchHistory();
|
||||
} else if (target === "relays") {
|
||||
this.populateProfileRelays();
|
||||
void this.refreshRelayHealthPanel({ forceRefresh: true, reason: "pane-select" });
|
||||
} else if (target === "messages") {
|
||||
this.resumeProfileMessages();
|
||||
void this.populateProfileMessages({ reason: "pane-select" });
|
||||
@@ -5949,6 +5980,166 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
updateRelayHealthStatus(message = "") {
|
||||
if (!this.relayHealthStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = typeof message === "string" ? message.trim() : "";
|
||||
this.relayHealthStatus.textContent = text;
|
||||
}
|
||||
|
||||
renderRelayHealthSnapshot(snapshot = []) {
|
||||
if (!this.relayHealthList) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
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 = "—";
|
||||
}
|
||||
checks.appendChild(checksLabel);
|
||||
checks.appendChild(checksValue);
|
||||
|
||||
details.appendChild(latency);
|
||||
details.appendChild(errors);
|
||||
details.appendChild(checks);
|
||||
|
||||
item.appendChild(header);
|
||||
item.appendChild(details);
|
||||
|
||||
this.relayHealthList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
handleRelayHealthTelemetryToggle() {
|
||||
const service = this.services?.relayHealthService;
|
||||
if (!service || !(this.relayHealthTelemetryToggle instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = service.setTelemetryOptIn(
|
||||
this.relayHealthTelemetryToggle.checked,
|
||||
);
|
||||
this.relayHealthTelemetryToggle.checked = enabled;
|
||||
this.updateRelayHealthStatus(
|
||||
enabled ? "Relay health telemetry enabled." : "Relay health telemetry disabled.",
|
||||
);
|
||||
}
|
||||
|
||||
async refreshRelayHealthPanel({ forceRefresh = false, reason = "" } = {}) {
|
||||
const service = this.services?.relayHealthService;
|
||||
if (!service || !this.relayHealthList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.relayHealthTelemetryToggle instanceof HTMLInputElement) {
|
||||
this.relayHealthTelemetryToggle.checked = service.getTelemetryOptIn();
|
||||
}
|
||||
|
||||
const snapshot = service.getSnapshot();
|
||||
this.renderRelayHealthSnapshot(snapshot);
|
||||
|
||||
if (!forceRefresh) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
if (this.relayHealthRefreshPromise) {
|
||||
return this.relayHealthRefreshPromise;
|
||||
}
|
||||
|
||||
const statusMessage = reason === "manual" ? "Refreshing relay health…" : "Checking relays…";
|
||||
this.updateRelayHealthStatus(statusMessage);
|
||||
|
||||
const refreshPromise = service
|
||||
.refresh()
|
||||
.then((latest) => {
|
||||
this.renderRelayHealthSnapshot(latest);
|
||||
this.updateRelayHealthStatus("Relay health updated.");
|
||||
return latest;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.updateRelayHealthStatus("Failed to refresh relay health.");
|
||||
this.showError("Failed to refresh relay health.");
|
||||
devLogger.warn("[profileModal] Relay health refresh failed:", error);
|
||||
return [];
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.relayHealthRefreshPromise === refreshPromise) {
|
||||
this.relayHealthRefreshPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.relayHealthRefreshPromise = refreshPromise;
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
async handleRelayOperation(meta = {}, {
|
||||
successMessage = "Relay preferences updated.",
|
||||
skipPublishIfUnchanged = true,
|
||||
@@ -6010,10 +6201,12 @@ export class ProfileModalController {
|
||||
this.showSuccess(unchangedMessage);
|
||||
}
|
||||
this.populateProfileRelays();
|
||||
void this.refreshRelayHealthPanel({ forceRefresh: true, reason: "relay-update" });
|
||||
return operationContext;
|
||||
}
|
||||
|
||||
this.populateProfileRelays();
|
||||
void this.refreshRelayHealthPanel({ forceRefresh: true, reason: "relay-update" });
|
||||
|
||||
if (operationContext.ok) {
|
||||
if (successMessage) {
|
||||
|
||||
Reference in new issue
Block a user