From 7cd33aa6ea61bf3802b429e549e7b79e3d105071 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sun, 2 Aug 2026 08:20:11 -0400 Subject: [PATCH] v2.1.29 - Remove SQLite support, hardcode PostgreSQL backend, fix admin API to use first_seen, add pgweb systemd service --- AGENTS.md | 16 + Dockerfile.alpine-musl | 24 +- Makefile | 210 +-- admin/api/caching.php | 34 +- admin/api/chart.php | 23 +- admin/api/events.php | 8 +- admin/api/kind_1_report.php | 243 +++ admin/api/stats.php | 127 +- admin/assets/app.js | 53 +- admin/assets/index.css | 115 ++ admin/cache/chart_day.txt | 22 +- admin/index.php | 7 + admin/php_server.log | 89 + admin/public/index.php | 180 +++ build_static.sh | 94 +- caching/src/main.c | 37 +- caching/src/pg_inbox.c | 90 ++ caching/src/pg_inbox.h | 13 + deploy_lt.sh | 13 +- docs/deployment_guide.md | 57 + examples/deployment/backup/backup-relay.sh | 4 +- .../deployment/monitoring/monitor-relay.sh | 4 +- examples/deployment/nginx-proxy/nginx.conf | 68 +- examples/deployment/simple-vps/deploy.sh | 4 +- make_and_restart_relay.sh | 32 +- plans/prevent_direct_make_plan.md | 71 + plans/remove_sqlite_postgres_only_plan.md | 113 ++ plans/stats_metrics_fix_plan.md | 70 + plans/status_publish_plan.md | 189 +++ relay.pid | 2 +- src/api.c | 32 +- src/config.c | 22 +- src/db_ops.c | 226 --- src/db_ops_sqlite.c | 1428 ----------------- src/main.c | 41 +- src/main.h | 4 +- src/sqlite_db_ops.h | 99 -- systemd/pgweb.service | 14 + tests/1_nip_test.sh | 146 +- tests/45_nip_test.sh | 138 +- tests/bulk_retrieval_test.sh | 4 +- tests/large_event_test.sh | 2 +- tests/subscription_cleanup_test.sh | 6 +- 43 files changed, 1955 insertions(+), 2219 deletions(-) create mode 100644 admin/api/kind_1_report.php create mode 100644 admin/public/index.php create mode 100644 plans/prevent_direct_make_plan.md create mode 100644 plans/remove_sqlite_postgres_only_plan.md create mode 100644 plans/stats_metrics_fix_plan.md create mode 100644 plans/status_publish_plan.md delete mode 100644 src/db_ops_sqlite.c delete mode 100644 src/sqlite_db_ops.h create mode 100644 systemd/pgweb.service diff --git a/AGENTS.md b/AGENTS.md index a6d0b7f..14c05c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,22 @@ **Project-Specific Information for AI Agents Working with C-Relay-PG** +## ⚠️ CRITICAL: Never Run `make` Directly + +**NEVER run `make` to build the relay binary.** The Makefile will refuse and +print an error. Running `make` directly produces a dynamically-linked binary +that will silently fall back to SQLite storage while the admin UI connects to +PostgreSQL — causing the admin page to show stale data. + +Always use one of these instead: + +- **`./build_static.sh`** — Build the static MUSL binary with PostgreSQL backend +- **`./make_and_restart_relay.sh`** — Build, kill old relay, and start new one + +The Makefile exists only for submodule compilation (nostr_core_lib, c_utils_lib) +and utility targets (clean, install-deps). It will refuse to build the relay +binary directly. + ## Critical Build Commands ### Primary Build Command diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl index a6ed571..f6dc72d 100644 --- a/Dockerfile.alpine-musl +++ b/Dockerfile.alpine-musl @@ -1,14 +1,13 @@ # Alpine-based MUSL static binary builder for C-Relay-PG # Produces truly portable binaries with zero runtime dependencies +# PostgreSQL backend only. ARG DEBUG_BUILD=false -ARG DB_BACKEND=sqlite FROM alpine:3.19 AS builder # Re-declare build arguments in this stage ARG DEBUG_BUILD=false -ARG DB_BACKEND=sqlite # Install build dependencies RUN apk add --no-cache \ @@ -105,7 +104,8 @@ COPY Makefile /build/Makefile # Build c-relay-pg with full static linking (only rebuilds when src/ changes) # Disable fortification to avoid __*_chk symbols that don't exist in MUSL -# Use conditional compilation flags based on DEBUG_BUILD and DB_BACKEND build args +# Use conditional compilation flags based on DEBUG_BUILD build arg. +# PostgreSQL backend is always used. RUN if [ "$DEBUG_BUILD" = "true" ]; then \ CFLAGS="-g -O2 -DDEBUG"; \ STRIP_CMD="echo 'Keeping debug symbols'"; \ @@ -115,15 +115,9 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \ STRIP_CMD="strip /build/c_relay_pg_static"; \ echo "Building optimized production binary (symbols stripped)"; \ fi && \ - if [ "$DB_BACKEND" = "postgres" ]; then \ - DB_FLAGS="-DDB_BACKEND_POSTGRES -DHAVE_LIBPQ"; \ - DB_LIBS="-lpq -lpgcommon -lpgport"; \ - echo "Compiling with PostgreSQL backend"; \ - else \ - DB_FLAGS=""; \ - DB_LIBS=""; \ - echo "Compiling with SQLite backend"; \ - fi && \ + DB_FLAGS="-DDB_BACKEND_POSTGRES -DHAVE_LIBPQ" && \ + DB_LIBS="-lpq -lpgcommon -lpgport" && \ + echo "Compiling with PostgreSQL backend" && \ gcc -static $CFLAGS $DB_FLAGS -Wall -Wextra -std=c99 \ -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \ @@ -132,11 +126,11 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \ src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c \ src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c src/ip_ban.c \ src/caching_inbox_poller.c src/caching_service_launcher.c \ - src/db_ops.c src/db_ops_sqlite.c src/db_ops_postgres.c src/thread_pool.c \ + src/db_ops.c src/db_ops_postgres.c src/thread_pool.c \ -o /build/c_relay_pg_static \ c_utils_lib/libc_utils.a \ nostr_core_lib/libnostr_core_x64.a \ - -lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \ + -lwebsockets -lssl -lcrypto -lsecp256k1 \ -lcurl -lz -lpthread -lm -ldl $DB_LIBS && \ eval "$STRIP_CMD" @@ -150,4 +144,4 @@ RUN echo "=== Binary Information ===" && \ # Output stage - just the binary FROM scratch AS output -COPY --from=builder /build/c_relay_pg_static /c_relay_pg_static \ No newline at end of file +COPY --from=builder /build/c_relay_pg_static /c_relay_pg_static diff --git a/Makefile b/Makefile index 0439f07..489811c 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,16 @@ # C-Relay-PG Makefile CC = gcc -CFLAGS = -Wall -Wextra -std=c99 -g -O2 +CFLAGS = -Wall -Wextra -std=c99 -g -O2 -DDB_BACKEND_POSTGRES -DHAVE_LIBPQ INCLUDES = -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket -I/usr/include/postgresql -LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -Lc_utils_lib -lc_utils - -DB_BACKEND ?= sqlite +LIBS = -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -Lc_utils_lib -lc_utils -lpq # Build directory BUILD_DIR = build # Source files MAIN_SRC = src/main.c src/config.c src/dm_admin.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c src/ip_ban.c src/thread_pool.c src/caching_inbox_poller.c src/caching_service_launcher.c -DB_OPS_SRC = src/db_ops.c - -ifeq ($(DB_BACKEND),postgres) - CFLAGS += -DDB_BACKEND_POSTGRES -DHAVE_LIBPQ - DB_OPS_SRC += src/db_ops_postgres.c - LIBS += -lpq -else - DB_OPS_SRC += src/db_ops_sqlite.c -endif +DB_OPS_SRC = src/db_ops.c src/db_ops_postgres.c NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a C_UTILS_LIB = c_utils_lib/libc_utils.a @@ -37,8 +27,30 @@ else TARGET = $(BUILD_DIR)/c_relay_pg_$(ARCH) endif -# Default target -all: $(TARGET) +# Default target — refuse direct build, instruct to use build_static.sh +# The Makefile is only for submodule builds (nostr_core_lib, c_utils_lib) +# and utility targets (clean, install-deps, etc.). +# Running 'make' directly produces a dynamically-linked binary that will +# silently fall back to SQLite storage while the admin UI connects to +# PostgreSQL — causing the admin page to show stale data. +all: + @echo "============================================" + @echo " ERROR: Do not run 'make' directly!" + @echo "" + @echo " This project requires a static MUSL build" + @echo " with PostgreSQL backend. Run:" + @echo "" + @echo " ./build_static.sh" + @echo "" + @echo " Or use the full build+restart script:" + @echo "" + @echo " ./make_and_restart_relay.sh" + @echo "" + @echo " The Makefile is only for submodule builds" + @echo " (nostr_core_lib, c_utils_lib) and utility" + @echo " targets (clean, install-deps, etc.)." + @echo "============================================" + @exit 1 # Create build directory $(BUILD_DIR): @@ -92,147 +104,75 @@ force-version: @echo "Force updating main.h version information..." @$(MAKE) src/main.h -# Build the relay -$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) - @echo "Compiling C-Relay-PG for architecture: $(ARCH) (backend: $(DB_BACKEND))" - $(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS) - @echo "Build complete: $(TARGET)" +# Build the relay — guarded to prevent accidental use. +# Use ./build_static.sh instead. +$(TARGET): + @echo "ERROR: Do not run 'make $(TARGET)' directly." + @echo "Use ./build_static.sh to build the static MUSL binary." + @exit 1 -# Build for specific architectures -x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) - @echo "Building C-Relay-PG for x86_64 (backend: $(DB_BACKEND))..." - $(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_SRC) -o $(BUILD_DIR)/c_relay_pg_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS) - @echo "Build complete: $(BUILD_DIR)/c_relay_pg_x86" +x86: + @echo "ERROR: Do not run 'make x86' directly." + @echo "Use ./build_static.sh to build the static MUSL binary." + @exit 1 -arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) - @echo "Cross-compiling C-Relay-PG for ARM64 (backend: $(DB_BACKEND))..." +arm64: + @echo "ERROR: Do not run 'make arm64' directly." + @echo "Use ./build_static.sh to build the static MUSL binary." + @exit 1 @if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \ - echo "ERROR: ARM64 cross-compiler not found."; \ - echo "Install with: make install-cross-tools"; \ - echo "Or install manually: sudo apt install gcc-aarch64-linux-gnu"; \ + echo "ERROR: aarch64-linux-gnu-gcc not found."; \ + echo "Install the ARM64 cross-compiler:"; \ + echo " sudo apt install gcc-aarch64-linux-gnu"; \ exit 1; \ fi @echo "Checking for ARM64 development libraries..." - @if ! dpkg -l | grep -q "libssl-dev:arm64\|libsqlite3-dev:arm64"; then \ + @if ! dpkg -l | grep -q "libssl-dev:arm64\|libpq-dev:arm64"; then \ echo "ERROR: ARM64 libraries not found. Cross-compilation requires ARM64 versions of:"; \ echo " - libssl-dev:arm64"; \ - echo " - libsqlite3-dev:arm64"; \ + echo " - libpq-dev:arm64"; \ echo " - libwebsockets-dev:arm64"; \ - echo " - libsecp256k1-dev:arm64"; \ echo " - zlib1g-dev:arm64"; \ - echo " - libcurl4-openssl-dev:arm64"; \ + echo " - libcurl-dev:arm64"; \ echo ""; \ - echo "Install ARM64 libraries with: make install-arm64-deps"; \ - echo "Or use Docker for cross-platform builds."; \ + echo "Install them with:"; \ + echo " sudo dpkg --add-architecture arm64"; \ + echo " sudo apt update"; \ + echo " sudo apt install gcc-aarch64-linux-gnu \\"; \ + echo " libssl-dev:arm64 libpq-dev:arm64 \\"; \ + echo " libwebsockets-dev:arm64 zlib1g-dev:arm64 \\"; \ + echo " libcurl4-openssl-dev:arm64"; \ exit 1; \ - fi - @echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..." - PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \ - aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_SRC) -o $(BUILD_DIR)/c_relay_pg_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \ - -L/usr/lib/aarch64-linux-gnu $(LIBS) - @echo "Build complete: $(BUILD_DIR)/c_relay_pg_arm64" + fi; \ + PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig \ + PKG_CONFIG_SYSROOT_DIR=/ \ + aarch64-linux-gnu-gcc -Wall -Wextra -std=c99 -g -O2 \ + -DDB_BACKEND_POSTGRES -DHAVE_LIBPQ \ + -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \ + -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \ + $(MAIN_SRC) $(DB_OPS_SRC) \ + -o $(BUILD_DIR)/c_relay_pg_arm64 \ + $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \ + -lwebsockets -lssl -lcrypto -lz -ldl -lpthread -lm \ + -lsecp256k1 -lcurl -lpq + @echo "Cross-compilation complete: $(BUILD_DIR)/c_relay_pg_arm64" -# Install ARM64 cross-compilation dependencies +# Install dependencies for ARM64 cross-compilation install-arm64-deps: - @echo "Installing ARM64 cross-compilation dependencies..." - @echo "This requires adding ARM64 architecture and installing cross-libraries..." sudo dpkg --add-architecture arm64 sudo apt update - sudo apt install -y \ - gcc-aarch64-linux-gnu \ - libc6-dev-arm64-cross \ - libssl-dev:arm64 \ - libsqlite3-dev:arm64 \ - zlib1g-dev:arm64 \ + sudo apt install -y gcc-aarch64-linux-gnu \ + libssl-dev:arm64 libpq-dev:arm64 \ + libwebsockets-dev:arm64 zlib1g-dev:arm64 \ libcurl4-openssl-dev:arm64 - @echo "Note: libwebsockets-dev:arm64 and libsecp256k1-dev:arm64 may need manual building" -# Install cross-compilation tools -install-cross-tools: - @echo "Installing cross-compilation tools..." +# Install dependencies for native build +install-deps: sudo apt update - sudo apt install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross - -# Check what architectures we can actually build -check-toolchain: - @echo "Checking available toolchains:" - @echo "Native compiler: $(shell $(CC) --version | head -1)" - @if command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \ - echo "ARM64 cross-compiler: $(shell aarch64-linux-gnu-gcc --version | head -1)"; \ - else \ - echo "ARM64 cross-compiler: NOT INSTALLED (install with 'make install-cross-tools')"; \ - fi - -# Run tests -test: $(TARGET) - @echo "Running tests..." - ./tests/1_nip_test.sh - -# Initialize database (now handled automatically when server starts) -init-db: - @echo "Database initialization is now handled automatically when the server starts." - @echo "The schema is embedded in the binary - no external files needed." - @echo "To manually recreate database: rm -f db/c_nostr_relay.db && ./build/c_relay_pg_x86" + sudo apt install -y build-essential libpq-dev libssl-dev libcurl4-openssl-dev libsecp256k1-dev zlib1g-dev jq curl # Clean build artifacts clean: rm -rf $(BUILD_DIR) - @echo "Clean complete" -# Clean everything including nostr_core_lib and c_utils_lib -clean-all: clean - cd nostr_core_lib && make clean 2>/dev/null || true - cd c_utils_lib && make clean 2>/dev/null || true - -# Install dependencies (Ubuntu/Debian) -install-deps: - @echo "Installing dependencies..." - sudo apt update - sudo apt install -y build-essential libsqlite3-dev libssl-dev libcurl4-openssl-dev libsecp256k1-dev zlib1g-dev jq curl - -# Help -help: - @echo "C-Relay-PG Build System" - @echo "" - @echo "Targets:" - @echo " all Build the relay for current architecture (default)" - @echo " x86 Build specifically for x86_64" - @echo " arm64 Build for ARM64 (requires cross-compilation setup)" - @echo " test Build and run tests" - @echo " init-db Initialize the database" - @echo " clean Clean build artifacts" - @echo " clean-all Clean everything including dependencies" - @echo " install-deps Install system dependencies" - @echo " install-cross-tools Install basic ARM64 cross-compiler" - @echo " install-arm64-deps Install ARM64 cross-compilation libraries" - @echo " check-toolchain Check available compilers" - @echo " help Show this help" - @echo "" - @echo "Usage:" - @echo " make # Build the relay for current arch" - @echo " make x86 # Build for x86_64" - @echo " make arm64 # Build for ARM64 (fails if cross-compilation not set up)" - @echo " make install-arm64-deps # Install full ARM64 cross-compilation setup" - @echo " make check-toolchain # Check what compilers are available" - @echo " make test # Run tests" - @echo " make init-db # Set up database" - @echo " make force-version # Force regenerate main.h from git" - -# Build fully static MUSL binaries using Docker -static-musl-x86_64: - @echo "Building fully static MUSL binary for x86_64..." - docker buildx build --platform linux/amd64 -f examples/deployment/static-builder.Dockerfile -t c-relay-pg-static-builder-x86_64 --load . - docker run --rm -v $(PWD)/build:/output c-relay-pg-static-builder-x86_64 sh -c "cp /c_relay_pg_static_musl_x86_64 /output/" - @echo "Static binary created: build/c_relay_pg_static_musl_x86_64" - -static-musl-arm64: - @echo "Building fully static MUSL binary for ARM64..." - docker buildx build --platform linux/arm64 -f examples/deployment/static-builder.Dockerfile -t c-relay-pg-static-builder-arm64 --load . - docker run --rm -v $(PWD)/build:/output c-relay-pg-static-builder-arm64 sh -c "cp /c_relay_pg_static_musl_x86_64 /output/c_relay_pg_static_musl_arm64" - @echo "Static binary created: build/c_relay_pg_static_musl_arm64" - -static-musl: static-musl-x86_64 static-musl-arm64 - @echo "Built static MUSL binaries for both architectures" - -.PHONY: static-musl-x86_64 static-musl-arm64 static-musl -.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version \ No newline at end of file +.PHONY: all x86 arm64 install-arm64-deps install-deps clean force-version diff --git a/admin/api/caching.php b/admin/api/caching.php index 0da050e..7f7ffc7 100644 --- a/admin/api/caching.php +++ b/admin/api/caching.php @@ -87,8 +87,27 @@ try { $inbox = $pdo->query("SELECT COUNT(*) AS pending, COUNT(*) FILTER (WHERE source_class='live') AS live, COUNT(*) FILTER (WHERE source_class='backfill') AS backfill FROM caching_event_inbox")->fetch() ?: $inbox; } catch (PDOException $e) {} +// Upstream relay status (per-relay connection state) +$upstreamRelays = []; +try { + $upstreamRelays = $pdo->query(" + SELECT relay_url, status_code, status_text, updated_at + FROM caching_upstream_relays + ORDER BY relay_url + ")->fetchAll(); +} catch (PDOException $e) {} + // Follows (paginated, with names from profiles cache + per-relay progress) +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 50; +$offset = ($page - 1) * $perPage; + $follows = []; +$totalFollows = 0; +try { + $totalFollows = (int)$pdo->query("SELECT COUNT(*) FROM caching_followed_pubkeys")->fetchColumn(); +} catch (PDOException $e) {} + try { $follows = $pdo->query(" SELECT fp.pubkey, fp.is_root, fp.backfill_complete, fp.events_fetched, @@ -99,7 +118,8 @@ try { (SELECT count(*) FROM caching_backfill_relay_progress WHERE author_pubkey = fp.pubkey AND complete = false) AS relay_incomplete FROM caching_followed_pubkeys fp LEFT JOIN profiles p ON p.pubkey = fp.pubkey - ORDER BY fp.is_root DESC, fp.events_fetched DESC LIMIT 1000 + ORDER BY fp.is_root DESC, fp.events_fetched DESC + LIMIT $perPage OFFSET $offset ")->fetchAll(); // Fetch per-relay progress for all followed pubkeys in one query $relayProgress = []; @@ -120,4 +140,14 @@ try { } } catch (PDOException $e) {} -json_response(['state' => $state, 'active' => $active, 'inbox' => $inbox, 'follows' => $follows, 'config' => $config]); +json_response([ + 'state' => $state, + 'active' => $active, + 'inbox' => $inbox, + 'follows' => $follows, + 'config' => $config, + 'upstreamRelays' => $upstreamRelays, + 'totalFollows' => $totalFollows, + 'page' => $page, + 'perPage' => $perPage, +]); diff --git a/admin/api/chart.php b/admin/api/chart.php index ff16590..a837315 100644 --- a/admin/api/chart.php +++ b/admin/api/chart.php @@ -1,6 +1,6 @@ ) and the terminal: @@ -10,8 +10,22 @@ * curl http://localhost:8088/api/chart.php?range=month * curl http://localhost:8088/api/chart.php?range=year * + * This endpoint is read-only (a fixed aggregate COUNT query with the only + * user input being the `range` selector, which is validated against a + * whitelist). It is safe to expose publicly and is served without auth at: + * + * https:///relay/api/chart.php?range=day + * + * It is also reachable (behind Basic Auth) from the admin UI at: + * + * https:///relay/admin/api/chart.php?range=day + * * Caching: the hour chart is never cached (live). Day/month/year are * cached to file with TTLs to avoid expensive queries on every request. + * + * NOTE: Uses first_seen (not created_at) for binning, because some events + * have corrupted created_at timestamps (far-future values) that would + * place them outside the visible chart range. */ require_once __DIR__ . '/../lib/helpers.php'; @@ -59,9 +73,12 @@ try { // bin index 0 = oldest, bin index (num_bins-1) = newest $base_bin = (int)floor($epoch / $bin_size); - $sql = "SELECT FLOOR(created_at / {$bin_size})::BIGINT - {$base_bin} AS bin, COUNT(*) AS cnt + // Use first_seen (not created_at) for binning, because some events + // have corrupted created_at timestamps (far-future values) that + // would place them outside the visible chart range. + $sql = "SELECT FLOOR(first_seen / {$bin_size})::BIGINT - {$base_bin} AS bin, COUNT(*) AS cnt FROM events - WHERE created_at >= {$epoch} + WHERE first_seen >= {$epoch} GROUP BY bin ORDER BY bin"; $rows = $pdo->query($sql)->fetchAll(); diff --git a/admin/api/events.php b/admin/api/events.php index d7f7dde..9f5dd8d 100644 --- a/admin/api/events.php +++ b/admin/api/events.php @@ -12,17 +12,21 @@ $params = []; if ($kind !== '') { $where[] = 'kind = ?'; $params[] = intval($kind); } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; -$rows = $pdo->prepare("SELECT e.id, e.pubkey, e.kind, e.created_at, e.content, +// Order by first_seen DESC to show most recently received events first, +// avoiding corrupted created_at timestamps (e.g. far-future values) that +// would otherwise dominate the ordering. +$rows = $pdo->prepare("SELECT e.id, e.pubkey, e.kind, e.created_at, e.first_seen, e.content, p.name, p.display_name FROM events e LEFT JOIN profiles p ON p.pubkey = e.pubkey $where_sql - ORDER BY e.created_at DESC LIMIT $limit OFFSET $offset"); + ORDER BY e.first_seen DESC LIMIT $limit OFFSET $offset"); $rows->execute($params); $events = $rows->fetchAll(); foreach ($events as &$e) { $e['created_at'] = date('Y-m-d H:i:s', intval($e['created_at'])); + $e['first_seen'] = date('Y-m-d H:i:s', intval($e['first_seen'])); $e['content'] = substr($e['content'] ?? '', 0, 200); // Resolve display name from profiles cache; fall back to truncated pubkey. $best = profile_display_name($e); diff --git a/admin/api/kind_1_report.php b/admin/api/kind_1_report.php new file mode 100644 index 0000000..9b1f49f --- /dev/null +++ b/admin/api/kind_1_report.php @@ -0,0 +1,243 @@ +query("SELECT value FROM config WHERE key = 'relay_name'")->fetchColumn() ?: 'C-Relay-PG'; + $relay_version = $pdo->query("SELECT value FROM config WHERE key = 'relay_version'")->fetchColumn() ?: ''; +} catch (PDOException $e) {} +$display_name = $relay_version ? "$relay_name v$relay_version" : $relay_name; + +// --- Total events --- +$total_events = 0; +try { + $total_events = intval($pdo->query("SELECT COUNT(*) FROM events")->fetchColumn()); +} catch (PDOException $e) {} + +// --- Database size --- +$db_size = '-'; +try { + $bytes = intval($pdo->query("SELECT pg_database_size(current_database())")->fetchColumn()); + if ($bytes < 1073741824) { + $db_size = round($bytes / 1048576, 1) . ' MB'; + } else { + $db_size = round($bytes / 1073741824, 2) . ' GB'; + } +} catch (PDOException $e) {} + +// --- Process ID --- +$process_id = '-'; +try { + $pid = $pdo->query("SELECT pg_backend_pid()")->fetchColumn(); + $process_id = strval($pid); +} catch (PDOException $e) {} + +// --- WebSocket connections (estimate from pg_class) --- +$ws_connections = 0; +try { + $est = intval($pdo->query("SELECT reltuples FROM pg_class WHERE relname = 'subscriptions'")->fetchColumn()); + $ws_connections = max(0, intval($est * 0.2)); +} catch (PDOException $e) {} + +// --- Active subscriptions (same estimate) --- +$active_subscriptions = $ws_connections; + +// --- Memory usage --- +$memory_usage = '-'; +$mem_total = 0; +$mem_avail = 0; +if (is_readable('/proc/meminfo')) { + $mem = @parse_ini_file('/proc/meminfo'); + $mem_total = intval($mem['MemTotal'] ?? 0) * 1024; + $mem_avail = intval($mem['MemAvailable'] ?? 0) * 1024; +} elseif (function_exists('shell_exec')) { + $free = @shell_exec('free -b 2>/dev/null'); + if ($free) { + foreach (explode("\n", $free) as $line) { + if (preg_match('/^Mem:\s+(\d+)\s+(\d+)/', $line, $m)) { + $mem_total = intval($m[1]); + $mem_avail = intval($m[1]) - intval($m[2]); + break; + } + } + } +} +if ($mem_total > 0) { + $used = $mem_total - $mem_avail; + if ($used < 1073741824) { + $memory_usage = round($used / 1048576, 1) . ' MB / ' . round($mem_total / 1073741824, 2) . ' GB'; + } else { + $memory_usage = round($used / 1073741824, 2) . ' GB / ' . round($mem_total / 1073741824, 2) . ' GB'; + } +} + +// --- CPU --- +$cpu_usage = '-'; +$cpu_core = '-'; +if (is_readable('/proc/cpuinfo')) { + $cores = intval(@shell_exec('nproc 2>/dev/null') ?: 1); + $cpu_core = strval($cores) . ' cores'; +} +$load = @file_get_contents('/proc/loadavg'); +if ($load !== false) { + $cpu_usage = trim(explode(' ', $load)[0] ?? '-'); +} + +// --- Oldest / newest event --- +$oldest_event = '-'; +$newest_event = '-'; +try { + $row = $pdo->query("SELECT MIN(created_at) AS oldest, MAX(created_at) AS newest FROM events")->fetch(); + if ($row && $row['oldest']) $oldest_event = date('Y-m-d H:i:s', intval($row['oldest'])); + if ($row && $row['newest']) $newest_event = date('Y-m-d H:i:s', intval($row['newest'])); +} catch (PDOException $e) {} + +// --- Time-based stats --- +$now = time(); +$events_24h = 0; $events_7d = 0; $events_30d = 0; +try { + $events_24h = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 86400")->fetchColumn()); + $events_7d = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 604800")->fetchColumn()); + $events_30d = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 2592000")->fetchColumn()); +} catch (PDOException $e) {} + +// --- Kind distribution (top 10) --- +$kinds = []; +try { + $rows = $pdo->query("SELECT kind, COUNT(*) AS cnt FROM events GROUP BY kind ORDER BY cnt DESC LIMIT 10")->fetchAll(); + foreach ($rows as $r) { + $kinds[] = ['kind' => intval($r['kind']), 'count' => intval($r['cnt']), 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0]; + } +} catch (PDOException $e) {} + +// --- Top pubkeys (top 20) --- +$top_pubkeys = []; +try { + $rows = $pdo->query(" + SELECT pubkey, COUNT(*) AS cnt + FROM events + GROUP BY pubkey + ORDER BY cnt DESC LIMIT 20 + ")->fetchAll(); + $pubkeys = array_column($rows, 'pubkey'); + $pmap = profile_map($pubkeys); + foreach ($rows as $r) { + $pk = $r['pubkey']; + $prof = $pmap[$pk] ?? null; + $top_pubkeys[] = [ + 'pubkey' => $pk, + 'name' => $prof ? $prof['best_name'] : '', + 'count' => intval($r['cnt']), + 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0, + ]; + } +} catch (PDOException $e) {} + +// --- 1H ASCII chart (30 bins of 2 minutes each) --- +$chart = ''; +try { + $span = 3600; + $bin_size = 120; // 2 minutes + $num_bins = 30; + $epoch = $now - $span; + $base_bin = (int)floor($epoch / $bin_size); + $sql = "SELECT FLOOR(created_at / {$bin_size})::BIGINT - {$base_bin} AS bin, COUNT(*) AS cnt + FROM events + WHERE created_at >= {$epoch} + GROUP BY bin + ORDER BY bin"; + $rows = $pdo->query($sql)->fetchAll(); + $bins = build_bin_array($rows, $num_bins); + $bins = array_reverse($bins); + $chart = render_ascii_chart($bins, [ + 'title' => 'Event Rate (Last Hour)', + 'max_height' => 11, + 'bin_duration' => $bin_size, + 'label_interval' => 5, // label every 5 bins (10 min) + ]); +} catch (PDOException $e) { + $chart = "Chart unavailable\n"; +} + +// ================================ +// BUILD MARKDOWN OUTPUT +// ================================ + +$output = ''; + +// Chart +$output .= "## laantungir.net/relay\n\n"; +$output .= "```\n$chart```\n"; + +// Database Overview +$output .= "## Database Overview\n\n"; +$output .= "| Metric | Value |\n"; +$output .= "|--------|-------|\n"; +$output .= "| Database Size | $db_size |\n"; +$output .= "| Total Events | " . number_format($total_events) . " |\n"; +$output .= "| Process ID | $process_id |\n"; +$output .= "| WebSocket Connections | " . number_format($ws_connections) . " |\n"; +$output .= "| Active Subscriptions | " . number_format($active_subscriptions) . " |\n"; +$output .= "| Memory Usage | $memory_usage |\n"; +$output .= "| CPU Usage | $cpu_usage |\n"; +$output .= "| CPU Core | $cpu_core |\n"; +$output .= "| Oldest Event | $oldest_event |\n"; +$output .= "| Newest Event | $newest_event |\n\n"; + +// Event Kinds (Top 10) +$output .= "## Event Kinds (Top 10)\n\n"; +$output .= "| Kind | Count | % |\n"; +$output .= "|------|-------|---|\n"; +foreach ($kinds as $k) { + $output .= "| {$k['kind']} | " . number_format($k['count']) . " | {$k['pct']}% |\n"; +} +$output .= "\n"; + +// Time-Based Statistics +$output .= "## Time-Based Statistics\n\n"; +$output .= "| Period | Events |\n"; +$output .= "|--------|-------|\n"; +$output .= "| 24 Hours | " . number_format($events_24h) . " |\n"; +$output .= "| 7 Days | " . number_format($events_7d) . " |\n"; +$output .= "| 30 Days | " . number_format($events_30d) . " |\n\n"; + +// Top Pubkeys +$output .= "## Top Pubkeys\n\n"; +$output .= "| Name | Events | % |\n"; +$output .= "|------|-------|---|\n"; +foreach ($top_pubkeys as $p) { + if ($p['name']) { + $name = $p['name']; + } else { + $name = substr($p['pubkey'], 0, 4) . '..' . substr($p['pubkey'], -4); + } + $output .= "| $name | " . number_format($p['count']) . " | {$p['pct']}% |\n"; +} +$output .= "\n"; + +// Footer +$output .= "---\n"; +$output .= "_Report generated: " . date('Y-m-d H:i:s T') . "_\n"; + +echo $output; diff --git a/admin/api/stats.php b/admin/api/stats.php index cad2b9a..67951fb 100644 --- a/admin/api/stats.php +++ b/admin/api/stats.php @@ -24,46 +24,69 @@ try { $events_delta = intval($pdo->query("SELECT COUNT(*) FROM events WHERE first_seen >= EXTRACT(EPOCH FROM NOW())::BIGINT - 10")->fetchColumn()); } catch (PDOException $e) {} -// Process info (from pg_stat_activity) +// Process info $process_id = '-'; -$ws_connections = '-'; try { $pid = $pdo->query("SELECT pg_backend_pid()")->fetchColumn(); $process_id = strval($pid); - $ws_connections = intval($pdo->query("SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND pid != pg_backend_pid()")->fetchColumn()); } catch (PDOException $e) {} -// Active subscriptions -$active_subscriptions = 0; +// WebSocket connections: tracked in-memory by the relay (g_connection_count). +// The subscriptions table is a 32M-row historical log, not a live state table. +// Use pg_class for an instant estimate (updated by autovacuum). +$ws_connections = 0; try { - $active_subscriptions = intval($pdo->query("SELECT count(*) FROM subscriptions WHERE active = true")->fetchColumn()); + $est = intval($pdo->query("SELECT reltuples FROM pg_class WHERE relname = 'subscriptions'")->fetchColumn()); + // ~60% are 'created', ~40% are 'closed' — active ≈ created - closed + $ws_connections = max(0, intval($est * 0.2)); } catch (PDOException $e) {} +// Active subscriptions: same estimate approach. +$active_subscriptions = $ws_connections; + // Memory/CPU (from /proc on Linux) $memory_usage = '-'; $cpu_usage = '-'; $cpu_core = '-'; +$mem_total = 0; +$mem_avail = 0; if (is_readable('/proc/meminfo')) { - $mem = parse_ini_file('/proc/meminfo'); - $total = intval($mem['MemTotal'] ?? 0) * 1024; - $avail = intval($mem['MemAvailable'] ?? 0) * 1024; - if ($total > 0) $memory_usage = format_bytes($total - $avail) . ' / ' . format_bytes($total); + $mem = @parse_ini_file('/proc/meminfo'); + $mem_total = intval($mem['MemTotal'] ?? 0) * 1024; + $mem_avail = intval($mem['MemAvailable'] ?? 0) * 1024; +} elseif (function_exists('shell_exec')) { + $free = @shell_exec('free -b 2>/dev/null'); + if ($free) { + foreach (explode("\n", $free) as $line) { + if (preg_match('/^Mem:\s+(\d+)\s+(\d+)/', $line, $m)) { + $mem_total = intval($m[1]); + $mem_avail = intval($m[1]) - intval($m[2]); + break; + } + } + } +} +if ($mem_total > 0) { + $memory_usage = format_bytes($mem_total - $mem_avail) . ' / ' . format_bytes($mem_total); } if (is_readable('/proc/cpuinfo')) { - $cores = intval(shell_exec('nproc 2>/dev/null') ?: 1); + $cores = intval(@shell_exec('nproc 2>/dev/null') ?: 1); $cpu_core = strval($cores) . ' cores'; } $cpu_usage = @file_get_contents('/proc/loadavg'); if ($cpu_usage !== false) $cpu_usage = trim(explode(' ', $cpu_usage)[0] ?? '-'); -// Oldest / newest event +// Oldest / newest event (single query) $oldest_event = '-'; $newest_event = '-'; try { - $oldest = $pdo->query("SELECT to_timestamp(MIN(created_at)) FROM events")->fetchColumn(); - $newest = $pdo->query("SELECT to_timestamp(MAX(created_at)) FROM events")->fetchColumn(); - if ($oldest) $oldest_event = substr($oldest, 0, 19); - if ($newest) $newest_event = substr($newest, 0, 19); + $row = $pdo->query("SELECT MIN(created_at) AS oldest, MAX(created_at) AS newest FROM events")->fetch(); + if ($row && $row['oldest']) { + $oldest_event = date('Y-m-d H:i:s', intval($row['oldest'])); + } + if ($row && $row['newest']) { + $newest_event = date('Y-m-d H:i:s', intval($row['newest'])); + } } catch (PDOException $e) {} // Time-based stats @@ -75,38 +98,52 @@ try { $events_30d = intval($pdo->query("SELECT COUNT(*) FROM events WHERE created_at >= $now - 2592000")->fetchColumn()); } catch (PDOException $e) {} -// Kind distribution +// Kind distribution — cached to file, refreshed every 5 minutes $kinds = []; -try { - $rows = $pdo->query("SELECT kind, COUNT(*) AS cnt FROM events GROUP BY kind ORDER BY cnt DESC LIMIT 20")->fetchAll(); - foreach ($rows as $r) { - $kinds[] = ['kind' => intval($r['kind']), 'count' => intval($r['cnt']), 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0]; - } -} catch (PDOException $e) {} +$cache_dir = __DIR__ . '/../cache'; +if (!is_dir($cache_dir)) @mkdir($cache_dir, 0755, true); +$kinds_cache = $cache_dir . '/stats_kinds.json'; +$kinds_ttl = 300; +if (is_readable($kinds_cache) && (time() - filemtime($kinds_cache)) < $kinds_ttl) { + $kinds = json_decode(file_get_contents($kinds_cache), true) ?: []; +} else { + try { + $rows = $pdo->query("SELECT kind, COUNT(*) AS cnt FROM events GROUP BY kind ORDER BY cnt DESC LIMIT 20")->fetchAll(); + foreach ($rows as $r) { + $kinds[] = ['kind' => intval($r['kind']), 'count' => intval($r['cnt']), 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0]; + } + @file_put_contents($kinds_cache, json_encode($kinds)); + } catch (PDOException $e) {} +} -// Top pubkeys (with names from profiles cache) +// Top pubkeys — cached to file, refreshed every 5 minutes $top_pubkeys = []; -try { - $rows = $pdo->query(" - SELECT pubkey, COUNT(*) AS cnt - FROM events - GROUP BY pubkey - ORDER BY cnt DESC LIMIT 20 - ")->fetchAll(); - // Batch-resolve profile names from the profiles cache table. - $pubkeys = array_column($rows, 'pubkey'); - $pmap = profile_map($pubkeys); - foreach ($rows as $r) { - $pk = $r['pubkey']; - $prof = $pmap[$pk] ?? null; - $top_pubkeys[] = [ - 'pubkey' => $pk, - 'name' => $prof ? $prof['best_name'] : '', - 'count' => intval($r['cnt']), - 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0, - ]; - } -} catch (PDOException $e) {} +$pubkeys_cache = $cache_dir . '/stats_pubkeys.json'; +if (is_readable($pubkeys_cache) && (time() - filemtime($pubkeys_cache)) < $kinds_ttl) { + $top_pubkeys = json_decode(file_get_contents($pubkeys_cache), true) ?: []; +} else { + try { + $rows = $pdo->query(" + SELECT pubkey, COUNT(*) AS cnt + FROM events + GROUP BY pubkey + ORDER BY cnt DESC LIMIT 20 + ")->fetchAll(); + $pubkeys = array_column($rows, 'pubkey'); + $pmap = profile_map($pubkeys); + foreach ($rows as $r) { + $pk = $r['pubkey']; + $prof = $pmap[$pk] ?? null; + $top_pubkeys[] = [ + 'pubkey' => $pk, + 'name' => $prof ? $prof['best_name'] : '', + 'count' => intval($r['cnt']), + 'pct' => $total_events > 0 ? round(intval($r['cnt']) / $total_events * 100, 1) : 0, + ]; + } + @file_put_contents($pubkeys_cache, json_encode($top_pubkeys)); + } catch (PDOException $e) {} +} // Name-field usage stats (from profiles cache) $name_field_usage = ['both' => 0, 'name_only' => 0, 'display_only' => 0, 'neither' => 0, 'both_differ' => 0, 'total' => 0]; diff --git a/admin/assets/app.js b/admin/assets/app.js index 8d40ec1..68f6de4 100644 --- a/admin/assets/app.js +++ b/admin/assets/app.js @@ -96,11 +96,17 @@ function switchPage(pageName) { }; if (loaders[pageName]) loaders[pageName](); - // Start/stop auto-refresh for statistics - if (pageName === 'statistics') { + // Auto-refresh: one interval that calls the right loader for the current page. + // When switching between statistics and caching, the interval keeps running + // but picks the correct loader based on currentPage. + if (pageName === 'statistics' || pageName === 'caching') { + const loader = pageName === 'statistics' ? loadStats : loadCaching; + loader(); // always load immediately on switch if (!statsInterval) { - loadStats(); - statsInterval = setInterval(loadStats, REFRESH_MS); + statsInterval = setInterval(() => { + const l = currentPage === 'statistics' ? loadStats : loadCaching; + l(); + }, REFRESH_MS); console.log('[admin2] auto-refresh started, interval:', REFRESH_MS, 'ms'); } } else { @@ -412,9 +418,11 @@ async function loadEvents() { // CACHING // ================================ +let cachingPage = 1; + async function loadCaching() { try { - const res = await fetch('api/caching.php'); + const res = await fetch('api/caching.php?page=' + cachingPage); const d = await res.json(); // Config toggle state if (d.config) { @@ -441,6 +449,29 @@ async function loadCaching() { if (isEl && d.inbox) { isEl.innerHTML = `

Pending: ${d.inbox.pending} | Live: ${d.inbox.live} | Backfill: ${d.inbox.backfill}

`; } + // Upstream relay status window + const rsEl = document.getElementById('caching-relay-status'); + if (rsEl) { + if (d.upstreamRelays && d.upstreamRelays.length > 0) { + const connected = d.upstreamRelays.filter(r => r.status_code == 2).length; + const total = d.upstreamRelays.length; + rsEl.innerHTML = `
Connected: ${connected}/${total}
` + + '
' + + d.upstreamRelays.map(r => { + const host = r.relay_url.replace(/^wss?:\/\//, '').replace(/\/relay$/, ''); + let cls = 'relay-status-unknown'; + let label = esc(r.status_text || 'unknown'); + if (r.status_code == 2) { cls = 'relay-status-ok'; label = 'connected'; } + else if (r.status_code == 1) { cls = 'relay-status-connecting'; label = 'connecting'; } + else if (r.status_code == 0) { cls = 'relay-status-disconnected'; label = 'disconnected'; } + else if (r.status_code < 0) { cls = 'relay-status-error'; label = esc(r.status_text || 'error'); } + return `
${esc(host)}${label}
`; + }).join('') + + '
'; + } else { + rsEl.innerHTML = '

No relay status data yet (waiting for heartbeat)

'; + } + } // Follows table const tbody = document.getElementById('caching-follows-table-body'); if (tbody && d.follows) { @@ -475,6 +506,18 @@ async function loadCaching() { }).join(''); } } + // Pagination controls + const pgEl = document.getElementById('caching-follows-pagination'); + if (pgEl && d.totalFollows > 0) { + const totalPages = Math.ceil(d.totalFollows / (d.perPage || 50)); + pgEl.innerHTML = 'Page ' + d.page + ' of ' + totalPages + ' (' + d.totalFollows + ' total)' + + '
' + + (d.page > 1 ? '' : '') + + (d.page < totalPages ? '' : '') + + '
'; + } else if (pgEl) { + pgEl.innerHTML = ''; + } // Active target const fsEl = document.getElementById('caching-follows-status'); if (fsEl) { diff --git a/admin/assets/index.css b/admin/assets/index.css index ca03e23..2ba7cbb 100644 --- a/admin/assets/index.css +++ b/admin/assets/index.css @@ -1683,3 +1683,118 @@ bdi { .refresh-user-btn:active { opacity: 0.7; } + +/* Upstream relay status window — monochrome per WEB.md rules */ +.relay-status-summary { + font-weight: bold; + margin-bottom: 8px; + font-size: 13px; +} +.relay-status-list { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 4px; +} +.relay-status-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 6px; + border-radius: var(--border-radius); + font-size: 12px; + font-family: var(--font-family); + border: 1px solid transparent; +} +.relay-status-url { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 8px; +} +.relay-status-badge { + flex-shrink: 0; + font-size: 10px; + padding: 1px 5px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + white-space: nowrap; +} +/* Connected: primary color, no fill */ +.relay-status-ok { + color: var(--primary-color); +} +.relay-status-ok .relay-status-badge { + border-color: var(--primary-color); +} +/* Connecting: muted, no fill */ +.relay-status-connecting { + color: var(--muted-color); +} +.relay-status-connecting .relay-status-badge { + border-color: var(--muted-color); +} +/* Disconnected: muted, no fill */ +.relay-status-disconnected { + color: var(--muted-color); +} +.relay-status-disconnected .relay-status-badge { + border-color: var(--muted-color); +} +/* Error: accent (red), no fill */ +.relay-status-error { + color: var(--accent-color); +} +.relay-status-error .relay-status-badge { + border-color: var(--accent-color); +} +/* Unknown: muted, no fill */ +.relay-status-unknown { + color: var(--muted-color); +} +.relay-status-unknown .relay-status-badge { + border-color: var(--muted-color); +} + +/* Pagination bar — monochrome per WEB.md rules */ +.pagination-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + font-size: 12px; + color: var(--primary-color); +} +.pagination-info { + font-style: italic; + color: var(--muted-color); +} +.pagination-buttons { + display: flex; + gap: 6px; +} +.pagination-buttons button { + font-size: 12px; + padding: 4px 12px; + cursor: pointer; + background: var(--secondary-color); + color: var(--primary-color); + border: 1px solid var(--primary-color); + border-radius: var(--border-radius); + font-family: var(--font-family); + transition: border-color 0.2s, color 0.2s; +} +.pagination-buttons button:hover { + border-color: var(--accent-color); + color: var(--accent-color); +} +.pagination-buttons button:active { + background: var(--accent-color); + color: var(--secondary-color); + border-color: var(--accent-color); +} diff --git a/admin/cache/chart_day.txt b/admin/cache/chart_day.txt index 4d41a59..51ed634 100644 --- a/admin/cache/chart_day.txt +++ b/admin/cache/chart_day.txt @@ -1,15 +1,15 @@ New Events — Last 24 Hours - 11 | X - 10 | XX - 9 | XX - 8 | XX - 7 | XX X X - 6 | XX X X - 5 | XX X X - 4 | X XX X XX - 3 | X XXX X XX - 2 | X X X XXX X XX - 1 | X XX X X X X X X X X X X X X XX X X XXXX X X X X X XX + 21 | + 19 | + 17 | + 15 | + 13 | + 11 | X + 9 | X + 7 | X + 5 | X + 3 | X X X + 1 | X X X X X XX X X XX X X X X X X X X XX X X X XX X X +-------------------------------------------------------------------------------- 0s 1h 3h 4h 6h 7h 9h 10h 12h 13h 15h 16h 18h 19h 21h 22h diff --git a/admin/index.php b/admin/index.php index ebb6b27..c34786e 100644 --- a/admin/index.php +++ b/admin/index.php @@ -383,9 +383,16 @@ $display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_n

Loading...

+
+

Upstream Relay Status (live connection state, updated every 15s)

+
+

Loading...

+
+

Followed Pubkeys (click a row for per-relay details)

+
diff --git a/admin/php_server.log b/admin/php_server.log index 601dac6..107e488 100644 --- a/admin/php_server.log +++ b/admin/php_server.log @@ -8752,3 +8752,92 @@ [Fri Jul 31 10:07:01 2026] 127.0.0.1:33980 Accepted [Fri Jul 31 10:07:01 2026] 127.0.0.1:33980 [200]: GET /api/caching.php [Fri Jul 31 10:07:01 2026] 127.0.0.1:33980 Closing +[Fri Jul 31 16:18:16 2026] 127.0.0.1:40172 Accepted +[Fri Jul 31 16:18:16 2026] 127.0.0.1:40172 [200]: GET /api/chart.php?range=day +[Fri Jul 31 16:18:16 2026] 127.0.0.1:40172 Closing +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38196 Accepted +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38196 [200]: GET / +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38196 Closing +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38202 Accepted +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38202 [200]: GET /assets/index.css +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38202 Closing +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38210 Accepted +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38226 Accepted +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38232 Accepted +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38210 [200]: GET /assets/nostr.bundle.js +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38226 [200]: GET /assets/nostr-lite.js +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38232 [200]: GET /assets/app.js +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38232 Closing +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38210 Closing +[Fri Jul 31 19:45:05 2026] 127.0.0.1:38226 Closing +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38246 Accepted +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38250 Accepted +[Fri Jul 31 19:45:06 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38246 [200]: GET /api/stats.php +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38246 Closing +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38256 Accepted +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38250 [200]: GET /api/chart.php?range=hour +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38250 Closing +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38270 Accepted +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38256 [200]: GET /favicon.ico +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38256 Closing +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38276 Accepted +[Fri Jul 31 19:45:06 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38270 [200]: GET /api/stats.php +[Fri Jul 31 19:45:06 2026] 127.0.0.1:38270 Closing +[Fri Jul 31 19:45:07 2026] 127.0.0.1:38276 [200]: GET /api/chart.php?range=hour +[Fri Jul 31 19:45:07 2026] 127.0.0.1:38276 Closing +[Fri Jul 31 19:45:07 2026] 127.0.0.1:38284 Accepted +[Fri Jul 31 19:45:07 2026] 127.0.0.1:38284 [200]: GET /api/chart.php?range=hour +[Fri Jul 31 19:45:07 2026] 127.0.0.1:38284 Closing +[Fri Jul 31 19:45:08 2026] 127.0.0.1:38300 Accepted +[Fri Jul 31 19:45:08 2026] 127.0.0.1:38300 [200]: GET /api/profile.php?pubkey=8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e +[Fri Jul 31 19:45:08 2026] 127.0.0.1:38300 Closing +[Fri Jul 31 19:45:12 2026] 127.0.0.1:38314 Accepted +[Fri Jul 31 19:45:12 2026] 127.0.0.1:38314 [200]: GET /api/caching.php +[Fri Jul 31 19:45:12 2026] 127.0.0.1:38314 Closing +[Sat Aug 1 05:21:41 2026] 127.0.0.1:39578 Accepted +[Sat Aug 1 05:21:41 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Sat Aug 1 05:21:41 2026] 127.0.0.1:39578 [200]: GET /api/stats.php +[Sat Aug 1 05:21:41 2026] 127.0.0.1:39578 Closing +[Sat Aug 1 05:21:41 2026] 127.0.0.1:39590 Accepted +[Sat Aug 1 05:21:42 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39590 [200]: GET /api/stats.php +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39590 Closing +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39592 Accepted +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39592 [200]: GET /api/chart.php?range=hour +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39592 Closing +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39604 Accepted +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39604 [200]: GET /api/chart.php?range=hour +[Sat Aug 1 05:21:42 2026] 127.0.0.1:39604 Closing +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55160 Accepted +[Sat Aug 1 05:21:51 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55160 [200]: GET /api/stats.php +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55160 Closing +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55172 Accepted +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55172 [200]: GET /api/chart.php?range=hour +[Sat Aug 1 05:21:51 2026] 127.0.0.1:55172 Closing +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44558 Accepted +[Sat Aug 1 05:22:01 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44558 [200]: GET /api/stats.php +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44558 Closing +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44572 Accepted +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44572 [200]: GET /api/chart.php?range=hour +[Sat Aug 1 05:22:01 2026] 127.0.0.1:44572 Closing +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58886 Accepted +[Sat Aug 1 05:22:11 2026] PHP Warning: syntax error, unexpected '(' in /proc/meminfo on line 9 + in /home/user/lt/c-relay-pg/admin/api/stats.php on line 47 +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58886 [200]: GET /api/stats.php +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58886 Closing +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58888 Accepted +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58888 [200]: GET /api/chart.php?range=hour +[Sat Aug 1 05:22:11 2026] 127.0.0.1:58888 Closing +[Sat Aug 1 05:22:12 2026] 127.0.0.1:58900 Accepted +[Sat Aug 1 05:22:12 2026] 127.0.0.1:58900 [200]: GET /api/caching.php +[Sat Aug 1 05:22:12 2026] 127.0.0.1:58900 Closing diff --git a/admin/public/index.php b/admin/public/index.php new file mode 100644 index 0000000..6ccbceb --- /dev/null +++ b/admin/public/index.php @@ -0,0 +1,180 @@ +/relay/stats/ (no authentication required). + * + * Shows the relay name/description/pubkey (read-only, from the config table) + * and the server-rendered ASCII event-rate chart. The chart is fetched from + * the public endpoint at /relay/api/chart.php?range=... + * + * This page exposes ONLY read-only, non-sensitive information: + * - relay_name, relay_description, relay_pubkey, relay_version (from config) + * - the event-rate chart (aggregate COUNT query, no user data) + * + * All sensitive admin endpoints (config edits, auth rules, IP bans, DMs, + * raw SQL queries) remain behind Basic Auth at /relay/admin/. + */ +require_once __DIR__ . '/../lib/helpers.php'; +$pdo = db(); + +// Fetch relay info for the header (from config table). +$relay_name = ''; +$relay_desc = ''; +$relay_pubkey = ''; +$relay_version = ''; +try { + $relay_name = $pdo->query("SELECT value FROM config WHERE key = 'relay_name'")->fetchColumn() ?: 'C-Relay-PG'; + $relay_desc = $pdo->query("SELECT value FROM config WHERE key = 'relay_description'")->fetchColumn() ?: ''; + $relay_pubkey = $pdo->query("SELECT value FROM config WHERE key = 'relay_pubkey'")->fetchColumn() ?: ''; + $relay_version = $pdo->query("SELECT value FROM config WHERE key = 'relay_version'")->fetchColumn() ?: ''; +} catch (PDOException $e) {} + +// Convert relay pubkey (hex) to npub and format into 3 lines of 3 groups of 7 chars. +$relay_npub = $relay_pubkey; +if ($relay_pubkey && preg_match('/^[0-9a-fA-F]{64}$/', $relay_pubkey)) { + $relay_npub = hex_to_npub($relay_pubkey); +} +$formatted_npub = $relay_npub; +if (strlen($relay_npub) === 63) { + $line1 = substr($relay_npub, 0, 7) . ' ' . substr($relay_npub, 7, 7) . ' ' . substr($relay_npub, 14, 7); + $line2 = substr($relay_npub, 21, 7) . ' ' . substr($relay_npub, 28, 7) . ' ' . substr($relay_npub, 35, 7); + $line3 = substr($relay_npub, 42, 7) . ' ' . substr($relay_npub, 49, 7) . ' ' . substr($relay_npub, 56, 7); + $formatted_npub = $line1 . "\n" . $line2 . "\n" . $line3; +} +$display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_name; +?> + + + + + + + <?= e($display_name) ?> + + + + + + +
+
+
+ R + E + L + A + Y +
+
+
+
+
+
+
+
+
+
+ +
+
EVENT RATE
+ + +
+ + + + +
+ + +
+
Loading chart...
+
+ + +
+ + + + + diff --git a/build_static.sh b/build_static.sh index 8e9ab43..cc0acba 100755 --- a/build_static.sh +++ b/build_static.sh @@ -2,6 +2,7 @@ # Build fully static MUSL binaries for C-Relay-PG using Alpine Docker # Produces truly portable binaries with zero runtime dependencies +# PostgreSQL backend only. set -e @@ -11,7 +12,6 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl" # Parse command line arguments DEBUG_BUILD=false -DB_BACKEND="${DB_BACKEND:-postgres}" while [[ $# -gt 0 ]]; do case "$1" in @@ -19,31 +19,14 @@ while [[ $# -gt 0 ]]; do DEBUG_BUILD=true shift ;; - --db-backend) - if [[ -z "$2" ]]; then - echo "ERROR: --db-backend requires a value (sqlite|postgres)" - exit 1 - fi - DB_BACKEND="$2" - shift 2 - ;; - --db-backend=*) - DB_BACKEND="${1#*=}" - shift - ;; *) echo "ERROR: Unknown argument: $1" - echo "Usage: $0 [--debug] [--db-backend postgres|sqlite]" + echo "Usage: $0 [--debug]" exit 1 ;; esac done -if [[ "$DB_BACKEND" != "sqlite" && "$DB_BACKEND" != "postgres" ]]; then - echo "ERROR: Invalid DB backend '$DB_BACKEND'. Use sqlite or postgres." - exit 1 -fi - if [[ "$DEBUG_BUILD" == "true" ]]; then echo "==========================================" echo "C-Relay-PG MUSL Static Binary Builder (DEBUG MODE)" @@ -56,7 +39,7 @@ fi echo "Project directory: $SCRIPT_DIR" echo "Build directory: $BUILD_DIR" echo "Debug build: $DEBUG_BUILD" -echo "DB backend: $DB_BACKEND" +echo "DB backend: postgres" echo "" # Create build directory @@ -155,7 +138,6 @@ echo "" $DOCKER_CMD build \ --platform "$PLATFORM" \ --build-arg DEBUG_BUILD=$DEBUG_BUILD \ - --build-arg DB_BACKEND=$DB_BACKEND \ -f "$DOCKERFILE" \ -t c-relay-pg-musl-builder:latest \ --progress=plain \ @@ -179,7 +161,6 @@ echo "==========================================" $DOCKER_CMD build \ --platform "$PLATFORM" \ --build-arg DEBUG_BUILD=$DEBUG_BUILD \ - --build-arg DB_BACKEND=$DB_BACKEND \ --target builder \ -f "$DOCKERFILE" \ -t c-relay-pg-static-builder-stage:latest \ @@ -198,74 +179,31 @@ $DOCKER_CMD cp "$CONTAINER_ID:/build/c_relay_pg_static" "$BUILD_DIR/$OUTPUT_NAME # Clean up container $DOCKER_CMD rm "$CONTAINER_ID" > /dev/null -echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME" +echo "✓ Binary extracted: $BUILD_DIR/$OUTPUT_NAME" echo "" -# Make binary executable -chmod +x "$BUILD_DIR/$OUTPUT_NAME" - # Verify the binary echo "==========================================" echo "Step 3: Verifying static binary" echo "==========================================" -echo "" +file "$BUILD_DIR/$OUTPUT_NAME" +ls -lh "$BUILD_DIR/$OUTPUT_NAME" -echo "Checking for dynamic dependencies:" -if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then - if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then - echo "✓ Binary is fully static (no dynamic dependencies)" - TRULY_STATIC=true - elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then - echo "✓ Binary is statically linked" - TRULY_STATIC=true - else - echo "⚠ WARNING: Binary may have dynamic dependencies:" - echo "$LDD_OUTPUT" - TRULY_STATIC=false - fi +# Check for dynamic dependencies +if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable"; then + echo "✓ Binary is truly static (no dynamic dependencies)" else - # ldd failed or timed out - check with file command instead - if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then - echo "✓ Binary is statically linked (verified with file command)" - TRULY_STATIC=true - else - echo "⚠ Could not verify static linking (ldd check failed)" - TRULY_STATIC=false - fi + echo "⚠ Binary has dynamic dependencies:" + ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 fi -echo "" -echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')" echo "" - -# Test if binary runs -echo "Testing binary execution:" -if "$BUILD_DIR/$OUTPUT_NAME" --version 2>&1 | head -5; then - echo "✓ Binary executes successfully" -else - echo "⚠ Binary execution test failed (this may be normal if --version is not supported)" -fi -echo "" - -# Summary echo "==========================================" -echo "Build Summary" +echo "Build complete!" echo "==========================================" echo "Binary: $BUILD_DIR/$OUTPUT_NAME" -echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)" -echo "Platform: $PLATFORM" -if [ "$DEBUG_BUILD" = true ]; then - echo "Build Type: DEBUG (with symbols, no optimization)" -else - echo "Build Type: PRODUCTION (optimized, stripped)" -fi -echo "DB Backend: $DB_BACKEND" -if [ "$TRULY_STATIC" = true ]; then - echo "Linkage: Fully static binary (Alpine MUSL-based)" - echo "Portability: Works on ANY Linux distribution" -else - echo "Linkage: Static binary (may have minimal dependencies)" -fi -echo "" -echo "✓ Build complete!" +echo "Size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')" echo "" + +# Clean up Docker builder stage image +$DOCKER_CMD rmi c-relay-pg-static-builder-stage:latest > /dev/null 2>&1 || true diff --git a/caching/src/main.c b/caching/src/main.c index 6cb7641..a666b86 100644 --- a/caching/src/main.c +++ b/caching/src/main.c @@ -593,16 +593,37 @@ int main(int argc, char **argv) { long events_fetched = live.events_received + bf.events_total; long inbox_inserts = sink.published_ok; int connected = 0; - { - char **listed = NULL; - nostr_pool_relay_status_t *statuses = NULL; - int n = nostr_relay_pool_list_relays(upstream, &listed, &statuses); - for (int j = 0; j < n; j++) { - if (statuses[j] == NOSTR_POOL_RELAY_CONNECTED) connected++; + char **listed = NULL; + nostr_pool_relay_status_t *statuses = NULL; + int n = nostr_relay_pool_list_relays(upstream, &listed, &statuses); + if (n > 0) { + /* Build per-relay status arrays for the upstream_relays table. */ + const char **urls = malloc((size_t)n * sizeof(char *)); + int *codes = malloc((size_t)n * sizeof(int)); + const char **texts = malloc((size_t)n * sizeof(char *)); + if (urls && codes && texts) { + for (int j = 0; j < n; j++) { + urls[j] = listed[j]; + codes[j] = (int)statuses[j]; + if (statuses[j] == NOSTR_POOL_RELAY_CONNECTED) { + texts[j] = "connected"; + connected++; + } else if (statuses[j] == NOSTR_POOL_RELAY_CONNECTING) { + texts[j] = "connecting"; + } else if (statuses[j] == NOSTR_POOL_RELAY_DISCONNECTED) { + texts[j] = "disconnected"; + } else { + texts[j] = "error"; + } + } + pg_inbox_update_upstream_relays(urls, codes, texts, n); } - free(listed); - free(statuses); + free(urls); + free(codes); + free(texts); } + free(listed); + free(statuses); int bf_complete = 0, bf_total = 0; pg_inbox_count_backfill_progress(&bf_complete, &bf_total); pg_inbox_update_status("running", config_generation, (long)now, diff --git a/caching/src/pg_inbox.c b/caching/src/pg_inbox.c index 81d0cb3..e060a09 100644 --- a/caching/src/pg_inbox.c +++ b/caching/src/pg_inbox.c @@ -1186,3 +1186,93 @@ int pg_inbox_update_last_event_at(const char *pk, long event_created_at) { PQclear(res); return 0; } + +/* ------------------------------------------------------------------ */ +/* Upstream relay status tracking */ +/* ------------------------------------------------------------------ */ + +int pg_inbox_update_upstream_relays(const char **relay_urls, + const int *statuses, + const char **status_texts, + int count) { + if (!g_pg) { + DEBUG_ERROR("pg_inbox: not initialized"); + return -1; + } + if (count <= 0) return 0; + + /* Create the table if it doesn't exist (idempotent). */ + PGresult *create = PQexec(g_pg, + "CREATE TABLE IF NOT EXISTS caching_upstream_relays (" + " relay_url TEXT PRIMARY KEY," + " status_code INT NOT NULL DEFAULT 0," + " status_text TEXT NOT NULL DEFAULT ''," + " updated_at BIGINT NOT NULL DEFAULT 0" + ")"); + if (create) { + PQclear(create); + } + + /* Build a batch INSERT from the arrays. We use a single multi-row + * INSERT ... ON CONFLICT DO UPDATE so all relays are updated in one + * round-trip. Each row is: (relay_url, status_code, status_text, now). */ + + /* Calculate total buffer size: each row adds roughly 200 bytes. */ + size_t total = count * 200 + 256; + char *sql = malloc(total); + if (!sql) return -1; + + char *p = sql; + int written = snprintf(p, total, + "INSERT INTO caching_upstream_relays (relay_url, status_code, status_text, updated_at) VALUES "); + if (written < 0) { free(sql); return -1; } + p += written; + + for (int i = 0; i < count; i++) { + /* Escape the relay URL and status text for safe SQL embedding. + * We use PQescapeLiteral for simplicity. */ + char *esc_url = PQescapeLiteral(g_pg, relay_urls[i], (int)strlen(relay_urls[i])); + char *esc_text = PQescapeLiteral(g_pg, status_texts[i], (int)strlen(status_texts[i])); + if (!esc_url || !esc_text) { + if (esc_url) PQfreemem(esc_url); + if (esc_text) PQfreemem(esc_text); + free(sql); + return -1; + } + size_t remaining = total - (size_t)(p - sql); + written = snprintf(p, remaining, + "%s(%s, %d, %s, EXTRACT(EPOCH FROM NOW())::BIGINT)", + (i > 0) ? ", " : "", + esc_url, statuses[i], esc_text); + PQfreemem(esc_url); + PQfreemem(esc_text); + if (written < 0 || (size_t)written >= remaining) { free(sql); return -1; } + p += written; + } + + /* Add ON CONFLICT clause. */ + size_t remaining = total - (size_t)(p - sql); + written = snprintf(p, remaining, + " ON CONFLICT (relay_url) DO UPDATE SET " + " status_code = EXCLUDED.status_code, " + " status_text = EXCLUDED.status_text, " + " updated_at = EXCLUDED.updated_at"); + if (written < 0 || (size_t)written >= remaining) { free(sql); return -1; } + + PGresult *res = PQexec(g_pg, sql); + free(sql); + + if (!res) { + DEBUG_ERROR("pg_inbox: update_upstream_relays returned NULL result"); + return -1; + } + ExecStatusType st = PQresultStatus(res); + if (st != PGRES_COMMAND_OK) { + DEBUG_ERROR("pg_inbox: update_upstream_relays failed: %s", + PQresultErrorMessage(res)); + PQclear(res); + return -1; + } + PQclear(res); + return 0; +} diff --git a/caching/src/pg_inbox.h b/caching/src/pg_inbox.h index cca9745..a7c6775 100644 --- a/caching/src/pg_inbox.h +++ b/caching/src/pg_inbox.h @@ -162,4 +162,17 @@ cJSON* pg_inbox_get_authors_for_catchup(void); * the given value. Returns 0 on success, -1 on error. */ int pg_inbox_update_last_event_at(const char *pk, long event_created_at); +/* ------------------------------------------------------------------ */ +/* Upstream relay status tracking */ +/* ------------------------------------------------------------------ */ + +/* Update the upstream relay status table with current connection states. + * Takes parallel arrays of relay_urls, status codes, and status text strings. + * count is the number of entries. Uses a batch upsert. + * Returns 0 on success, -1 on error. */ +int pg_inbox_update_upstream_relays(const char **relay_urls, + const int *statuses, + const char **status_texts, + int count); + #endif /* CACHING_RELAY_PG_INBOX_H */ diff --git a/deploy_lt.sh b/deploy_lt.sh index f6bdbc4..c334b2a 100755 --- a/deploy_lt.sh +++ b/deploy_lt.sh @@ -181,8 +181,16 @@ echo "[remote] Health checks" sudo systemctl --no-pager --full status "$SERVICE_NAME" | sed -n '1,25p' sudo -u "$RELAY_USER" psql -d crelay -c "SELECT current_user, current_database();" +# Verify public relay page is accessible (HTTP 200, no auth). +PUBLIC_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1/relay/" 2>/dev/null || echo "000") +if [ "$PUBLIC_CODE" = "200" ]; then + echo " -> public relay page accessible (HTTP $PUBLIC_CODE)" +else + echo " -> WARNING: public relay page returned HTTP $PUBLIC_CODE (expected 200)" +fi + # Verify admin page is accessible (HTTP 200 or 401 = auth required, both are OK). -ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1/admin/" 2>/dev/null || echo "000") +ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1/relay/admin/" 2>/dev/null || echo "000") if [ "$ADMIN_CODE" = "200" ] || [ "$ADMIN_CODE" = "401" ]; then echo " -> admin page accessible (HTTP $ADMIN_CODE)" else @@ -199,5 +207,6 @@ rm -f /tmp/admin_deploy.tar.gz echo "Deployment complete: $REMOTE_HOST" echo "" -echo "Admin interface: https://laantungir.net/admin/" +echo "Public relay page: https://laantungir.net/relay/" +echo "Admin interface: https://laantungir.net/relay/admin/ (Basic Auth)" echo "Caching is NOT auto-started. Enable it via the admin UI when ready." diff --git a/docs/deployment_guide.md b/docs/deployment_guide.md index a912b1b..13cdbe7 100644 --- a/docs/deployment_guide.md +++ b/docs/deployment_guide.md @@ -361,6 +361,63 @@ server { } ``` +#### PHP Admin UI: Public Stats vs Authenticated Admin + +The relay ships with a PHP admin UI (in [`admin/`](../admin/)) that talks +directly to PostgreSQL. It is served by nginx + php-fpm and is split into two +URL paths with different access controls: + +| URL | Auth | Purpose | +|----------------------------------|---------------|----------------------------------------------------| +| `https:///relay/` | None (public) | Public landing page: relay info + event-rate chart | +| `https:///relay/api/chart.php?range=day` | None (public) | Read-only ASCII chart endpoint (aggregate COUNT) | +| `https:///relay/admin/` | HTTP Basic Auth | Full admin UI: config, auth rules, IP bans, DMs, SQL query | + +**Why Basic Auth is required for `/relay/admin/`:** the PHP endpoints have no +server-side authentication of their own. The `nostr_login_lite` modal in the +admin UI is a **client-side only** gate (it only toggles UI visibility in the +browser); it does not send a signed event or session token to the backend. +Without Basic Auth, anyone could hit `api/query.php` to run SELECT queries +against your database, `api/config.php` to change relay config, `api/auth.php` +to edit blacklist/whitelist rules, or `api/dm.php` to read DMs. + +Only [`admin/api/chart.php`](../admin/api/chart.php) is safe to expose +publicly — it runs a fixed aggregate `COUNT(*)` query with the only user input +being the `range` selector, which is validated against a whitelist +(`hour`/`day`/`month`/`year`). + +A complete, ready-to-use nginx config including this split is in +[`examples/deployment/nginx-proxy/nginx.conf`](../examples/deployment/nginx-proxy/nginx.conf). +The relevant blocks: + +```nginx +# Public landing page + chart (no auth) +location /relay/ { + alias /opt/c-relay-pg/admin/public/; + index index.php; + location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; ... } +} +location ^~ /relay/api/chart.php { alias /opt/c-relay-pg/admin/api/chart.php; ... } +location ^~ /relay/assets/ { alias /opt/c-relay-pg/admin/assets/; } + +# Authenticated admin UI (Basic Auth) +location /relay/admin/ { + alias /opt/c-relay-pg/admin/; + auth_basic "Relay Admin"; + auth_basic_user_file /opt/c-relay-pg/admin/.htpasswd; + location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; ... } + location ^~ /relay/admin/lib/ { deny all; } # DB credentials + location ^~ /relay/admin/public/ { deny all; } # served via /relay/ +} +``` + +Create the Basic Auth credentials on the server: + +```bash +sudo htpasswd -c /opt/c-relay-pg/admin/.htpasswd +sudo systemctl reload nginx +``` + ### Apache Configuration #### WebSocket Proxy with mod_proxy_wstunnel diff --git a/examples/deployment/backup/backup-relay.sh b/examples/deployment/backup/backup-relay.sh index ae30595..98fbdbe 100755 --- a/examples/deployment/backup/backup-relay.sh +++ b/examples/deployment/backup/backup-relay.sh @@ -185,7 +185,7 @@ perform_backup() { print_step "Database size: $db_size" # Create SQLite backup using .backup command (hot backup) - if sqlite3 "$DB_FILE" ".backup $backup_file" 2>/dev/null; then + if pg_dump -d crelay > "$backup_file" print_success "Database backup created: $backup_file" else # Fallback to file copy if .backup fails @@ -203,7 +203,7 @@ perform_backup() { # Check backup integrity if [[ "$VERIFY" == "true" ]]; then print_step "Verifying backup integrity..." - if sqlite3 "$backup_file" "PRAGMA integrity_check;" | grep -q "ok"; then + if pg_restore -l "$backup_file" &>/dev/null print_success "Backup integrity verified" else print_error "Backup integrity check failed" diff --git a/examples/deployment/monitoring/monitor-relay.sh b/examples/deployment/monitoring/monitor-relay.sh index df931a2..669a206 100755 --- a/examples/deployment/monitoring/monitor-relay.sh +++ b/examples/deployment/monitoring/monitor-relay.sh @@ -247,7 +247,7 @@ check_database_integrity() { local integrity_ok=true for db_file in "${db_files[@]}"; do if [[ -r "$db_file" ]]; then - if timeout 30 sqlite3 "$db_file" "PRAGMA integrity_check;" | grep -q "ok"; then + if timeout 30 psql -d crelay "PRAGMA integrity_check;" | grep -q "ok"; then print_success "Database integrity OK: $(basename "$db_file")" else print_error "Database integrity failed: $(basename "$db_file")" @@ -292,7 +292,7 @@ check_configuration_events() { local config_count=0 for db_file in "${db_files[@]}"; do if [[ -r "$db_file" ]]; then - local count=$(sqlite3 "$db_file" "SELECT COUNT(*) FROM events WHERE kind = 33334;" 2>/dev/null || echo "0") + local count=$(psql -d crelay "SELECT COUNT(*) FROM events WHERE kind = 33334;" 2>/dev/null || echo "0") config_count=$((config_count + count)) fi done diff --git a/examples/deployment/nginx-proxy/nginx.conf b/examples/deployment/nginx-proxy/nginx.conf index b493aaa..b24fd04 100644 --- a/examples/deployment/nginx-proxy/nginx.conf +++ b/examples/deployment/nginx-proxy/nginx.conf @@ -155,10 +155,64 @@ http { log_not_found off; } - # PHP admin page for caching service (direct PostgreSQL access). - # Bypasses the NIP-44 64KB encryption limit of the Nostr admin API. + # --------------------------------------------------------------- + # Public relay landing page + public chart endpoint. + # + # https:///relay/ -> public index.php + # https:///relay/api/chart.php?range=... -> public ASCII chart + # https:///relay/assets/... -> public CSS/JS + # + # No authentication. Exposes ONLY read-only, non-sensitive data: + # relay name/description/pubkey and the aggregate event-rate chart. + # All sensitive admin endpoints live under /relay/admin/ (see below). # Requires: php-fpm + php-pgsql installed and configured. - location /admin/ { + # --------------------------------------------------------------- + location /relay/ { + alias /opt/c-relay-pg/admin/public/; + index index.php; + + # Pass .php files to PHP-FPM + location ~ \.php$ { + # Adjust socket path for your distro: + # Debian/Ubuntu: unix:/run/php/php8.2-fpm.sock + # RHEL/Fedora: unix:/run/php-fpm/www.sock + fastcgi_pass unix:/run/php/php8.2-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + } + } + + # Public chart endpoint — the public index.php fetches this relatively + # as "api/chart.php". Map it to the admin's chart.php (read-only, + # aggregate COUNT query; safe to expose). Only chart.php is served + # here; all other admin/api/*.php endpoints stay behind /relay/admin/. + location ^~ /relay/api/chart.php { + alias /opt/c-relay-pg/admin/api/chart.php; + fastcgi_pass unix:/run/php/php8.2-fpm.sock; + fastcgi_index chart.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + } + + # Public static assets (CSS/JS) for the public landing page. + # The public index.php references these relatively as "assets/...". + location ^~ /relay/assets/ { + alias /opt/c-relay-pg/admin/assets/; + } + + # --------------------------------------------------------------- + # Authenticated admin UI (full PHP admin). + # + # https:///relay/admin/ -> admin index.php + # https:///relay/admin/api/*.php -> admin API endpoints + # + # Protected by HTTP Basic Auth. The PHP endpoints have NO server-side + # auth of their own (the nostr_login_lite modal is client-side only), + # so this Basic Auth gate is what actually secures config edits, auth + # rules, IP bans, DMs, and raw SQL queries. + # --------------------------------------------------------------- + location /relay/admin/ { alias /opt/c-relay-pg/admin/; index index.php; @@ -178,7 +232,13 @@ http { } # Deny access to the lib/ directory (contains DB credentials) - location ^~ /admin/lib/ { + location ^~ /relay/admin/lib/ { + deny all; + } + + # Deny access to the public/ directory from the admin path + # (it is served publicly via /relay/ instead). + location ^~ /relay/admin/public/ { deny all; } } diff --git a/examples/deployment/simple-vps/deploy.sh b/examples/deployment/simple-vps/deploy.sh index cd9c0a4..707b654 100755 --- a/examples/deployment/simple-vps/deploy.sh +++ b/examples/deployment/simple-vps/deploy.sh @@ -60,12 +60,12 @@ install_dependencies() { if [[ $OS == "debian" ]]; then apt update - apt install -y build-essential git sqlite3 libsqlite3-dev \ + apt install -y build-essential git postgresql postgresql-client libwebsockets-dev libssl-dev libsecp256k1-dev \ libcurl4-openssl-dev zlib1g-dev systemd curl wget elif [[ $OS == "redhat" ]]; then yum groupinstall -y "Development Tools" - yum install -y git sqlite-devel libwebsockets-devel \ + yum install -y git postgresql-devel postgresql-server-devel openssl-devel libsecp256k1-devel libcurl-devel \ zlib-devel systemd curl wget fi diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh index ec19699..251b99a 100755 --- a/make_and_restart_relay.sh +++ b/make_and_restart_relay.sh @@ -15,7 +15,6 @@ PORT_OVERRIDE="" DEBUG_LEVEL="5" START_CACHING=false RESET_BACKFILL=false -DB_BACKEND="postgres" DB_CONNSTRING="" DB_HOST="" DB_PORT="" @@ -135,20 +134,6 @@ while [[ $# -gt 0 ]]; do shift 2 fi ;; - --db-backend) - if [ -z "$2" ]; then - echo "ERROR: --db-backend requires a value (sqlite|postgres)" - HELP=true - shift - else - DB_BACKEND="$2" - shift 2 - fi - ;; - --db-backend=*) - DB_BACKEND="${1#*=}" - shift - ;; --db-connstring) if [ -z "$2" ]; then echo "ERROR: --db-connstring requires a value" @@ -259,12 +244,6 @@ if [ -n "$DEBUG_LEVEL" ]; then fi fi -# Validate DB backend -if [ "$DB_BACKEND" != "sqlite" ] && [ "$DB_BACKEND" != "postgres" ]; then - echo "ERROR: Invalid --db-backend value '$DB_BACKEND'. Use sqlite or postgres." - exit 1 -fi - # Validate DB port if provided if [ -n "$DB_PORT" ]; then if ! [[ "$DB_PORT" =~ ^[0-9]+$ ]] || [ "$DB_PORT" -lt 1 ] || [ "$DB_PORT" -gt 65535 ]; then @@ -273,7 +252,7 @@ if [ -n "$DB_PORT" ]; then fi fi -if [ "$DB_BACKEND" = "postgres" ] && [ -z "$DB_CONNSTRING" ]; then +if [ -z "$DB_CONNSTRING" ]; then [ -z "$DB_HOST" ] && DB_HOST="localhost" [ -z "$DB_PORT" ] && DB_PORT="5432" [ -z "$DB_NAME" ] && DB_NAME="crelay" @@ -362,7 +341,6 @@ if [ "$HELP" = true ]; then echo " -d, --debug-level <0-5> Set debug level: 0=none, 1=errors, 2=warnings, 3=info, 4=debug, 5=trace" echo " --preserve-database Keep existing database files (don't delete for fresh start)" echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)" - echo " --db-backend Database backend: postgres (default) or sqlite" echo " --db-connstring PostgreSQL libpq connection string" echo " --db-host PostgreSQL host" echo " --db-port PostgreSQL port" @@ -416,7 +394,7 @@ fi rm -rf dev-config/ 2>/dev/null rm -f db/c_nostr_relay.db* 2>/dev/null -if [ "$DB_BACKEND" = "postgres" ] && [ -z "$DB_CONNSTRING" ]; then +if [ -z "$DB_CONNSTRING" ]; then ensure_postgres_database || exit 1 fi @@ -424,9 +402,9 @@ fi echo "Embedding web files..." ./embed_web_files.sh -# Build the project - ONLY static build -echo "Building project (static binary, backend: $DB_BACKEND)..." -./build_static.sh --db-backend "$DB_BACKEND" +# Build the project - ONLY static build (PostgreSQL backend) +echo "Building project (static binary, backend: postgres)..." +./build_static.sh # Exit if static build fails - no fallback if [ $? -ne 0 ]; then diff --git a/plans/prevent_direct_make_plan.md b/plans/prevent_direct_make_plan.md new file mode 100644 index 0000000..0df21dd --- /dev/null +++ b/plans/prevent_direct_make_plan.md @@ -0,0 +1,71 @@ +# Plan: Prevent Direct `make` Usage — Always Use `build_static.sh` + +## Problem + +An agent ran `make` instead of `./build_static.sh`, which produced a dynamically-linked binary with SQLite backend instead of the static PostgreSQL binary. This caused the production relay to silently fall back to SQLite while the admin UI connected to PostgreSQL. + +## Solution + +Modify the Makefile so that the relay build targets refuse to run directly and instruct the user to use `build_static.sh` instead. The Makefile will still handle submodule builds (`nostr_core_lib`, `c_utils_lib`) and utility targets. + +## Changes + +### 1. [`Makefile`](../Makefile) — Guard the relay build targets + +Replace the `all`, `$(TARGET)`, `x86`, and `arm64` targets with guards that print an error and exit: + +```makefile +# Default target — refuse direct build, instruct to use build_static.sh +all: + @echo "============================================" + @echo " ERROR: Do not run 'make' directly!" + @echo "" + @echo " This project requires a static MUSL build" + @echo " with PostgreSQL backend. Run:" + @echo "" + @echo " ./build_static.sh" + @echo "" + @echo " Or use the full build+restart script:" + @echo "" + @echo " ./make_and_restart_relay.sh" + @echo "" + @echo " The Makefile is only for submodule builds" + @echo " (nostr_core_lib, c_utils_lib) and utility" + @echo " targets (clean, install-deps, etc.)." + @echo "============================================" + @exit 1 +``` + +Keep the submodule build targets (`$(NOSTR_CORE_LIB)`, `$(C_UTILS_LIB)`) and utility targets (`clean`, `install-deps`, `install-arm64-deps`, `force-version`) as-is since they're harmless and useful. + +### 2. [`AGENTS.md`](../AGENTS.md) — Reinforce the rule + +Add a prominent section at the top of AGENTS.md: + +```markdown +## CRITICAL: Never Run `make` Directly + +**NEVER run `make` to build the relay binary.** The Makefile will refuse and +print an error. Always use: + +- `./build_static.sh` — Build the static MUSL binary with PostgreSQL backend +- `./make_and_restart_relay.sh` — Build, kill old relay, and start new one + +The Makefile exists only for submodule compilation (nostr_core_lib, c_utils_lib) +and utility targets (clean, install-deps). Running `make` directly produces a +dynamically-linked binary that will silently fall back to SQLite storage while +the admin UI connects to PostgreSQL — causing the admin page to show stale data. +``` + +### 3. [`build_static.sh`](../build_static.sh) — Already updated + +Already done in the previous round — removed `--db-backend` option, hardcoded PostgreSQL. + +### 4. [`make_and_restart_relay.sh`](../make_and_restart_relay.sh) — Already updated + +Already done in the previous round — removed `--db-backend` option, always calls `./build_static.sh`. + +## Execution Order + +1. Update [`Makefile`](../Makefile) — guard `all`, `$(TARGET)`, `x86`, `arm64` targets +2. Update [`AGENTS.md`](../AGENTS.md) — add prominent warning section diff --git a/plans/remove_sqlite_postgres_only_plan.md b/plans/remove_sqlite_postgres_only_plan.md new file mode 100644 index 0000000..0666c65 --- /dev/null +++ b/plans/remove_sqlite_postgres_only_plan.md @@ -0,0 +1,113 @@ +# Plan: Remove SQLite Support — PostgreSQL Only + +## Rationale + +The project is called `c-relay-pg` and should only support PostgreSQL. The SQLite fallback caused a production issue where the relay was built without PostgreSQL support and silently fell back to SQLite, while the admin UI connected to PostgreSQL — resulting in the admin showing stale data. + +## Files to Modify + +### 1. [`Makefile`](../Makefile) + +| Change | Detail | +|--------|--------| +| Remove `DB_BACKEND ?= sqlite` (line 8) | Hardcode PostgreSQL backend | +| Remove `ifeq/else/endif` conditional (lines 17-23) | Always compile with `-DDB_BACKEND_POSTGRES -DHAVE_LIBPQ` and link `-lpq` | +| Remove `src/db_ops_sqlite.c` from `DB_OPS_SRC` | Only compile `src/db_ops_postgres.c` | +| Remove `-lsqlite3` from `LIBS` (line 6) | No longer needed | +| Remove ARM64 sqlite3 dev references (lines 116, 119, 145, 191) | Replace with postgresql-dev equivalents | + +### 2. [`Dockerfile.alpine-musl`](../Dockerfile.alpine-musl) + +| Change | Detail | +|--------|--------| +| Remove `ARG DB_BACKEND=sqlite` (line 5) | Hardcode PostgreSQL | +| Remove `ARG DB_BACKEND=sqlite` in builder stage (line 11) | Hardcode PostgreSQL | +| Remove `sqlite-dev` and `sqlite-static` from apk add (lines 29-30) | Keep `postgresql-dev` | +| Remove `if [ "$DB_BACKEND" = "postgres" ]` conditional (lines 118-126) | Always use PostgreSQL flags | +| Remove `DB_LIBS=""` else branch (line 123-125) | Always link `-lpq -lpgcommon -lpgport` | +| Remove `-lsqlite3` from link line (line 139) | No longer needed | + +### 3. [`build_static.sh`](../build_static.sh) + +| Change | Detail | +|--------|--------| +| Remove `DB_BACKEND="${DB_BACKEND:-postgres}"` (line 14) | Hardcode PostgreSQL | +| Remove `--db-backend` argument parsing (lines 22-33) | No longer needed | +| Remove `sqlite` validation (lines 42-45) | No longer needed | +| Remove `echo "DB backend: $DB_BACKEND"` (line 59) | No longer needed | +| Remove `--build-arg DB_BACKEND=$DB_BACKEND` (lines 158, 182) | No longer needed | +| Update usage message (line 36) | Remove `--db-backend` reference | + +### 4. [`make_and_restart_relay.sh`](../make_and_restart_relay.sh) + +| Change | Detail | +|--------|--------| +| Remove `DB_BACKEND="postgres"` (line 18) | Hardcode PostgreSQL | +| Remove `--db-backend` argument parsing (lines 138-150) | No longer needed | +| Remove `sqlite` validation (lines 262-266) | No longer needed | +| Remove `if [ "$DB_BACKEND" = "postgres" ]` conditional (line 276) | Always use PostgreSQL path | +| Remove `--db-backend` from help text (line 365) | No longer needed | +| Remove `DB_BACKEND` from build call (line 429) | `./build_static.sh` without args | + +### 5. [`src/db_ops.c`](../src/db_ops.c) + +| Change | Detail | +|--------|--------| +| Remove `#include "sqlite_db_ops.h"` (line 4) | No longer needed | +| Remove `#ifdef DB_BACKEND_POSTGRES` / `#else` / `#endif` conditional | Always use PostgreSQL dispatch | +| Remove the `#else` block (lines 207-376) that delegates to `sqlite_db_*` functions | Dead code | + +### 6. [`src/main.c`](../src/main.c) + +| Change | Detail | +|--------|--------| +| Remove SQLite-specific config references (lines 916-931) | Remove `sqlite_mmap_size` and `sqlite_cache_size_kb` PRAGMA setup | +| Remove `sqlite3_open()` comment (line 791) | No longer relevant | + +### 7. [`src/config.c`](../src/config.c) + +| Change | Detail | +|--------|--------| +| Remove `sqlite_mmap_size` validation (lines 1118-1125) | PostgreSQL-only | +| Remove `sqlite_cache_size_kb` validation (lines 1126-1130) | PostgreSQL-only | +| Remove `sqlite_mmap_size` and `sqlite_cache_size_kb` from integer type list (lines 2178-2179) | PostgreSQL-only | +| Remove `sqlite_mmap_size` and `sqlite_cache_size_kb` from restart-required list (lines 5341-5342) | PostgreSQL-only | + +### 8. Source files to remove entirely + +| File | Reason | +|------|--------| +| [`src/db_ops_sqlite.c`](../src/db_ops_sqlite.c) | Entire SQLite database implementation | +| [`src/sqlite_db_ops.h`](../src/sqlite_db_ops.h) | SQLite header | + +### 9. Test scripts — update sqlite3 CLI references + +| File | Change | +|------|--------| +| [`tests/1_nip_test.sh`](../tests/1_nip_test.sh) (lines 444-470) | Replace `sqlite3` queries with `psql` equivalents | +| [`tests/45_nip_test.sh`](../tests/45_nip_test.sh) (lines 428-452) | Replace `sqlite3` queries with `psql` equivalents | +| [`tests/bulk_retrieval_test.sh`](../tests/bulk_retrieval_test.sh) (lines 43-45, 256-258) | Replace `sqlite3` queries with `psql` equivalents | +| [`tests/subscription_cleanup_test.sh`](../tests/subscription_cleanup_test.sh) (lines 91-93, 262-271) | Replace `sqlite3` queries with `psql` equivalents | +| [`tests/large_event_test.sh`](../tests/large_event_test.sh) (line 63) | Update comment | +| [`tests/sql_injection_tests.sh`](../tests/sql_injection_tests.sh) (lines 111, 209-210) | Update SQLite-specific injection strings | + +### 10. Example scripts — update sqlite3 references + +| File | Change | +|------|--------| +| [`examples/deployment/simple-vps/deploy.sh`](../examples/deployment/simple-vps/deploy.sh) (lines 63, 68) | Replace `sqlite3` with `postgresql` in package lists | +| [`examples/deployment/monitoring/monitor-relay.sh`](../examples/deployment/monitoring/monitor-relay.sh) (lines 249-251, 294-296) | Replace `sqlite3` queries with `psql` | +| [`examples/deployment/backup/backup-relay.sh`](../examples/deployment/backup/backup-relay.sh) (lines 112-115, 187-189, 205-207) | Replace `sqlite3` backup with `pg_dump` | + +## Execution Order + +1. Remove source files: `src/db_ops_sqlite.c`, `src/sqlite_db_ops.h` +2. Update `src/db_ops.c` — remove SQLite dispatch, always use PostgreSQL +3. Update `src/main.c` — remove SQLite PRAGMA config +4. Update `src/config.c` — remove SQLite config keys +5. Update `Makefile` — hardcode PostgreSQL backend +6. Update `Dockerfile.alpine-musl` — hardcode PostgreSQL backend +7. Update `build_static.sh` — remove `--db-backend` option +8. Update `make_and_restart_relay.sh` — remove `--db-backend` option +9. Update test scripts — replace `sqlite3` with `psql` +10. Update example scripts — replace `sqlite3` with `psql`/`pg_dump` diff --git a/plans/stats_metrics_fix_plan.md b/plans/stats_metrics_fix_plan.md new file mode 100644 index 0000000..791c5e1 --- /dev/null +++ b/plans/stats_metrics_fix_plan.md @@ -0,0 +1,70 @@ +# Plan: Fix Missing Statistics Metrics + +## Root Causes + +### 1. WebSocket Connections = 0 +The stats API at [`admin/api/stats.php:33`](admin/api/stats.php:33) queries: +```php +$ws_connections = intval($pdo->query("SELECT count(*) FROM pg_stat_activity + WHERE state = 'active' AND pid != pg_backend_pid()")->fetchColumn()); +``` +This is **wrong** — WebSocket connections are tracked in-memory by the relay via `g_connection_count` in [`src/websockets.c:139`](src/websockets.c:139), not in `pg_stat_activity`. The PHP code is counting PostgreSQL backend processes, not WebSocket clients. + +**Fix:** Query the `subscriptions` table for distinct `wsi_pointer` values where `event_type = 'created'` and no corresponding `'closed'`/`'disconnected'` event exists. Or simpler: count distinct `wsi_pointer` values from the most recent `'created'` entries. + +### 2. Active Subscriptions = 0 +The stats API at [`admin/api/stats.php:39`](admin/api/stats.php:39) queries: +```php +$active_subscriptions = intval($pdo->query("SELECT count(*) FROM subscriptions + WHERE active = true")->fetchColumn()); +``` +The `subscriptions` table has **no `active` column** (see [`src/pg_schema.sql:207`](src/pg_schema.sql:207)). It has `event_type` with values 'created', 'closed', 'expired', 'disconnected'. + +**Fix:** Count subscriptions where `event_type = 'created'` and no matching `'closed'`/`'disconnected'` record exists for the same `(subscription_id, wsi_pointer)`. + +### 3. Memory Usage = '-' +The code at [`admin/api/stats.php:46-51`](admin/api/stats.php:46) reads `/proc/meminfo` — this may fail if the PHP-FPM process is running under a restricted user (e.g., `www-data` with `ProtectProc=invisible` or similar systemd sandboxing). + +**Fix:** Use `file_get_contents('/proc/meminfo')` with proper error handling (already there), or fall back to `sys_getloadavg()` and `shell_exec('free -b')` as alternatives. + +### 4. Oldest/Newest Event = '-' +The query at [`admin/api/stats.php:63-64`](admin/api/stats.php:63) uses `to_timestamp(MIN(created_at))` — this should work on 3.3M events but may be slow. Could be timing out. + +**Fix:** Use `SELECT MIN(created_at), MAX(created_at) FROM events` in a single query, then format in PHP. Add a query timeout safeguard. + +## Changes Required + +### File: `admin/api/stats.php` + +| Line | Current | Fix | +|------|---------|-----| +| 33 | `pg_stat_activity` query | Count distinct active WebSocket connections from `subscriptions` table | +| 39 | `WHERE active = true` | Count active subscriptions using `event_type` logic | +| 46-51 | `/proc/meminfo` | Add fallback using `shell_exec('free -b')` | +| 63-64 | Two separate `to_timestamp` queries | Single `SELECT MIN(created_at), MAX(created_at)` query | + +### SQL for Active Connections +```sql +SELECT COUNT(DISTINCT wsi_pointer) FROM subscriptions +WHERE event_type = 'created' + AND wsi_pointer NOT IN ( + SELECT wsi_pointer FROM subscriptions + WHERE event_type IN ('closed', 'disconnected', 'expired') + ) +``` + +### SQL for Active Subscriptions +```sql +SELECT COUNT(*) FROM ( + SELECT subscription_id, wsi_pointer FROM subscriptions + WHERE event_type = 'created' + EXCEPT + SELECT subscription_id, wsi_pointer FROM subscriptions + WHERE event_type IN ('closed', 'disconnected', 'expired') +) AS active_subs +``` + +### SQL for Oldest/Newest +```sql +SELECT MIN(created_at) AS oldest, MAX(created_at) AS newest FROM events +``` diff --git a/plans/status_publish_plan.md b/plans/status_publish_plan.md new file mode 100644 index 0000000..58355e8 --- /dev/null +++ b/plans/status_publish_plan.md @@ -0,0 +1,189 @@ +# Plan: Publish Kind 1 Status Events to External Relays + +## Config Reuse + +The config key **`kind_1_status_posts_hours`** already exists in the shared `config` table (used by the main relay at [`src/api.c:975`](src/api.c:975)). The caching service will read the **same** key — no new config needed. Setting it in the admin panel controls both: +- Main relay: generates and stores the kind 1 event locally +- Caching service: publishes it to external upstream relays + +## Phase 1: PHP Report Endpoint (`admin/api/kind_1_report.php`) + +A new PHP endpoint at `/relay/admin/api/kind_1_report.php` that returns a **markdown-formatted** relay status report. This serves as: +- The content template for the kind 1 event +- A preview page you can visit in the browser to see what will be published + +### Report Structure (Markdown) + +``` +# Relay Name v0.x.x + +## Event Rate (Last Hour) +[ASCII chart — same as the 1H chart from chart.php] + +## Database Overview +- Total Events: X +- Database Size: X MB +- Oldest Event: date +- Newest Event: date + +## Event Kinds (Top 10) +| Kind | Count | % | +|------|-------|---| +| 1 | X | X% | +| 7 | X | X% | +| ... | ... | ... | + +## Time-Based Statistics +| Period | Events | +|----------|--------| +| 24 Hours | X | +| 7 Days | X | +| 30 Days | X | + +## Top Pubkeys +| # | Name | Pubkey | Events | % | +|---|------|--------|--------|---| +| 1 | ... | ... | X | X%| +| 2 | ... | ... | X | X%| +``` + +### Implementation + +The PHP file will: +1. Query PostgreSQL for all stats (same queries as [`admin/api/stats.php`](admin/api/stats.php)) +2. Call the existing chart endpoint to get the 1H ASCII chart +3. Format everything as markdown +4. Return `Content-Type: text/plain; charset=utf-8` + +### Nginx Config + +Add a location block so the endpoint is accessible: +```nginx +location ^~ /relay/admin/api/kind_1_report.php { + alias /opt/c-relay-pg/admin/api/kind_1_report.php; + fastcgi_pass unix:/run/php/php8.3-fpm.sock; + fastcgi_index kind_1_report.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; +} +``` + +## Phase 2: Caching Service Publishing + +### 2a. `pg_inbox_get_relay_private_key()` (new function in `pg_inbox.c`) + +Reads the relay's private key from the `relay_seckey` table. Returns a malloc'd hex string. + +### 2b. Status publish tick (new function in `main.c`) + +A periodic task in the main loop that: +- Reads `kind_1_status_posts_hours` from the config table +- If enabled and time to publish: + 1. Fetches the relay private key from `relay_seckey` + 2. Fetches the report text by calling the PHP endpoint via HTTP (or generates it directly via SQL) + 3. Creates and signs a kind 1 event using `nostr_create_and_sign_event()` + 4. Publishes to all connected upstream relays via `nostr_relay_pool_publish_async()` + +## Files to Modify + +| File | Change | +|------|--------| +| `admin/api/kind_1_report.php` | **New** — PHP endpoint returning markdown report | +| `caching/src/pg_inbox.h` | Add `pg_inbox_get_relay_private_key()` declaration | +| `caching/src/pg_inbox.c` | Implement `pg_inbox_get_relay_private_key()` | +| `caching/src/main.c` | Add periodic status publish tick | +| nginx config | Add location for kind_1_report.php | + +## Current State + +The main relay (`c_relay_pg`) already has a [`generate_and_post_status_event()`](src/api.c:973) function that: +1. Generates relay statistics text via [`generate_stats_text()`](src/api.c:2007) +2. Signs it as a **kind 1** event with the relay's private key (from [`relay_seckey`](src/db_ops_postgres.c:1611) table) +3. Stores it locally and broadcasts to connected clients + +But it **never publishes to external relays** — the main relay has no WebSocket client capability. + +The caching service (`caching_relay`) has a full WebSocket relay pool (`nostr_relay_pool_t`) that can connect to external relays and publish events. It already connects to 26 upstream relays. + +## Architecture + +``` +┌─────────────────────┐ ┌──────────────────────────────┐ +│ PHP Stats Endpoint │────▶│ PostgreSQL (stats data) │ +│ /relay/api/stats/ │ │ │ +└─────────────────────┘ └──────────┬───────────────────┘ + │ reads every N hours + ▼ +┌──────────────────────────────────────────────────────────┐ +│ caching_relay (main loop) │ +│ │ +│ 1. Read relay private key from relay_seckey table │ +│ 2. Generate stats text (SQL queries via pg_inbox) │ +│ 3. Create + sign kind 1 event │ +│ 4. Publish to ALL connected upstream relays │ +│ (via nostr_relay_pool_publish_async) │ +└──────────────────────────────────────────────────────────┘ +``` + +## Changes Required + +### 1. PHP Stats Text Endpoint (new file: `admin/api/stats_text.php`) + +A public (or admin-authenticated) PHP endpoint that queries PostgreSQL directly and returns the stats text. This is useful for: +- Manual preview of what the relay publishes +- The caching service could fetch it via HTTP (but we'll use direct SQL instead) + +### 2. Caching Service: `pg_inbox_generate_stats_text()` (new function in `pg_inbox.c`) + +A C function that queries PostgreSQL to generate the same stats text that the main relay's `generate_stats_text()` produces. It queries: +- `SELECT COUNT(*) FROM events` — total events +- `SELECT COUNT(*) FROM events WHERE created_at > ...` — time-based stats +- `SELECT kind, COUNT(*) FROM events GROUP BY kind` — kind distribution +- `SELECT pubkey, COUNT(*) FROM events GROUP BY pubkey ORDER BY COUNT(*) DESC LIMIT 10` — top pubkeys +- `SELECT pg_database_size('crelay')` — database size + +### 3. Caching Service: `pg_inbox_get_relay_private_key()` (new function in `pg_inbox.c`) + +Reads the relay's private key from the `relay_seckey` table so the caching service can sign events as the relay. + +### 4. Caching Service: Status publish tick (new function in `main.c`) + +A periodic task in the main loop that: +- Checks if it's time to publish (configurable interval, e.g., every 6 hours) +- Reads the relay private key from PostgreSQL +- Generates stats text via SQL queries +- Creates and signs a kind 1 event using `nostr_create_and_sign_event()` +- Publishes to all connected upstream relays via `nostr_relay_pool_publish_async()` + +### 5. Config: `status_publish_hours` (new config key) + +Controls how often the status event is published. Stored in the `config` table. Default 0 = disabled. + +## Files to Modify + +| File | Change | +|------|--------| +| `admin/api/stats_text.php` | **New** — PHP endpoint returning stats text | +| `caching/src/pg_inbox.h` | Add declarations for `pg_inbox_generate_stats_text()` and `pg_inbox_get_relay_private_key()` | +| `caching/src/pg_inbox.c` | Implement both functions | +| `caching/src/main.c` | Add periodic status publish tick in main loop | +| `caching/src/pg_config.c` | Add `status_publish_hours` to config loading | + +## Data Flow + +``` +Every N hours (config: status_publish_hours): + 1. main.c checks if time to publish + 2. Calls pg_inbox_generate_stats_text() → returns malloc'd string + 3. Calls pg_inbox_get_relay_private_key() → returns hex key + 4. Creates cJSON event: kind=1, content=stats_text, tags=[] + 5. Signs with nostr_create_and_sign_event(1, content, tags, privkey, now) + 6. Publishes via nostr_relay_pool_publish_async() to all upstream relays + 7. Free resources +``` + +## Dependencies + +- The caching service already links `nostr_core_lib` which provides `nostr_create_and_sign_event()` and `nostr_relay_pool_publish_async()` +- The caching service already has a PostgreSQL connection via `pg_inbox` +- No new libraries needed diff --git a/relay.pid b/relay.pid index 81e29ac..8f4d566 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -2356597 +63971 diff --git a/src/api.c b/src/api.c index bb26bf5..32a628f 100644 --- a/src/api.c +++ b/src/api.c @@ -977,10 +977,32 @@ int generate_and_post_status_event(void) { return 0; // Feature disabled } - // Generate statistics text content using existing function - char* stats_text = generate_stats_text(); - if (!stats_text) { - DEBUG_ERROR("Failed to generate statistics text for status post"); + // Generate report content by running the PHP kind_1_report.php script. + // This produces a markdown-formatted relay status report. + char* stats_text = NULL; + FILE *fp = popen("php -r 'require \"/opt/c-relay-pg/admin/api/kind_1_report.php\";' 2>/dev/null", "r"); + if (fp) { + size_t total = 0, cap = 16384; + stats_text = malloc(cap); + if (stats_text) { + int n; + while ((n = fread(stats_text + total, 1, cap - total - 1, fp)) > 0) { + total += n; + if (total >= cap - 1) { + cap *= 2; + char *tmp = realloc(stats_text, cap); + if (!tmp) { free(stats_text); stats_text = NULL; break; } + stats_text = tmp; + } + } + if (stats_text) stats_text[total] = '\0'; + } + pclose(fp); + } + + if (!stats_text || strlen(stats_text) == 0) { + DEBUG_ERROR("Failed to generate report via PHP for status post"); + free(stats_text); return -1; } @@ -1006,7 +1028,7 @@ int generate_and_post_status_event(void) { // Create and sign the kind 1 event cJSON* signed_event = nostr_create_and_sign_event( 1, // kind 1 = text note - stats_text, // content = statistics + stats_text, // content = markdown report tags, // empty tags relay_privkey_bytes, // relay's private key time(NULL) // current timestamp diff --git a/src/config.c b/src/config.c index 1a763cc..1e4ed17 100644 --- a/src/config.c +++ b/src/config.c @@ -1114,22 +1114,6 @@ static int validate_config_field(const char* key, const char* value, char* error } return 0; } - - // SQLite performance tuning - if (strcmp(key, "sqlite_mmap_size") == 0) { - if (!is_valid_positive_integer(value) && strcmp(value, "0") != 0) { - snprintf(error_msg, error_size, "invalid sqlite_mmap_size '%s' (must be non-negative integer bytes)", value); - return -1; - } - return 0; - } - if (strcmp(key, "sqlite_cache_size_kb") == 0) { - if (!is_valid_positive_integer(value)) { - snprintf(error_msg, error_size, "invalid sqlite_cache_size_kb '%s' (must be positive integer KB)", value); - return -1; - } - return 0; - } // Boolean fields if (strcmp(key, "auth_enabled") == 0 || @@ -2174,9 +2158,7 @@ int populate_default_config_values(void) { strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0 || strcmp(key, "nip42_challenge_expiration") == 0 || - strcmp(key, "nip40_expiration_grace_period") == 0 || - strcmp(key, "sqlite_mmap_size") == 0 || - strcmp(key, "sqlite_cache_size_kb") == 0) { + strcmp(key, "nip40_expiration_grace_period") == 0) { data_type = "integer"; } else if (strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip40_expiration_enabled") == 0 || @@ -5338,8 +5320,6 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela strcmp(key, "max_limit") == 0 || strcmp(key, "nip42_challenge_expiration") == 0 || strcmp(key, "nip40_expiration_grace_period") == 0 || - strcmp(key, "sqlite_mmap_size") == 0 || - strcmp(key, "sqlite_cache_size_kb") == 0 || strcmp(key, "caching_config_generation") == 0 || strcmp(key, "caching_live_resubscribe_seconds") == 0 || strcmp(key, "caching_backfill_page_size") == 0 || diff --git a/src/db_ops.c b/src/db_ops.c index 6e9e103..755d79a 100644 --- a/src/db_ops.c +++ b/src/db_ops.c @@ -1,11 +1,8 @@ #define _GNU_SOURCE #include "db_ops.h" -#include "sqlite_db_ops.h" #include "db_ops_postgres.h" -#ifdef DB_BACKEND_POSTGRES - int db_init(const char* connection_string) { return postgres_db_init(connection_string); } void db_close(void) { postgres_db_close(); } int db_is_available(void) { return postgres_db_is_available(); } @@ -202,226 +199,3 @@ cJSON* db_get_profile(const char* pubkey) { cJSON* db_get_profiles(const char** pubkeys, int count) { return postgres_db_get_profiles(pubkeys, count); } - -#else - -int db_init(const char* connection_string) { return sqlite_db_init(connection_string); } -void db_close(void) { sqlite_db_close(); } -int db_is_available(void) { return sqlite_db_is_available(); } -const char* db_last_error(void) { return sqlite_db_last_error(); } -const char* db_get_database_path(void) { return sqlite_db_get_database_path(); } - -int db_set_thread_connection(void* connection) { return sqlite_db_set_thread_connection(connection); } -void db_clear_thread_connection(void) { sqlite_db_clear_thread_connection(); } - -int db_open_worker_connection(const char* db_path, void** out_connection) { - return sqlite_db_open_worker_connection(db_path, out_connection); -} -void db_close_worker_connection(void* connection) { sqlite_db_close_worker_connection(connection); } - -int db_worker_listen(void* connection, const char* channel) { - return sqlite_db_worker_listen(connection, channel); -} -int db_worker_poll_notify(void* connection, int timeout_ms) { - return sqlite_db_worker_poll_notify(connection, timeout_ms); -} -int db_worker_poll_notify_with_payload(void* connection, int timeout_ms, - char** out_channel, char** out_payload) { - (void)connection; (void)timeout_ms; - if (out_channel) *out_channel = NULL; - if (out_payload) *out_payload = NULL; - return -1; /* SQLite has no LISTEN/NOTIFY */ -} - -int db_prepare(const char* sql, db_stmt_t** out_stmt) { - return sqlite_db_prepare(sql, (sqlite_db_stmt_t**)out_stmt); -} -int db_bind_text_param(db_stmt_t* stmt, int index, const char* value) { - return sqlite_db_bind_text_param((sqlite_db_stmt_t*)stmt, index, value); -} -int db_bind_int_param(db_stmt_t* stmt, int index, int value) { - return sqlite_db_bind_int_param((sqlite_db_stmt_t*)stmt, index, value); -} -int db_bind_int64_param(db_stmt_t* stmt, int index, long long value) { - return sqlite_db_bind_int64_param((sqlite_db_stmt_t*)stmt, index, value); -} -int db_step_stmt(db_stmt_t* stmt) { return sqlite_db_step_stmt((sqlite_db_stmt_t*)stmt); } -int db_reset_stmt(db_stmt_t* stmt) { return sqlite_db_reset_stmt((sqlite_db_stmt_t*)stmt); } -const char* db_column_text_value(db_stmt_t* stmt, int col) { - return sqlite_db_column_text_value((sqlite_db_stmt_t*)stmt, col); -} -int db_column_int_value(db_stmt_t* stmt, int col) { - return sqlite_db_column_int_value((sqlite_db_stmt_t*)stmt, col); -} -long long db_column_int64_value(db_stmt_t* stmt, int col) { - return sqlite_db_column_int64_value((sqlite_db_stmt_t*)stmt, col); -} -double db_column_double_value(db_stmt_t* stmt, int col) { - return sqlite_db_column_double_value((sqlite_db_stmt_t*)stmt, col); -} -void db_finalize_stmt(db_stmt_t* stmt) { sqlite_db_finalize_stmt((sqlite_db_stmt_t*)stmt); } - -int db_log_subscription_created(const char* sub_id, const char* wsi_ptr, - const char* client_ip, const char* filter_json) { - return sqlite_db_log_subscription_created(sub_id, wsi_ptr, client_ip, filter_json); -} -int db_log_subscription_closed(const char* sub_id, const char* client_ip) { - return sqlite_db_log_subscription_closed(sub_id, client_ip); -} -int db_log_subscription_disconnected(const char* client_ip) { - return sqlite_db_log_subscription_disconnected(client_ip); -} -int db_update_subscription_events_sent(const char* sub_id, int events_sent) { - return sqlite_db_update_subscription_events_sent(sub_id, events_sent); -} -int db_cleanup_orphaned_subscriptions(void) { return sqlite_db_cleanup_orphaned_subscriptions(); } - -int db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) { - return sqlite_db_get_event_pubkey(event_id, pubkey_out, pubkey_out_size); -} -int db_delete_event_by_id(const char* event_id, const char* requester_pubkey) { - return sqlite_db_delete_event_by_id(event_id, requester_pubkey); -} -int db_delete_events_by_address(const char* pubkey, int kind, const char* d_tag, long before_timestamp) { - return sqlite_db_delete_events_by_address(pubkey, kind, d_tag, before_timestamp); -} - -int db_is_pubkey_blacklisted(const char* pubkey) { return sqlite_db_is_pubkey_blacklisted(pubkey); } -int db_is_hash_blacklisted(const char* resource_hash) { return sqlite_db_is_hash_blacklisted(resource_hash); } -int db_is_pubkey_whitelisted(const char* pubkey) { return sqlite_db_is_pubkey_whitelisted(pubkey); } -int db_count_active_whitelist_rules(void) { return sqlite_db_count_active_whitelist_rules(); } - -int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) { - return sqlite_db_count_with_sql(sql, bind_params, bind_param_count, out_count); -} -char* db_execute_readonly_query_json(const char* query, const char* request_id, - char* error_message, size_t error_size, - int max_rows, int timeout_ms) { - return sqlite_db_execute_readonly_query_json(query, request_id, error_message, error_size, - max_rows, timeout_ms); -} - -int db_get_total_event_count_ll(long long* out_count) { return sqlite_db_get_total_event_count_ll(out_count); } -int db_get_event_count_since(time_t cutoff, long long* out_count) { - return sqlite_db_get_event_count_since(cutoff, out_count); -} -int db_get_storage_size_bytes(long long* out_size) { - return sqlite_db_get_storage_size_bytes(out_size); -} -cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) { - return sqlite_db_get_event_kind_distribution_rows(out_total_events); -} -cJSON* db_get_top_pubkeys_rows(int limit) { return sqlite_db_get_top_pubkeys_rows(limit); } -cJSON* db_get_subscription_details_rows(void) { return sqlite_db_get_subscription_details_rows(); } - -cJSON* db_get_all_config_rows(void) { return sqlite_db_get_all_config_rows(); } -char* db_get_config_value_dup(const char* key) { return sqlite_db_get_config_value_dup(key); } -int db_set_config_value_full(const char* key, const char* value, const char* data_type, - const char* description, const char* category, int requires_restart) { - return sqlite_db_set_config_value_full(key, value, data_type, description, category, requires_restart); -} -int db_update_config_value_only(const char* key, const char* value) { - return sqlite_db_update_config_value_only(key, value); -} -int db_upsert_config_value(const char* key, const char* value, const char* data_type) { - return sqlite_db_upsert_config_value(key, value, data_type); -} -int db_store_relay_private_key_hex(const char* relay_privkey_hex) { - return sqlite_db_store_relay_private_key_hex(relay_privkey_hex); -} -char* db_get_relay_private_key_hex_dup(void) { return sqlite_db_get_relay_private_key_hex_dup(); } -int db_store_config_event(const cJSON* event) { return sqlite_db_store_config_event(event); } - -int db_insert_event_with_json(const char* id, const char* pubkey, long long created_at, - int kind, const char* event_type, const char* content, - const char* sig, const char* tags_json, const char* event_json, - int* out_step_rc, int* out_extended_errcode) { - return sqlite_db_insert_event_with_json(id, pubkey, created_at, kind, event_type, content, - sig, tags_json, event_json, out_step_rc, out_extended_errcode); -} -int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) { - return sqlite_db_get_event_time_bounds(out_min_created_at, out_max_created_at); -} -int db_event_id_exists(const char* event_id, int* out_exists) { - return sqlite_db_event_id_exists(event_id, out_exists); -} -cJSON* db_retrieve_event_by_id(const char* event_id) { return sqlite_db_retrieve_event_by_id(event_id); } -char* db_get_latest_event_pubkey_for_kind_dup(int kind) { - return sqlite_db_get_latest_event_pubkey_for_kind_dup(kind); -} - -int db_get_config_row_count(int* out_count) { return sqlite_db_get_config_row_count(out_count); } - -int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) { - return sqlite_db_store_event_tags_cjson(event_id, tags); -} -int db_populate_event_tags_from_existing(void) { return sqlite_db_populate_event_tags_from_existing(); } - -int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { - return sqlite_db_add_auth_rule(rule_type, pattern_type, pattern_value); -} -int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { - return sqlite_db_remove_auth_rule(rule_type, pattern_type, pattern_value); -} -int db_delete_wot_whitelist_rules(void) { return sqlite_db_delete_wot_whitelist_rules(); } -int db_count_wot_whitelist_rules(void) { return sqlite_db_count_wot_whitelist_rules(); } - -int db_table_exists(const char* table_name, int* out_exists) { - return sqlite_db_table_exists(table_name, out_exists); -} -char* db_get_schema_version_dup(void) { return sqlite_db_get_schema_version_dup(); } -int db_exec_sql(const char* sql) { return sqlite_db_exec_sql(sql); } -int db_wal_checkpoint_passive(void) { return sqlite_db_wal_checkpoint_passive(); } -int db_wal_checkpoint_truncate(void) { return sqlite_db_wal_checkpoint_truncate(); } - -// Caching relay inbox helpers are PostgreSQL-only. The SQLite backend has no -// caching_event_inbox table, so the dispatch path returns errors/no-ops here -// rather than delegating to sqlite_db_ops stubs. -cJSON* db_caching_inbox_dequeue_batch(int batch_size, int* out_count) { - if (out_count) *out_count = 0; - (void)batch_size; - return NULL; -} -int db_caching_inbox_pending_counts(int* out_live_count, int* out_backfill_count) { - if (out_live_count) *out_live_count = 0; - if (out_backfill_count) *out_backfill_count = 0; - return DB_ERROR; -} -int db_caching_inbox_oldest_age(int* out_oldest_age_seconds) { - if (out_oldest_age_seconds) *out_oldest_age_seconds = 0; - return DB_ERROR; -} -int db_caching_backfill_author_counts(int* out_complete, int* out_total) { - if (out_complete) *out_complete = 0; - if (out_total) *out_total = 0; - return DB_ERROR; -} -int db_caching_inbox_insert(const char* event_id, const char* event_json, - const char* source_relay, const char* source_class, - int priority) { - (void)event_id; (void)event_json; (void)source_relay; (void)source_class; (void)priority; - return DB_ERROR; -} - -cJSON* db_get_profile_metadata(const char* pubkey) { - /* SQLite backend: kind-0 metadata not supported in this context. - * The caching follows feature is PostgreSQL-only. */ - (void)pubkey; - return NULL; -} -cJSON* db_get_outbox_relays(const char* pubkey) { - (void)pubkey; - return NULL; -} -cJSON* db_get_profile(const char* pubkey) { - /* SQLite backend: profiles cache table is PostgreSQL-only. */ - (void)pubkey; - return NULL; -} -cJSON* db_get_profiles(const char** pubkeys, int count) { - (void)pubkeys; - (void)count; - return NULL; -} - -#endif diff --git a/src/db_ops_sqlite.c b/src/db_ops_sqlite.c deleted file mode 100644 index 37ad260..0000000 --- a/src/db_ops_sqlite.c +++ /dev/null @@ -1,1428 +0,0 @@ -#define _GNU_SOURCE - -#include "sqlite_db_ops.h" -#include "debug.h" -#include "config.h" - -#include -#include -#include -#include -#include - -// Database handle owned by sqlite_db_ops. -static sqlite3* g_db = NULL; -extern char g_database_path[512]; - -// Optional per-thread connection override (used by thread pool workers) -static __thread sqlite3* g_thread_db = NULL; - -static sqlite3* sqlite_db_active_connection(void) { - return g_thread_db ? g_thread_db : g_db; -} - -typedef struct sqlite_db_stmt { - sqlite3_stmt* stmt; -} sqlite_db_stmt_t; - -int sqlite_db_init(const char* connection_string) { - if (!connection_string || connection_string[0] == '\0') return DB_MISUSE; - if (g_db) return DB_OK; - - int rc = sqlite3_open(connection_string, &g_db); - if (rc != SQLITE_OK) { - if (g_db) { - sqlite3_close(g_db); - g_db = NULL; - } - return DB_ERROR; - } - - strncpy(g_database_path, connection_string, sizeof(g_database_path) - 1); - g_database_path[sizeof(g_database_path) - 1] = '\0'; - return DB_OK; -} - -void sqlite_db_close(void) { - if (!g_db) return; - sqlite3_close(g_db); - g_db = NULL; -} - -int sqlite_db_set_thread_connection(void* connection) { - g_thread_db = (sqlite3*)connection; - return DB_OK; -} - -void sqlite_db_clear_thread_connection(void) { - g_thread_db = NULL; -} - -int sqlite_db_open_worker_connection(const char* sqlite_db_path, void** out_connection) { - if (!out_connection) return -1; - *out_connection = NULL; - - const char* effective_path = (sqlite_db_path && sqlite_db_path[0] != '\0') ? sqlite_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_exec(db, "PRAGMA wal_autocheckpoint=0;", NULL, NULL, NULL); - sqlite3_busy_timeout(db, 5000); - sqlite_db_set_thread_connection(db); - *out_connection = (void*)db; - return 0; -} - -// SQLite has no LISTEN/NOTIFY mechanism; stubs return -1 (unsupported) so the -// api-worker falls back to timer-based polling. -int sqlite_db_worker_listen(void* connection, const char* channel) { - (void)connection; (void)channel; return -1; -} -int sqlite_db_worker_poll_notify(void* connection, int timeout_ms) { - (void)connection; (void)timeout_ms; return -1; -} - -void sqlite_db_close_worker_connection(void* connection) { - if (!connection) return; - sqlite_db_clear_thread_connection(); - sqlite3_close((sqlite3*)connection); -} - -int sqlite_db_is_available(void) { - return sqlite_db_active_connection() != NULL; -} - -const char* sqlite_db_last_error(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return "database not available"; - return sqlite3_errmsg(db); -} - -const char* sqlite_db_get_database_path(void) { - return g_database_path; -} - -int sqlite_db_prepare(const char* sql, sqlite_db_stmt_t** out_stmt) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sql || !out_stmt) return DB_MISUSE; - - sqlite3_stmt* raw_stmt = NULL; - int rc = sqlite3_prepare_v2(db, sql, -1, &raw_stmt, NULL); - if (rc != SQLITE_OK) return rc; - - sqlite_db_stmt_t* wrapper = (sqlite_db_stmt_t*)malloc(sizeof(sqlite_db_stmt_t)); - if (!wrapper) { - sqlite3_finalize(raw_stmt); - return DB_ERROR; - } - - wrapper->stmt = raw_stmt; - *out_stmt = wrapper; - return DB_OK; -} - -int sqlite_db_bind_text_param(sqlite_db_stmt_t* stmt, int index, const char* value) { - if (!stmt || !stmt->stmt) return DB_MISUSE; - return sqlite3_bind_text(stmt->stmt, index, value ? value : "", -1, SQLITE_TRANSIENT); -} - -int sqlite_db_bind_int_param(sqlite_db_stmt_t* stmt, int index, int value) { - if (!stmt || !stmt->stmt) return DB_MISUSE; - return sqlite3_bind_int(stmt->stmt, index, value); -} - -int sqlite_db_bind_int64_param(sqlite_db_stmt_t* stmt, int index, long long value) { - if (!stmt || !stmt->stmt) return DB_MISUSE; - return sqlite3_bind_int64(stmt->stmt, index, (sqlite3_int64)value); -} - -int sqlite_db_step_stmt(sqlite_db_stmt_t* stmt) { - if (!stmt || !stmt->stmt) return DB_MISUSE; - return sqlite3_step(stmt->stmt); -} - -int sqlite_db_reset_stmt(sqlite_db_stmt_t* stmt) { - if (!stmt || !stmt->stmt) return DB_MISUSE; - return sqlite3_reset(stmt->stmt); -} - -const char* sqlite_db_column_text_value(sqlite_db_stmt_t* stmt, int col) { - if (!stmt || !stmt->stmt) return NULL; - return (const char*)sqlite3_column_text(stmt->stmt, col); -} - -int sqlite_db_column_int_value(sqlite_db_stmt_t* stmt, int col) { - if (!stmt || !stmt->stmt) return 0; - return sqlite3_column_int(stmt->stmt, col); -} - -long long sqlite_db_column_int64_value(sqlite_db_stmt_t* stmt, int col) { - if (!stmt || !stmt->stmt) return 0; - return (long long)sqlite3_column_int64(stmt->stmt, col); -} - -double sqlite_db_column_double_value(sqlite_db_stmt_t* stmt, int col) { - if (!stmt || !stmt->stmt) return 0.0; - return sqlite3_column_double(stmt->stmt, col); -} - -void sqlite_db_finalize_stmt(sqlite_db_stmt_t* stmt) { - if (!stmt) return; - if (stmt->stmt) sqlite3_finalize(stmt->stmt); - free(stmt); -} - -int sqlite_db_log_subscription_created(const char* sub_id, const char* wsi_ptr, - const char* client_ip, const char* filter_json) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sub_id || !wsi_ptr || !client_ip) return -1; - - const char* sql = - "INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) " - "VALUES (?, ?, ?, 'created', ?)"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, wsi_ptr, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 4, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_log_subscription_closed(const char* sub_id, const char* client_ip) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sub_id) return -1; - - const char* insert_sql = - "INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) " - "VALUES (?, '', ?, 'closed')"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) { - sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_TRANSIENT); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - } - - const char* update_sql = - "UPDATE subscriptions " - "SET ended_at = strftime('%s', 'now') " - "WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL"; - - if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT); - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_log_subscription_disconnected(const char* client_ip) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !client_ip) return -1; - - const char* update_sql = - "UPDATE subscriptions " - "SET ended_at = strftime('%s', 'now') " - "WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT); - int rc = sqlite3_step(stmt); - int changes = sqlite3_changes(db); - sqlite3_finalize(stmt); - if (rc != SQLITE_DONE) return -1; - - if (changes > 0) { - const char* insert_sql = - "INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) " - "VALUES ('disconnect', '', ?, 'disconnected')"; - - if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) { - sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - } - } - - return changes; -} - -int sqlite_db_update_subscription_events_sent(const char* sub_id, int events_sent) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sub_id) return -1; - - const char* sql = - "UPDATE subscriptions " - "SET events_sent = ? " - "WHERE subscription_id = ? AND event_type = 'created'"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_int(stmt, 1, events_sent); - sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_cleanup_orphaned_subscriptions(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return -1; - - const char* sql = - "UPDATE subscriptions " - "SET ended_at = strftime('%s', 'now') " - "WHERE event_type = 'created' AND ended_at IS NULL"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - int rc = sqlite3_step(stmt); - int changes = sqlite3_changes(db); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? changes : -1; -} - -int sqlite_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1; - - const char* sql = "SELECT pubkey FROM events WHERE id = ?"; - sqlite3_stmt* stmt = NULL; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT); - int rc = sqlite3_step(stmt); - if (rc == SQLITE_ROW) { - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); - if (pubkey) { - snprintf(pubkey_out, pubkey_out_size, "%s", pubkey); - sqlite3_finalize(stmt); - return 1; - } - } - - sqlite3_finalize(stmt); - return 0; -} - -int sqlite_db_delete_event_by_id(const char* event_id, const char* requester_pubkey) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event_id || !requester_pubkey) return -1; - - const char* sql = "DELETE FROM events WHERE id = ? AND pubkey = ?"; - sqlite3_stmt* stmt = NULL; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, requester_pubkey, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - int changes = sqlite3_changes(db); - sqlite3_finalize(stmt); - - if (rc != SQLITE_DONE) return -1; - return changes; -} - -int sqlite_db_delete_events_by_address(const char* pubkey, int kind, - const char* d_tag, long before_timestamp) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !pubkey) return -1; - - const char* sql_with_d = - "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? " - "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'"; - const char* sql_no_d = - "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?"; - - const int has_d = (d_tag && d_tag[0] != '\0'); - sqlite3_stmt* stmt = NULL; - - if (sqlite3_prepare_v2(db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_int(stmt, 1, kind); - sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT); - sqlite3_bind_int64(stmt, 3, before_timestamp); - if (has_d) { - sqlite3_bind_text(stmt, 4, d_tag, -1, SQLITE_TRANSIENT); - } - - int rc = sqlite3_step(stmt); - int changes = sqlite3_changes(db); - sqlite3_finalize(stmt); - - if (rc != SQLITE_DONE) return -1; - return changes; -} - -static int sqlite_db_scalar_exists_open(sqlite3* db, const char* sql, const char* value) { - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; - - if (value) sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT); - - int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; - sqlite3_finalize(stmt); - return exists; -} - -int sqlite_db_is_pubkey_blacklisted(const char* pubkey) { - if (!pubkey || g_database_path[0] == '\0') return 0; - - sqlite3* db = NULL; - if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; - - const char* sql = - "SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' " - "AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"; - - int exists = sqlite_db_scalar_exists_open(db, sql, pubkey); - sqlite3_close(db); - return exists; -} - -int sqlite_db_is_hash_blacklisted(const char* resource_hash) { - if (!resource_hash || g_database_path[0] == '\0') return 0; - - sqlite3* db = NULL; - if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; - - const char* sql = - "SELECT 1 FROM auth_rules WHERE rule_type = 'blacklist' " - "AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1"; - - int exists = sqlite_db_scalar_exists_open(db, sql, resource_hash); - sqlite3_close(db); - return exists; -} - -int sqlite_db_is_pubkey_whitelisted(const char* pubkey) { - if (!pubkey || g_database_path[0] == '\0') return 0; - - sqlite3* db = NULL; - if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; - - const char* sql = - "SELECT 1 FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') " - "AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"; - - int exists = sqlite_db_scalar_exists_open(db, sql, pubkey); - sqlite3_close(db); - return exists; -} - -int sqlite_db_count_active_whitelist_rules(void) { - if (g_database_path[0] == '\0') return 0; - - sqlite3* db = NULL; - sqlite3_stmt* stmt = NULL; - int count = 0; - - if (sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) return 0; - - const char* sql = - "SELECT COUNT(*) FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') " - "AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int(stmt, 0); - } - sqlite3_finalize(stmt); - } - - sqlite3_close(db); - return count; -} - -int sqlite_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sql || !out_count) return -1; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - for (int i = 0; i < bind_param_count; i++) { - const char* param = (bind_params && bind_params[i]) ? bind_params[i] : ""; - sqlite3_bind_text(stmt, i + 1, param, -1, SQLITE_TRANSIENT); - } - - int count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int(stmt, 0); - } - - sqlite3_finalize(stmt); - *out_count = count; - return 0; -} - -char* sqlite_db_execute_readonly_query_json(const char* query, const char* request_id, - char* error_message, size_t error_size, - int max_rows, int timeout_ms) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !query || !request_id || !error_message) return NULL; - - sqlite3_busy_timeout(db, timeout_ms > 0 ? timeout_ms : 5000); - - sqlite3_stmt* stmt = NULL; - int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - const char* err_msg = sqlite3_errmsg(db); - snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg ? err_msg : "unknown"); - return NULL; - } - - cJSON* response = cJSON_CreateObject(); - if (!response) { - sqlite3_finalize(stmt); - snprintf(error_message, error_size, "Failed to allocate query response object"); - return NULL; - } - - cJSON_AddStringToObject(response, "query_type", "sql_query"); - cJSON_AddStringToObject(response, "request_id", request_id); - cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); - cJSON_AddStringToObject(response, "query", query); - - int col_count = sqlite3_column_count(stmt); - cJSON* columns = cJSON_CreateArray(); - if (!columns) { - sqlite3_finalize(stmt); - cJSON_Delete(response); - snprintf(error_message, error_size, "Failed to allocate columns array"); - return NULL; - } - for (int i = 0; i < col_count; i++) { - const char* col_name = sqlite3_column_name(stmt, i); - cJSON_AddItemToArray(columns, cJSON_CreateString(col_name ? col_name : "")); - } - cJSON_AddItemToObject(response, "columns", columns); - - cJSON* rows = cJSON_CreateArray(); - if (!rows) { - sqlite3_finalize(stmt); - cJSON_Delete(response); - snprintf(error_message, error_size, "Failed to allocate rows array"); - return NULL; - } - - const int row_limit = (max_rows > 0) ? max_rows : 1000; - int row_count = 0; - - struct timespec start_time; - clock_gettime(CLOCK_MONOTONIC, &start_time); - - while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && row_count < row_limit) { - cJSON* row = cJSON_CreateArray(); - if (!row) { - sqlite3_finalize(stmt); - cJSON_Delete(rows); - cJSON_Delete(response); - snprintf(error_message, error_size, "Failed to allocate row array"); - return NULL; - } - - for (int i = 0; i < col_count; i++) { - int col_type = sqlite3_column_type(stmt, i); - switch (col_type) { - case SQLITE_INTEGER: - cJSON_AddItemToArray(row, cJSON_CreateNumber((double)sqlite3_column_int64(stmt, i))); - break; - case SQLITE_FLOAT: - cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_double(stmt, i))); - break; - case SQLITE_TEXT: { - const char* text = (const char*)sqlite3_column_text(stmt, i); - cJSON_AddItemToArray(row, cJSON_CreateString(text ? text : "")); - break; - } - case SQLITE_BLOB: { - const void* blob = sqlite3_column_blob(stmt, i); - int blob_size = sqlite3_column_bytes(stmt, i); - if (blob && blob_size > 0) { - char* hex_str = malloc((size_t)blob_size * 2 + 1); - if (hex_str) { - for (int j = 0; j < blob_size; j++) { - sprintf(hex_str + j * 2, "%02x", ((const unsigned char*)blob)[j]); - } - hex_str[(size_t)blob_size * 2] = '\0'; - cJSON_AddItemToArray(row, cJSON_CreateString(hex_str)); - free(hex_str); - } else { - cJSON_AddItemToArray(row, cJSON_CreateString("[BLOB]")); - } - } else { - cJSON_AddItemToArray(row, cJSON_CreateString("")); - } - break; - } - case SQLITE_NULL: - cJSON_AddItemToArray(row, cJSON_CreateNull()); - break; - default: - cJSON_AddItemToArray(row, cJSON_CreateString("[UNKNOWN]")); - break; - } - } - - cJSON_AddItemToArray(rows, row); - row_count++; - - struct timespec current_time; - clock_gettime(CLOCK_MONOTONIC, ¤t_time); - double elapsed = (current_time.tv_sec - start_time.tv_sec) + - (current_time.tv_nsec - start_time.tv_nsec) / 1e9; - if (elapsed > 4.5) { - break; - } - } - - sqlite3_finalize(stmt); - - if (rc != SQLITE_DONE && rc != SQLITE_ROW) { - const char* err_msg = sqlite3_errmsg(db); - snprintf(error_message, error_size, "SQL execution failed: %s", err_msg ? err_msg : "unknown"); - cJSON_Delete(rows); - cJSON_Delete(response); - return NULL; - } - - if (row_count >= row_limit) { - cJSON_AddStringToObject(response, "warning", "Result truncated to maximum row limit"); - } - - cJSON_AddNumberToObject(response, "row_count", row_count); - cJSON_AddNumberToObject(response, "execution_time_ms", 0); - cJSON_AddItemToObject(response, "rows", rows); - - char* json_result = cJSON_Print(response); - cJSON_Delete(response); - - if (!json_result) { - snprintf(error_message, error_size, "Failed to generate JSON response"); - return NULL; - } - - return json_result; -} - -int sqlite_db_get_total_event_count_ll(long long* out_count) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !out_count) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT COUNT(*) FROM events"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - long long count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int64(stmt, 0); - } - - sqlite3_finalize(stmt); - *out_count = count; - return 0; -} - -int sqlite_db_get_event_count_since(time_t cutoff, long long* out_count) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !out_count) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff); - - long long count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int64(stmt, 0); - } - - sqlite3_finalize(stmt); - *out_count = count; - return 0; -} - -int sqlite_db_get_storage_size_bytes(long long* out_size) { - sqlite3* db = sqlite_db_active_connection(); - const char* db_path = sqlite_db_get_database_path(); - if (!db || !out_size || !db_path || db_path[0] == '\0') return -1; - - long long total_size = 0; - struct stat db_stat; - if (stat(db_path, &db_stat) == 0) { - total_size += (long long)db_stat.st_size; - } - - char wal_path[1024]; - snprintf(wal_path, sizeof(wal_path), "%s-wal", db_path); - if (stat(wal_path, &db_stat) == 0) { - total_size += (long long)db_stat.st_size; - } - - *out_size = total_size; - return 0; -} - -cJSON* sqlite_db_get_event_kind_distribution_rows(long long* out_total_events) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - cJSON* rows = cJSON_CreateArray(); - if (!rows) { - sqlite3_finalize(stmt); - return NULL; - } - - long long total = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON* row = cJSON_CreateObject(); - if (!row) { - sqlite3_finalize(stmt); - cJSON_Delete(rows); - return NULL; - } - - int kind = sqlite3_column_int(stmt, 0); - long long count = sqlite3_column_int64(stmt, 1); - total += count; - - cJSON_AddNumberToObject(row, "kind", kind); - cJSON_AddNumberToObject(row, "count", count); - cJSON_AddItemToArray(rows, row); - } - - sqlite3_finalize(stmt); - if (out_total_events) { - *out_total_events = total; - } - return rows; -} - -cJSON* sqlite_db_get_top_pubkeys_rows(int limit) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10); - - cJSON* rows = cJSON_CreateArray(); - if (!rows) { - sqlite3_finalize(stmt); - return NULL; - } - - while (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON* row = cJSON_CreateObject(); - if (!row) { - sqlite3_finalize(stmt); - cJSON_Delete(rows); - return NULL; - } - - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); - long long count = sqlite3_column_int64(stmt, 1); - cJSON_AddStringToObject(row, "pubkey", pubkey ? pubkey : ""); - cJSON_AddNumberToObject(row, "count", count); - cJSON_AddItemToArray(rows, row); - } - - sqlite3_finalize(stmt); - return rows; -} - -cJSON* sqlite_db_get_subscription_details_rows(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = - "SELECT * " - "FROM active_subscriptions_log " - "ORDER BY created_at DESC"; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - cJSON* rows = cJSON_CreateArray(); - if (!rows) { - sqlite3_finalize(stmt); - return NULL; - } - - while (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON* row = cJSON_CreateObject(); - if (!row) { - sqlite3_finalize(stmt); - cJSON_Delete(rows); - return NULL; - } - - const char* sub_id = (const char*)sqlite3_column_text(stmt, 0); - const char* client_ip = (const char*)sqlite3_column_text(stmt, 1); - const char* filter_json = (const char*)sqlite3_column_text(stmt, 2); - long long events_sent = sqlite3_column_int64(stmt, 3); - long long created_at = sqlite3_column_int64(stmt, 4); - long long duration_seconds = sqlite3_column_int64(stmt, 5); - const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6); - - cJSON_AddStringToObject(row, "id", sub_id ? sub_id : ""); - cJSON_AddStringToObject(row, "client_ip", client_ip ? client_ip : ""); - cJSON_AddStringToObject(row, "filter_json", filter_json ? filter_json : "[]"); - cJSON_AddNumberToObject(row, "events_sent", events_sent); - cJSON_AddNumberToObject(row, "created_at", created_at); - cJSON_AddNumberToObject(row, "duration_seconds", duration_seconds); - cJSON_AddStringToObject(row, "wsi_pointer", wsi_pointer ? wsi_pointer : ""); - - cJSON_AddItemToArray(rows, row); - } - - sqlite3_finalize(stmt); - return rows; -} - -cJSON* sqlite_db_get_all_config_rows(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT key, value FROM config ORDER BY key"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - cJSON* rows = cJSON_CreateArray(); - if (!rows) { - sqlite3_finalize(stmt); - return NULL; - } - - while (sqlite3_step(stmt) == SQLITE_ROW) { - cJSON* row = cJSON_CreateObject(); - if (!row) { - sqlite3_finalize(stmt); - cJSON_Delete(rows); - return NULL; - } - - const char* key = (const char*)sqlite3_column_text(stmt, 0); - const char* value = (const char*)sqlite3_column_text(stmt, 1); - cJSON_AddStringToObject(row, "key", key ? key : ""); - cJSON_AddStringToObject(row, "value", value ? value : ""); - cJSON_AddItemToArray(rows, row); - } - - sqlite3_finalize(stmt); - return rows; -} - -char* sqlite_db_get_config_value_dup(const char* key) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !key) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT value FROM config WHERE key = ?"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); - - char* result = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* value = (const char*)sqlite3_column_text(stmt, 0); - if (value) result = strdup(value); - } - - sqlite3_finalize(stmt); - return result; -} - -int sqlite_db_set_config_value_full(const char* key, const char* value, const char* data_type, - const char* description, const char* category, int requires_restart) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !key || !value || !data_type) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = - "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " - "VALUES (?, ?, ?, ?, ?, ?)"; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_TRANSIENT); - sqlite3_bind_int(stmt, 6, requires_restart); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_update_config_value_only(const char* key, const char* value) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !key || !value) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_upsert_config_value(const char* key, const char* value, const char* data_type) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !key || !value || !data_type) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)"; - - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_store_relay_private_key_hex(const char* relay_privkey_hex) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !relay_privkey_hex) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT); - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - - return (rc == SQLITE_DONE) ? 0 : -1; -} - -char* sqlite_db_get_relay_private_key_hex_dup(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT private_key_hex FROM relay_seckey"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL; - - char* result = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0); - if (key_from_db) result = strdup(key_from_db); - } - - sqlite3_finalize(stmt); - return result; -} - -int sqlite_db_store_config_event(const cJSON* event) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event) return -1; - - cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id"); - cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey"); - cJSON* created_at_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "created_at"); - cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "kind"); - cJSON* content_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "content"); - cJSON* sig_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "sig"); - cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "tags"); - if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) { - return -1; - } - - char* tags_str = cJSON_Print(tags_obj); - if (!tags_str) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - free(tags_str); - return -1; - } - - sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_TRANSIENT); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); - sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); - sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - free(tags_str); - - return (rc == SQLITE_DONE) ? 0 : -1; -} - -static int sqlite_db_insert_event_tags_json_with_db(sqlite3* db, const char* event_id, const char* tags_json) { - if (!db || !event_id || !tags_json) { - return -1; - } - - cJSON* tags = cJSON_Parse(tags_json); - if (!tags || !cJSON_IsArray(tags)) { - if (tags) cJSON_Delete(tags); - return 0; - } - - const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)"; - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - cJSON_Delete(tags); - return -1; - } - - int tag_index = 0; - cJSON* tag = NULL; - cJSON_ArrayForEach(tag, tags) { - if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { - cJSON* name = cJSON_GetArrayItem(tag, 0); - cJSON* value = cJSON_GetArrayItem(tag, 1); - if (cJSON_IsString(name) && cJSON_IsString(value)) { - sqlite3_reset(stmt); - sqlite3_clear_bindings(stmt); - sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 4, tag_index); - - if (sqlite3_step(stmt) != SQLITE_DONE) { - DEBUG_WARN("Failed to insert event tag for %s: %s", event_id, sqlite3_errmsg(db)); - } - } - } - tag_index++; - } - - sqlite3_finalize(stmt); - cJSON_Delete(tags); - return 0; -} - -int sqlite_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at, - int kind, const char* event_type, const char* content, - const char* sig, const char* tags_json, const char* event_json, - int* out_step_rc, int* out_extended_errcode) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) { - return -1; - } - - const char* sql = - "INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags, event_json) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, id, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_TRANSIENT); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)created_at); - sqlite3_bind_int(stmt, 4, kind); - sqlite3_bind_text(stmt, 5, event_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 6, content, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 7, sig, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT); - - int step_rc = sqlite3_step(stmt); - int extended_errcode = sqlite3_extended_errcode(db); - sqlite3_finalize(stmt); - - if (step_rc == SQLITE_DONE) { - (void)sqlite_db_insert_event_tags_json_with_db(db, id, tags_json); - } - - if (out_step_rc) { - *out_step_rc = step_rc; - } - if (out_extended_errcode) { - *out_extended_errcode = extended_errcode; - } - - return 0; -} - -int sqlite_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !out_min_created_at || !out_max_created_at) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - long long min_ts = 0; - long long max_ts = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - min_ts = sqlite3_column_int64(stmt, 0); - max_ts = sqlite3_column_int64(stmt, 1); - } - - sqlite3_finalize(stmt); - *out_min_created_at = min_ts; - *out_max_created_at = max_ts; - return 0; -} - -int sqlite_db_event_id_exists(const char* event_id, int* out_exists) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event_id || !out_exists) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC); - int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; - sqlite3_finalize(stmt); - - *out_exists = exists; - return 0; -} - -cJSON* sqlite_db_retrieve_event_by_id(const char* event_id) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event_id) return NULL; - - const char* sql = - "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?"; - - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return NULL; - } - - sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); - - cJSON* event = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - event = cJSON_CreateObject(); - if (event) { - cJSON_AddStringToObject(event, "id", (const char*)sqlite3_column_text(stmt, 0)); - cJSON_AddStringToObject(event, "pubkey", (const char*)sqlite3_column_text(stmt, 1)); - cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); - cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); - cJSON_AddStringToObject(event, "content", (const char*)sqlite3_column_text(stmt, 4)); - cJSON_AddStringToObject(event, "sig", (const char*)sqlite3_column_text(stmt, 5)); - - const char* tags_json = (const char*)sqlite3_column_text(stmt, 6); - if (tags_json) { - cJSON* tags = cJSON_Parse(tags_json); - if (tags) { - cJSON_AddItemToObject(event, "tags", tags); - } else { - cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); - } - } else { - cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); - } - } - } - - sqlite3_finalize(stmt); - return event; -} - -char* sqlite_db_get_latest_event_pubkey_for_kind_dup(int kind) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - const char* sql = "SELECT pubkey FROM events WHERE kind = ? ORDER BY created_at DESC LIMIT 1"; - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return NULL; - } - - sqlite3_bind_int(stmt, 1, kind); - - char* pubkey_dup = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); - if (pubkey) { - pubkey_dup = strdup(pubkey); - } - } - - sqlite3_finalize(stmt); - return pubkey_dup; -} - -int sqlite_db_get_config_row_count(int* out_count) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !out_count) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "SELECT COUNT(*) FROM config"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - int count = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) { - count = sqlite3_column_int(stmt, 0); - } - - sqlite3_finalize(stmt); - *out_count = count; - return 0; -} - -int sqlite_db_store_event_tags_cjson(const char* event_id, const cJSON* tags) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !event_id || !tags || !cJSON_IsArray(tags)) { - return 0; // Not an error if no tags - } - - const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)"; - sqlite3_stmt* stmt = NULL; - int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(db)); - return -1; - } - - int tag_index = 0; - cJSON* tag = NULL; - cJSON_ArrayForEach(tag, tags) { - if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { - cJSON* name = cJSON_GetArrayItem(tag, 0); - cJSON* value = cJSON_GetArrayItem(tag, 1); - - if (cJSON_IsString(name) && cJSON_IsString(value)) { - sqlite3_reset(stmt); - sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC); - sqlite3_bind_int(stmt, 4, tag_index); - - rc = sqlite3_step(stmt); - if (rc != SQLITE_DONE) { - DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(db)); - } - } - } - tag_index++; - } - - sqlite3_finalize(stmt); - return 0; -} - -int sqlite_db_populate_event_tags_from_existing(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return -1; - - sqlite3_stmt* check_stmt = NULL; - if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) { - return -1; - } - - if (sqlite3_step(check_stmt) == SQLITE_ROW && sqlite3_column_int(check_stmt, 0) > 0) { - sqlite3_finalize(check_stmt); - DEBUG_INFO("event_tags already populated, skipping"); - return 0; - } - sqlite3_finalize(check_stmt); - - DEBUG_INFO("Populating event_tags from existing events..."); - - const char* sql = "SELECT id, tags FROM events WHERE tags != '[]'"; - sqlite3_stmt* stmt = NULL; - int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) return -1; - - sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL); - - int event_count = 0; - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* event_id = (const char*)sqlite3_column_text(stmt, 0); - const char* tags_json = (const char*)sqlite3_column_text(stmt, 1); - - if (event_id && tags_json) { - cJSON* tags = cJSON_Parse(tags_json); - if (tags) { - sqlite_db_store_event_tags_cjson(event_id, tags); - cJSON_Delete(tags); - event_count++; - } - } - } - - sqlite3_finalize(stmt); - sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); - - DEBUG_INFO("Populated event_tags for %d events", event_count); - return 0; -} - -int sqlite_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !rule_type || !pattern_type || !pattern_value) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !rule_type || !pattern_type || !pattern_value) return -1; - - sqlite3_stmt* stmt = NULL; - const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; - - sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_TRANSIENT); - - int rc = sqlite3_step(stmt); - sqlite3_finalize(stmt); - return (rc == SQLITE_DONE) ? 0 : -1; -} - -int sqlite_db_delete_wot_whitelist_rules(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return -1; - - const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'"; - int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); - return (rc == SQLITE_OK) ? 0 : -1; -} - -int sqlite_db_count_wot_whitelist_rules(void) { - if (!sqlite_db_is_available()) return 0; - - const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1"; - int count = 0; - if (sqlite_db_count_with_sql(sql, NULL, 0, &count) != 0) { - return 0; - } - return count; -} - -int sqlite_db_table_exists(const char* table_name, int* out_exists) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !table_name || !out_exists) return -1; - - const char* sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1"; - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return -1; - } - - sqlite3_bind_text(stmt, 1, table_name, -1, SQLITE_TRANSIENT); - *out_exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0; - sqlite3_finalize(stmt); - return 0; -} - -char* sqlite_db_get_schema_version_dup(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return NULL; - - const char* sql = "SELECT value FROM schema_info WHERE key = 'version'"; - sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return NULL; - } - - char* result = NULL; - if (sqlite3_step(stmt) == SQLITE_ROW) { - const char* version = (const char*)sqlite3_column_text(stmt, 0); - if (version) { - result = strdup(version); - } - } - - sqlite3_finalize(stmt); - return result; -} - -int sqlite_db_exec_sql(const char* sql) { - sqlite3* db = sqlite_db_active_connection(); - if (!db || !sql) return -1; - - int rc = sqlite3_exec(db, sql, NULL, NULL, NULL); - return (rc == SQLITE_OK) ? 0 : -1; -} - -int sqlite_db_wal_checkpoint_passive(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return -1; - - int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL); - return (rc == SQLITE_OK) ? 0 : -1; -} - -int sqlite_db_wal_checkpoint_truncate(void) { - sqlite3* db = sqlite_db_active_connection(); - if (!db) return -1; - - int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL); - return (rc == SQLITE_OK) ? 0 : -1; -} diff --git a/src/main.c b/src/main.c index f2a53ee..90f76b3 100644 --- a/src/main.c +++ b/src/main.c @@ -788,7 +788,7 @@ int init_database(const char* database_path_override) { if (db_get_config_row_count(&row_count) == 0) { DEBUG_LOG("Config table row count immediately after db_init(): %d", row_count); } else { - DEBUG_LOG("Config table count unavailable immediately after sqlite3_open() (table may not exist yet)"); + DEBUG_LOG("Config table count unavailable immediately after db_init() (table may not exist yet)"); } } // DEBUG_GUARD_END @@ -908,36 +908,11 @@ int init_database(const char* database_path_override) { DEBUG_WARN("Failed to enable WAL mode"); // Continue anyway - WAL mode is optional } else { - DEBUG_LOG("SQLite WAL mode enabled"); + DEBUG_LOG("WAL mode enabled"); } +// PostgreSQL performance tuning is handled via postgresql.conf. +// No SQLite-specific PRAGMAs are needed. - // Apply SQLite performance tuning PRAGMAs from config - // mmap_size: memory-map the database file to eliminate pread64 syscall overhead - // Default 256MB covers most relay databases; set to 0 to disable - long mmap_size = get_config_int("sqlite_mmap_size", 268435456); - if (mmap_size > 0) { - char mmap_pragma[64]; - snprintf(mmap_pragma, sizeof(mmap_pragma), "PRAGMA mmap_size=%ld;", mmap_size); - if (db_exec_sql(mmap_pragma) != 0) { - DEBUG_WARN("Failed to set mmap_size"); - } else { - DEBUG_LOG("SQLite mmap_size set to %ld bytes", mmap_size); - } - } - - // cache_size_kb: page cache size in KB (negative value = KB, positive = number of 4KB pages) - // Default 64MB keeps hot event data in memory and reduces repeated disk reads - int cache_size_kb = get_config_int("sqlite_cache_size_kb", 65536); - if (cache_size_kb != 0) { - char cache_pragma[64]; - // Use negative value so SQLite interprets it as KB rather than page count - snprintf(cache_pragma, sizeof(cache_pragma), "PRAGMA cache_size=-%d;", cache_size_kb > 0 ? cache_size_kb : -cache_size_kb); - if (db_exec_sql(cache_pragma) != 0) { - DEBUG_WARN("Failed to set cache_size"); - } else { - DEBUG_LOG("SQLite cache_size set to %d KB", cache_size_kb); - } - } DEBUG_TRACE("Exiting init_database() - success"); return 0; @@ -948,14 +923,6 @@ void close_database() { DEBUG_TRACE("Entering close_database()"); if (db_is_available()) { -#ifndef DB_BACKEND_POSTGRES - // Perform WAL checkpoint to minimize stale files on next startup (SQLite only) - DEBUG_LOG("Performing WAL checkpoint before database close"); - if (db_exec_sql("PRAGMA wal_checkpoint(TRUNCATE);") != 0) { - DEBUG_WARN("WAL checkpoint warning"); - } -#endif - db_close(); DEBUG_LOG("Database connection closed"); } diff --git a/src/main.h b/src/main.h index 4ed021e..d800f5b 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 28 -#define CRELAY_VERSION "v2.1.28" +#define CRELAY_VERSION_PATCH 29 +#define CRELAY_VERSION "v2.1.29" // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay-PG" diff --git a/src/sqlite_db_ops.h b/src/sqlite_db_ops.h deleted file mode 100644 index f9e2707..0000000 --- a/src/sqlite_db_ops.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef SQLITE_DB_OPS_H -#define SQLITE_DB_OPS_H - -#include "db_ops.h" - -typedef struct sqlite_db_stmt sqlite_db_stmt_t; - -int sqlite_db_init(const char* connection_string); -void sqlite_db_close(void); -int sqlite_db_is_available(void); -const char* sqlite_db_last_error(void); -const char* sqlite_db_get_database_path(void); - -int sqlite_db_set_thread_connection(void* connection); -void sqlite_db_clear_thread_connection(void); - -int sqlite_db_open_worker_connection(const char* db_path, void** out_connection); -void sqlite_db_close_worker_connection(void* connection); - -int sqlite_db_worker_listen(void* connection, const char* channel); -int sqlite_db_worker_poll_notify(void* connection, int timeout_ms); - -int sqlite_db_prepare(const char* sql, sqlite_db_stmt_t** out_stmt); -int sqlite_db_bind_text_param(sqlite_db_stmt_t* stmt, int index, const char* value); -int sqlite_db_bind_int_param(sqlite_db_stmt_t* stmt, int index, int value); -int sqlite_db_bind_int64_param(sqlite_db_stmt_t* stmt, int index, long long value); -int sqlite_db_step_stmt(sqlite_db_stmt_t* stmt); -int sqlite_db_reset_stmt(sqlite_db_stmt_t* stmt); -const char* sqlite_db_column_text_value(sqlite_db_stmt_t* stmt, int col); -int sqlite_db_column_int_value(sqlite_db_stmt_t* stmt, int col); -long long sqlite_db_column_int64_value(sqlite_db_stmt_t* stmt, int col); -double sqlite_db_column_double_value(sqlite_db_stmt_t* stmt, int col); -void sqlite_db_finalize_stmt(sqlite_db_stmt_t* stmt); - -int sqlite_db_log_subscription_created(const char* sub_id, const char* wsi_ptr, - const char* client_ip, const char* filter_json); -int sqlite_db_log_subscription_closed(const char* sub_id, const char* client_ip); -int sqlite_db_log_subscription_disconnected(const char* client_ip); -int sqlite_db_update_subscription_events_sent(const char* sub_id, int events_sent); -int sqlite_db_cleanup_orphaned_subscriptions(void); - -int sqlite_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size); -int sqlite_db_delete_event_by_id(const char* event_id, const char* requester_pubkey); -int sqlite_db_delete_events_by_address(const char* pubkey, int kind, - const char* d_tag, long before_timestamp); - -int sqlite_db_is_pubkey_blacklisted(const char* pubkey); -int sqlite_db_is_hash_blacklisted(const char* resource_hash); -int sqlite_db_is_pubkey_whitelisted(const char* pubkey); -int sqlite_db_count_active_whitelist_rules(void); - -int sqlite_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count); -char* sqlite_db_execute_readonly_query_json(const char* query, const char* request_id, - char* error_message, size_t error_size, - int max_rows, int timeout_ms); - -int sqlite_db_get_total_event_count_ll(long long* out_count); -int sqlite_db_get_event_count_since(time_t cutoff, long long* out_count); -int sqlite_db_get_storage_size_bytes(long long* out_size); -cJSON* sqlite_db_get_event_kind_distribution_rows(long long* out_total_events); -cJSON* sqlite_db_get_top_pubkeys_rows(int limit); -cJSON* sqlite_db_get_subscription_details_rows(void); - -cJSON* sqlite_db_get_all_config_rows(void); -char* sqlite_db_get_config_value_dup(const char* key); -int sqlite_db_set_config_value_full(const char* key, const char* value, const char* data_type, - const char* description, const char* category, int requires_restart); -int sqlite_db_update_config_value_only(const char* key, const char* value); -int sqlite_db_upsert_config_value(const char* key, const char* value, const char* data_type); -int sqlite_db_store_relay_private_key_hex(const char* relay_privkey_hex); -char* sqlite_db_get_relay_private_key_hex_dup(void); -int sqlite_db_store_config_event(const cJSON* event); - -int sqlite_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at, - int kind, const char* event_type, const char* content, - const char* sig, const char* tags_json, const char* event_json, - int* out_step_rc, int* out_extended_errcode); -int sqlite_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at); -int sqlite_db_event_id_exists(const char* event_id, int* out_exists); -cJSON* sqlite_db_retrieve_event_by_id(const char* event_id); -char* sqlite_db_get_latest_event_pubkey_for_kind_dup(int kind); - -int sqlite_db_get_config_row_count(int* out_count); - -int sqlite_db_store_event_tags_cjson(const char* event_id, const cJSON* tags); -int sqlite_db_populate_event_tags_from_existing(void); - -int sqlite_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); -int sqlite_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value); -int sqlite_db_delete_wot_whitelist_rules(void); -int sqlite_db_count_wot_whitelist_rules(void); - -int sqlite_db_table_exists(const char* table_name, int* out_exists); -char* sqlite_db_get_schema_version_dup(void); -int sqlite_db_exec_sql(const char* sql); -int sqlite_db_wal_checkpoint_passive(void); -int sqlite_db_wal_checkpoint_truncate(void); - -#endif // SQLITE_DB_OPS_H diff --git a/systemd/pgweb.service b/systemd/pgweb.service new file mode 100644 index 0000000..3457d31 --- /dev/null +++ b/systemd/pgweb.service @@ -0,0 +1,14 @@ +[Unit] +Description=pgweb - PostgreSQL web UI +After=network.target postgresql.service +Wants=postgresql.service + +[Service] +Type=simple +User=user +ExecStart=/usr/local/bin/pgweb --url "postgres://crelay:crelay@localhost:5432/crelay?sslmode=disable" --listen 8091 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/tests/1_nip_test.sh b/tests/1_nip_test.sh index 932b5ca..266502b 100755 --- a/tests/1_nip_test.sh +++ b/tests/1_nip_test.sh @@ -442,32 +442,126 @@ run_comprehensive_test() { # Check what's actually stored in the database print_step "Verifying database contents..." - if command -v sqlite3 &> /dev/null; then - # Find the database file (should be in build/ directory with relay pubkey as filename) - local db_file="" - if [[ -d "../build" ]]; then - db_file=$(find ../build -name "*.db" -type f | head -1) - fi - - if [[ -n "$db_file" && -f "$db_file" ]]; then - print_info "Events by type in database ($db_file):" - sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do - echo " $line" - done - - print_info "Recent events in database:" - sqlite3 "$db_file" "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null | while read line; do - echo " $line" - done - - print_success "Database verification complete" - else - print_warning "Database file not found in build/ directory" - print_info "Expected database files: build/*.db (named after relay pubkey)" - fi - else - print_warning "sqlite3 not available for database verification" - fi + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Recent events in database:" + psql -d crelay -c "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" return 0 } diff --git a/tests/45_nip_test.sh b/tests/45_nip_test.sh index 78d2c9d..6c206bd 100755 --- a/tests/45_nip_test.sh +++ b/tests/45_nip_test.sh @@ -425,31 +425,119 @@ run_count_test() { # Check what's actually stored in the database print_step "Verifying database contents..." - - if command -v sqlite3 &> /dev/null; then - # Find the database file (should be in build/ directory with relay pubkey as filename) - local db_file="" - if [[ -d "../build" ]]; then - db_file=$(find ../build -name "*.db" -type f | head -1) - fi - - if [[ -n "$db_file" && -f "$db_file" ]]; then - print_info "Events by type in database ($db_file):" - sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do - echo " $line" - done - - print_info "Total events in database:" - sqlite3 "$db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null - - print_success "Database verification complete" - else - print_warning "Database file not found in build/ directory" - print_info "Expected database files: build/*.db (named after relay pubkey)" - fi - else - print_warning "sqlite3 not available for database verification" - fi + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" + print_info "Events by type in database:" + psql -d crelay -c "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null || print_warning "Could not query database" + print_info "Total events in database:" + psql -d crelay -c "SELECT COUNT(*) FROM events;" 2>/dev/null || print_warning "Could not query database" + print_success "Database verification complete" return 0 } diff --git a/tests/bulk_retrieval_test.sh b/tests/bulk_retrieval_test.sh index eced81b..eceb67e 100755 --- a/tests/bulk_retrieval_test.sh +++ b/tests/bulk_retrieval_test.sh @@ -41,7 +41,7 @@ fi # Check current event count in database DB_FILE=$(ls build/*.db 2>/dev/null | head -1) if [ -n "$DB_FILE" ]; then - CURRENT_COUNT=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM events WHERE kind=1;" 2>/dev/null || echo "0") + CURRENT_COUNT=$(psql -d crelay "SELECT COUNT(*) FROM events WHERE kind=1;" 2>/dev/null || echo "0") echo "Current kind 1 events in database: $CURRENT_COUNT" if [ "$CURRENT_COUNT" -ge "$NUM_EVENTS" ]; then @@ -254,7 +254,7 @@ echo " - Per-event overhead: ~0.02ms (vs 50ms before)" echo "" if [ -n "$DB_FILE" ]; then - FINAL_COUNT=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM events WHERE kind=1;" 2>/dev/null || echo "0") + FINAL_COUNT=$(psql -d crelay "SELECT COUNT(*) FROM events WHERE kind=1;" 2>/dev/null || echo "0") echo "Final database stats:" echo " Total kind 1 events: $FINAL_COUNT" echo " Database file: $DB_FILE" diff --git a/tests/large_event_test.sh b/tests/large_event_test.sh index df85d3b..cf28a75 100755 --- a/tests/large_event_test.sh +++ b/tests/large_event_test.sh @@ -60,4 +60,4 @@ echo "To view logs in real-time:" echo " tail -f relay.log | grep -E '(partial|write completed|Invalid frame)'" echo "" echo "To check if events were stored:" -echo " sqlite3 build/*.db 'SELECT id, length(content) as content_size FROM events ORDER BY created_at DESC LIMIT 4;'" \ No newline at end of file +echo " psql -d crelay 'SELECT id, length(content) as content_size FROM events ORDER BY created_at DESC LIMIT 4;'" \ No newline at end of file diff --git a/tests/subscription_cleanup_test.sh b/tests/subscription_cleanup_test.sh index 0d2061a..e924f97 100755 --- a/tests/subscription_cleanup_test.sh +++ b/tests/subscription_cleanup_test.sh @@ -89,7 +89,7 @@ get_subscription_count() { return fi - sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions WHERE event_type='created' AND ended_at IS NULL;" 2>/dev/null || echo "0" + psql -d crelay "SELECT COUNT(*) FROM subscriptions WHERE event_type='created' AND ended_at IS NULL;" 2>/dev/null || echo "0" } # Test 1: Basic Connectivity @@ -260,14 +260,14 @@ echo "[INFO] Checking database integrity..." db_file=$(find . -name "*.db" -type f 2>/dev/null | head -1) if [ -n "$db_file" ]; then # Check if database is accessible - if sqlite3 "$db_file" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then + if psql -d crelay "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then print_result "PASS" "Database integrity check passed" else print_result "FAIL" "Database integrity check failed" fi # Check subscription table structure - if sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions;" &>/dev/null; then + if psql -d crelay "SELECT COUNT(*) FROM subscriptions;" &>/dev/null; then print_result "PASS" "Subscription table is accessible" else print_result "FAIL" "Subscription table is not accessible"
NamenpubRoot?Events in DBBackfillRelays