Files
client/www/c-relay-pg.html
T

1033 lines
34 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>C-Relay-PG</title>
<link rel="stylesheet" href="./css/client.css" />
<script>
(function () {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-mode');
if (document.body) {
document.body.classList.add('dark-mode');
}
}
})();
</script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
<script>
window.__CRELAY_BOOT_LOG__ = [];
function __crelayBoot(message, isError = false) {
const line = `[c-relay-pg boot] ${message}`;
window.__CRELAY_BOOT_LOG__.push(line);
if (isError) {
console.error(line);
} else {
console.log(line);
}
const sink = document.getElementById('bootLog');
if (sink) {
sink.textContent = `${line}\n` + sink.textContent;
}
}
window.addEventListener('error', (e) => {
__crelayBoot(`window error: ${e.message} @ ${e.filename}:${e.lineno}`, true);
});
window.addEventListener('unhandledrejection', (e) => {
const reason = e?.reason?.message || String(e?.reason || 'unknown rejection');
__crelayBoot(`unhandled rejection: ${reason}`, true);
});
</script>
<script src="./js/vendor/svg.min.js"></script>
<style media="screen">
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
align-content: flex-start !important;
gap: 10px !important;
padding: 20px;
overflow-y: auto;
}
#divAdminNav {
display: flex;
flex-direction: column;
gap: 4px;
border-bottom: 1px solid var(--muted-color);
margin-bottom: 10px;
padding-bottom: 10px;
}
.adminNavItem {
border: 1px solid var(--muted-color);
padding: 8px;
cursor: pointer;
font-size: 80%;
user-select: none;
}
.adminNavItem:hover {
border-color: var(--accent-color);
color: var(--accent-color);
}
.adminNavItem.active {
border-color: var(--primary-color);
background: var(--muted-color);
font-weight: bold;
}
.adminSection {
display: none;
border: 2px solid var(--primary-color);
padding: 10px;
background: var(--secondary-color);
}
.adminSection.active {
display: block;
}
.adminSectionHeader {
font-family: pageKanji;
font-size: 120%;
margin-bottom: 10px;
border-bottom: 2px solid var(--muted-color);
padding-bottom: 5px;
}
.adminGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.adminCard {
border: 1px solid var(--muted-color);
padding: 10px;
}
.adminCardTitle {
font-weight: bold;
margin-bottom: 8px;
font-size: 90%;
}
.adminValue {
font-size: 80%;
white-space: pre-wrap;
word-break: break-word;
}
.adminTable {
width: 100%;
border-collapse: collapse;
font-size: 80%;
}
.adminTable th,
.adminTable td {
border: 1px solid var(--muted-color);
padding: 6px;
text-align: left;
vertical-align: top;
}
.adminTable th {
background: var(--primary-color);
color: var(--secondary-color);
}
.adminRow {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
align-items: center;
}
.adminInput,
.adminTextArea,
.adminSelect {
border: 1px solid var(--primary-color);
background: var(--secondary-color);
color: var(--primary-color);
padding: 8px;
font-family: var(--font-family);
font-size: 80%;
min-width: 220px;
}
.adminTextArea {
width: 100%;
min-height: 120px;
resize: vertical;
}
.adminBtn {
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
padding: 8px 12px;
cursor: pointer;
font-family: var(--font-family);
font-size: 80%;
}
.adminBtn:hover {
background: var(--accent-color);
color: var(--secondary-color);
border-color: var(--accent-color);
}
.adminLog {
border: 1px solid var(--muted-color);
min-height: 120px;
max-height: 320px;
overflow: auto;
font-size: 75%;
padding: 8px;
white-space: pre-wrap;
word-break: break-word;
}
.mono {
font-family: monospace;
}
</style>
</head>
<body>
<div id="divSvgHam" class="divHeaderButtons"></div>
<div id="divHeader">
<div id="divHeaderFlexLeft"></div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText">C-Relay-PG Admin</div>
</div>
<div id="divHeaderFlexRight"></div>
</div>
<div id="divBody">
<div id="section-statistics" class="adminSection active">
<div class="adminSectionHeader">Statistics</div>
<div class="adminRow">
<button class="adminBtn" id="btnRefreshStats">Refresh Monitoring</button>
</div>
<div class="adminGrid">
<div class="adminCard">
<div class="adminCardTitle">CPU Metrics (kind 24567)</div>
<div id="statsCpu" class="adminValue mono">Waiting for monitoring events...</div>
</div>
<div class="adminCard">
<div class="adminCardTitle">Time Stats (kind 24567)</div>
<div id="statsTime" class="adminValue mono">Waiting for monitoring events...</div>
</div>
<div class="adminCard">
<div class="adminCardTitle">Top Pubkeys (kind 24567)</div>
<div id="statsTop" class="adminValue mono">Waiting for monitoring events...</div>
</div>
<div class="adminCard">
<div class="adminCardTitle">Event Kinds (kind 24567)</div>
<div id="statsKinds" class="adminValue mono">Waiting for monitoring events...</div>
</div>
</div>
</div>
<div id="section-subscriptions" class="adminSection">
<div class="adminSectionHeader">Subscriptions</div>
<div class="adminLog mono" id="subscriptionLog">No subscription details received yet.</div>
</div>
<div id="section-configuration" class="adminSection">
<div class="adminSectionHeader">Configuration</div>
<div class="adminRow">
<button class="adminBtn" id="btnConfigRefresh">Fetch Config</button>
</div>
<div class="adminRow">
<input class="adminInput" id="cfgKey" placeholder="config key" />
<input class="adminInput" id="cfgValue" placeholder="config value" />
<button class="adminBtn" id="btnConfigSet">Set Config</button>
</div>
<div class="adminLog mono" id="configLog">Configuration responses appear here.</div>
</div>
<div id="section-authorization" class="adminSection">
<div class="adminSectionHeader">Authorization</div>
<div class="adminRow">
<input class="adminInput mono" id="authPubkey" placeholder="pubkey hex" />
<button class="adminBtn" id="btnWhitelist">Add Whitelist</button>
<button class="adminBtn" id="btnBlacklist">Add Blacklist</button>
<button class="adminBtn" id="btnAuthQuery">Auth Query</button>
</div>
<div class="adminLog mono" id="authLog">Authorization responses appear here.</div>
</div>
<div id="section-ip-bans" class="adminSection">
<div class="adminSectionHeader">IP Bans</div>
<div class="adminRow">
<input class="adminInput" id="banIp" placeholder="IP address" />
<input class="adminInput" id="banDuration" placeholder="duration sec (e.g. 86400)" value="86400" />
<button class="adminBtn" id="btnBanIp">Ban IP</button>
<button class="adminBtn" id="btnIpQuery">Query IP Bans</button>
</div>
<div class="adminLog mono" id="ipBanLog">IP ban responses appear here.</div>
</div>
<div id="section-relay-events" class="adminSection">
<div class="adminSectionHeader">Relay Events</div>
<div class="adminGrid">
<div class="adminCard">
<div class="adminCardTitle">Publish Kind 0 Metadata</div>
<div class="adminRow"><input class="adminInput" id="kind0Name" placeholder="name" /></div>
<div class="adminRow"><textarea class="adminTextArea" id="kind0About" placeholder="about"></textarea></div>
<div class="adminRow"><button class="adminBtn" id="btnPublishKind0">Publish Kind 0</button></div>
</div>
<div class="adminCard">
<div class="adminCardTitle">Live Feed (excluding kinds 23456/23457/24567)</div>
<div class="adminLog mono" id="liveEventLog">Waiting for relay events...</div>
</div>
</div>
</div>
<div id="section-dm" class="adminSection">
<div class="adminSectionHeader">DM</div>
<div class="adminRow">
<textarea class="adminTextArea" id="dmMessage" placeholder="Send admin command as DM payload text"></textarea>
</div>
<div class="adminRow">
<button class="adminBtn" id="btnSendDm">Send NIP-17 DM</button>
</div>
<div class="adminLog mono" id="dmLog">DM activity appears here.</div>
</div>
<div id="section-database" class="adminSection">
<div class="adminSectionHeader">Database Query</div>
<div class="adminRow">
<textarea class="adminTextArea" id="sqlQuery" placeholder="SELECT * FROM events LIMIT 10"></textarea>
</div>
<div class="adminRow">
<button class="adminBtn" id="btnRunSql">Run SQL Query</button>
</div>
<div class="adminLog mono" id="sqlLog">SQL responses appear here.</div>
</div>
<div class="adminSection active">
<div class="adminSectionHeader">System</div>
<div class="adminTableWrap">
<table class="adminTable">
<tbody>
<tr><th>Relay WS</th><td id="sysRelayWs" class="mono"></td></tr>
<tr><th>Relay HTTP</th><td id="sysRelayHttp" class="mono"></td></tr>
<tr><th>Relay Pubkey</th><td id="sysRelayPubkey" class="mono">Loading...</td></tr>
<tr><th>User Pubkey</th><td id="sysUserPubkey" class="mono">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="adminSection active">
<div class="adminSectionHeader">Boot Diagnostics</div>
<div id="bootLog" class="adminLog mono">Boot log initialized.</div>
</div>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<div id="divSideNav">
<div id="divSideNavHeader"></div>
<div id="divSideNavBody">
<div id="divAdminNav" class="sidenavSection">
<div class="sidenavSectionTitle">Admin Pages</div>
<div class="sidenavSectionList">
<div class="adminNavItem active" data-section="statistics">Statistics</div>
<div class="adminNavItem" data-section="subscriptions">Subscriptions</div>
<div class="adminNavItem" data-section="configuration">Configuration</div>
<div class="adminNavItem" data-section="authorization">Authorization</div>
<div class="adminNavItem" data-section="ip-bans">IP Bans</div>
<div class="adminNavItem" data-section="relay-events">Relay Events</div>
<div class="adminNavItem" data-section="dm">DM</div>
<div class="adminNavItem" data-section="database">Database Query</div>
</div>
</div>
<div id="divFiles"></div>
</div>
<div id="divAiSection" class="sidenavSection">
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
<div id="divAiList" class="sidenavSectionList">
<div id="divAiProvidersList">No saved providers yet.</div>
</div>
</div>
<div id="divRelaySection">
<div id="divRelaySectionTitle">リレー</div>
<div id="divRelayList">Loading relays...</div>
</div>
<div id="divBlossomSection">
<div id="divBlossomSectionTitle">ブロッサム</div>
<div id="divBlossomList">Loading blossom servers...</div>
</div>
<div id="divVersionBar">
<span id="versionDisplay">v0.0.1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
<div id="themeToggleHamburgerContainer"></div>
</button>
<button id="logoutButton" title="Logout">
<div id="logoutHamburgerContainer"></div>
</button>
</div>
</div>
</div>
<script src="./nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<script type="module">
import {
initNDKPage,
getPubkey, injectHeaderAvatar,
subscribe,
publishEvent,
disconnect,
getVersion,
updateVersionDisplay,
getUserSettings,
patchUserSettings,
onUserSettings
} from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
const ADMIN_RELAY_WS_URL = 'wss://relay.laantungir.net';
const ADMIN_RELAY_HTTP_URL = 'https://relay.laantungir.net';
const SUBSCRIPTION_ONLY_MODE = true;
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[c-relay-pg.html ${VERSION}] Loading...`);
let updateIntervalId = null;
let currentPubkey = null;
let isAuthenticated = false;
let authMode = 'required';
let authedPageInitialized = false;
let relayActivityListenersBound = false;
let hamburgerInstance = null;
let isNavOpen = false;
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
let pageSettings = {};
let unsubscribeUserSettings = null;
// Admin-specific state (additive to template)
let relayPubkey = '';
let adminInitialized = false;
let adminUiBound = false;
const adminSubscriptions = [];
const divBody = document.getElementById("divBody");
const divSideNav = document.getElementById("divSideNav");
const divSideNavBody = document.getElementById("divSideNavBody");
const divFooterCenter = document.getElementById("divFooterCenter");
const divFooterRight = document.getElementById("divFooterRight");
const MAX_LOG_LINES = 200;
function appendLog(id, msg) {
const el = document.getElementById(id);
if (!el) return;
const ts = new Date().toISOString();
const next = `[${ts}] ${msg}`;
const lines = (el.textContent || '').split('\n').filter(Boolean);
lines.unshift(next);
if (lines.length > MAX_LOG_LINES) lines.length = MAX_LOG_LINES;
el.textContent = lines.join('\n');
}
function setSystemInfo() {
const wsEl = document.getElementById('sysRelayWs');
const httpEl = document.getElementById('sysRelayHttp');
const relayPkEl = document.getElementById('sysRelayPubkey');
const userPkEl = document.getElementById('sysUserPubkey');
if (wsEl) wsEl.textContent = ADMIN_RELAY_WS_URL;
if (httpEl) httpEl.textContent = ADMIN_RELAY_HTTP_URL;
if (relayPkEl) relayPkEl.textContent = relayPubkey || 'Unavailable';
if (userPkEl) userPkEl.textContent = currentPubkey || 'Unavailable';
}
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
if (!logoutHamburger) {
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
logoutHamburger.animateTo('x');
}
if (!themeToggleHamburger) {
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
const savedTheme = localStorage.getItem('theme');
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
const initialShape = isDarkMode ? 'moon' : 'circle';
themeToggleHamburger.animateTo(initialShape);
}
}
function closeNav() {
divSideNav.style.width = "0vw";
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) {
hamburgerInstance.animateTo('burger');
}
}
function toggleNav() {
if (isNavOpen) {
closeNav();
} else {
openNav();
}
}
function hasTargetPubkeyInUrl() {
const params = new URLSearchParams(window.location.search || '');
const npub = String(params.get('npub') || '').trim();
const pubkey = String(params.get('pubkey') || '').trim();
return Boolean(npub || pubkey);
}
function resolveAuthModeFromUrl() {
const params = new URLSearchParams(window.location.search || '');
const explicitAuth = String(params.get('auth') || '').trim().toLowerCase();
if (explicitAuth === 'required' || explicitAuth === 'optional' || explicitAuth === 'none') {
return explicitAuth;
}
if (hasTargetPubkeyInUrl()) {
return 'optional';
}
return 'required';
}
function isAuthRequiredError(error) {
const message = String(error?.message || error || '').toLowerCase();
return message.includes('authentication required');
}
async function initializeAuthentication(mode) {
if (mode === 'required') {
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
return;
}
if (mode === 'none') {
isAuthenticated = false;
currentPubkey = null;
return;
}
try {
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
} catch (error) {
if (isAuthRequiredError(error)) {
console.log('[c-relay-pg.html] Optional auth mode: continuing unauthenticated');
isAuthenticated = false;
currentPubkey = null;
return;
}
throw error;
}
}
async function fetchRelayInfo() {
const resp = await fetch(ADMIN_RELAY_HTTP_URL, {
method: 'GET',
headers: { 'Accept': 'application/nostr+json' }
});
if (!resp.ok) throw new Error(`NIP-11 fetch failed: ${resp.status}`);
const info = await resp.json();
if (!info.pubkey) throw new Error('NIP-11 missing relay pubkey');
relayPubkey = String(info.pubkey);
setSystemInfo();
return info;
}
async function encryptForRelay(content) {
if (!window.nostr?.nip44?.encrypt) {
throw new Error('window.nostr.nip44.encrypt not available');
}
return await window.nostr.nip44.encrypt(relayPubkey, content);
}
async function decryptFromRelay(content) {
if (!window.nostr?.nip44?.decrypt) {
throw new Error('window.nostr.nip44.decrypt not available');
}
return await window.nostr.nip44.decrypt(relayPubkey, content);
}
async function sendAdminCommand(commandArray, sink = 'configLog') {
if (!relayPubkey) throw new Error('relay pubkey unavailable');
const encrypted = await encryptForRelay(JSON.stringify(commandArray));
const event = {
created_at: Math.floor(Date.now() / 1000),
kind: 23456,
tags: [['p', relayPubkey]],
content: encrypted
};
appendLog(sink, `CMD -> ${JSON.stringify(commandArray)}`);
await publishEvent(event);
}
function handleMonitoringEvent(event) {
const dTag = (event.tags || []).find((t) => t && t[0] === 'd');
const d = dTag ? dTag[1] : 'unknown';
if (SUBSCRIPTION_ONLY_MODE) {
console.log('[c-relay-pg.html] MONITOR EVENT kind=24567 processed', {
dTag: d,
contentLength: typeof event.content === 'string' ? event.content.length : 0
});
}
if (d === 'cpu_metrics') document.getElementById('statsCpu').textContent = event.content;
else if (d === 'time_stats') document.getElementById('statsTime').textContent = event.content;
else if (d === 'top_pubkeys') document.getElementById('statsTop').textContent = event.content;
else if (d === 'event_kinds') document.getElementById('statsKinds').textContent = event.content;
else if (d === 'subscription_details') appendLog('subscriptionLog', event.content);
else appendLog('configLog', `MONITOR ${d}: ${event.content}`);
}
async function handleAdminResponse(event) {
try {
const plain = await decryptFromRelay(event.content);
appendLog('configLog', `RESP <- ${plain}`);
appendLog('authLog', `RESP <- ${plain}`);
appendLog('ipBanLog', `RESP <- ${plain}`);
appendLog('sqlLog', `RESP <- ${plain}`);
appendLog('dmLog', `RESP <- ${plain}`);
} catch (err) {
appendLog('configLog', `Failed to decrypt 23457: ${err.message}`);
}
}
let liveEventCount = 0;
function handleLiveEvent(event) {
if ([23456, 23457, 24567].includes(event.kind)) return;
liveEventCount++;
// sample to keep UI responsive on busy relays
if (liveEventCount % 10 !== 0) return;
const shortId = String(event.id || '').slice(0, 12);
const shortPk = String(event.pubkey || '').slice(0, 12);
appendLog('liveEventLog', `#${liveEventCount} kind=${event.kind} id=${shortId} pubkey=${shortPk}`);
}
function activateSection(sectionKey) {
document.querySelectorAll('.adminNavItem').forEach((item) => {
item.classList.toggle('active', item.dataset.section === sectionKey);
});
document.querySelectorAll('.adminSection').forEach((el) => {
el.classList.remove('active');
});
const target = document.getElementById(`section-${sectionKey}`);
if (target) target.classList.add('active');
}
function bindAdminUi() {
if (adminUiBound) return;
adminUiBound = true;
document.querySelectorAll('.adminNavItem').forEach((item) => {
item.addEventListener('click', () => activateSection(item.dataset.section));
});
document.getElementById('btnRefreshStats').addEventListener('click', async () => {
await sendAdminCommand(['system_command', 'system_status'], 'configLog');
});
document.getElementById('btnConfigRefresh').addEventListener('click', async () => {
await sendAdminCommand(['config_query', 'all'], 'configLog');
});
document.getElementById('btnConfigSet').addEventListener('click', async () => {
const key = document.getElementById('cfgKey').value.trim();
const value = document.getElementById('cfgValue').value.trim();
if (!key) return appendLog('configLog', 'Missing config key');
await sendAdminCommand([key, value], 'configLog');
});
document.getElementById('btnWhitelist').addEventListener('click', async () => {
const pk = document.getElementById('authPubkey').value.trim();
if (!pk) return appendLog('authLog', 'Missing pubkey');
await sendAdminCommand(['whitelist', 'pubkey', pk], 'authLog');
});
document.getElementById('btnBlacklist').addEventListener('click', async () => {
const pk = document.getElementById('authPubkey').value.trim();
if (!pk) return appendLog('authLog', 'Missing pubkey');
await sendAdminCommand(['blacklist', 'pubkey', pk], 'authLog');
});
document.getElementById('btnAuthQuery').addEventListener('click', async () => {
await sendAdminCommand(['auth_query', 'all'], 'authLog');
});
document.getElementById('btnBanIp').addEventListener('click', async () => {
const ip = document.getElementById('banIp').value.trim();
const duration = document.getElementById('banDuration').value.trim() || '86400';
if (!ip) return appendLog('ipBanLog', 'Missing IP');
await sendAdminCommand(['ip_ban', 'add', ip, duration], 'ipBanLog');
});
document.getElementById('btnIpQuery').addEventListener('click', async () => {
await sendAdminCommand(['ip_ban', 'query', 'all'], 'ipBanLog');
});
document.getElementById('btnPublishKind0').addEventListener('click', async () => {
const name = document.getElementById('kind0Name').value.trim();
const about = document.getElementById('kind0About').value.trim();
const event = {
created_at: Math.floor(Date.now() / 1000),
kind: 0,
tags: [],
content: JSON.stringify({ name, about })
};
await publishEvent(event);
appendLog('liveEventLog', 'Published kind 0 metadata');
});
document.getElementById('btnSendDm').addEventListener('click', async () => {
const message = document.getElementById('dmMessage').value.trim();
if (!message) return appendLog('dmLog', 'Missing message');
await sendAdminCommand(['dm', message], 'dmLog');
});
document.getElementById('btnRunSql').addEventListener('click', async () => {
const q = document.getElementById('sqlQuery').value.trim();
if (!q) return appendLog('sqlLog', 'Missing SQL query');
await sendAdminCommand(['sql_query', q], 'sqlLog');
});
}
async function setupAdminSubscriptions() {
const nowMinus60 = Math.floor(Date.now() / 1000) - 60;
const monitoringFilter = SUBSCRIPTION_ONLY_MODE
? { kinds: [24567], since: nowMinus60 }
: { kinds: [24567], authors: [relayPubkey], since: nowMinus60 };
const monitoringOptions = { closeOnEose: false, cacheUsage: 'ONLY_RELAY' };
console.log('[c-relay-pg.html] SUBSCRIBE ATTEMPT kind=24567', {
relayPubkey,
filter: monitoringFilter,
options: monitoringOptions,
mode: SUBSCRIPTION_ONLY_MODE ? 'subscription-only' : 'normal'
});
const monitoringSub = subscribe(monitoringFilter, monitoringOptions);
adminSubscriptions.push(monitoringSub);
console.log('[c-relay-pg.html] SUBSCRIBE RESULT kind=24567', {
success: !!monitoringSub,
hasCloseMethod: !!(monitoringSub && typeof monitoringSub.close === 'function'),
subscription: monitoringSub
});
window.addEventListener('ndkEvent', async (evt) => {
const event = evt.detail;
if (!event) return;
if (event.kind !== 24567) return;
console.log('[c-relay-pg.html] SUBSCRIBE EVENT kind=24567', {
id: event.id,
pubkey: event.pubkey,
created_at: event.created_at,
tagsCount: Array.isArray(event.tags) ? event.tags.length : 0,
contentPreview: typeof event.content === 'string' ? event.content.slice(0, 180) : event.content
});
return handleMonitoringEvent(event);
});
}
async function initializeAdminFeatures() {
if (!isAuthenticated || adminInitialized) return;
if (!SUBSCRIPTION_ONLY_MODE) {
bindAdminUi();
await fetchRelayInfo();
} else {
console.log('[c-relay-pg.html] SUBSCRIPTION_ONLY_MODE active: admin UI actions disabled');
}
await setupAdminSubscriptions();
console.log('[c-relay-pg.html] passive mode enabled: no startup admin publish');
adminInitialized = true;
__crelayBoot('admin features initialized');
}
async function initializeAuthenticatedPageFeatures() {
if (!isAuthenticated || authedPageInitialized) return;
if (SUBSCRIPTION_ONLY_MODE) {
console.log('[c-relay-pg.html] SUBSCRIPTION_ONLY_MODE active: template authed UI features paused');
authedPageInitialized = true;
return;
}
await injectHeaderAvatar(currentPubkey);
console.log('[c-relay-pg.html] Authenticated as:', currentPubkey);
try {
pageSettings = await getUserSettings();
} catch (error) {
console.warn('[c-relay-pg.html] getUserSettings failed:', error);
pageSettings = {};
}
if (!unsubscribeUserSettings) {
unsubscribeUserSettings = onUserSettings((settings) => {
pageSettings = settings || {};
});
}
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
await UpdateFooter();
if (!updateIntervalId) {
updateIntervalId = setInterval(UpdateFooter, 1000);
}
if (!relayActivityListenersBound) {
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
// console.log(`[c-relay-pg.html] Relay activity: ${relayUrl} - ${activity}`, stats);
setRelayActivityState(relayUrl, activity);
});
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'relayActivity') {
const { relayUrl, activity } = event.data;
// console.log(`[c-relay-pg.html] Relay activity: ${relayUrl} - ${activity}`);
setRelayActivityState(relayUrl, activity);
}
});
relayActivityListenersBound = true;
}
authedPageInitialized = true;
}
async function promptLoginIfNeeded() {
if (isAuthenticated) return true;
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
await initializeAuthenticatedPageFeatures();
await initializeAdminFeatures();
return true;
}
const UpdateFooter = async () => {
try {
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
divFooterCenter.innerHTML = '';
divFooterRight.innerHTML = '';
} catch (error) {
console.error('[c-relay-pg.html] Error updating footer:', error);
}
};
const Logout = async () => {
console.log("[c-relay-pg.html] Starting logout process...");
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
adminSubscriptions.forEach((s) => {
try { s.close(); } catch (_) {}
});
disconnect();
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
localStorage.clear();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
window.indexedDB.deleteDatabase(db.name);
}
}
}
console.log("[c-relay-pg.html] Logged out, reloading page");
location.reload(true);
};
(async function main() {
__crelayBoot('main() start');
try {
if (!SUBSCRIPTION_ONLY_MODE) {
initHamburgerMenu();
__crelayBoot('ui initialized');
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) {
divSvgHam.addEventListener('click', toggleNav);
}
const themeToggleButton = document.getElementById('themeToggleButton');
const logoutButton = document.getElementById('logoutButton');
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
isDarkMode = !isDarkMode;
if (isDarkMode) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
window.location.reload();
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
if (!isAuthenticated) {
await promptLoginIfNeeded();
return;
}
await Logout();
} catch (error) {
console.error('Logout/login action failed:', error);
}
});
}
} else {
__crelayBoot('subscription-only mode: template UI runtime disabled');
}
authMode = resolveAuthModeFromUrl();
__crelayBoot(`auth mode: ${authMode}`);
await initializeAuthentication(authMode);
__crelayBoot('initNDK/auth complete');
if (!SUBSCRIPTION_ONLY_MODE) {
await initializeAuthenticatedPageFeatures();
__crelayBoot('template authenticated feature init complete');
} else {
__crelayBoot('template authenticated feature init skipped');
}
await initializeAdminFeatures();
if (!SUBSCRIPTION_ONLY_MODE) {
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
if (sidenavWasOpen === 'true') {
localStorage.removeItem('sidenavWasOpen');
openNav();
}
if (!isAuthenticated && (authMode === 'optional' || authMode === 'none')) {
divFooterCenter.textContent = 'Public mode';
divFooterRight.textContent = 'Sign in from side menu for private features';
}
await updateVersionDisplay();
}
setSystemInfo();
__crelayBoot('initialization complete');
console.log('[c-relay-pg.html] Initialization complete');
} catch (error) {
__crelayBoot(`fatal initialization failure: ${error.message}`, true);
console.error('[c-relay-pg.html] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<div style="font-size: 24px; margin-bottom: 20px; color: red;">Initialization Error</div>
<div style="font-size: 16px; color: #666;">${error.message}</div>
<div style="margin-top: 20px;">
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
</div>
</div>`;
}
})();
</script>
</body>
</html>