11 KiB
C-Relay-PG Admin Page Migration Plan
Overview
Migrate the relay admin dashboard from an embedded HTTP-served page (api/index.html + api/index.js) to a full client-ndk page (c-relay-pg.html) built on the client-ndk template. The page will live in the client-ndk project (~/lt/client-ndk/www/) alongside other pages and communicate with the relay purely through Nostr protocol subscriptions.
Why This Migration
The current /api page is served directly by the relay's lws-main thread, which causes:
- Slow page delivery — HTTP file serving competes with WebSocket event processing on a single thread
- Incomplete page loads — 10-second
timeout_secskills connections when the main thread is busy - 6+ sequential HTTP round-trips — each asset requires a two-phase header/body delivery cycle
- Unnecessary complexity — embedded file generation,
lws_set_wsi_user()overwriting, session data type dispatch
The admin page already communicates entirely via Nostr protocol:
- Kind 23456 → Admin commands (encrypted, sent by admin)
- Kind 23457 → Admin responses (encrypted, sent by relay)
- Kind 24567 → Monitoring/stats events (ephemeral, broadcast by relay)
- NIP-11 → Relay info (fetched via HTTP, independent of file serving)
- NIP-17 → DM admin commands (gift-wrapped)
Moving to client-ndk means the relay serves zero HTML — it only speaks Nostr protocol.
Architecture
graph LR
subgraph client-ndk project
A[c-relay-pg.html] --> B[init-ndk.mjs]
B --> C[NDK SharedWorker]
end
subgraph c-relay-pg
D[WebSocket Server]
E[api-worker thread]
end
C -->|kind 23456 admin cmds| D
D -->|kind 23457 responses| C
E -->|kind 24567 monitoring| D
D -->|kind 24567 events| C
C -->|kind 1059 NIP-17 DMs| D
Current Admin Page Sections
| Section | Data Source | Communication |
|---|---|---|
| Statistics | kind 24567 monitoring events with d-tags: event_kinds, time_stats, top_pubkeys, cpu_metrics | Subscribe to kind 24567 |
| Subscriptions | kind 24567 monitoring events with d-tag: subscription_details | Subscribe to kind 24567 |
| Configuration | kind 23457 admin responses | Send kind 23456 config_query, receive kind 23457 |
| Authorization | kind 23457 admin responses | Send kind 23456 auth rule commands, receive kind 23457 |
| IP Bans | kind 23457 admin responses | Send kind 23456 ip_ban_* commands, receive kind 23457 |
| Relay Events | Live subscription to all events + kind 0/10050/10002 management | Standard Nostr subscriptions |
| DM | NIP-17 gift-wrapped DMs kind 1059 | NIP-17 encrypt/wrap/publish |
| SQL Query | kind 23457 admin responses | Send kind 23456 sql_query, receive kind 23457 |
Design Decisions
- Full client-ndk page — Uses
client.css,init-ndk.mjs,HamburgerMorphing,relay-ui.mjs,blossom-ui.mjs,ai-ui.mjs— all standard template infrastructure stays intact - Sidenav preserved — All existing sidenav sections (Relay, Blossom, AI) remain. Admin page navigation links are added to
divSideNavBodyas clickable items - Page-specific CSS — Follows client-ndk convention: page styles in inline
<style>block, using only CSS variables fromclient.css - Hardcoded relay URL —
wss://relay.laantungir.netfor now; future enhancement to support relay selection - Auth mode: required — Admin page requires authentication
Migration Steps
Step 1: Create c-relay-pg.html from Template
Start from template.html in client-ndk. Customize:
- Title:
C-Relay-PG - Header text:
C-Relay-PG Admin - Auth mode:
required(default) divBodycontent: All 8 admin sections, each in its own container div, alldisplay:noneexcept Statistics (default)divSideNavBody: Add navigation links for each section:<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>
Step 2: Port Admin Communication Layer
Replace SimplePool + nostr-lite.js with NDK equivalents:
| Current index.js | New c-relay-pg.html |
|---|---|
new SimplePool() |
subscribe() from init-ndk.mjs |
relayPool.subscribeMany() |
subscribe(filter, opts) |
relayPool.publish() |
publishEvent(event) |
window.nostr.signEvent() |
Handled by NDK MessageBasedSigner |
window.NostrTools.nip42 |
Handled by NDK NIP-42 support |
window.NostrTools.nip44 |
window.NostrTools.nip44 from nostr.bundle.js — same as before |
nlLite nostr-login-lite |
initNDKPage() + getPubkey() |
Key function — sendAdminCommand(commandArray):
async function sendAdminCommand(commandArray) {
const conversationKey = window.NostrTools.nip44.v2.utils.getConversationKey(
adminPrivateKey, relayPubkey
);
const encrypted = window.NostrTools.nip44.v2.encrypt(
JSON.stringify(commandArray), conversationKey
);
const event = {
created_at: Math.floor(Date.now() / 1000),
kind: 23456,
tags: [['p', relayPubkey]],
content: encrypted
};
return await publishEvent(event);
}
Note: NIP-44 encryption requires the admin's private key. The current page uses window.nostr.nip44.encrypt() which delegates to the browser extension. NDK's publishEvent handles signing, but encryption must be done before publishing. We'll use window.nostr.nip44.encrypt() if available (NIP-07 extension), same as the current page does.
Step 3: Port Subscription Setup
Using NDK's subscribe() and window.addEventListener('ndkEvent', ...):
// Subscribe to admin responses + monitoring events from relay
const adminSub = subscribe(
{ kinds: [23457, 24567], authors: [RELAY_PUBKEY] },
{ closeOnEose: false, cacheUsage: 'ONLY_RELAY' }
);
// Live event feed
const liveSub = subscribe(
{ limit: 50 },
{ closeOnEose: false, cacheUsage: 'ONLY_RELAY' }
);
// Route events to handlers
window.addEventListener('ndkEvent', (e) => {
const evt = e.detail;
if (evt.kind === 23457) processAdminResponse(evt);
else if (evt.kind === 24567) processMonitoringEvent(evt);
else addEventToLiveFeed(evt);
});
Step 4: Port NIP-44 Encryption
Admin commands require NIP-44 encryption. The template already loads nostr.bundle.js which includes window.NostrTools.nip44. Additionally, window.nostr.nip44.encrypt() is available from NIP-07 extensions. Use the same approach as the current page.
Step 5: Port UI Sections into divBody
Each section's HTML goes into divBody as a container div. Page-specific styles go in the inline <style> block following client-ndk conventions:
- Use CSS variables only (no hardcoded colors)
- Tables use
var(--primary-color),var(--muted-color),var(--accent-color) - Buttons follow the
var(--button-*)pattern - Font is always
var(--font-family)
Sections to port:
- Statistics — DB overview table, event kinds table, time stats table, top pubkeys table, event rate chart area
- Subscriptions — Active subscription details table
- Configuration — Config key/value table with inline edit, refresh button
- Authorization — Auth rules table, whitelist/blacklist inputs, WoT level selector
- IP Bans — Ban stats, manual ban form, IP whitelist, ban list table with filters
- Relay Events — Live event feed table, kind 0/10050/10002 management forms
- DM — NIP-17 message textarea + send button, inbox display
- SQL Query — Query dropdown, SQL textarea, execute button, results table
Step 6: Page-Specific CSS
Following client-ndk rules from client.css:
- Override
#divBodylayout:flex-direction: column; overflow-y: auto; padding: 20px; - Admin section containers: hidden by default, shown when nav item clicked
- Table styles: use
var(--primary-color)for headers,var(--muted-color)for borders - Button styles: border + transparent background, hover with
var(--accent-color) - All in inline
<style media="screen">block in<head>
Step 7: Relay URL — Hardcoded for Now
const RELAY_WS_URL = 'wss://relay.laantungir.net';
const RELAY_HTTP_URL = 'https://relay.laantungir.net';
NIP-11 fetch to get relay pubkey:
const nip11 = await fetch(RELAY_HTTP_URL, {
headers: { 'Accept': 'application/nostr+json' }
}).then(r => r.json());
const RELAY_PUBKEY = nip11.pubkey;
Step 8: Admin Verification
Same flow as current page:
- Get relay pubkey from NIP-11
- Send kind 23456
system_statuscommand - If relay responds with kind 23457, user is verified as admin
- Show admin sections; otherwise show access denied
Step 9 (Optional, Later): Remove Embedded HTTP Serving from Relay
After migration is complete and tested:
- Remove
handle_embedded_file_request()fromsrc/api.c - Remove
handle_embedded_file_writeable()fromsrc/api.c - Remove embedded file dispatch from
LWS_CALLBACK_HTTPinsrc/websockets.c - Remove
embed_web_files.shandsrc/embedded_web_content.c - Keep NIP-11 serving (lightweight and required by protocol)
File Changes Summary
New File (in client-ndk project: ~/lt/client-ndk/www/)
c-relay-pg.html— Complete admin page, self-contained with inline<style>and<script type="module">
Files Unchanged
- All relay C code for kind 23456/23457/24567 processing — stays exactly the same
src/dm_admin.c— NIP-17 DM handling stays the samesrc/nip011.c— NIP-11 stays the same- All client-ndk shared files (client.css, init-ndk.mjs, relay-ui.mjs, etc.) — no modifications needed
Files to Modify Later (optional relay cleanup)
src/api.c— Remove embedded file serving functionssrc/websockets.c— Remove embedded file dispatch from HTTP callbackMakefile— Remove embedded web content compilation step
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| NIP-44 encryption via window.nostr.nip44 | Same mechanism as current page — no change in encryption approach |
| NDK SharedWorker relay connection | Use ONLY_RELAY cache usage to ensure direct relay communication |
| Large page size with all sections inline | Sections are display:none by default — no performance impact |
| Event rate chart from text_graph.js | Port chart logic inline or skip initially |
| Relay pubkey needed before subscriptions | Fetch NIP-11 first in initialization sequence |