Files
c-relay-pg/plans/c_relay_pg_html_migration_plan.md

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_secs kills 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

  1. 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
  2. Sidenav preserved — All existing sidenav sections (Relay, Blossom, AI) remain. Admin page navigation links are added to divSideNavBody as clickable items
  3. Page-specific CSS — Follows client-ndk convention: page styles in inline <style> block, using only CSS variables from client.css
  4. Hardcoded relay URLwss://relay.laantungir.net for now; future enhancement to support relay selection
  5. 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)
  • divBody content: All 8 admin sections, each in its own container div, all display:none except 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:

  1. Statistics — DB overview table, event kinds table, time stats table, top pubkeys table, event rate chart area
  2. Subscriptions — Active subscription details table
  3. Configuration — Config key/value table with inline edit, refresh button
  4. Authorization — Auth rules table, whitelist/blacklist inputs, WoT level selector
  5. IP Bans — Ban stats, manual ban form, IP whitelist, ban list table with filters
  6. Relay Events — Live event feed table, kind 0/10050/10002 management forms
  7. DM — NIP-17 message textarea + send button, inbox display
  8. SQL Query — Query dropdown, SQL textarea, execute button, results table

Step 6: Page-Specific CSS

Following client-ndk rules from client.css:

  • Override #divBody layout: 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:

  1. Get relay pubkey from NIP-11
  2. Send kind 23456 system_status command
  3. If relay responds with kind 23457, user is verified as admin
  4. 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() from src/api.c
  • Remove handle_embedded_file_writeable() from src/api.c
  • Remove embedded file dispatch from LWS_CALLBACK_HTTP in src/websockets.c
  • Remove embed_web_files.sh and src/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 same
  • src/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 functions
  • src/websockets.c — Remove embedded file dispatch from HTTP callback
  • Makefile — 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