mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor DM settings into a dedicated modal
- Created `DMSettingsModalController` to manage the new settings modal UI. - Added `components/dm-settings-modal.html` with sections for Privacy and DM Relay Hints. - Updated `AppShell.js` to delegate the settings action to `onOpenSettings` callback, bypassing the legacy popover. - Updated `ProfileModalController.js` to wire up the new modal controller, handling state persistence for relays and privacy settings. - This improves the UX for managing NIP-17 DM relay hints and privacy options. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
6c51b3d6f2
commit
9485003ffd
6 files changed
+441
-81
No files matched your search
@@ -0,0 +1,108 @@
|
||||
<div
|
||||
id="dmSettingsModal"
|
||||
class="bv-modal hidden"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dmSettingsTitle"
|
||||
tabindex="-1"
|
||||
data-ds="new"
|
||||
>
|
||||
<div class="bv-modal-backdrop"></div>
|
||||
|
||||
<div
|
||||
class="bv-modal__panel w-full max-w-2xl h-auto max-h-[90vh] rounded-xl border border-border/50 bg-surface shadow-2xl overflow-hidden flex flex-col"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-b border-border/60 p-4 flex items-center justify-between bg-surface-2">
|
||||
<h2 id="dmSettingsTitle" class="text-lg font-bold text-text">DM Settings</h2>
|
||||
<button id="closeDmSettingsModal" class="modal-close btn-ghost btn-icon p-1 rounded-md text-muted hover:text-text focus-ring">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-8 bg-surface">
|
||||
|
||||
<!-- Privacy Settings -->
|
||||
<section class="space-y-4">
|
||||
<h3 class="text-base font-semibold text-text">Privacy</h3>
|
||||
<div class="card p-4 space-y-4">
|
||||
<label class="flex items-center justify-between gap-3 cursor-pointer">
|
||||
<div class="space-y-1">
|
||||
<span class="block text-sm font-medium text-text">Read receipts</span>
|
||||
<span class="block text-xs text-muted">Let others know when you’ve seen their messages.</span>
|
||||
</div>
|
||||
<input type="checkbox" id="dmSettingsReadReceipts" class="toggle" />
|
||||
</label>
|
||||
<div class="h-px bg-border/40"></div>
|
||||
<label class="flex items-center justify-between gap-3 cursor-pointer">
|
||||
<div class="space-y-1">
|
||||
<span class="block text-sm font-medium text-text">Typing indicators</span>
|
||||
<span class="block text-xs text-muted">Show when you’re typing a message.</span>
|
||||
</div>
|
||||
<input type="checkbox" id="dmSettingsTypingIndicators" class="toggle" />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Relay Hints -->
|
||||
<section class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">DM Relay Hints</h3>
|
||||
<p class="text-sm text-muted">
|
||||
Publish relay hints for direct messages as <span class="font-medium">kind:10050</span> events. Use WSS-only relay URLs so encrypted DM delivery stays secure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card p-5 space-y-5">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<label class="flex-1 text-sm text-muted-strong" for="dmSettingsRelayInput">
|
||||
<span class="mb-2 block font-medium">Add a DM relay</span>
|
||||
<input
|
||||
id="dmSettingsRelayInput"
|
||||
type="text"
|
||||
inputmode="url"
|
||||
autocomplete="off"
|
||||
class="input"
|
||||
placeholder="wss://relay.example.com"
|
||||
/>
|
||||
</label>
|
||||
<button id="dmSettingsRelayAddBtn" type="button" class="btn focus-ring">
|
||||
Add relay
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-muted-strong uppercase tracking-wider">Your list</span>
|
||||
</div>
|
||||
<ul id="dmSettingsRelayList" class="grid-stack space-y-2" data-orientation="vertical"></ul>
|
||||
<p id="dmSettingsRelayEmpty" class="text-sm text-muted italic hidden">No DM relay hints configured.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 pt-2 border-t border-border/40">
|
||||
<span id="dmSettingsRelayStatus" class="text-xs text-muted" role="status" aria-live="polite"></span>
|
||||
<button id="dmSettingsRelayPublishBtn" type="button" class="btn-ghost focus-ring text-xs">
|
||||
Publish relay hints
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,6 +179,12 @@ export class AppShell {
|
||||
settingsBtn.ariaLabel = "Direct message settings";
|
||||
settingsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (typeof onOpenSettings === "function") {
|
||||
onOpenSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
let popover = sidebar.querySelector(".dm-settings-popover");
|
||||
if (popover) {
|
||||
popover.remove();
|
||||
@@ -198,9 +204,6 @@ export class AppShell {
|
||||
const relaySettings = DMRelaySettings({ document: doc });
|
||||
popover.appendChild(relaySettings);
|
||||
sidebarHeader.appendChild(popover);
|
||||
if (typeof onOpenSettings === "function") {
|
||||
onOpenSettings(popover);
|
||||
}
|
||||
|
||||
const closeHandler = (event) => {
|
||||
if (!popover.contains(event.target) && event.target !== settingsBtn && !settingsBtn.contains(event.target)) {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { userLogger } from "../../utils/logger.js";
|
||||
import { sanitizeRelayList } from "../../nostr/nip46Client.js";
|
||||
|
||||
export class DMSettingsModalController {
|
||||
constructor() {
|
||||
this.modal = null;
|
||||
this.initialized = false;
|
||||
this.callbacks = {};
|
||||
this.state = {
|
||||
privacySettings: {},
|
||||
relayHints: [],
|
||||
};
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
// Check if already in DOM (e.g. from server-side render or previous load)
|
||||
if (document.getElementById("dmSettingsModal")) {
|
||||
this.modal = document.getElementById("dmSettingsModal");
|
||||
this.bindElements();
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("components/dm-settings-modal.html");
|
||||
if (!response.ok) throw new Error("HTML not found");
|
||||
const html = await response.text();
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
|
||||
document.body.appendChild(template.content);
|
||||
|
||||
this.modal = document.getElementById("dmSettingsModal");
|
||||
this.bindElements();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
userLogger.error("Failed to load DM settings modal:", error);
|
||||
}
|
||||
}
|
||||
|
||||
bindElements() {
|
||||
if (!this.modal) return;
|
||||
|
||||
this.closeButton = this.modal.querySelector("#closeDmSettingsModal");
|
||||
this.backdrop = this.modal.querySelector(".bv-modal-backdrop");
|
||||
|
||||
// Privacy
|
||||
this.readReceiptsToggle = this.modal.querySelector("#dmSettingsReadReceipts");
|
||||
this.typingIndicatorsToggle = this.modal.querySelector("#dmSettingsTypingIndicators");
|
||||
|
||||
// Relays
|
||||
this.relayInput = this.modal.querySelector("#dmSettingsRelayInput");
|
||||
this.relayAddBtn = this.modal.querySelector("#dmSettingsRelayAddBtn");
|
||||
this.relayList = this.modal.querySelector("#dmSettingsRelayList");
|
||||
this.relayEmpty = this.modal.querySelector("#dmSettingsRelayEmpty");
|
||||
this.relayPublishBtn = this.modal.querySelector("#dmSettingsRelayPublishBtn");
|
||||
this.relayStatus = this.modal.querySelector("#dmSettingsRelayStatus");
|
||||
|
||||
// Events
|
||||
this.closeButton?.addEventListener("click", () => this.hide());
|
||||
this.backdrop?.addEventListener("click", () => this.hide());
|
||||
|
||||
this.readReceiptsToggle?.addEventListener("change", (e) => {
|
||||
this.handlePrivacyChange("readReceiptsEnabled", e.target.checked);
|
||||
});
|
||||
|
||||
this.typingIndicatorsToggle?.addEventListener("change", (e) => {
|
||||
this.handlePrivacyChange("typingIndicatorsEnabled", e.target.checked);
|
||||
});
|
||||
|
||||
this.relayAddBtn?.addEventListener("click", () => this.handleAddRelay());
|
||||
this.relayInput?.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.handleAddRelay();
|
||||
}
|
||||
});
|
||||
|
||||
this.relayPublishBtn?.addEventListener("click", () => this.handlePublish());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Object} options.privacySettings { readReceiptsEnabled, typingIndicatorsEnabled }
|
||||
* @param {Array} options.relayHints Array of relay URLs
|
||||
* @param {Function} options.onPrivacyChange (key, value) => void
|
||||
* @param {Function} options.onAddRelay (url) => void
|
||||
* @param {Function} options.onRemoveRelay (url) => void
|
||||
* @param {Function} options.onPublishRelays (urls) => Promise<result>
|
||||
*/
|
||||
async show(options = {}) {
|
||||
if (!this.initialized) await this.load();
|
||||
if (!this.modal) return;
|
||||
|
||||
this.callbacks = {
|
||||
onPrivacyChange: options.onPrivacyChange || (() => {}),
|
||||
onAddRelay: options.onAddRelay || (() => {}),
|
||||
onRemoveRelay: options.onRemoveRelay || (() => {}),
|
||||
onPublishRelays: options.onPublishRelays || (async () => {}),
|
||||
};
|
||||
|
||||
this.state.privacySettings = options.privacySettings || {};
|
||||
this.state.relayHints = Array.isArray(options.relayHints) ? [...options.relayHints] : [];
|
||||
|
||||
this.refreshUI();
|
||||
|
||||
this.modal.classList.remove("hidden");
|
||||
this.modal.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("modal-open");
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.modal) return;
|
||||
this.modal.classList.add("hidden");
|
||||
this.modal.setAttribute("aria-hidden", "true");
|
||||
// Check if other modals are open before removing body class
|
||||
// For now simple removal is likely fine or we can rely on ProfileModal logic handling global state
|
||||
// But since this opens *on top* of profile modal usually, we might want to be careful.
|
||||
// If Profile Modal is open, body should stay modal-open.
|
||||
// We can just check if profileModal is not hidden?
|
||||
// Or just leave it if we assume profile modal is behind.
|
||||
// However, for correctness:
|
||||
const profileModal = document.getElementById("profileModal");
|
||||
if (!profileModal || profileModal.classList.contains("hidden")) {
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
}
|
||||
|
||||
refreshUI() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// Privacy
|
||||
if (this.readReceiptsToggle) {
|
||||
this.readReceiptsToggle.checked = !!this.state.privacySettings.readReceiptsEnabled;
|
||||
}
|
||||
if (this.typingIndicatorsToggle) {
|
||||
this.typingIndicatorsToggle.checked = !!this.state.privacySettings.typingIndicatorsEnabled;
|
||||
}
|
||||
|
||||
// Relays
|
||||
this.renderRelayList();
|
||||
if (this.relayStatus) this.relayStatus.textContent = "";
|
||||
}
|
||||
|
||||
renderRelayList() {
|
||||
if (!this.relayList || !this.relayEmpty) return;
|
||||
|
||||
this.relayList.innerHTML = "";
|
||||
const relays = this.state.relayHints;
|
||||
|
||||
if (relays.length === 0) {
|
||||
this.relayList.classList.add("hidden");
|
||||
this.relayEmpty.classList.remove("hidden");
|
||||
} else {
|
||||
this.relayList.classList.remove("hidden");
|
||||
this.relayEmpty.classList.add("hidden");
|
||||
|
||||
relays.forEach(url => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "flex items-center justify-between gap-3 p-3 bg-surface-2 rounded-md border border-border/50";
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.className = "text-sm text-text break-all font-mono";
|
||||
span.textContent = url;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
removeBtn.className = "btn-ghost btn-icon p-1 text-muted hover:text-critical focus-ring";
|
||||
removeBtn.ariaLabel = `Remove ${url}`;
|
||||
removeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
|
||||
|
||||
removeBtn.addEventListener("click", () => this.handleRemoveRelay(url));
|
||||
|
||||
li.appendChild(span);
|
||||
li.appendChild(removeBtn);
|
||||
this.relayList.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handlePrivacyChange(key, value) {
|
||||
this.state.privacySettings[key] = value;
|
||||
this.callbacks.onPrivacyChange(key, value);
|
||||
}
|
||||
|
||||
handleAddRelay() {
|
||||
const rawValue = this.relayInput?.value?.trim() || "";
|
||||
if (!rawValue) return;
|
||||
|
||||
const sanitized = sanitizeRelayList([rawValue]);
|
||||
const url = sanitized[0];
|
||||
|
||||
if (!url) {
|
||||
this.setRelayStatus("Invalid WSS URL.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.relayHints.includes(url)) {
|
||||
this.setRelayStatus("Relay already added.", "error");
|
||||
this.relayInput.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.relayHints.push(url);
|
||||
this.renderRelayList();
|
||||
this.relayInput.value = "";
|
||||
this.setRelayStatus("Relay added (unsaved). Publish to save.", "info");
|
||||
|
||||
this.callbacks.onAddRelay(url);
|
||||
}
|
||||
|
||||
handleRemoveRelay(url) {
|
||||
this.state.relayHints = this.state.relayHints.filter(r => r !== url);
|
||||
this.renderRelayList();
|
||||
this.setRelayStatus("Relay removed (unsaved).", "info");
|
||||
|
||||
this.callbacks.onRemoveRelay(url);
|
||||
}
|
||||
|
||||
async handlePublish() {
|
||||
if (this.state.relayHints.length === 0) {
|
||||
this.setRelayStatus("Add at least one relay before publishing.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setRelayStatus("Publishing...", "info");
|
||||
this.relayPublishBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await this.callbacks.onPublishRelays(this.state.relayHints);
|
||||
if (result && result.ok) {
|
||||
const acceptedCount = Array.isArray(result.accepted) ? result.accepted.length : 0;
|
||||
this.setRelayStatus(`Published to ${acceptedCount} relay(s).`, "success");
|
||||
} else {
|
||||
this.setRelayStatus("Failed to publish.", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.setRelayStatus("Error publishing relays.", "error");
|
||||
userLogger.error("Failed to publish DM relays:", error);
|
||||
} finally {
|
||||
this.relayPublishBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
setRelayStatus(message, type = "info") {
|
||||
if (!this.relayStatus) return;
|
||||
this.relayStatus.textContent = message;
|
||||
|
||||
this.relayStatus.classList.remove("text-status-success", "text-status-danger", "text-muted");
|
||||
|
||||
if (type === "error") {
|
||||
this.relayStatus.classList.add("text-status-danger");
|
||||
} else if (type === "success") {
|
||||
this.relayStatus.classList.add("text-status-success");
|
||||
} else {
|
||||
this.relayStatus.classList.add("text-muted");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
setLinkPreviewAutoFetch,
|
||||
} from "../utils/linkPreviewSettings.js";
|
||||
import { SubscriptionHistoryController } from "./subscriptionHistoryController.js";
|
||||
import { DMSettingsModalController } from "./dm/DMSettingsModalController.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
@@ -1432,6 +1433,7 @@ export class ProfileModalController {
|
||||
this.adminEmptyMessages = new Map();
|
||||
this.hashtagPreferencesUnsubscribe = null;
|
||||
this.subscriptionHistoryController = new SubscriptionHistoryController();
|
||||
this.dmSettingsModalController = new DMSettingsModalController();
|
||||
|
||||
if (
|
||||
this.subscriptionsService &&
|
||||
@@ -2498,6 +2500,69 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
openDmSettingsModal() {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.showError("Please sign in to manage DM settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
const privacySettings = this.getDmPrivacySettingsSnapshot();
|
||||
const relayHints = this.getActiveDmRelayPreferences();
|
||||
|
||||
this.dmSettingsModalController.show({
|
||||
privacySettings,
|
||||
relayHints,
|
||||
onPrivacyChange: (key, value) => {
|
||||
this.persistDmPrivacySettings({ [key]: value });
|
||||
if (key === "readReceiptsEnabled") {
|
||||
this.showStatus(
|
||||
value ? "Read receipts enabled." : "Read receipts disabled.",
|
||||
);
|
||||
} else if (key === "typingIndicatorsEnabled") {
|
||||
this.showStatus(
|
||||
value ? "Typing indicators enabled." : "Typing indicators disabled.",
|
||||
);
|
||||
}
|
||||
},
|
||||
onPublishRelays: async (urls) => {
|
||||
return this.handleDmSettingsPublish(urls);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmSettingsPublish(relays) {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
return { ok: false, error: "not-logged-in" };
|
||||
}
|
||||
|
||||
// Update local state first
|
||||
this.setActiveDmRelayPreferences(relays);
|
||||
|
||||
// Publish using existing logic
|
||||
const callback = this.callbacks.onPublishDmRelayPreferences;
|
||||
if (!callback || callback === noop) {
|
||||
return { ok: false, error: "unavailable" };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await callback({
|
||||
pubkey: owner,
|
||||
relays,
|
||||
controller: this,
|
||||
});
|
||||
|
||||
if (result?.ok) {
|
||||
this.populateDmRelayPreferences(); // Refresh old UI just in case
|
||||
return result;
|
||||
}
|
||||
return { ok: false, error: result?.error || "failed" };
|
||||
} catch (error) {
|
||||
return { ok: false, error: error };
|
||||
}
|
||||
}
|
||||
|
||||
buildDmRecipientContext(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalized) {
|
||||
@@ -4644,9 +4709,7 @@ export class ProfileModalController {
|
||||
this.handleTypingIndicatorsToggle(enabled);
|
||||
},
|
||||
onOpenSettings: () => {
|
||||
this.cacheDmRelayElements();
|
||||
this.bindDmRelayControls();
|
||||
this.populateDmRelayPreferences();
|
||||
this.openDmSettingsModal();
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -1,75 +0,0 @@
|
||||
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