c00d4fe157
Martin now starts at z17, so z14 sample tile returned 404. Rebuild timeout increased from 15 to 30 min for z16 builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
238 lines
7.4 KiB
TypeScript
238 lines
7.4 KiB
TypeScript
/**
|
|
* 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<Response> {
|
|
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: 17, x: 73640, y: 47720, source: "gis_terenuri" },
|
|
];
|
|
|
|
export async function GET() {
|
|
const result: Record<string, unknown> = {
|
|
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<string, unknown> };
|
|
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<string, string>[] = [];
|
|
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);
|
|
}
|
|
|
|
async function getPmtilesInfo(): Promise<{ size: string; lastModified: string } | null> {
|
|
if (!PMTILES_URL) return null;
|
|
try {
|
|
const res = await fetchWithTimeout(PMTILES_URL, 3000);
|
|
return {
|
|
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 {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
// Get current PMTiles state before rebuild
|
|
const before = await getPmtilesInfo();
|
|
try {
|
|
const webhookRes = 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,
|
|
action: "rebuild",
|
|
webhookStatus: webhookRes.status,
|
|
previousPmtiles: before,
|
|
message: `Webhook trimis la N8N (HTTP ${webhookRes.status}). Rebuild-ul ruleaza ~8 min. Urmareste PMTiles last-modified.`,
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
return NextResponse.json({ error: `Webhook esuat: ${msg}` }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
if (action === "check-rebuild") {
|
|
// Check if PMTiles was updated since a given timestamp
|
|
const previousLastModified = (body as { previousLastModified?: string }).previousLastModified;
|
|
const current = await getPmtilesInfo();
|
|
const changed = !!current && !!previousLastModified && current.lastModified !== previousLastModified;
|
|
return NextResponse.json({
|
|
ok: true,
|
|
action: "check-rebuild",
|
|
current,
|
|
changed,
|
|
message: changed
|
|
? `Rebuild finalizat! PMTiles actualizat: ${current?.size}, ${current?.lastModified}`
|
|
: "Rebuild in curs...",
|
|
});
|
|
}
|
|
|
|
if (action === "warm-cache") {
|
|
const sources = ["gis_terenuri", "gis_cladiri"];
|
|
let total = 0;
|
|
let hits = 0;
|
|
let misses = 0;
|
|
let errors = 0;
|
|
const promises: Promise<void>[] = [];
|
|
for (const source of sources) {
|
|
for (let x = 9200; x <= 9210; x++) {
|
|
for (let y = 5960; y <= 5970; y++) {
|
|
total++;
|
|
promises.push(
|
|
fetchWithTimeout(`${TILE_CACHE_INTERNAL}/${source}/14/${x}/${y}`, 30000)
|
|
.then((res) => {
|
|
const cache = res.headers.get("x-cache-status") ?? "";
|
|
if (cache === "HIT") hits++;
|
|
else misses++;
|
|
})
|
|
.catch(() => { errors++; }),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
return NextResponse.json({
|
|
ok: true,
|
|
action: "warm-cache",
|
|
total,
|
|
hits,
|
|
misses,
|
|
errors,
|
|
message: `${total} tile-uri procesate: ${hits} HIT, ${misses} MISS (nou incarcate), ${errors} erori`,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
|
}
|