From 9bab9db4df1e635d505104ee881d4e79304d2b7f Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 28 Mar 2026 09:44:38 +0200 Subject: [PATCH] feat(geoportal): N8N webhook on sync completion + tile cache monitoring - weekend-deep-sync.ts: fire webhook to N8N_WEBHOOK_URL after each sync cycle (N8N triggers tippecanoe PMTiles rebuild via SSH on host) - nginx tile-cache: add stub_status at /status, custom log format with cache status - Add tile-cache-stats.sh: shows HIT/MISS ratio, cache size, slow tiles - docker-compose: add N8N_WEBHOOK_URL env var Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 2 ++ nginx/tile-cache.conf | 12 ++++++++ scripts/tile-cache-stats.sh | 30 +++++++++++++++++++ .../parcel-sync/services/weekend-deep-sync.ts | 27 +++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 scripts/tile-cache-stats.sh diff --git a/docker-compose.yml b/docker-compose.yml index 487b375..71c0a63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,8 @@ services: - NOTIFICATION_CRON_SECRET=1547a198feca43af6c05622588c6d3b820bad5163b8c20175b2b5bbf8fc1a987 # Weekend Deep Sync email reports (comma-separated for multiple recipients) - WEEKEND_SYNC_EMAIL=${WEEKEND_SYNC_EMAIL:-} + # N8N webhook — triggers PMTiles rebuild after sync cycle + - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-} # Portal-only users (comma-separated, redirected to /portal) - PORTAL_ONLY_USERS=dtiurbe,d.tiurbe # Address Book API (inter-service auth for external tools) diff --git a/nginx/tile-cache.conf b/nginx/tile-cache.conf index 8cbaf30..464f315 100644 --- a/nginx/tile-cache.conf +++ b/nginx/tile-cache.conf @@ -8,7 +8,13 @@ proxy_cache_path /var/cache/nginx/tiles inactive=7d use_temp_path=off; +# Log format with cache status for monitoring (docker logs tile-cache | grep HIT/MISS) +log_format tiles '$remote_addr [$time_local] "$request" $status ' + 'cache=$upstream_cache_status size=$body_bytes_sent ' + 'time=$request_time'; + server { + access_log /var/log/nginx/access.log tiles; listen 80; server_name _; @@ -19,6 +25,12 @@ server { add_header Content-Type text/plain; } + # nginx status (active connections, request counts) — for monitoring + location = /status { + access_log off; + stub_status on; + } + # Martin catalog endpoint (no cache) location = /catalog { proxy_pass http://martin:3000/catalog; diff --git a/scripts/tile-cache-stats.sh b/scripts/tile-cache-stats.sh new file mode 100644 index 0000000..2ce58e8 --- /dev/null +++ b/scripts/tile-cache-stats.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# tile-cache-stats.sh — Show tile cache hit/miss statistics +# Usage: ./scripts/tile-cache-stats.sh [MINUTES] +# Reads recent nginx logs from tile-cache container. + +set -euo pipefail + +MINUTES="${1:-60}" + +echo "=== Tile Cache Stats (last ${MINUTES}min) ===" +echo "" + +# Get nginx status (active connections) +echo "--- Connections ---" +curl -s "http://10.10.10.166:3010/status" 2>/dev/null || echo "(status endpoint unavailable)" +echo "" + +# Parse recent logs for cache hit/miss ratio +echo "--- Cache Performance ---" +docker logs tile-cache --since "${MINUTES}m" 2>/dev/null | \ + grep -oP 'cache=\K\w+' | sort | uniq -c | sort -rn || echo "(no logs in timeframe)" + +echo "" +echo "--- Cache Size ---" +docker exec tile-cache du -sh /var/cache/nginx/tiles/ 2>/dev/null || echo "(cannot read cache dir)" + +echo "" +echo "--- Slowest Tiles (>1s) ---" +docker logs tile-cache --since "${MINUTES}m" 2>/dev/null | \ + grep -oP 'time=\K[0-9.]+' | awk '$1 > 1.0 {print $1"s"}' | sort -rn | head -5 || echo "(none)" diff --git a/src/modules/parcel-sync/services/weekend-deep-sync.ts b/src/modules/parcel-sync/services/weekend-deep-sync.ts index 65b0fb6..3fe9b67 100644 --- a/src/modules/parcel-sync/services/weekend-deep-sync.ts +++ b/src/modules/parcel-sync/services/weekend-deep-sync.ts @@ -401,6 +401,8 @@ export async function runWeekendDeepSync(): Promise { console.log( `[weekend-sync] Ciclu complet #${state.completedCycles}! Reset pentru urmatorul ciclu.`, ); + // Notify N8N to rebuild PMTiles (overview tiles for geoportal) + await fireSyncWebhook(state.completedCycles); } await saveState(state); @@ -534,3 +536,28 @@ async function sendStatusEmail( console.warn(`[weekend-sync] Nu s-a putut trimite email: ${msg}`); } } + +/* ------------------------------------------------------------------ */ +/* N8N Webhook — trigger PMTiles rebuild after sync cycle */ +/* ------------------------------------------------------------------ */ + +async function fireSyncWebhook(cycle: number): Promise { + const url = process.env.N8N_WEBHOOK_URL; + if (!url) return; + + try { + await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + event: "weekend-sync-cycle-complete", + cycle, + timestamp: new Date().toISOString(), + }), + }); + console.log(`[weekend-sync] Webhook trimis la N8N (ciclu #${cycle})`); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[weekend-sync] Webhook N8N esuat: ${msg}`); + } +}