Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd33aa6ea |
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Vendored
+11
-11
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ·
|
||||
<a href="wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?>">wss://<?= e($_SERVER['HTTP_HOST'] ?? '') ?></a>
|
||||
· 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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+4
-37
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;'"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user