Compare commits

...
1 Commits
43 changed files with 1955 additions and 2219 deletions
+16
View File
@@ -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
+9 -15
View File
@@ -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
COPY --from=builder /build/c_relay_pg_static /c_relay_pg_static
+75 -135
View File
@@ -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
.PHONY: all x86 arm64 install-arm64-deps install-deps clean force-version
+32 -2
View File
@@ -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,
]);
+20 -3
View File
@@ -1,6 +1,6 @@
<?php
/**
* admin2/api/chart.php — Standalone ASCII chart endpoint.
* admin/api/chart.php — Standalone ASCII chart endpoint.
*
* Returns a plain-text ASCII X-bar chart of event counts over time.
* Works in both the browser (injected into a <div>) 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://<domain>/relay/api/chart.php?range=day
*
* It is also reachable (behind Basic Auth) from the admin UI at:
*
* https://<domain>/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();
+6 -2
View File
@@ -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);
+243
View File
@@ -0,0 +1,243 @@
<?php
/**
* admin/api/kind_1_report.php — Markdown relay status report.
*
* Returns a markdown-formatted relay status report suitable for use as
* a kind 1 event content. Includes:
* - Relay name header
* - 1H ASCII chart (event rate)
* - Database overview
* - Top 10 event kinds
* - Time-based statistics
* - Top pubkeys
*
* Accessible at: /relay/admin/api/kind_1_report.php
*/
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/ascii_chart.php';
header('Content-Type: text/plain; charset=utf-8');
$pdo = db();
// --- Relay info ---
$relay_name = 'C-Relay-PG';
$relay_version = '';
try {
$relay_name = $pdo->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;
+82 -45
View File
@@ -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];
+48 -5
View File
@@ -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 = `<p>Pending: ${d.inbox.pending} | Live: ${d.inbox.live} | Backfill: ${d.inbox.backfill}</p>`;
}
// 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 = `<div class="relay-status-summary">Connected: ${connected}/${total}</div>` +
'<div class="relay-status-list">' +
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 `<div class="relay-status-row ${cls}"><span class="relay-status-url" title="${esc(r.relay_url)}">${esc(host)}</span><span class="relay-status-badge">${label}</span></div>`;
}).join('') +
'</div>';
} else {
rsEl.innerHTML = '<p style="color:var(--muted-color);font-style:italic">No relay status data yet (waiting for heartbeat)</p>';
}
}
// 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 = '<span class="pagination-info">Page ' + d.page + ' of ' + totalPages + ' (' + d.totalFollows + ' total)</span>' +
'<div class="pagination-buttons">' +
(d.page > 1 ? '<button type="button" onclick="cachingPage=' + (d.page - 1) + '; loadCaching()"> Prev</button>' : '') +
(d.page < totalPages ? '<button type="button" onclick="cachingPage=' + (d.page + 1) + '; loadCaching()">Next </button>' : '') +
'</div>';
} else if (pgEl) {
pgEl.innerHTML = '';
}
// Active target
const fsEl = document.getElementById('caching-follows-status');
if (fsEl) {
+115
View File
@@ -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);
}
+11 -11
View File
@@ -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
+7
View File
@@ -383,9 +383,16 @@ $display_name = $relay_version ? ($relay_name . ' ' . $relay_version) : $relay_n
<p>Loading...</p>
</div>
</div>
<div class="input-group">
<h3>Upstream Relay Status <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(live connection state, updated every 15s)</span></h3>
<div id="caching-relay-status" class="status-display">
<p>Loading...</p>
</div>
</div>
<div class="input-group">
<h3>Followed Pubkeys <span style="font-size:11px;font-weight:normal;color:var(--text-muted,#888)">(click a row for per-relay details)</span></h3>
<div id="caching-follows-status" class="status-message"></div>
<div id="caching-follows-pagination" class="pagination-bar"></div>
<div class="config-table-container">
<table class="config-table" id="caching-follows-table">
<thead><tr><th>Name</th><th>npub</th><th>Root?</th><th>Events in DB</th><th>Backfill</th><th>Relays</th></tr></thead>
+89
View File
@@ -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
+180
View File
@@ -0,0 +1,180 @@
<?php
/**
* admin/public/index.php — Public relay stats page.
*
* Served at https://<domain>/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;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($display_name) ?></title>
<link rel="stylesheet" href="/relay/assets/index.css">
<style>
/* Public page uses the admin theme but hides the nav and centers content. */
.side-nav, .side-nav-overlay { display: none !important; }
.public-wrap { max-width: 1100px; margin: 0 auto; padding: 20px; }
.public-chart-box {
background: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 12px;
margin-top: 16px;
}
.public-footer {
margin-top: 24px;
padding-top: 12px;
border-top: var(--border-width) solid var(--border-color);
font-size: 12px;
color: var(--muted-color);
text-align: center;
}
.public-footer a { color: var(--accent-color); text-decoration: none; }
.event-rate-chart-container {
white-space: pre;
font-family: var(--font-family);
font-size: 12px;
line-height: 1.2;
overflow-x: auto;
}
</style>
</head>
<body>
<!-- Header with title and relay info (read-only) -->
<div class="section">
<div class="header-content">
<div class="header-title">
<span class="relay-letter" data-letter="R">R</span>
<span class="relay-letter" data-letter="E">E</span>
<span class="relay-letter" data-letter="L">L</span>
<span class="relay-letter" data-letter="A">A</span>
<span class="relay-letter" data-letter="Y">Y</span>
</div>
<div class="relay-info">
<div id="relay-name" class="relay-name"><?= e($display_name) ?></div>
<div id="relay-description" class="relay-description"><?= e($relay_desc) ?></div>
<div id="relay-pubkey-container" class="relay-pubkey-container" title="Click to copy npub">
<div id="relay-pubkey" class="relay-pubkey"><?= e($formatted_npub) ?></div>
</div>
</div>
</div>
</div>
<div class="public-wrap">
<div class="section-header">EVENT RATE</div>
<!-- Range selector tabs -->
<div class="chart-range-tabs">
<button class="chart-tab active" data-range="hour">1H</button>
<button class="chart-tab" data-range="day">1D</button>
<button class="chart-tab" data-range="month">1M</button>
<button class="chart-tab" data-range="year">1Y</button>
</div>
<!-- Server-rendered ASCII chart (fetched from the public chart endpoint) -->
<div class="public-chart-box">
<div id="event-rate-chart" class="event-rate-chart-container">Loading chart...</div>
</div>
<div class="public-footer">
Public relay statistics &middot;
<a href="wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?>">wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?></a>
&middot; Admin? visit <a href="/relay/admin/">/relay/admin/</a>
</div>
</div>
<script>
// Minimal chart loader — fetches the public ASCII chart endpoint and
// injects the plain-text response into the chart container. No auth.
const chartEl = document.getElementById('event-rate-chart');
let currentRange = 'hour';
let chartTimer = null;
async function loadChart(range) {
currentRange = range;
if (!chartEl) return;
chartEl.textContent = 'Loading chart...';
try {
const res = await fetch('/relay/api/chart.php?range=' + encodeURIComponent(range));
if (!res.ok) { chartEl.textContent = 'Chart load failed (HTTP ' + res.status + ')'; return; }
const text = await res.text();
chartEl.textContent = text || '(no data)';
} catch (e) {
chartEl.textContent = 'Chart load failed: ' + e.message;
}
}
document.querySelectorAll('.chart-tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.chart-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const range = btn.getAttribute('data-range');
loadChart(range);
// Hour chart is live — auto-refresh every 30s. Others are cached server-side.
if (chartTimer) { clearInterval(chartTimer); chartTimer = null; }
if (range === 'hour') {
chartTimer = setInterval(() => loadChart(currentRange), 30000);
}
});
});
// Initial load + live refresh for the default (hour) range.
loadChart('hour');
chartTimer = setInterval(() => loadChart(currentRange), 30000);
// Click-to-copy the relay npub.
const pkContainer = document.getElementById('relay-pubkey-container');
if (pkContainer) {
pkContainer.style.cursor = 'pointer';
pkContainer.addEventListener('click', () => {
const txt = (document.getElementById('relay-pubkey').textContent || '').replace(/\s+/g, '');
if (txt && navigator.clipboard) navigator.clipboard.writeText(txt);
});
}
</script>
</body>
</html>
+16 -78
View File
@@ -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
+29 -8
View File
@@ -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,
+90
View File
@@ -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;
}
+13
View File
@@ -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 */
+11 -2
View File
@@ -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."
+57
View File
@@ -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://<domain>/relay/` | None (public) | Public landing page: relay info + event-rate chart |
| `https://<domain>/relay/api/chart.php?range=day` | None (public) | Read-only ASCII chart endpoint (aggregate COUNT) |
| `https://<domain>/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 <username>
sudo systemctl reload nginx
```
### Apache Configuration
#### WebSocket Proxy with mod_proxy_wstunnel
+2 -2
View File
@@ -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"
@@ -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
+64 -4
View File
@@ -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://<domain>/relay/ -> public index.php
# https://<domain>/relay/api/chart.php?range=... -> public ASCII chart
# https://<domain>/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://<domain>/relay/admin/ -> admin index.php
# https://<domain>/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;
}
}
+2 -2
View File
@@ -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
+5 -27
View File
@@ -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 <name> Database backend: postgres (default) or sqlite"
echo " --db-connstring <str> PostgreSQL libpq connection string"
echo " --db-host <host> PostgreSQL host"
echo " --db-port <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
+71
View File
@@ -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
+113
View File
@@ -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`
+70
View File
@@ -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
```
+189
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
2356597
63971
+27 -5
View File
@@ -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
+1 -21
View File
@@ -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 ||
-226
View File
@@ -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
-1428
View File
File diff suppressed because it is too large Load Diff
+4 -37
View File
@@ -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");
}
+2 -2
View File
@@ -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"
-99
View File
@@ -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
+14
View File
@@ -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
+120 -26
View File
@@ -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
}
+113 -25
View File
@@ -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
}
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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;'"
echo " psql -d crelay 'SELECT id, length(content) as content_size FROM events ORDER BY created_at DESC LIMIT 4;'"
+3 -3
View File
@@ -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"