Compare commits

..

3 Commits

14 changed files with 480 additions and 137 deletions

View File

@@ -121,8 +121,8 @@ fuser -k 8888/tcp
- Event filtering done at C level, not SQL level for NIP-40 expiration
### Configuration Override Behavior
- CLI port override only affects first-time startup
- After database creation, all config comes from events
- CLI port override applies during first-time startup and existing relay restarts
- After database creation, all config comes from events (but CLI overrides can still be applied)
- Database path cannot be changed after initialization
## Non-Obvious Pitfalls

View File

@@ -1109,3 +1109,123 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
border-radius: var(--border-radius);
box-sizing: border-box;
}
/* ================================
SIDE NAVIGATION MENU
================================ */
.side-nav {
position: fixed;
top: 0;
left: -300px;
width: 280px;
height: 100vh;
background: var(--secondary-color);
border-right: var(--border-width) solid var(--border-color);
z-index: 1000;
transition: left 0.3s ease;
overflow-y: auto;
padding-top: 80px;
}
.side-nav.open {
left: 0;
}
.side-nav-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
.side-nav-overlay.show {
display: block;
}
.nav-menu {
list-style: none;
padding: 0;
margin: 0;
}
.nav-menu li {
border-bottom: var(--border-width) solid var(--muted-color);
}
.nav-menu li:last-child {
border-bottom: none;
}
.nav-item {
display: block;
padding: 15px 20px;
color: var(--primary-color);
text-decoration: none;
font-family: var(--font-family);
font-size: 16px;
font-weight: bold;
transition: all 0.2s ease;
cursor: pointer;
border: 2px solid var(--secondary-color);
background: none;
width: 100%;
text-align: left;
}
.nav-item:hover {
border: 2px solid var(--secondary-color);
background:var(--muted-color);
color: var(--accent-color);
}
.nav-item.active {
text-decoration: underline;
padding-left: 16px;
}
.nav-footer {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
padding: 0 20px;
}
.nav-footer-btn {
display: block;
width: 100%;
padding: 12px 20px;
margin-bottom: 8px;
color: var(--primary-color);
border: 1px solid var(--border-color);
border-radius: 4px;
font-family: var(--font-family);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-footer-btn:hover {
background:var(--muted-color);
border-color: var(--accent-color);
}
.nav-footer-btn:last-child {
margin-bottom: 0;
}
.header-title.clickable {
cursor: pointer;
transition: all 0.2s ease;
}
.header-title.clickable:hover {
opacity: 0.8;
}

View File

@@ -9,11 +9,30 @@
</head>
<body>
<!-- Side Navigation Menu -->
<nav class="side-nav" id="side-nav">
<ul class="nav-menu">
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
<li><button class="nav-item" data-page="dm">DM</button></li>
<li><button class="nav-item" data-page="database">Database Query</button></li>
</ul>
<div class="nav-footer">
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
<button class="nav-footer-btn" id="nav-logout-btn">LOGOUT</button>
</div>
</nav>
<!-- Side Navigation Overlay -->
<div class="side-nav-overlay" id="side-nav-overlay"></div>
<!-- Header with title and profile display -->
<div class="section">
<div class="header-content">
<div class="header-title">
<div class="header-title clickable" id="header-title">
<span class="relay-letter" data-letter="R">R</span>
<span class="relay-letter" data-letter="E">E</span>
<span class="relay-letter" data-letter="L">L</span>
@@ -34,10 +53,7 @@
<span id="header-user-name" class="header-user-name">Loading...</span>
</div>
<!-- Logout dropdown -->
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
<button type="button" id="dark-mode-btn" class="logout-btn">🌙 DARK MODE</button>
<button type="button" id="logout-btn" class="logout-btn">LOGOUT</button>
</div>
<!-- Dropdown menu removed - buttons moved to sidebar -->
</div>
</div>

View File

@@ -35,6 +35,10 @@ let statsAutoRefreshInterval = null;
let countdownInterval = null;
let countdownSeconds = 10;
// Side navigation state
let currentPage = 'statistics'; // Default page
let sideNavOpen = false;
// SQL Query state
let pendingSqlQueries = new Map();
@@ -48,8 +52,6 @@ const loginModalContainer = document.getElementById('login-modal-container');
const profileArea = document.getElementById('profile-area');
const headerUserImage = document.getElementById('header-user-image');
const headerUserName = document.getElementById('header-user-name');
const logoutDropdown = document.getElementById('logout-dropdown');
const logoutBtn = document.getElementById('logout-btn');
// Legacy elements (kept for backward compatibility)
const persistentUserName = document.getElementById('persistent-user-name');
@@ -484,36 +486,33 @@ function handleLogoutEvent() {
// Update visibility of admin sections based on login and relay connection status
function updateAdminSectionsVisibility() {
const divConfig = document.getElementById('div_config');
const authRulesSection = document.getElementById('authRulesSection');
const databaseStatisticsSection = document.getElementById('databaseStatisticsSection');
const subscriptionDetailsSection = document.getElementById('subscriptionDetailsSection');
const nip17DMSection = document.getElementById('nip17DMSection');
const sqlQuerySection = document.getElementById('sqlQuerySection');
const shouldShow = isLoggedIn && isRelayConnected;
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
if (databaseStatisticsSection) databaseStatisticsSection.style.display = shouldShow ? 'block' : 'none';
if (subscriptionDetailsSection) subscriptionDetailsSection.style.display = shouldShow ? 'block' : 'none';
if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none';
if (sqlQuerySection) sqlQuerySection.style.display = shouldShow ? 'block' : 'none';
// If logged in and connected, show the current page, otherwise hide all sections
if (shouldShow) {
// Show the current page
switchPage(currentPage);
// Start/stop auto-refresh based on visibility
if (shouldShow && databaseStatisticsSection && databaseStatisticsSection.style.display === 'block') {
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
sendStatsQuery().catch(error => {
console.log('Auto-fetch statistics failed: ' + error.message);
});
// startStatsAutoRefresh(); // DISABLED - using real-time monitoring events instead
// Also load configuration and auth rules automatically when sections become visible
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed: ' + error.message);
});
loadAuthRules().catch(error => {
console.log('Auto-load auth rules failed: ' + error.message);
});
// Load data for the current page
loadCurrentPageData();
} else {
// Hide all sections when not logged in or not connected
const sections = [
'databaseStatisticsSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
'nip17DMSection',
'sqlQuerySection'
];
sections.forEach(sectionId => {
const section = document.getElementById(sectionId);
if (section) {
section.style.display = 'none';
}
});
stopStatsAutoRefresh();
}
@@ -521,6 +520,31 @@ function updateAdminSectionsVisibility() {
updateCountdownDisplay();
}
// Load data for the current page
function loadCurrentPageData() {
switch (currentPage) {
case 'statistics':
// Load statistics immediately (no auto-refresh - using real-time monitoring events)
sendStatsQuery().catch(error => {
console.log('Auto-fetch statistics failed: ' + error.message);
});
break;
case 'configuration':
// Load configuration
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed: ' + error.message);
});
break;
case 'authorization':
// Load auth rules
loadAuthRules().catch(error => {
console.log('Auto-load auth rules failed: ' + error.message);
});
break;
// Other pages don't need initial data loading
}
}
// Show login modal
function showLoginModal() {
if (loginModal && loginModalContainer) {
@@ -553,10 +577,6 @@ function hideProfileFromHeader() {
if (profileArea) {
profileArea.style.display = 'none';
}
// Also hide logout dropdown if visible
if (logoutDropdown) {
logoutDropdown.style.display = 'none';
}
}
// Update login/logout UI visibility (legacy function - kept for backward compatibility)
@@ -2076,45 +2096,8 @@ async function sendConfigUpdateCommand(configObjects) {
// Profile area click handler for logout dropdown
function toggleLogoutDropdown(event) {
if (!logoutDropdown) return;
// Only toggle if clicking on the image, not the text or container
if (event.target === headerUserImage) {
const isVisible = logoutDropdown.style.display === 'block';
logoutDropdown.style.display = isVisible ? 'none' : 'block';
}
}
// Close logout dropdown when clicking outside
document.addEventListener('click', function(event) {
if (profileArea && logoutDropdown && !profileArea.contains(event.target)) {
logoutDropdown.style.display = 'none';
}
});
// Initialize profile area click handler
if (profileArea) {
profileArea.addEventListener('click', toggleLogoutDropdown);
}
// Initialize logout button handler
if (logoutBtn) {
logoutBtn.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent profile area click
logout();
});
}
// Initialize dark mode button handler
const darkModeBtn = document.getElementById('dark-mode-btn');
if (darkModeBtn) {
darkModeBtn.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent profile area click
toggleDarkMode();
});
}
// Profile area click handler removed - dropdown moved to sidebar
// Logout and dark mode buttons are now in the sidebar footer
// Initialize relay pubkey container click handler for clipboard copy
const relayPubkeyContainer = document.getElementById('relay-pubkey-container');
@@ -3988,9 +3971,9 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
// Extract values from periods array
monitoringData.periods.forEach(period => {
if (period.period === '24h') timeStats.last_24h = period.event_count;
else if (period.period === '7d') timeStats.last_7d = period.event_count;
else if (period.period === '30d') timeStats.last_30d = period.event_count;
if (period.period === '24h') timeStats.last_24h = period.count;
else if (period.period === '7d') timeStats.last_7d = period.count;
else if (period.period === '30d') timeStats.last_30d = period.count;
});
populateStatsTime({ time_stats: timeStats });
@@ -4537,9 +4520,9 @@ function toggleDarkMode() {
}
function updateDarkModeButton(isDarkMode) {
const darkModeBtn = document.getElementById('dark-mode-btn');
if (darkModeBtn) {
darkModeBtn.textContent = isDarkMode ? 'LIGHT MODE' : 'DARK MODE';
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
if (navDarkModeBtn) {
navDarkModeBtn.textContent = isDarkMode ? 'LIGHT MODE' : 'DARK MODE';
}
}
@@ -4556,6 +4539,97 @@ function initializeDarkMode() {
}
}
// Side navigation functions
function toggleSideNav() {
const sideNav = document.getElementById('side-nav');
const overlay = document.getElementById('side-nav-overlay');
if (sideNavOpen) {
sideNav.classList.remove('open');
overlay.classList.remove('show');
sideNavOpen = false;
} else {
sideNav.classList.add('open');
overlay.classList.add('show');
sideNavOpen = true;
}
}
function closeSideNav() {
const sideNav = document.getElementById('side-nav');
const overlay = document.getElementById('side-nav-overlay');
sideNav.classList.remove('open');
overlay.classList.remove('show');
sideNavOpen = false;
}
function switchPage(pageName) {
// Update current page
currentPage = pageName;
// Update navigation active state
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-page') === pageName) {
item.classList.add('active');
}
});
// Hide all sections
const sections = [
'databaseStatisticsSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
'nip17DMSection',
'sqlQuerySection'
];
sections.forEach(sectionId => {
const section = document.getElementById(sectionId);
if (section) {
section.style.display = 'none';
}
});
// Show selected section
const pageMap = {
'statistics': 'databaseStatisticsSection',
'subscriptions': 'subscriptionDetailsSection',
'configuration': 'div_config',
'authorization': 'authRulesSection',
'dm': 'nip17DMSection',
'database': 'sqlQuerySection'
};
const targetSectionId = pageMap[pageName];
if (targetSectionId) {
const targetSection = document.getElementById(targetSectionId);
if (targetSection) {
targetSection.style.display = 'block';
}
}
// Special handling for configuration page - ensure config-display is visible and refresh data
if (pageName === 'configuration') {
const configDisplay = document.getElementById('config-display');
if (configDisplay) {
configDisplay.classList.remove('hidden');
}
// Always refresh configuration data when navigating to config page
fetchConfiguration().catch(error => {
console.log('Failed to refresh configuration on page switch: ' + error.message);
});
}
// Close side navigation
closeSideNav();
log(`Switched to page: ${pageName}`, 'INFO');
}
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
@@ -4563,6 +4637,12 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize dark mode
initializeDarkMode();
// Initialize sidebar button text
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
if (navDarkModeBtn) {
navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE';
}
// Start RELAY letter animation
startRelayAnimation();
@@ -4571,6 +4651,9 @@ document.addEventListener('DOMContentLoaded', () => {
initializeEventRateChart();
}, 1000); // Delay to ensure text_graph.js is loaded
// Initialize side navigation
initializeSideNavigation();
// Ensure admin sections are hidden by default on page load
updateAdminSectionsVisibility();
@@ -4581,6 +4664,59 @@ document.addEventListener('DOMContentLoaded', () => {
}, 100);
});
// Initialize side navigation event handlers
function initializeSideNavigation() {
// Header title click handler
const headerTitle = document.getElementById('header-title');
if (headerTitle) {
headerTitle.addEventListener('click', toggleSideNav);
}
// Overlay click handler
const overlay = document.getElementById('side-nav-overlay');
if (overlay) {
overlay.addEventListener('click', closeSideNav);
}
// Navigation item click handlers
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
const pageName = e.target.getAttribute('data-page');
if (pageName) {
switchPage(pageName);
}
});
});
// Footer button handlers
const navDarkModeBtn = document.getElementById('nav-dark-mode-btn');
const navLogoutBtn = document.getElementById('nav-logout-btn');
if (navDarkModeBtn) {
navDarkModeBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleDarkMode();
// Update button text after toggle
setTimeout(() => {
navDarkModeBtn.textContent = document.body.classList.contains('dark-mode') ? 'LIGHT MODE' : 'DARK MODE';
}, 10);
closeSideNav();
});
}
if (navLogoutBtn) {
navLogoutBtn.addEventListener('click', (e) => {
e.stopPropagation();
logout();
closeSideNav();
});
}
// Set initial page
switchPage(currentPage);
}
// ================================
// SQL QUERY FUNCTIONS
// ================================

View File

@@ -1,3 +1,19 @@
#!/bin/bash
# Copy the binary to the deployment location
cp build/c_relay_x86 ~/Storage/c_relay/crelay
# Copy the local service file to systemd
sudo cp systemd/c-relay-local.service /etc/systemd/system/
# Reload systemd daemon to pick up the new service
sudo systemctl daemon-reload
# Enable the service (if not already enabled)
sudo systemctl enable c-relay-local.service
# Restart the service
sudo systemctl restart c-relay-local.service
# Show service status
sudo systemctl status c-relay-local.service --no-pager -l

View File

@@ -133,6 +133,11 @@ if [ -n "$PORT_OVERRIDE" ]; then
fi
fi
# Validate strict port flag (only makes sense with port override)
if [ "$USE_TEST_KEYS" = true ] && [ -z "$PORT_OVERRIDE" ]; then
echo "WARNING: --strict-port is always used with test keys. Consider specifying a custom port with -p."
fi
# Validate debug level if provided
if [ -n "$DEBUG_LEVEL" ]; then
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
@@ -163,6 +168,8 @@ if [ "$HELP" = true ]; then
echo " $0 # Fresh start with random keys"
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
echo " $0 -p 7777 --strict-port # Fail if port 7777 unavailable (no fallback)"
echo " $0 -p 8080 --strict-port -d=3 # Custom port with strict binding and debug"
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
echo " $0 -d=5 # Start with debug level 5 (trace)"
echo " $0 --preserve-database # Preserve existing database and keys"

View File

@@ -85,3 +85,7 @@ sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde
sudo ufw allow 8888/tcp
sudo ufw delete allow 8888/tcp
lsof -i :7777
kill $(lsof -t -i :7777)
kill -9 $(lsof -t -i :7777)

View File

@@ -1 +1 @@
3615986
1018941

View File

@@ -837,26 +837,7 @@ int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_op
// NOTE: Database is already initialized in main.c before calling this function
// Config table should already exist with complete configuration
// Check if CLI overrides need to be applied
int has_overrides = 0;
if (cli_options) {
if (cli_options->port_override > 0) has_overrides = 1;
if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = 1;
if (cli_options->relay_privkey_override[0] != '\0') has_overrides = 1;
}
if (has_overrides) {
// Apply CLI overrides to existing database
DEBUG_INFO("Applying CLI overrides to existing database");
if (apply_cli_overrides_atomic(cli_options) != 0) {
DEBUG_ERROR("Failed to apply CLI overrides to existing database");
return -1;
}
} else {
// No CLI overrides - config table is already available
DEBUG_INFO("No CLI overrides - config table is already available");
}
// CLI overrides will be applied after this function returns in main.c
return 0;
}

File diff suppressed because one or more lines are too long

View File

@@ -315,14 +315,35 @@ int init_database(const char* database_path_override) {
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
// Check config table row count immediately after database open
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
int rc = sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL);
if (rc == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count);
}
sqlite3_finalize(stmt);
} else {
DEBUG_LOG("Config table does not exist yet (first-time startup)");
// Capture and log the actual SQLite error instead of assuming table doesn't exist
const char* err_msg = sqlite3_errmsg(g_db);
DEBUG_LOG("Failed to prepare config table query: %s (error code: %d)", err_msg, rc);
// Check if it's actually a missing table vs other error
if (rc == SQLITE_ERROR) {
// Try to check if config table exists
sqlite3_stmt* check_stmt;
int check_rc = sqlite3_prepare_v2(g_db, "SELECT name FROM sqlite_master WHERE type='table' AND name='config'", -1, &check_stmt, NULL);
if (check_rc == SQLITE_OK) {
int has_table = (sqlite3_step(check_stmt) == SQLITE_ROW);
sqlite3_finalize(check_stmt);
if (has_table) {
DEBUG_LOG("Config table EXISTS but query failed - possible database corruption or locking issue");
} else {
DEBUG_LOG("Config table does not exist yet (first-time startup)");
}
} else {
DEBUG_LOG("Failed to check table existence: %s (error code: %d)", sqlite3_errmsg(g_db), check_rc);
}
}
}
}
// DEBUG_GUARD_END
@@ -1435,7 +1456,7 @@ void print_usage(const char* program_name) {
printf("Options:\n");
printf(" -h, --help Show this help message\n");
printf(" -v, --version Show version information\n");
printf(" -p, --port PORT Override relay port (first-time startup only)\n");
printf(" -p, --port PORT Override relay port (first-time startup and existing relay restarts)\n");
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
printf(" -a, --admin-pubkey KEY Override admin public key (64-char hex or npub)\n");
printf(" -r, --relay-privkey KEY Override relay private key (64-char hex or nsec)\n");
@@ -1445,13 +1466,14 @@ void print_usage(const char* program_name) {
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
printf(" On first startup, keys are automatically generated and printed once.\n");
printf(" Command line options like --port only apply during first-time setup.\n");
printf(" Command line options like --port apply during first-time setup and existing relay restarts.\n");
printf(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\n");
printf("Port Binding:\n");
printf(" Default: Try up to 10 consecutive ports if requested port is busy\n");
printf(" --strict-port: Fail immediately if exact requested port is unavailable\n");
printf(" --strict-port works with any custom port specified via -p or --port\n");
printf("\n");
printf("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name);
@@ -1798,7 +1820,7 @@ int main(int argc, char* argv[]) {
return 1;
}
// Setup existing relay (sets database path and loads config)
// Setup existing relay FIRST (sets database path)
if (startup_existing_relay(relay_pubkey, &cli_options) != 0) {
DEBUG_ERROR("Failed to setup existing relay");
cleanup_configuration_system();
@@ -1811,23 +1833,7 @@ int main(int argc, char* argv[]) {
return 1;
}
// Check config table row count before database initialization
{
sqlite3* temp_db = NULL;
if (sqlite3_open(g_database_path, &temp_db) == SQLITE_OK) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(temp_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
printf(" Config table row count before database initialization: %d\n", row_count);
}
sqlite3_finalize(stmt);
}
sqlite3_close(temp_db);
}
}
// Initialize database with existing database path
// Initialize database with the database path set by startup_existing_relay()
DEBUG_TRACE("Initializing existing database");
if (init_database(g_database_path) != 0) {
DEBUG_ERROR("Failed to initialize existing database");
@@ -1842,6 +1848,20 @@ int main(int argc, char* argv[]) {
}
DEBUG_LOG("Existing database initialized");
// Apply CLI overrides atomically (now that database is initialized)
if (apply_cli_overrides_atomic(&cli_options) != 0) {
DEBUG_ERROR("Failed to apply CLI overrides for existing relay");
cleanup_configuration_system();
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {
free(existing_files[i]);
}
free(existing_files);
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
@@ -2010,8 +2030,8 @@ int main(int argc, char* argv[]) {
// Start WebSocket Nostr relay server (port from configuration)
int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag
// Start WebSocket Nostr relay server (port from CLI override or configuration)
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
// Cleanup
cleanup_relay_info();

View File

@@ -10,10 +10,10 @@
#define MAIN_H
// Version information (auto-updated by build system)
#define VERSION "v0.7.35"
#define VERSION "v0.7.38"
#define VERSION_MAJOR 0
#define VERSION_MINOR 7
#define VERSION_PATCH 35
#define VERSION_PATCH 38
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"

View File

@@ -25,6 +25,9 @@ int validate_timestamp_range(long since, long until, char* error_message, size_t
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
// Forward declaration for monitoring function
void monitoring_on_subscription_change(void);
// Global database variable
extern sqlite3* g_db;

View File

@@ -0,0 +1,40 @@
[Unit]
Description=C Nostr Relay Server (Local Development)
Documentation=https://github.com/your-repo/c-relay
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=teknari
WorkingDirectory=/home/teknari/Storage/c_relay
Environment=DEBUG_LEVEL=0
ExecStart=/home/teknari/Storage/c_relay/crelay --port 7777 --debug-level=$DEBUG_LEVEL
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=c-relay-local
# Security settings (relaxed for local development)
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/home/teknari/Storage/c_relay
PrivateTmp=true
# Network security
PrivateNetwork=false
RestrictAddressFamilies=AF_INET AF_INET6
# Resource limits
LimitNOFILE=65536
LimitNPROC=4096
# Event-based configuration system
# No environment variables needed - all configuration is stored as Nostr events
# Database files (<relay_pubkey>.db) are created automatically in WorkingDirectory
# Admin keys are generated and displayed only during first startup
[Install]
WantedBy=multi-user.target