diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl index 770a163..b63d820 100644 --- a/Dockerfile.alpine-musl +++ b/Dockerfile.alpine-musl @@ -130,6 +130,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \ 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/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 \ -o /build/c_relay_pg_static \ c_utils_lib/libc_utils.a \ diff --git a/Makefile b/Makefile index 0bc8238..0439f07 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c99 -g -O2 -INCLUDES = -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket +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 @@ -11,7 +11,7 @@ DB_BACKEND ?= sqlite 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 +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) diff --git a/api/index.html b/api/index.html index 0344667..9cbd7de 100644 --- a/api/index.html +++ b/api/index.html @@ -18,6 +18,7 @@
+ @@ -579,6 +580,130 @@ WEB OF TRUST + +Service status will appear here after connecting.
+Inbox status will appear here after connecting.
+Start or stop the external caching service process. Requires caching_service_binary_path and caching_service_pg_conn to be set in Configuration.
Service status unavailable (caching_status command not implemented on relay).
'; + } + if (inboxStatusEl) { + inboxStatusEl.innerHTML = 'Inbox status unavailable (caching_status command not implemented on relay).
'; + } + + // Best-effort: send caching_status system command. Response handling is added in + // handleSystemCommandResponse() below; if unimplemented, the status blocks remain + // at the "unavailable" message set above. + try { + await sendAdminCommand(['system_command', 'caching_status']); + } catch (e) { + console.log('caching_status command not available: ' + e.message); + } + + } catch (error) { + console.error('Failed to fetch caching status:', error); + const statusEl = document.getElementById('caching-config-status'); + if (statusEl) { + statusEl.innerHTML = `Failed to load caching status: ${escapeHtml(error.message)}`; + } + } +} + +// Apply caching configuration: collect form values and send config_update +async function applyCachingConfig() { + const statusEl = document.getElementById('caching-config-status'); + try { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to apply caching configuration'); + } + + const configObjects = []; + + CACHING_CONFIG_FIELDS.forEach(field => { + const el = document.getElementById(field.field); + if (!el) return; + + let value = el.value; + + if (field.type === 'textarea-list') { + // Convert newlines back to comma-separated for storage + value = value.split('\n').map(s => s.trim()).filter(s => s.length > 0).join(','); + } else if (field.type === 'integer') { + // Normalize empty to 0 + value = String(value).trim(); + if (value === '') value = '0'; + } else { + value = String(value).trim(); + } + + let dataType = 'string'; + if (field.type === 'integer') { + dataType = 'integer'; + } else if (field.type === 'select') { + dataType = 'boolean'; + } + + configObjects.push({ + key: field.key, + value: value, + data_type: dataType, + category: 'caching' + }); + }); + + if (statusEl) { + statusEl.innerHTML = 'Applying caching configuration...'; + } + + // Send all caching config keys in a single config_update command + await sendConfigUpdateCommand(configObjects); + + // Increment caching_config_generation so the caching service picks up changes + const currentGen = getCachingConfigValue('caching_config_generation'); + let nextGen = 1; + if (currentGen !== null && !isNaN(parseInt(currentGen, 10))) { + nextGen = parseInt(currentGen, 10) + 1; + } + await sendConfigUpdateCommand([{ + key: 'caching_config_generation', + value: String(nextGen), + data_type: 'integer', + category: 'caching' + }]); + + if (statusEl) { + statusEl.innerHTML = '✓ Caching configuration applied successfully.'; + } + log('Caching configuration applied successfully', 'INFO'); + + // Refresh status to reflect applied values + setTimeout(() => fetchCachingStatus().catch(() => {}), 1500); + + } catch (error) { + console.error('Failed to apply caching configuration:', error); + if (statusEl) { + statusEl.innerHTML = `Failed to apply configuration: ${escapeHtml(error.message)}`; + } + log('Failed to apply caching configuration: ' + error.message, 'ERROR'); + } +} + +// Reset backfill progress via system_command +async function resetBackfillProgress() { + const statusEl = document.getElementById('caching-config-status'); + try { + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to reset backfill progress'); + } + + const confirmed = confirm('Reset all backfill progress? This will cause the caching service to re-fetch events for all backfill windows.'); + if (!confirmed) { + return; + } + + if (statusEl) { + statusEl.innerHTML = 'Resetting backfill progress...'; + } + + await sendAdminCommand(['system_command', 'caching_reset_progress']); + + if (statusEl) { + statusEl.innerHTML = '✓ Backfill progress reset command sent.'; + } + log('Backfill progress reset command sent', 'INFO'); + + } catch (error) { + console.error('Failed to reset backfill progress:', error); + if (statusEl) { + statusEl.innerHTML = `Failed to reset backfill progress: ${escapeHtml(error.message)}`; + } + log('Failed to reset backfill progress: ' + error.message, 'ERROR'); + } +} + +// Handle caching_status system command responses (service + inbox status) +function handleCachingStatusResponse(responseData) { + const serviceStatusEl = document.getElementById('caching-service-status'); + const inboxStatusEl = document.getElementById('caching-inbox-status'); + + if (responseData.status !== 'success') { + const msg = responseData.message || 'Unknown error'; + if (serviceStatusEl) { + serviceStatusEl.innerHTML = `Service status error: ${escapeHtml(msg)}
`; + } + if (inboxStatusEl) { + inboxStatusEl.innerHTML = `Inbox status error: ${escapeHtml(msg)}
`; + } + return; + } + + const data = responseData.data || responseData; + + // Service status block + if (serviceStatusEl) { + const service = data.service || data.caching_service || {}; + const enabled = service.enabled !== undefined ? service.enabled : (data.enabled !== undefined ? data.enabled : null); + const running = service.running !== undefined ? service.running : (data.running !== undefined ? data.running : null); + const connectedRelays = service.connected_relays !== undefined ? service.connected_relays : (data.connected_relays !== undefined ? data.connected_relays : null); + const eventsCached = service.events_cached !== undefined ? service.events_cached : (data.events_cached !== undefined ? data.events_cached : null); + + let html = '