/** * GET /api/geoportal/monitor — tile infrastructure status * POST /api/geoportal/monitor — trigger actions (rebuild, warm-cache) */ import { NextRequest, NextResponse } from "next/server"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const TILE_CACHE_INTERNAL = "http://tile-cache:80"; const MARTIN_INTERNAL = "http://martin:3000"; const PMTILES_URL = process.env.NEXT_PUBLIC_PMTILES_URL || ""; const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL || ""; type NginxStatus = { activeConnections: number; accepts: number; handled: number; requests: number; reading: number; writing: number; waiting: number; }; function parseNginxStatus(text: string): NginxStatus { const lines = text.trim().split("\n"); const active = parseInt(lines[0]?.match(/\d+/)?.[0] ?? "0", 10); const counts = lines[2]?.trim().split(/\s+/).map(Number) ?? [0, 0, 0]; const rw = lines[3]?.match(/\d+/g)?.map(Number) ?? [0, 0, 0]; return { activeConnections: active, accepts: counts[0] ?? 0, handled: counts[1] ?? 0, requests: counts[2] ?? 0, reading: rw[0] ?? 0, writing: rw[1] ?? 0, waiting: rw[2] ?? 0, }; } async function fetchWithTimeout(url: string, timeoutMs = 5000): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { signal: controller.signal, cache: "no-store" }); } finally { clearTimeout(timer); } } // Sample tile coordinates for cache testing (Romania, z8) const SAMPLE_TILES = [ { z: 8, x: 143, y: 91, source: "gis_uats_z8" }, { z: 14, x: 9205, y: 5965, source: "gis_terenuri" }, ]; export async function GET() { const result: Record = { timestamp: new Date().toISOString(), }; // 1. Nginx status try { const res = await fetchWithTimeout(`${TILE_CACHE_INTERNAL}/status`); if (res.ok) { result.nginx = parseNginxStatus(await res.text()); } else { result.nginx = { error: `HTTP ${res.status}` }; } } catch { result.nginx = { error: "tile-cache unreachable" }; } // 2. Martin catalog try { const res = await fetchWithTimeout(`${MARTIN_INTERNAL}/catalog`); if (res.ok) { const catalog = await res.json() as { tiles?: Record }; const sources = Object.keys(catalog.tiles ?? {}); result.martin = { status: "ok", sources, sourceCount: sources.length }; } else { result.martin = { error: `HTTP ${res.status}` }; } } catch { result.martin = { error: "martin unreachable" }; } // 3. PMTiles info if (PMTILES_URL) { try { const res = await fetchWithTimeout(PMTILES_URL, 3000); result.pmtiles = { url: PMTILES_URL, status: res.ok ? "ok" : `HTTP ${res.status}`, size: res.headers.get("content-length") ? `${(parseInt(res.headers.get("content-length") ?? "0", 10) / 1024 / 1024).toFixed(1)} MB` : "unknown", lastModified: res.headers.get("last-modified") ?? "unknown", }; } catch { result.pmtiles = { url: PMTILES_URL, error: "unreachable" }; } } else { result.pmtiles = { status: "not configured" }; } // 4. Cache test — request sample tiles and check X-Cache-Status const cacheTests: Record[] = []; for (const tile of SAMPLE_TILES) { try { const url = `${TILE_CACHE_INTERNAL}/${tile.source}/${tile.z}/${tile.x}/${tile.y}`; const res = await fetchWithTimeout(url, 10000); cacheTests.push({ tile: `${tile.source}/${tile.z}/${tile.x}/${tile.y}`, status: `${res.status}`, cache: res.headers.get("x-cache-status") ?? "unknown", }); } catch { cacheTests.push({ tile: `${tile.source}/${tile.z}/${tile.x}/${tile.y}`, status: "error", cache: "unreachable", }); } } result.cacheTests = cacheTests; // 5. Config summary result.config = { martinUrl: process.env.NEXT_PUBLIC_MARTIN_URL ?? "(not set)", pmtilesUrl: PMTILES_URL || "(not set)", n8nWebhook: N8N_WEBHOOK_URL ? "configured" : "not set", }; return NextResponse.json(result); } export async function POST(request: NextRequest) { const body = await request.json() as { action?: string }; const action = body.action; if (action === "rebuild") { if (!N8N_WEBHOOK_URL) { return NextResponse.json({ error: "N8N_WEBHOOK_URL not configured" }, { status: 400 }); } try { await fetch(N8N_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event: "manual-rebuild", timestamp: new Date().toISOString(), }), }); return NextResponse.json({ ok: true, message: "Rebuild triggered via N8N" }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return NextResponse.json({ error: `Webhook failed: ${msg}` }, { status: 500 }); } } if (action === "warm-cache") { // Warm cache from within the container (tile-cache is on same Docker network) const sources = ["gis_terenuri", "gis_cladiri"]; let warmed = 0; const promises: Promise[] = []; for (const source of sources) { // Bucharest area z14 for (let x = 9200; x <= 9210; x++) { for (let y = 5960; y <= 5970; y++) { promises.push( fetchWithTimeout(`${TILE_CACHE_INTERNAL}/${source}/14/${x}/${y}`, 30000) .then(() => { warmed++; }) .catch(() => { /* noop */ }), ); } } } await Promise.all(promises); return NextResponse.json({ ok: true, message: `Cache warmed: ${warmed} tiles` }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); }