Compare commits

...
13 Commits
Author SHA1 Message Date
Your Name 927659ece1 v1.2.49 - Fix: MEM% bar format (bar then MB), move CPU Core row after CPU Usage 2026-02-25 07:37:50 -04:00
Your Name b6ff4150b4 v1.2.48 - Add CPU% and MEM% ASCII bar graphs to API page stats 2026-02-25 07:33:06 -04:00
Your Name 63bc526163 v1.2.47 - Fix build: use getter function for g_connection_count (was static, can't extern) 2026-02-25 07:24:17 -04:00
Your Name a1f712236a v1.2.46 - Fix: move extern g_connection_count to file scope in api.c 2026-02-25 07:22:14 -04:00
Your Name 06e6c17b7b v1.2.45 - Add WebSocket Connections to system status: api.c sends active_connections, HTML+JS display it 2026-02-25 07:18:42 -04:00
Your Name 0751a7c55c v1.2.44 - IP Bans: show idle+auth bans in stats/status, fix filter buttons, add idle ban columns to query 2026-02-25 07:12:07 -04:00
Your Name f1728932a9 v1.2.43 - IP Bans page: fix filter buttons, add whitelist management UI, compact table rows 2026-02-25 07:08:20 -04:00
Your Name ef8bdef2a8 v1.2.42 - IP Bans page: fix filter buttons, add whitelist management UI, compact table rows 2026-02-25 07:08:12 -04:00
Your Name c11a8ba292 v1.2.41 - Fix IP Bans page: route ip_bans SQL responses to handleIpBansResponse 2026-02-25 06:59:22 -04:00
Your Name 0f124fe575 v1.2.40 - IP Bans page: show whitelisted IPs with star icon, hide Unban button for whitelisted IPs 2026-02-25 06:54:35 -04:00
Your Name 6b20452fab v1.2.39 - Fix IP Bans page: convert rows+columns SQL response format to objects for display 2026-02-25 06:53:50 -04:00
Your Name 10c19bc243 v1.2.38 - Add idle_ban_whitelist config: comma-separated IPs that are never idle-banned 2026-02-25 06:48:41 -04:00
Your Name 3d7aa2196f v1.2.37 - Fix: executeSqlQueryRaw uses relayPool/sendAdminCommand instead of undefined pool variable 2026-02-25 06:43:29 -04:00
10 changed files with 265 additions and 72 deletions
+13
View File
@@ -1486,3 +1486,16 @@ body.dark-mode .admin-verification-content {
font-size: 12px;
}
/* ================================
IP BANS TABLE - compact rows
================================ */
#ip-bans-table td, #ip-bans-table th {
padding: 4px 8px;
line-height: 1.3;
font-size: 13px;
}
#ip-bans-table button {
padding: 2px 8px;
font-size: 12px;
}
+23 -4
View File
@@ -112,6 +112,10 @@
<td>Process ID</td>
<td id="process-id">-</td>
</tr>
<tr>
<td>WebSocket Connections</td>
<td id="websocket-connections">-</td>
</tr>
<tr>
<td>Active Subscriptions</td>
<td id="active-subscriptions">-</td>
@@ -120,14 +124,14 @@
<td>Memory Usage</td>
<td id="memory-usage">-</td>
</tr>
<tr>
<td>CPU Core</td>
<td id="cpu-core">-</td>
</tr>
<tr>
<td>CPU Usage</td>
<td id="cpu-usage">-</td>
</tr>
<tr>
<td>CPU Core</td>
<td id="cpu-core">-</td>
</tr>
<tr>
<td>Oldest Event</td>
<td id="oldest-event">-</td>
@@ -435,6 +439,21 @@ WEB OF TRUST
<div id="add-ban-status" class="status-message"></div>
</div>
<!-- Whitelist Management -->
<div class="input-group">
<h3>IP Whitelist (Never Banned)</h3>
<p style="font-size:13px;opacity:0.8;">IPs in this list are never idle-banned. Comma-separated.</p>
<div class="form-group">
<label for="whitelist-ip-input">Add IP to Whitelist:</label>
<input type="text" id="whitelist-ip-input" placeholder="103.81.231.220">
</div>
<div class="inline-buttons">
<button type="button" id="add-whitelist-btn">ADD TO WHITELIST</button>
</div>
<div id="whitelist-status" class="status-message"></div>
<div id="whitelist-current" style="margin-top:8px;font-size:13px;"></div>
</div>
<!-- Filter Controls -->
<div class="input-group">
<div class="inline-buttons">
+164 -57
View File
@@ -4492,20 +4492,38 @@ function updateStatsFromCpuMonitoringEvent(monitoringData) {
if (monitoringData.process_id !== undefined) {
updateStatsCell('process-id', monitoringData.process_id.toString());
}
if (monitoringData.active_connections !== undefined) {
updateStatsCell('websocket-connections', monitoringData.active_connections.toString());
}
if (monitoringData.memory_usage_mb !== undefined) {
updateStatsCell('memory-usage', monitoringData.memory_usage_mb.toFixed(1) + ' MB');
}
// MEM% bar using memory_percent from server
if (monitoringData.memory_percent !== undefined) {
const memPct = Math.min(100, Math.max(0, monitoringData.memory_percent));
updateStatsCell('memory-usage', makeAsciiBar(memPct) + ' ' + monitoringData.memory_usage_mb.toFixed(1) + ' MB');
}
if (monitoringData.current_cpu_core !== undefined) {
updateStatsCell('cpu-core', 'Core ' + monitoringData.current_cpu_core);
}
// Calculate CPU usage percentage if we have the data
// CPU% using delta between samples
if (monitoringData.process_cpu_time !== undefined && monitoringData.system_cpu_time !== undefined) {
// For now, just show the raw process CPU time (simplified)
// In a real implementation, you'd calculate deltas over time
updateStatsCell('cpu-usage', monitoringData.process_cpu_time + ' ticks');
if (window._prevCpuSample) {
const procDelta = monitoringData.process_cpu_time - window._prevCpuSample.proc;
const sysDelta = monitoringData.system_cpu_time - window._prevCpuSample.sys;
if (sysDelta > 0) {
const cpuPct = Math.min(100, Math.max(0, (procDelta / sysDelta) * 100));
updateStatsCell('cpu-usage', makeAsciiBar(cpuPct));
}
}
window._prevCpuSample = {
proc: monitoringData.process_cpu_time,
sys: monitoringData.system_cpu_time
};
}
} catch (error) {
@@ -4895,6 +4913,13 @@ function formatTimestamp(timestamp) {
return date.toLocaleString();
}
// Generate ASCII progress bar: [###############.....] 75%
function makeAsciiBar(pct) {
const filled = Math.round(pct / 5); // 20 chars = 100%
const empty = 20 - filled;
return '[' + '#'.repeat(filled) + '.'.repeat(empty) + '] ' + pct.toFixed(1) + '%';
}
// Update statistics cell with flash animation if value changed
function updateStatsCell(cellId, newValue) {
const cell = document.getElementById(cellId);
@@ -5568,6 +5593,12 @@ function handleSqlQueryResponse(response) {
console.log('=== HANDLING SQL QUERY RESPONSE ===');
console.log('Response:', response);
// Route IP bans queries to the IP bans handler
if (response.query && response.query.includes('ip_bans')) {
handleIpBansResponse(response);
return;
}
// Always display SQL query results when received
displaySqlQueryResults(response);
@@ -6196,36 +6227,23 @@ function initializeRelayEvents() {
// ================================
let ipBansFilter = 'all'; // 'all', 'banned', 'expired'
let ipBansCachedResults = null; // Cache last fetched results for client-side filtering
// Load IP bans from database
async function loadIpBans() {
const query = "SELECT ip, failure_count, ban_count, banned_until, first_failure, has_authed_successfully, last_success_at, total_connections, total_failures, total_successes, first_seen FROM ip_bans ORDER BY banned_until DESC, total_failures DESC";
const query = "SELECT ip, failure_count, ban_count, banned_until, first_failure, has_authed_successfully, last_success_at, total_connections, total_failures, total_successes, first_seen, idle_failure_count, idle_ban_count, idle_banned_until FROM ip_bans ORDER BY MAX(banned_until, idle_banned_until) DESC, total_failures DESC";
await executeSqlQueryRaw(query, 'ip_bans_load');
}
// Execute SQL query and handle IP bans response
async function executeSqlQueryRaw(query, queryId) {
if (!pool || pool.length === 0) {
if (!relayPool) {
log('Not connected to relay', 'ERROR');
return;
}
const relay = pool[0];
if (!relay) {
log('No relay connection available', 'ERROR');
return;
}
// Create a kind 23456 event with SQL query
const event = {
kind: 23456,
content: query,
tags: [['t', 'sql_query'], ['id', queryId]],
created_at: Math.floor(Date.now() / 1000)
};
try {
await relay.publish(event);
await sendAdminCommand(['sql_query', query, queryId]);
log('Loading IP bans...', 'INFO');
} catch (error) {
log('Failed to load IP bans: ' + error.message, 'ERROR');
@@ -6234,7 +6252,22 @@ async function executeSqlQueryRaw(query, queryId) {
// Handle IP bans SQL response
function handleIpBansResponse(responseData) {
if (!responseData.results || responseData.results.length === 0) {
// Convert rows+columns format to array of objects
let results = [];
if (responseData._cachedResults) {
results = responseData._cachedResults;
} else if (responseData.rows && responseData.columns) {
const cols = responseData.columns;
results = responseData.rows.map(row => {
const obj = {};
cols.forEach((col, i) => { obj[col] = row[i]; });
return obj;
});
} else if (responseData.results) {
results = responseData.results;
}
if (results.length === 0) {
document.getElementById('ip-bans-tbody').innerHTML = '<tr><td colspan="7" style="text-align: center;">No IP bans found</td></tr>';
document.getElementById('ip-bans-total').textContent = '0';
document.getElementById('ip-bans-active').textContent = '0';
@@ -6243,24 +6276,26 @@ function handleIpBansResponse(responseData) {
}
const now = Math.floor(Date.now() / 1000);
let totalIPs = responseData.results.length;
let totalIPs = results.length;
let currentlyBanned = 0;
let totalBans = 0;
// Helper: is this row currently banned (auth or idle)
const isRowBanned = row => (row.banned_until > now) || (row.idle_banned_until > now);
const isRowExpired = row => !isRowBanned(row) && ((row.ban_count > 0) || (row.idle_ban_count > 0));
// Filter results based on current filter
let filteredResults = responseData.results;
let filteredResults = results;
if (ipBansFilter === 'banned') {
filteredResults = responseData.results.filter(row => row.banned_until > now);
filteredResults = results.filter(isRowBanned);
} else if (ipBansFilter === 'expired') {
filteredResults = responseData.results.filter(row => row.banned_until <= now && row.banned_until > 0);
filteredResults = results.filter(isRowExpired);
}
// Calculate stats
responseData.results.forEach(row => {
totalBans += parseInt(row.ban_count || 0);
if (row.banned_until > now) {
currentlyBanned++;
}
// Calculate stats (include both auth and idle bans)
results.forEach(row => {
totalBans += parseInt(row.ban_count || 0) + parseInt(row.idle_ban_count || 0);
if (isRowBanned(row)) currentlyBanned++;
});
// Update stats
@@ -6268,25 +6303,44 @@ function handleIpBansResponse(responseData) {
document.getElementById('ip-bans-active').textContent = currentlyBanned;
document.getElementById('ip-bans-issued').textContent = totalBans;
// Cache results for client-side filtering
ipBansCachedResults = results;
// Get whitelist for display
const whitelistStr = (currentConfig && currentConfig.idle_ban_whitelist) || '';
const whitelistedIPs = new Set(whitelistStr.split(',').map(s => s.trim()).filter(s => s.length > 0));
// Render table
const tbody = document.getElementById('ip-bans-tbody');
tbody.innerHTML = filteredResults.map(row => {
const isBanned = row.banned_until > now;
const status = isBanned ? '🔴 Banned' : (row.ban_count > 0 ? '🟡 Expired' : '🟢 Clean');
const bannedUntil = row.banned_until > 0 ? new Date(row.banned_until * 1000).toLocaleString() : '-';
const isBanned = isRowBanned(row);
const isIdleBanned = row.idle_banned_until > now;
const isAuthBanned = row.banned_until > now;
const isWhitelisted = whitelistedIPs.has(row.ip);
let statusLabel;
if (isWhitelisted) statusLabel = '⭐ Whitelisted';
else if (isAuthBanned && isIdleBanned) statusLabel = '🔴 Banned (auth+idle)';
else if (isAuthBanned) statusLabel = '🔴 Banned (auth)';
else if (isIdleBanned) statusLabel = '🔴 Banned (idle)';
else if ((row.ban_count > 0) || (row.idle_ban_count > 0)) statusLabel = '🟡 Expired';
else statusLabel = '🟢 Clean';
// Show the later of the two ban expiry times
const effectiveBannedUntil = Math.max(row.banned_until || 0, row.idle_banned_until || 0);
const bannedUntil = effectiveBannedUntil > 0 ? new Date(effectiveBannedUntil * 1000).toLocaleString() : '-';
const failures = row.total_failures || 0;
const authedSuccessfully = row.has_authed_successfully ? '✅ Yes' : '❌ No';
const connectionAttempts = row.total_connections || 0;
return `<tr>
<td>${escapeHtml(row.ip)}</td>
<td>${status}</td>
<td>${statusLabel}</td>
<td>${bannedUntil}</td>
<td>${failures}</td>
<td>${authedSuccessfully}</td>
<td>${connectionAttempts}</td>
<td>
${isBanned ? `<button type="button" onclick="unbanIp('${escapeHtml(row.ip)}')">Unban</button>` : ''}
${isBanned && !isWhitelisted ? `<button type="button" onclick="unbanIp('${escapeHtml(row.ip)}')">Unban</button>` : ''}
<button type="button" onclick="deleteIpBan('${escapeHtml(row.ip)}')">Delete</button>
</td>
</tr>`;
@@ -6351,7 +6405,7 @@ async function deleteIpBan(ip) {
setTimeout(() => loadIpBans(), 500);
}
// Set filter and reload
// Set filter and re-render from cache (no re-fetch needed)
function setIpBansFilter(filter) {
ipBansFilter = filter;
@@ -6360,8 +6414,12 @@ function setIpBansFilter(filter) {
document.getElementById('ip-ban-filter-banned').classList.toggle('active', filter === 'banned');
document.getElementById('ip-ban-filter-expired').classList.toggle('active', filter === 'expired');
// Reload with new filter
loadIpBans();
// Re-render from cache if available, otherwise fetch
if (ipBansCachedResults !== null) {
handleIpBansResponse({ rows: null, columns: null, _cachedResults: ipBansCachedResults });
} else {
loadIpBans();
}
}
// Escape HTML to prevent XSS
@@ -6372,6 +6430,63 @@ function escapeHtml(text) {
return div.innerHTML;
}
// Add IP to whitelist
async function addToWhitelist() {
const input = document.getElementById('whitelist-ip-input');
const status = document.getElementById('whitelist-status');
const ip = input.value.trim();
if (!ip) { status.innerHTML = '<span style="color:red">Enter an IP address</span>'; return; }
// Get current whitelist from config
const current = (currentConfig && currentConfig.idle_ban_whitelist) || '';
const ips = current.split(',').map(s => s.trim()).filter(s => s.length > 0);
if (ips.includes(ip)) { status.innerHTML = '<span style="color:orange">Already whitelisted</span>'; return; }
ips.push(ip);
const newValue = ips.join(', ');
try {
await sendAdminCommand(['config_set', 'idle_ban_whitelist', newValue]);
status.innerHTML = `<span style="color:green">✅ ${ip} added to whitelist</span>`;
input.value = '';
updateWhitelistDisplay(newValue);
// Update local config cache
if (currentConfig) currentConfig.idle_ban_whitelist = newValue;
} catch (e) {
status.innerHTML = `<span style="color:red">Failed: ${e.message}</span>`;
}
}
// Update whitelist display
function updateWhitelistDisplay(whitelistStr) {
const div = document.getElementById('whitelist-current');
if (!div) return;
const ips = (whitelistStr || '').split(',').map(s => s.trim()).filter(s => s.length > 0);
if (ips.length === 0) {
div.innerHTML = '<em>No IPs whitelisted</em>';
} else {
div.innerHTML = 'Current whitelist: ' + ips.map(ip =>
`<span style="background:var(--accent-color,#ff0000);color:white;padding:1px 6px;border-radius:3px;margin:2px;display:inline-block">
${escapeHtml(ip)}
<button onclick="removeFromWhitelist('${escapeHtml(ip)}')" style="background:none;border:none;color:white;cursor:pointer;padding:0 0 0 4px;font-size:11px">✕</button>
</span>`
).join('');
}
}
// Remove IP from whitelist
async function removeFromWhitelist(ip) {
const current = (currentConfig && currentConfig.idle_ban_whitelist) || '';
const ips = current.split(',').map(s => s.trim()).filter(s => s.length > 0 && s !== ip);
const newValue = ips.join(', ');
try {
await sendAdminCommand(['config_set', 'idle_ban_whitelist', newValue]);
updateWhitelistDisplay(newValue);
if (currentConfig) currentConfig.idle_ban_whitelist = newValue;
} catch (e) {
log('Failed to remove from whitelist: ' + e.message, 'ERROR');
}
}
// Initialize IP Bans event listeners
function initIpBansEventListeners() {
const addBanBtn = document.getElementById('add-ban-btn');
@@ -6379,13 +6494,14 @@ function initIpBansEventListeners() {
const filterAllBtn = document.getElementById('ip-ban-filter-all');
const filterBannedBtn = document.getElementById('ip-ban-filter-banned');
const filterExpiredBtn = document.getElementById('ip-ban-filter-expired');
const addWhitelistBtn = document.getElementById('add-whitelist-btn');
if (addBanBtn) {
addBanBtn.addEventListener('click', banIp);
}
if (refreshBtn) {
refreshBtn.addEventListener('click', loadIpBans);
refreshBtn.addEventListener('click', () => { ipBansCachedResults = null; loadIpBans(); });
}
if (filterAllBtn) {
@@ -6400,24 +6516,15 @@ function initIpBansEventListeners() {
filterExpiredBtn.addEventListener('click', () => setIpBansFilter('expired'));
}
if (addWhitelistBtn) {
addWhitelistBtn.addEventListener('click', addToWhitelist);
}
// Show current whitelist on page load
updateWhitelistDisplay(currentConfig && currentConfig.idle_ban_whitelist);
console.log('IP Bans event listeners initialized');
}
// Handle SQL query responses for IP bans
const originalHandleSqlQueryResponse = handleSqlQueryResponse;
handleSqlQueryResponse = function(response) {
console.log('=== HANDLING SQL QUERY RESPONSE ===');
console.log('Response:', response);
// Check if this is an IP bans query
if (response.query_id && (response.query_id.startsWith('ip_ban') || response.query_id === 'ip_bans_load')) {
handleIpBansResponse(response);
return;
}
// Call original handler for other queries
return originalHandleSqlQueryResponse(response);
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', initIpBansEventListeners);
+1 -1
View File
@@ -1 +1 @@
2303481
2391505
+25 -2
View File
@@ -13,6 +13,7 @@ extern void log_query_execution(const char* query_type, const char* sub_id,
#include <pthread.h>
#include <libwebsockets.h>
#include <cjson/cJSON.h>
int get_active_connection_count(void);
#include <sqlite3.h>
#include <time.h>
#include <sys/stat.h>
@@ -1291,13 +1292,16 @@ cJSON* query_cpu_metrics(void) {
pid_t pid = getpid();
cJSON_AddNumberToObject(cpu_stats, "process_id", (double)pid);
// Get memory usage from /proc/self/status
// Get active WebSocket connection count
cJSON_AddNumberToObject(cpu_stats, "active_connections", (double)get_active_connection_count());
// Get memory usage from /proc/self/status and calculate MEM%
unsigned long rss_kb = 0;
FILE* mem_stat = fopen("/proc/self/status", "r");
if (mem_stat) {
char line[256];
while (fgets(line, sizeof(line), mem_stat)) {
if (strncmp(line, "VmRSS:", 6) == 0) {
unsigned long rss_kb;
if (sscanf(line, "VmRSS: %lu kB", &rss_kb) == 1) {
double rss_mb = rss_kb / 1024.0;
cJSON_AddNumberToObject(cpu_stats, "memory_usage_mb", rss_mb);
@@ -1308,6 +1312,25 @@ cJSON* query_cpu_metrics(void) {
fclose(mem_stat);
}
// Get total system memory from /proc/meminfo for MEM%
if (rss_kb > 0) {
FILE* meminfo = fopen("/proc/meminfo", "r");
if (meminfo) {
char line[256];
while (fgets(line, sizeof(line), meminfo)) {
if (strncmp(line, "MemTotal:", 9) == 0) {
unsigned long total_kb;
if (sscanf(line, "MemTotal: %lu kB", &total_kb) == 1 && total_kb > 0) {
double mem_pct = (rss_kb * 100.0) / total_kb;
cJSON_AddNumberToObject(cpu_stats, "memory_percent", mem_pct);
}
break;
}
}
fclose(meminfo);
}
}
return cpu_stats;
}
File diff suppressed because one or more lines are too long
+26
View File
@@ -250,9 +250,34 @@ void ip_ban_save_to_db(sqlite3* db) {
DEBUG_TRACE("IP ban table saved: %d entries written to DB", saved);
}
// Check if an IP is in the idle_ban_whitelist config (comma-separated list)
static int ip_is_whitelisted(const char* ip) {
const char* whitelist = get_config_value("idle_ban_whitelist");
if (!whitelist || whitelist[0] == '\0') return 0;
// Make a mutable copy to tokenize
char buf[1024];
strncpy(buf, whitelist, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char* token = strtok(buf, ",");
while (token) {
// Trim leading/trailing spaces
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') { *end = '\0'; end--; }
if (strcmp(token, ip) == 0) return 1;
token = strtok(NULL, ",");
}
return 0;
}
int ip_ban_is_banned(const char* ip) {
if (!ip || !g_initialized) return 0;
// Whitelisted IPs are never banned
if (ip_is_whitelisted(ip)) return 0;
pthread_mutex_lock(&g_ban_mutex);
int idx = find_slot(ip);
if (idx < 0 || g_ban_table[idx].state == IP_BAN_EMPTY) {
@@ -359,6 +384,7 @@ void ip_ban_record_failure(const char* ip) {
void ip_ban_record_idle_failure(const char* ip) {
if (!ip || !g_initialized) return;
if (!get_config_bool("idle_ban_enabled", 1)) return;
if (ip_is_whitelisted(ip)) return; // Never record idle failures for whitelisted IPs
int threshold = get_config_int("idle_ban_threshold", 1);
int window_sec = get_config_int("idle_ban_window_sec", 30);
+2 -2
View File
@@ -13,8 +13,8 @@
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define CRELAY_VERSION_MAJOR 1
#define CRELAY_VERSION_MINOR 2
#define CRELAY_VERSION_PATCH 36
#define CRELAY_VERSION "v1.2.36"
#define CRELAY_VERSION_PATCH 49
#define CRELAY_VERSION "v1.2.49"
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
+2
View File
@@ -130,6 +130,8 @@ static tracked_connection_t g_connections[MAX_TRACKED_CONNECTIONS];
static int g_connection_count = 0;
static pthread_mutex_t g_connections_lock = PTHREAD_MUTEX_INITIALIZER;
int get_active_connection_count(void) { return g_connection_count; }
static void connection_list_add(struct lws* wsi, struct per_session_data* pss) {
pthread_mutex_lock(&g_connections_lock);
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
+3
View File
@@ -96,6 +96,9 @@ struct per_session_data {
int is_websocket; // 1 if this is a WebSocket connection, 0 for HTTP
};
// Get current active WebSocket connection count
int get_active_connection_count(void);
// NIP-11 HTTP session data structure for managing buffer lifetime
struct nip11_session_data {
int type; // 0 for NIP-11