Files
c-relay/api/index.html
2025-09-16 15:52:27 -04:00

1200 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C-Relay Admin API</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background-color: white;
color: black;
line-height: 1.4;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
border-bottom: 2px solid black;
padding-bottom: 10px;
margin-bottom: 30px;
font-weight: normal;
font-size: 24px;
}
h2 {
margin: 30px 0 15px 0;
font-weight: normal;
border-left: 4px solid black;
padding-left: 10px;
font-size: 16px;
}
.section {
border: 1px solid black;
padding: 20px;
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
input,
textarea,
select,
button {
width: 100%;
padding: 8px;
border: 1px solid black;
font-family: 'Courier New', monospace;
font-size: 14px;
background-color: white;
color: black;
}
button {
background-color: black;
color: white;
cursor: pointer;
margin: 5px 0;
font-weight: bold;
}
button:hover {
background-color: #333;
}
button:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border: 1px solid black;
font-weight: bold;
}
.status.connected {
background-color: black;
color: white;
}
.status.disconnected {
background-color: white;
color: black;
}
.status.authenticated {
background-color: black;
color: white;
}
.status.error {
background-color: white;
color: black;
border: 2px solid black;
}
.config-table {
border: 1px solid black;
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
.config-table th,
.config-table td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
.config-table th {
background-color: black;
color: white;
font-weight: bold;
}
.json-display {
background-color: white;
border: 1px solid black;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
}
.log-panel {
height: 200px;
overflow-y: auto;
border: 1px solid black;
padding: 10px;
font-size: 12px;
background-color: white;
}
.log-entry {
margin-bottom: 5px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.log-timestamp {
font-weight: bold;
}
.inline-buttons {
display: flex;
gap: 10px;
}
.inline-buttons button {
flex: 1;
}
.user-info {
padding: 10px;
border: 1px solid black;
margin: 10px 0;
background-color: white;
}
.user-pubkey {
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
margin: 5px 0;
}
.hidden {
display: none;
}
#login-section {
text-align: center;
padding: 20px;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.inline-buttons {
flex-direction: column;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>C-RELAY ADMIN API</h1>
<!-- Testing Section - Always Visible -->
<div class="section">
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
<div class="input-group">
<label for="relay-url">Relay URL:</label>
<input type="text" id="relay-url" value="ws://localhost:8888" placeholder="ws://localhost:8888">
</div>
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
<div id="config-display" class="hidden">
<div id="config-view-mode">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
<div class="inline-buttons">
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
</div>
</div>
<div id="config-edit-mode" class="hidden">
<h3>Edit Configuration</h3>
<div id="config-form" class="section">
<!-- Dynamic form will be generated here -->
</div>
<div class="inline-buttons">
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
<button type="button" id="cancel-edit-btn">CANCEL</button>
</div>
</div>
<div id="config-raw-display">
<h3>Raw Event JSON:</h3>
<div class="json-display" id="raw-config-json"></div>
</div>
</div>
</div>
<!-- Login Section -->
<div id="login-section">
<div class="section">
<h2>NOSTR AUTHENTICATION</h2>
<p>Please login with your Nostr identity to access the admin interface.</p>
<!-- nostr-lite login UI will be injected here -->
</div>
</div>
<!-- Main Interface (hidden until logged in) -->
<div id="main-interface" class="hidden">
<!-- User Info Section -->
<div class="section">
<h2>LOGGED IN USER</h2>
<div class="user-info">
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
<div><strong>Public Key:</strong>
<div class="user-pubkey" id="user-pubkey">Loading...</div>
</div>
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
</div>
<button type="button" id="logout-btn">LOGOUT</button>
</div>
<!-- Command Section -->
<div class="section">
<h2>ADMIN COMMANDS</h2>
<div class="input-group">
<label for="command-type">Command Type:</label>
<select id="command-type">
<option value="">Select command type...</option>
<option value="update_config">Update Configuration</option>
<option value="restart_relay">Restart Relay</option>
<option value="get_status">Get Status</option>
</select>
</div>
<div class="input-group">
<label for="command-payload">Command Payload (JSON):</label>
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
</div>
<div class="input-group">
<label>Event Preview:</label>
<div class="json-display" id="event-preview">No event constructed</div>
</div>
<button type="button" id="send-command-btn">SEND COMMAND</button>
</div>
<!-- Log Section -->
<div class="section">
<h2>EVENT LOG</h2>
<div class="log-panel" id="log-panel">
<div class="log-entry">
<span class="log-timestamp">SYSTEM:</span> Interface loaded. Login and connect to relay to begin.
</div>
</div>
<button type="button" id="clear-log-btn">CLEAR LOG</button>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
<!-- <script src="../nostr_login_lite/lite/nostr.bundle.js"></script> -->
<!-- Load NOSTR_LOGIN_LITE main library -->
<!-- <script src="../nostr_login_lite/lite/nostr-lite.js"></script> -->
<!-- Load the official nostr-tools bundle (still needed for login and profile loading) -->
<script src="./nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library -->
<script src="./nostr-lite.js"></script>
<script>
// Global error handler to prevent page refreshes
window.addEventListener('error', function (e) {
console.error('Global error caught:', e.error);
console.error('Error message:', e.message);
console.error('Error filename:', e.filename);
console.error('Error line:', e.lineno);
e.preventDefault(); // Prevent default browser error handling
return true; // Prevent page refresh
});
window.addEventListener('unhandledrejection', function (e) {
console.error('Unhandled promise rejection:', e.reason);
e.preventDefault(); // Prevent default browser error handling
return true; // Prevent page refresh
});
// Global state
let nlLite = null;
let userPubkey = null;
let isLoggedIn = false;
let currentConfig = null;
let relayPool = null;
// DOM elements
const loginSection = document.getElementById('login-section');
const mainInterface = document.getElementById('main-interface');
const userName = document.getElementById('user-name');
const userPubkeyDisplay = document.getElementById('user-pubkey');
const userAbout = document.getElementById('user-about');
const logoutBtn = document.getElementById('logout-btn');
const relayUrl = document.getElementById('relay-url');
const relayStatus = document.getElementById('relay-status');
const fetchConfigBtn = document.getElementById('fetch-config-btn');
const configStatus = document.getElementById('config-status');
const configDisplay = document.getElementById('config-display');
const configViewMode = document.getElementById('config-view-mode');
const configEditMode = document.getElementById('config-edit-mode');
const configTableBody = document.getElementById('config-table-body');
const configForm = document.getElementById('config-form');
const rawConfigJson = document.getElementById('raw-config-json');
const copyConfigBtn = document.getElementById('copy-config-btn');
const editConfigBtn = document.getElementById('edit-config-btn');
const saveConfigBtn = document.getElementById('save-config-btn');
const cancelEditBtn = document.getElementById('cancel-edit-btn');
const commandType = document.getElementById('command-type');
const commandPayload = document.getElementById('command-payload');
const eventPreview = document.getElementById('event-preview');
const sendCommandBtn = document.getElementById('send-command-btn');
const logPanel = document.getElementById('log-panel');
const clearLogBtn = document.getElementById('clear-log-btn');
// Utility functions
function log(message, type = 'INFO') {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logMessage = `${timestamp} [${type}]: ${message}`;
// Always log to browser console so we don't lose logs on refresh
console.log(logMessage);
// Also log to UI if elements exist
if (logPanel) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
logEntry.innerHTML = `<span class="log-timestamp">${timestamp} [${type}]:</span> ${message}`;
logPanel.appendChild(logEntry);
logPanel.scrollTop = logPanel.scrollHeight;
}
}
// Initialize NOSTR_LOGIN_LITE
async function initializeApp() {
try {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
icon: '', // Clean display without icon placeholders
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
getUserInfo: true, // Enable profile fetching
getUserRelay: [ // Specific relays for profile fetching
'wss://relay.laantungir.net'
]
}
});
nlLite = window.NOSTR_LOGIN_LITE;
console.log('Nostr login system initialized');
// Listen for authentication events
window.addEventListener('nlMethodSelected', handleAuthEvent);
} catch (error) {
console.log('Failed to initialize Nostr login: ' + error.message);
}
}
// Handle authentication events
function handleAuthEvent(event) {
const { pubkey, method, error } = event.detail;
if (method && pubkey) {
userPubkey = pubkey;
isLoggedIn = true;
console.log(`Login successful! Method: ${method}`);
console.log(`Public key: ${pubkey}`);
showMainInterface();
loadUserProfile();
// Automatically fetch configuration after login
setTimeout(() => {
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed: ' + error.message);
});
}, 1000);
console.log('Login successful. Auto-fetching configuration...');
} else if (error) {
console.log(`Authentication error: ${error}`);
}
}
// Show main interface after login
function showMainInterface() {
loginSection.classList.add('hidden');
mainInterface.classList.remove('hidden');
userPubkeyDisplay.textContent = userPubkey;
}
// Load user profile using nostr-tools pool
async function loadUserProfile() {
if (!userPubkey) return;
console.log('Loading user profile...');
userName.textContent = 'Loading...';
userAbout.textContent = 'Loading...';
try {
// Create a SimplePool instance
relayPool = new window.NostrTools.SimplePool();
const relays = ['wss://relay.laantungir.net'];
// Get profile event (kind 0) for the user
const events = await relayPool.querySync(relays, {
kinds: [0],
authors: [userPubkey],
limit: 1
});
if (events.length > 0) {
const profile = JSON.parse(events[0].content);
displayProfile(profile);
} else {
userName.textContent = 'Anonymous User';
userAbout.textContent = 'No profile found';
}
} catch (error) {
console.log('Profile loading failed: ' + error.message);
userName.textContent = 'Error loading profile';
userAbout.textContent = error.message;
}
}
// Display profile data
function displayProfile(profile) {
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
const about = profile.about || 'No description provided';
userName.textContent = name;
userAbout.textContent = about;
console.log(`Profile loaded for: ${name}`);
}
// Logout function
async function logout() {
console.log('Logging out...');
try {
// Clean up profile relay pool
if (relayPool) {
const relays = ['wss://relay.laantungir.net'];
relayPool = null;
}
// Clean up configuration WebSocket
if (configWebSocket) {
console.log('Closing configuration WebSocket...');
configWebSocket.close();
configWebSocket = null;
subscriptionId = null;
}
await nlLite.logout();
userPubkey = null;
isLoggedIn = false;
currentConfig = null;
// Reset UI
mainInterface.classList.add('hidden');
loginSection.classList.remove('hidden');
updateConfigStatus(false);
relayStatus.textContent = 'READY TO FETCH';
relayStatus.className = 'status disconnected';
console.log('Logged out successfully');
} catch (error) {
console.log('Logout failed: ' + error.message);
}
}
function updateConfigStatus(loaded) {
if (loaded) {
configStatus.textContent = 'CONFIGURATION LOADED';
configStatus.className = 'status connected';
configDisplay.classList.remove('hidden');
} else {
configStatus.textContent = 'NO CONFIGURATION LOADED';
configStatus.className = 'status disconnected';
configDisplay.classList.add('hidden');
}
}
// Global subscription state
let configWebSocket = null;
let subscriptionId = null;
// Generate random subscription ID
function generateSubId() {
return Math.random().toString(36).substring(2, 15);
}
// Configuration subscription using direct WebSocket connection
async function subscribeToConfiguration() {
try {
console.log('=== STARTING DIRECT WEBSOCKET CONFIGURATION SUBSCRIPTION ===');
if (!isLoggedIn) {
console.log('WARNING: Not logged in, but proceeding with subscription test');
}
const url = relayUrl.value.trim();
if (!url) {
console.error('Please enter a relay URL');
return false;
}
console.log(`Connecting to relay via WebSocket: ${url}`);
relayStatus.textContent = 'CONNECTING...';
relayStatus.className = 'status connected';
// Clean up existing connection
if (configWebSocket) {
console.log('Closing existing WebSocket connection');
configWebSocket.close();
configWebSocket = null;
}
// Try different URL formats if the first one fails
const urlsToTry = [url];
if (url.includes('127.0.0.1')) {
urlsToTry.push(url.replace('127.0.0.1', 'localhost'));
} else if (url.includes('localhost')) {
urlsToTry.push(url.replace('localhost', '127.0.0.1'));
}
console.log('URLs to try:', urlsToTry);
// Create WebSocket connection
configWebSocket = new WebSocket(urlsToTry[0]);
subscriptionId = generateSubId();
console.log(`Generated subscription ID: ${subscriptionId}`);
console.log('WebSocket readyState after creation:', configWebSocket.readyState);
return new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => {
console.error('WebSocket connection timeout');
relayStatus.textContent = 'CONNECTION TIMEOUT';
relayStatus.className = 'status error';
if (configWebSocket) {
configWebSocket.close();
}
reject(new Error('Connection timeout'));
}, 10000); // 10 second timeout
configWebSocket.onopen = function (event) {
console.log('WebSocket connection established');
clearTimeout(timeoutId);
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
relayStatus.className = 'status connected';
// Send REQ message to subscribe to kind 33334 events
const reqMessage = [
"REQ",
subscriptionId,
{
"kinds": [33334],
"limit": 50
}
];
console.log('Sending REQ message:', JSON.stringify(reqMessage));
configWebSocket.send(JSON.stringify(reqMessage));
// Set up another timeout for receiving EOSE
timeoutId = setTimeout(() => {
console.log('No EOSE received within timeout, but connection is active');
relayStatus.textContent = 'SUBSCRIBED - WAITING FOR EVENTS';
relayStatus.className = 'status connected';
}, 5000);
resolve(true);
};
configWebSocket.onmessage = function (event) {
try {
const message = JSON.parse(event.data);
console.log('Received message:', message);
if (Array.isArray(message)) {
const [messageType, subId, eventData] = message;
if (messageType === "EVENT" && subId === subscriptionId) {
console.log('=== CONFIGURATION EVENT RECEIVED VIA WEBSOCKET ===');
console.log('Event data:', eventData);
console.log('Event kind:', eventData.kind);
console.log('Event tags:', eventData.tags);
console.log('Event pubkey:', eventData.pubkey);
console.log('=== END EVENT UPDATE ===');
// Display the configuration event
displayConfiguration(eventData);
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
} else if (messageType === "EOSE" && subId === subscriptionId) {
clearTimeout(timeoutId);
console.log('EOSE received - End of stored events');
console.log('Current config after EOSE:', currentConfig);
if (!currentConfig) {
console.log('No configuration events were received');
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
configStatus.className = 'status error';
relayStatus.textContent = 'SUBSCRIBED - NO EVENTS FOUND';
relayStatus.className = 'status error';
} else {
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
}
} else if (messageType === "NOTICE") {
console.log('Received NOTICE:', eventData || message[1]);
} else if (messageType === "OK") {
console.log('Received OK response:', message);
}
}
} catch (parseError) {
console.error('Error parsing message:', parseError);
console.log('Raw message data:', event.data);
}
};
configWebSocket.onerror = function (error) {
clearTimeout(timeoutId);
console.error('WebSocket error:', error);
console.error('WebSocket URL that failed:', urlsToTry[0]);
console.error('WebSocket readyState on error:', configWebSocket?.readyState);
console.error('Error event details:', {
type: error.type,
target: error.target,
timeStamp: error.timeStamp
});
relayStatus.textContent = 'WEBSOCKET ERROR - Check Console';
relayStatus.className = 'status error';
reject(new Error(`WebSocket connection error to ${urlsToTry[0]}`));
};
configWebSocket.onclose = function (event) {
clearTimeout(timeoutId);
console.log('WebSocket connection closed:', event.code, event.reason);
relayStatus.textContent = 'CONNECTION CLOSED';
relayStatus.className = 'status error';
configWebSocket = null;
subscriptionId = null;
};
});
} catch (error) {
console.error('Configuration subscription failed:', error.message);
console.error('Configuration subscription failed:', error);
console.error('Error stack:', error.stack);
relayStatus.textContent = 'SUBSCRIPTION FAILED';
relayStatus.className = 'status error';
return false;
}
}
// Legacy function name for compatibility - now calls subscription
async function fetchConfiguration() {
return await subscribeToConfiguration();
}
function displayConfiguration(event) {
try {
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
console.log('Event received for display:', event);
currentConfig = event;
// Clear existing table
configTableBody.innerHTML = '';
// Display basic event info
const basicInfo = [
['Event ID', event.id],
['Public Key', event.pubkey],
['Created At', new Date(event.created_at * 1000).toISOString()],
['Kind', event.kind],
['Content', event.content]
];
console.log(`Adding ${basicInfo.length} basic info rows`);
basicInfo.forEach(([key, value]) => {
const row = document.createElement('tr');
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
configTableBody.appendChild(row);
});
// Display tags
console.log(`Processing ${event.tags.length} tags`);
event.tags.forEach(tag => {
if (tag.length >= 2) {
const row = document.createElement('tr');
row.innerHTML = `<td>${tag[0]}</td><td>${tag[1]}</td><td>-</td>`;
configTableBody.appendChild(row);
}
});
// Display raw JSON
rawConfigJson.textContent = JSON.stringify(event, null, 2);
console.log('Configuration display completed successfully');
updateConfigStatus(true);
} catch (error) {
console.error('Error in displayConfiguration:', error.message);
console.error('Display configuration error:', error);
}
}
// Configuration editing functions
function generateConfigForm(event) {
if (!event || !event.tags) {
console.log('No configuration event to edit');
return;
}
configForm.innerHTML = '';
// Define field types and validation for different config parameters
const fieldTypes = {
'auth_enabled': 'boolean',
'nip42_auth_required_events': 'boolean',
'nip42_auth_required_subscriptions': 'boolean',
'nip40_expiration_enabled': 'boolean',
'nip40_expiration_strict': 'boolean',
'nip40_expiration_filter': 'boolean',
'relay_port': 'number',
'max_connections': 'number',
'pow_min_difficulty': 'number',
'nip42_challenge_expiration': 'number',
'nip40_expiration_grace_period': 'number',
'max_subscriptions_per_client': 'number',
'max_total_subscriptions': 'number',
'max_filters_per_subscription': 'number',
'max_event_tags': 'number',
'max_content_length': 'number',
'max_message_length': 'number',
'default_limit': 'number',
'max_limit': 'number'
};
const descriptions = {
'relay_pubkey': 'Relay Public Key (Read-only)',
'auth_enabled': 'Enable Authentication',
'nip42_auth_required_events': 'Require Auth for Events',
'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions',
'nip42_auth_required_kinds': 'Auth Required Event Kinds',
'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)',
'relay_port': 'Relay Port',
'max_connections': 'Maximum Connections',
'relay_description': 'Relay Description',
'relay_contact': 'Relay Contact',
'relay_software': 'Relay Software URL',
'relay_version': 'Relay Version',
'pow_min_difficulty': 'Minimum PoW Difficulty',
'pow_mode': 'PoW Mode',
'nip40_expiration_enabled': 'Enable Event Expiration',
'nip40_expiration_strict': 'Strict Expiration Mode',
'nip40_expiration_filter': 'Filter Expired Events',
'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)',
'max_subscriptions_per_client': 'Max Subscriptions per Client',
'max_total_subscriptions': 'Max Total Subscriptions',
'max_filters_per_subscription': 'Max Filters per Subscription',
'max_event_tags': 'Max Event Tags',
'max_content_length': 'Max Content Length',
'max_message_length': 'Max Message Length',
'default_limit': 'Default Query Limit',
'max_limit': 'Maximum Query Limit'
};
// Process configuration tags
const configData = {};
event.tags.forEach(tag => {
if (tag.length >= 2 && tag[0] !== 'd') { // Skip 'd' tag (relay identifier)
configData[tag[0]] = tag[1];
}
});
// Create form fields for each configuration parameter
Object.entries(configData).forEach(([key, value]) => {
const fieldType = fieldTypes[key] || 'text';
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const fieldGroup = document.createElement('div');
fieldGroup.className = 'input-group';
const label = document.createElement('label');
label.textContent = description;
label.setAttribute('for', `config-${key}`);
let input;
if (fieldType === 'boolean') {
input = document.createElement('select');
input.innerHTML = `
<option value="true" ${value === 'true' ? 'selected' : ''}>true</option>
<option value="false" ${value === 'false' ? 'selected' : ''}>false</option>
`;
} else if (fieldType === 'number') {
input = document.createElement('input');
input.type = 'number';
input.value = value;
input.min = '0';
} else {
input = document.createElement('input');
input.type = 'text';
input.value = value;
}
input.id = `config-${key}`;
input.name = key;
// Make relay_pubkey read-only
if (key === 'relay_pubkey' || key === 'd') {
input.disabled = true;
}
fieldGroup.appendChild(label);
fieldGroup.appendChild(input);
configForm.appendChild(fieldGroup);
});
console.log('Configuration form generated');
}
function enterEditMode() {
if (!currentConfig) {
console.log('No configuration loaded to edit');
return;
}
generateConfigForm(currentConfig);
configViewMode.classList.add('hidden');
configEditMode.classList.remove('hidden');
console.log('Entered edit mode');
}
function exitEditMode() {
configViewMode.classList.remove('hidden');
configEditMode.classList.add('hidden');
configForm.innerHTML = '';
console.log('Exited edit mode');
}
async function saveConfiguration() {
if (!isLoggedIn || !userPubkey) {
console.log('Must be logged in to save configuration');
return;
}
if (!currentConfig) {
console.log('No current configuration to update');
return;
}
try {
console.log('Building new configuration event...');
// Collect form data
const formData = new FormData();
const formInputs = configForm.querySelectorAll('input, select');
const newTags = [];
// Preserve the 'd' tag (relay identifier) from original event
const dTag = currentConfig.tags.find(tag => tag[0] === 'd');
if (dTag) {
newTags.push(dTag);
}
// Add updated configuration tags
formInputs.forEach(input => {
if (!input.disabled && input.name) {
newTags.push([input.name, input.value]);
}
});
// Create new kind 33334 event
const newEvent = {
kind: 33334,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: newTags,
content: currentConfig.content || 'C Nostr Relay Configuration'
};
console.log('Signing event with window.nostr...');
// Sign the event using window.nostr (NIP-07 interface)
const signedEvent = await window.nostr.signEvent(newEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed - no signature returned');
}
console.log('Event signed successfully');
console.log('Signed event:', signedEvent);
// Publish the event to the relay
await publishConfigurationEvent(signedEvent);
} catch (error) {
console.log('Configuration save failed: ' + error.message);
console.error('Save configuration error:', error);
}
}
async function publishConfigurationEvent(signedEvent) {
try {
const url = relayUrl.value.trim();
if (!url) {
throw new Error('No relay URL specified');
}
console.log('Publishing configuration event via WebSocket...');
// Create a new WebSocket connection for publishing
const publishWs = new WebSocket(url);
return new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => {
console.error('Publish timeout');
publishWs.close();
reject(new Error('Publish timeout'));
}, 10000);
publishWs.onopen = function () {
console.log('Publish WebSocket connected, sending event...');
// Send EVENT message
const eventMessage = ["EVENT", signedEvent];
console.log('Sending EVENT message:', JSON.stringify(eventMessage));
publishWs.send(JSON.stringify(eventMessage));
};
publishWs.onmessage = function (event) {
try {
const message = JSON.parse(event.data);
console.log('Publish response:', message);
if (Array.isArray(message)) {
const [messageType, eventId, success, errorMsg] = message;
if (messageType === "OK") {
clearTimeout(timeoutId);
publishWs.close();
if (success) {
console.log('Configuration published successfully!');
console.log('The updated configuration should appear automatically via subscription');
// Exit edit mode
exitEditMode();
resolve(true);
} else {
console.error('Publish rejected:', errorMsg);
reject(new Error(`Publish rejected: ${errorMsg}`));
}
}
}
} catch (parseError) {
console.error('Error parsing publish response:', parseError);
}
};
publishWs.onerror = function (error) {
clearTimeout(timeoutId);
console.error('Publish WebSocket error:', error);
publishWs.close();
reject(new Error('Publish WebSocket error'));
};
publishWs.onclose = function () {
clearTimeout(timeoutId);
};
});
} catch (error) {
console.log('Configuration publish failed: ' + error.message);
console.error('Publish error:', error);
throw error;
}
}
function updateEventPreview() {
const type = commandType.value;
const payload = commandPayload.value.trim();
if (!type || !userPubkey) {
eventPreview.textContent = 'No event constructed';
return;
}
const event = {
kind: 1,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', 'RELAY_PUBLIC_KEY'],
['command', type]
],
content: payload || '{}',
id: 'EVENT_ID_PLACEHOLDER',
sig: 'SIGNATURE_PLACEHOLDER'
};
eventPreview.textContent = JSON.stringify(event, null, 2);
}
// Event handlers
logoutBtn.addEventListener('click', logout);
fetchConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
fetchConfiguration().catch(error => {
console.log('Manual fetch configuration failed: ' + error.message);
});
});
copyConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
if (currentConfig) {
navigator.clipboard.writeText(JSON.stringify(currentConfig, null, 2))
.then(() => console.log('Configuration copied to clipboard'))
.catch(err => console.log('Failed to copy: ' + err.message));
}
});
editConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
enterEditMode();
});
saveConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
saveConfiguration().catch(error => {
console.log('Save configuration failed: ' + error.message);
});
});
cancelEditBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
exitEditMode();
});
commandType.addEventListener('change', updateEventPreview);
commandPayload.addEventListener('input', updateEventPreview);
sendCommandBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const type = commandType.value;
if (!type) {
console.log('Please select a command type');
return;
}
console.log(`Command sending not yet implemented: ${type}`);
});
clearLogBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
});
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
setTimeout(initializeApp, 100);
});
</script>
</body>
</html>