12 KiB
Relay Admin Page Migration Plan
Goal
Migrate the relay admin functionality from the internally-served api/index.html + api/index.js (served by the C relay binary) to an externally-hosted relay-admin.html page in the client-ndk project. This page uses the NDK SharedWorker architecture (template.html pattern) and communicates with the relay via kind 23456/23457/24567 Nostr events.
Current State
Working Reference: c-relay-pg.html
- Successfully receives kind 24567 monitoring events
- Has
SUBSCRIPTION_ONLY_MODE = true— admin UI actions are disabled - Contains all admin section HTML (Statistics, Subscriptions, Configuration, Authorization, IP Bans, Relay Events, DM, Database Query)
- Has sidenav with admin page links
- Has
sendAdminCommand(),encryptForRelay(),decryptFromRelay(),fetchRelayInfo()functions - Has
handleMonitoringEvent()andhandleAdminResponse()event handlers
Target File: relay-admin.html
- Currently an exact copy of
template.html - Has full template infrastructure: hamburger menu, sidenav, footer, auth system, relay-ui, blossom-ui, ai-ui
- Has the newer
injectHeaderLoginButtonimport (slightly newer than c-relay-pg.html) - Empty
divBody— ready for admin content
Legacy Internal Pages: api/index.html + api/index.js
- 6,779 lines of JavaScript — the full admin UI
- Uses
nostr-tools SimplePooldirectly (not NDK) - Uses
NOSTR_LOGIN_LITEfor auth (not NDK SharedWorker) - Has rich UI: config tables with inline editing, IP ban management with filters, SQL query console with history, WoT management, relay event forms
- All admin commands go through
sendAdminCommand()→ kind 23456 with NIP-44 encryption - Responses come back as kind 23457 events, decrypted and routed to handlers
Architecture Comparison
graph TD
subgraph Legacy - api/index.html
A1[NOSTR_LOGIN_LITE auth] --> A2[SimplePool direct WS]
A2 --> A3[Kind 23456 admin commands]
A2 --> A4[Kind 24567 monitoring sub]
A3 --> A5[Kind 23457 responses]
end
subgraph New - relay-admin.html
B1[NDK SharedWorker auth] --> B2[NDK subscribe/publishEvent]
B2 --> B3[Kind 23456 admin commands]
B2 --> B4[Kind 24567 monitoring sub]
B3 --> B5[Kind 23457 responses]
end
Key Difference: NDK vs SimplePool
| Aspect | Legacy api/index.js | New relay-admin.html |
|---|---|---|
| Auth | NOSTR_LOGIN_LITE direct |
initNDKPage() via SharedWorker |
| Subscriptions | SimplePool.subscribeMany() |
subscribe() from init-ndk.mjs |
| Publishing | SimplePool.publish() + manual signing |
publishEvent() — auto-signed by worker |
| Encryption | window.nostr.nip44.encrypt/decrypt |
Same — window.nostr.nip44.encrypt/decrypt |
| Event listening | SimplePool callbacks | window.addEventListener('ndkEvent', ...) |
| Relay URL | Auto-derived from page URL | Configurable constant ADMIN_RELAY_WS_URL |
Migration Strategy
The approach is to merge the admin functionality from c-relay-pg.html into relay-admin.html, removing the SUBSCRIPTION_ONLY_MODE restriction and enabling full admin UI. We use the NDK publishEvent() and subscribe() APIs instead of SimplePool.
What stays from template (relay-admin.html already has)
- Full hamburger menu + sidenav infrastructure
- Footer with relay status animations
- Auth system with required/optional/none modes
- Relay-ui, blossom-ui, ai-ui sidenav sections
- Theme toggle, logout, version display
What gets added from c-relay-pg.html
- Admin nav items in sidenav
- Admin section HTML panels in divBody
- Admin-specific CSS styles
- Relay pubkey fetch via NIP-11
- NIP-44 encrypt/decrypt helpers
sendAdminCommand()for kind 23456- Kind 24567 monitoring subscription
- Kind 23457 response handling with per-section routing
- All admin UI button handlers
- Boot diagnostics panel
- System info panel
Detailed Implementation Steps
Step 1: Add Admin Nav Links to Sidenav
The admin links go directly inside divSideNavBody as simple styled items — not inside a collapsible .sidenavSection container. They should be always-visible, non-collapsible links that match the font/styling of the sidenav sections but remain in the open body area. Place them before divFiles:
<div id="divSideNavBody">
<!-- Admin nav links - always visible, not collapsible -->
<div id="divAdminNav">
<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 id="divFiles"></div>
</div>
The .adminNavItem styling uses the same font-family and sizing as sidenav section content (font-size: 70%, font-family: var(--font-family)) with a simple border-bottom separator, hover accent color, and bold+background for the active state. No section title header, no collapse toggle icon.
Step 2: Add Admin Section HTML Panels
Port the section HTML from c-relay-pg.html into divBody. Each section follows the pattern:
<div id="section-{name}" class="adminSection">
<div class="adminSectionHeader">{Title}</div>
<!-- Section-specific content -->
</div>
Sections to port:
- Statistics — CPU metrics, time stats, top pubkeys, event kinds cards
- Subscriptions — Subscription details log
- Configuration — Config key/value inputs, fetch/set buttons, log
- Authorization — Pubkey input, whitelist/blacklist/query buttons, log
- IP Bans — IP input, duration, ban/query buttons, log
- Relay Events — Kind 0 metadata form, live event feed
- DM — Message textarea, send button, log
- Database Query — SQL textarea, run button, log
- System (always visible) — Relay WS/HTTP URLs, relay/user pubkeys
- Boot Diagnostics (always visible) — Boot log
Step 3: Add Admin CSS
Port the admin-specific styles from c-relay-pg.html <style> block. These use CSS variables from client.css so they integrate with the theme system:
#divBodyflex layout overrides.adminNavItemstyles with hover/active states.adminSectionshow/hide with.activeclass.adminGrid,.adminCard,.adminTablelayout.adminInput,.adminTextArea,.adminBtnform elements.adminLogscrollable log panels
Step 4: Add Admin State Variables
const ADMIN_RELAY_WS_URL = 'wss://relay.laantungir.net';
const ADMIN_RELAY_HTTP_URL = 'https://relay.laantungir.net';
let relayPubkey = '';
let adminInitialized = false;
let adminUiBound = false;
const adminSubscriptions = [];
const MAX_LOG_LINES = 200;
Step 5: Add Core Admin Functions
Port from c-relay-pg.html:
appendLog(id, msg)— Prepend timestamped messages to log panelssetSystemInfo()— Populate system info tablefetchRelayInfo()— NIP-11 fetch to get relay pubkeyencryptForRelay(content)— NIP-44 encrypt usingwindow.nostr.nip44.encryptdecryptFromRelay(content)— NIP-44 decrypt usingwindow.nostr.nip44.decryptsendAdminCommand(commandArray, sink)— Encrypt + publish kind 23456 event viapublishEvent()
Step 6: Add Subscription Setup
async function setupAdminSubscriptions() {
// Subscribe to kind 24567 monitoring events
const monitoringSub = subscribe(
{ kinds: [24567], since: Math.floor(Date.now() / 1000) - 60 },
{ closeOnEose: false, cacheUsage: 'ONLY_RELAY' }
);
// Subscribe to kind 23457 admin responses
const responseSub = subscribe(
{ kinds: [23457], '#p': [currentPubkey], since: Math.floor(Date.now() / 1000) - 60 },
{ closeOnEose: false, cacheUsage: 'ONLY_RELAY' }
);
// Listen for events via ndkEvent window event
window.addEventListener('ndkEvent', async (evt) => {
const event = evt.detail;
if (event.kind === 24567) handleMonitoringEvent(event);
else if (event.kind === 23457) await handleAdminResponse(event);
else handleLiveEvent(event);
});
}
Step 7: Add Event Handlers
Port from c-relay-pg.html:
handleMonitoringEvent(event)— Route d-tag values to stat cardshandleAdminResponse(event)— Decrypt kind 23457 and route to logshandleLiveEvent(event)— Sample non-admin events to live feedactivateSection(sectionKey)— Toggle active section visibilitybindAdminUi()— Wire up all button click handlers
Step 8: Add Button Handlers
All handlers follow the same pattern: gather input → sendAdminCommand(array, logId):
- btnRefreshStats →
['system_command', 'system_status'] - btnConfigRefresh →
['config_query', 'all'] - btnConfigSet →
[key, value] - btnWhitelist →
['whitelist', 'pubkey', pk] - btnBlacklist →
['blacklist', 'pubkey', pk] - btnAuthQuery →
['auth_query', 'all'] - btnBanIp →
['ip_ban', 'add', ip, duration] - btnIpQuery →
['ip_ban', 'query', 'all'] - btnPublishKind0 → Direct
publishEvent()with kind 0 - btnSendDm →
['dm', message] - btnRunSql →
['sql_query', query]
Step 9: Integration into main() Flow
(async function main() {
// 1. Template init (hamburger, theme, auth)
initHamburgerMenu();
// ... existing template setup ...
// 2. Auth
await initializeAuthentication(authMode);
await initializeAuthenticatedPageFeatures();
// 3. Admin init (NEW)
await initializeAdminFeatures();
// 4. Template finalization
await updateVersionDisplay();
})();
Where initializeAdminFeatures():
async function initializeAdminFeatures() {
if (!isAuthenticated || adminInitialized) return;
bindAdminUi();
await fetchRelayInfo();
await setupAdminSubscriptions();
setSystemInfo();
adminInitialized = true;
}
Response Routing Enhancement
The current c-relay-pg.html broadcasts all responses to all logs. For a better UX, we should parse the decrypted response and route to the appropriate log:
async function handleAdminResponse(event) {
const plain = await decryptFromRelay(event.content);
const parsed = JSON.parse(plain);
// Route based on query_type
if (parsed.query_type === 'config_all' || parsed.query_type === 'config_update') {
appendLog('configLog', plain);
} else if (parsed.query_type === 'auth_query' || parsed.query_type === 'auth_rule') {
appendLog('authLog', plain);
} else if (parsed.query_type === 'sql_query') {
appendLog('sqlLog', plain);
} else if (parsed.query_type === 'ip_ban') {
appendLog('ipBanLog', plain);
} else {
// Fallback: broadcast to all
appendLog('configLog', `RESP <- ${plain}`);
}
}
Files Modified
| File | Location | Change |
|---|---|---|
relay-admin.html |
~/lt/client-ndk/www/ |
Add admin HTML, CSS, and JavaScript |
Files NOT Modified
template.html— Stays as the clean templatec-relay-pg.html— Stays as the debug/test referenceapi/index.html/api/index.js— Legacy, stays for now (still served by relay binary)- No C code changes needed — the relay already handles kind 23456/23457/24567
Future Enhancements (Out of Scope)
- Rich config table with inline editing (like api/index.html has)
- IP ban table with filter tabs
- SQL query history dropdown
- WoT management section
- Event rate chart visualization
- Admin verification handshake