diff --git a/docs/agent_browser_testing.md b/docs/agent_browser_testing.md new file mode 100644 index 0000000..611cbe1 --- /dev/null +++ b/docs/agent_browser_testing.md @@ -0,0 +1,190 @@ +# Agent Browser Testing Guide for C-Relay + +This document explains how to use the `agent-browser` CLI tool to test the c-relay admin web UI from an AI agent context (no physical display required). + +## Prerequisites + +- **agent-browser** installed globally: `npm install -g agent-browser` +- Binary location: `/home/user/.nvm/versions/node/v24.14.1/bin/agent-browser` +- The relay must be running locally (default port 8888) +- Test keys configured in `.test_keys` + +## Starting the Relay for Local Testing + +```bash +./make_and_restart_relay.sh -t +``` + +This reads `.test_keys` and starts the relay with: +- `ADMIN_PUBKEY` — the hex public key of the admin account +- `ADMIN_PRIVKEY` — the hex secret key (used for browser login, not by the relay itself) +- `SERVER_PRIVKEY` — the relay's own private key + +## Key URLs + +| URL | Purpose | +|-----|---------| +| `http://127.0.0.1:8888/api/index.html` | Admin web UI (the correct entry point) | +| `http://127.0.0.1:8888/` | Returns 406 without NIP-11 Accept header — **do not use for browser testing** | +| `http://127.0.0.1:8888/` with `Accept: application/nostr+json` | NIP-11 relay info JSON | + +**Important:** The root URL `/` is a WebSocket/NIP-11 endpoint, not an HTML page. Always use `/api/index.html` for browser testing. + +## Admin Login Credentials + +The admin nsec for the current `.test_keys` configuration: + +``` +nsec: nsec1zkn0hlt4jvcvn9yt5p7ea4m7f82pf3ph0gj3d4rlcz4s64x5z86satv9qm +hex: 15a6fbfd759330c9948ba07d9ed77e49d414c4377a2516d47fc0ab0d54d411f5 +npub: npub1tlyzk6slzt8d989f4pn3synk98fea64ea3jmrdc7dtd0kw8uxlas9a9fwr +hex pubkey: 5fc82b6a1f12ced29ca9a86718127629d39eeab9ec65b1b71e6adafb38fc37fb +``` + +## Complete Login Flow with agent-browser + +### Step 1: Open the admin UI + +```bash +agent-browser open http://127.0.0.1:8888/api/index.html +agent-browser wait --load networkidle +``` + +### Step 2: Take an interactive snapshot to see what is on screen + +```bash +agent-browser snapshot -i +``` + +You will see a login modal with buttons like: +- `"Browser Extension"` — skip this +- `"Local Key"` — **use this one** +- `"Seed Phrase"` — skip +- `"Nostr Connect"` — skip +- `"Read Only"` — skip + +### Step 3: Click "Local Key" + +```bash +agent-browser click @e15 +``` + +(The ref number may vary — use the ref from the snapshot output for the "Local Key" button.) + +After clicking, a text input appears asking for the secret key. + +### Step 4: Enter the admin nsec + +```bash +agent-browser fill @e14 "nsec1zkn0hlt4jvcvn9yt5p7ea4m7f82pf3ph0gj3d4rlcz4s64x5z86satv9qm" +``` + +(Use the ref from the snapshot for the textbox element.) + +### Step 5: Click "Import Key" + +```bash +agent-browser snapshot -i +``` + +Check the snapshot — the "Import Key" button should now be enabled. Click it: + +```bash +agent-browser click @e15 +``` + +### Step 6: Click "Continue" on the success screen + +After import, a success screen appears with "Continue" button: + +```bash +agent-browser wait 800 +agent-browser snapshot -i +agent-browser click @e19 +``` + +(Use the ref from the snapshot for the "Continue" button.) + +### Step 7: Wait for admin UI to load + +```bash +agent-browser wait 5000 +agent-browser snapshot -i +``` + +You should now see the admin dashboard with: +- Statistics table (Database Size, Total Events, PID, etc.) +- Navigation buttons (Statistics, Subscriptions, Configuration, Authorization, etc.) +- Admin profile area showing "admin" label + +## Navigating Admin Sections + +After login, use the sidebar navigation buttons: + +```bash +# View configuration +agent-browser click @e8 # Configuration button ref + +# View authorization rules +agent-browser click @e9 # Authorization button ref + +# View statistics +agent-browser click @e6 # Statistics button ref + +# Always snapshot after navigation to see results +agent-browser wait 2500 +agent-browser snapshot -i +``` + +## Checking for Errors + +```bash +# View browser console logs +agent-browser console + +# View JavaScript errors +agent-browser errors + +# View relay server logs +tail -n 100 relay.log +``` + +## Chained Command Example (Full Login in One Shot) + +```bash +agent-browser open http://127.0.0.1:8888/api/index.html && \ +agent-browser wait --load networkidle && \ +agent-browser snapshot -i +``` + +Then use refs from the snapshot to complete login steps. + +## Tips for AI Agents + +1. **Always use `/api/index.html`** — never the root URL +2. **Use `snapshot -i`** after every action to see the current interactive elements and their refs +3. **Refs change** between snapshots — always re-snapshot before clicking +4. **Wait after clicks** — use `agent-browser wait 2000` (milliseconds) between actions that trigger async operations +5. **The login flow has 3 screens**: method selection → key input → success confirmation +6. **Console logs are cumulative** — they show all logs since page load, which is useful for debugging admin API responses +7. **The relay log** at `relay.log` shows server-side processing of admin commands +8. **Command chaining** with `&&` works — the browser daemon persists between commands + +## Verifying Admin API is Working + +After login, the statistics page should show populated data: +- Database Size (e.g., "4 KB") +- Process ID (the relay PID) +- WebSocket Connections count +- Memory Usage + +If these show "-" or "Loading...", check: +1. `relay.log` for errors +2. `agent-browser console` for JavaScript errors +3. `agent-browser errors` for page-level errors + +## Closing the Browser + +```bash +agent-browser close --all +``` diff --git a/relay.pid b/relay.pid index 8711356..12a23bd 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -763795 +777740 diff --git a/src/api.c b/src/api.c index 12304d1..355eafd 100644 --- a/src/api.c +++ b/src/api.c @@ -19,7 +19,6 @@ int get_active_connection_count(void); #include #include #include -#include #include "api.h" #include "embedded_web_content.h" #include "config.h" @@ -56,38 +55,12 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f // Forward declaration for CPU metrics query function cJSON* query_cpu_metrics(void); -static int api_open_temp_db_connection(sqlite3** out_db) { - if (!out_db) { - return -1; - } - - *out_db = NULL; - const char* db_path = db_get_database_path(); - if (!db_path || db_path[0] == '\0') { - return -1; - } - - if (sqlite3_open_v2(db_path, out_db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) { - if (*out_db) { - sqlite3_close(*out_db); - *out_db = NULL; - } - return -1; - } - - sqlite3_exec(*out_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); - sqlite3_busy_timeout(*out_db, 5000); - db_set_thread_connection(*out_db); - return 0; +static int api_open_temp_db_connection(void** out_db) { + return db_open_worker_connection(db_get_database_path(), out_db); } -static void api_close_temp_db_connection(sqlite3* db) { - if (!db) { - return; - } - - db_clear_thread_connection(); - sqlite3_close(db); +static void api_close_temp_db_connection(void* db) { + db_close_worker_connection(db); } // Monitoring system helper functions @@ -97,7 +70,7 @@ int get_monitoring_throttle_seconds(void) { // Query event kind distribution from database cJSON* query_event_kind_distribution(void) { - sqlite3* temp_db = NULL; + void* temp_db = NULL; if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) { DEBUG_ERROR("Database not available for monitoring query"); return NULL; @@ -137,7 +110,7 @@ cJSON* query_event_kind_distribution(void) { // Query time-based statistics from database cJSON* query_time_based_statistics(void) { - sqlite3* temp_db = NULL; + void* temp_db = NULL; if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) { DEBUG_ERROR("Database not available for time stats query"); return NULL; @@ -190,7 +163,7 @@ cJSON* query_time_based_statistics(void) { // Query top pubkeys by event count from database cJSON* query_top_pubkeys(void) { - sqlite3* temp_db = NULL; + void* temp_db = NULL; if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) { DEBUG_ERROR("Database not available for top pubkeys query"); return NULL; @@ -237,7 +210,7 @@ cJSON* query_top_pubkeys(void) { // Query detailed subscription information from database log (ADMIN ONLY) // Uses subscriptions table instead of in-memory iteration to avoid mutex contention cJSON* query_subscription_details(void) { - sqlite3* temp_db = NULL; + void* temp_db = NULL; if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) { DEBUG_ERROR("Database not available for subscription details query"); return NULL; @@ -1210,7 +1183,7 @@ cJSON* query_cpu_metrics(void) { // Generate stats JSON from database queries char* generate_stats_json(void) { - sqlite3* temp_db = NULL; + void* temp_db = NULL; if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) { DEBUG_ERROR("Database not available for stats generation"); return NULL; diff --git a/src/config.c b/src/config.c index a067d7d..0385ab8 100644 --- a/src/config.c +++ b/src/config.c @@ -31,7 +31,6 @@ #include #include #include - // External database connection (from main.c) // External shutdown flag (from main.c) @@ -111,6 +110,14 @@ static char g_temp_relay_privkey[65] = {0}; // Runtime cache for relay private key to support strict mode (g_db = NULL) static char g_cached_relay_privkey[65] = {0}; +static int config_open_temp_db_connection(void** out_db) { + return db_open_worker_connection(db_get_database_path(), out_db); +} + +static void config_close_temp_db_connection(void* db) { + db_close_worker_connection(db); +} + // ================================ // UTILITY FUNCTIONS // ================================ @@ -2990,7 +2997,8 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!db_is_available()) { + void* temp_db = NULL; + if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3082,6 +3090,9 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ if (!admin_pubkey) { cJSON_Delete(response); cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } @@ -3090,12 +3101,18 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { cJSON_Delete(response); cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } return 0; } cJSON_Delete(response); } cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "failed to send auth query response"); return -1; } @@ -3104,7 +3121,8 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_ int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!db_is_available()) { + void* temp_db = NULL; + if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3197,6 +3215,9 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro if (!admin_pubkey) { cJSON_Delete(response); cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } @@ -3205,12 +3226,18 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { cJSON_Delete(response); cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } return 0; } cJSON_Delete(response); } cJSON_Delete(results_array); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "failed to send config query response"); return -1; } @@ -3713,7 +3740,8 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; - if (!db_is_available()) { + void* temp_db = NULL; + if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) { snprintf(error_message, error_size, "database not available"); return -1; } @@ -3819,6 +3847,9 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s if (!admin_pubkey) { cJSON_Delete(response); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } @@ -3826,10 +3857,16 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { cJSON_Delete(response); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } return 0; } cJSON_Delete(response); + if (temp_db) { + config_close_temp_db_connection(temp_db); + } snprintf(error_message, error_size, "failed to send stats query response"); return -1; } diff --git a/src/db_ops.c b/src/db_ops.c index beb2904..c8a410e 100644 --- a/src/db_ops.c +++ b/src/db_ops.c @@ -57,6 +57,33 @@ void db_clear_thread_connection(void) { g_thread_db = NULL; } +int db_open_worker_connection(const char* db_path, void** out_connection) { + if (!out_connection) return -1; + *out_connection = NULL; + + const char* effective_path = (db_path && db_path[0] != '\0') ? db_path : g_database_path; + if (!effective_path || effective_path[0] == '\0') return -1; + + sqlite3* db = NULL; + int rc = sqlite3_open_v2(effective_path, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc != SQLITE_OK) { + if (db) sqlite3_close(db); + return -1; + } + + sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); + sqlite3_busy_timeout(db, 5000); + db_set_thread_connection(db); + *out_connection = (void*)db; + return 0; +} + +void db_close_worker_connection(void* connection) { + if (!connection) return; + db_clear_thread_connection(); + sqlite3_close((sqlite3*)connection); +} + int db_is_available(void) { return db_active_connection() != NULL; } diff --git a/src/db_ops.h b/src/db_ops.h index 542e45c..b4bacc4 100644 --- a/src/db_ops.h +++ b/src/db_ops.h @@ -16,6 +16,10 @@ const char* db_get_database_path(void); int db_set_thread_connection(void* connection); void db_clear_thread_connection(void); +// Worker/runtime SQLite connection lifecycle (kept internal to db_ops.c) +int db_open_worker_connection(const char* db_path, void** out_connection); +void db_close_worker_connection(void* connection); + // DB result codes (backend-agnostic) #define DB_OK 0 #define DB_ERROR 1 diff --git a/src/main.h b/src/main.h index 51388b9..ac5993e 100644 --- a/src/main.h +++ b/src/main.h @@ -13,8 +13,8 @@ // Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define CRELAY_VERSION_MAJOR 2 #define CRELAY_VERSION_MINOR 1 -#define CRELAY_VERSION_PATCH 6 -#define CRELAY_VERSION "v2.1.6" +#define CRELAY_VERSION_PATCH 7 +#define CRELAY_VERSION "v2.1.7" // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/src/thread_pool.c b/src/thread_pool.c index cabd7de..4711464 100644 --- a/src/thread_pool.c +++ b/src/thread_pool.c @@ -5,7 +5,6 @@ #include "db_ops.h" #include "ip_ban.h" -#include #include #include #include @@ -199,24 +198,12 @@ static void wake_event_loop(void) { } } -static sqlite3* open_worker_connection(void) { - if (g_pool.db_path[0] == '\0') { +static void* open_worker_connection(void) { + void* db_conn = NULL; + if (db_open_worker_connection(g_pool.db_path, &db_conn) != 0) { return NULL; } - - sqlite3* db = NULL; - int rc = sqlite3_open_v2(g_pool.db_path, &db, SQLITE_OPEN_READWRITE, NULL); - if (rc != SQLITE_OK) { - if (db) { - sqlite3_close(db); - } - return NULL; - } - - sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); - sqlite3_busy_timeout(db, 5000); - - return db; + return db_conn; } static void complete_job_with_result(thread_pool_job_node_t* node, @@ -502,14 +489,12 @@ static void* reader_worker_main(void* arg) { snprintf(thread_name, sizeof(thread_name), "db-read-%d", reader_index); pthread_setname_np(pthread_self(), thread_name); - sqlite3* worker_db = open_worker_connection(); + void* worker_db = open_worker_connection(); if (!worker_db) { DEBUG_ERROR("Reader worker failed to open SQLite connection"); return NULL; } - db_set_thread_connection(worker_db); - while (g_pool.running) { thread_pool_job_node_t* node = queue_pop(&g_pool.read_q); if (!node) break; @@ -517,8 +502,7 @@ static void* reader_worker_main(void* arg) { execute_read_job(node); } - db_clear_thread_connection(); - sqlite3_close(worker_db); + db_close_worker_connection(worker_db); return NULL; } @@ -527,14 +511,12 @@ static void* writer_worker_main(void* arg) { pthread_setname_np(pthread_self(), "db-write"); - sqlite3* worker_db = open_worker_connection(); + void* worker_db = open_worker_connection(); if (!worker_db) { DEBUG_ERROR("Writer worker failed to open SQLite connection"); return NULL; } - db_set_thread_connection(worker_db); - while (g_pool.running) { thread_pool_job_node_t* node = queue_pop(&g_pool.write_q); if (!node) break; @@ -542,8 +524,7 @@ static void* writer_worker_main(void* arg) { execute_write_job(node); } - db_clear_thread_connection(); - sqlite3_close(worker_db); + db_close_worker_connection(worker_db); return NULL; } diff --git a/src/websockets.c b/src/websockets.c index 9c723f7..4f166c5 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -18,8 +18,6 @@ #include #include -#include - // Include nostr_core_lib for Nostr functionality #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" @@ -531,22 +529,10 @@ static void* async_event_worker_main(void* arg) { (void)arg; pthread_setname_np(pthread_self(), "event-worker"); - sqlite3* worker_db = NULL; + void* worker_db = NULL; const char* db_path = db_get_database_path(); - if (db_path && db_path[0] != '\0') { - if (sqlite3_open_v2(db_path, &worker_db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK) { - sqlite3_exec(worker_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); - sqlite3_busy_timeout(worker_db, 5000); - db_set_thread_connection(worker_db); - } else { - if (worker_db) { - sqlite3_close(worker_db); - worker_db = NULL; - } - DEBUG_WARN("event-worker: failed to open dedicated DB connection; using default connection context"); - } - } else { - DEBUG_WARN("event-worker: database path unavailable; using default connection context"); + if (db_open_worker_connection(db_path, &worker_db) != 0) { + DEBUG_WARN("event-worker: failed to open dedicated DB connection; using default connection context"); } while (g_async_event_worker_running) { @@ -620,8 +606,7 @@ static void* async_event_worker_main(void* arg) { } if (worker_db) { - db_clear_thread_connection(); - sqlite3_close(worker_db); + db_close_worker_connection(worker_db); } return NULL;