"use client"; import { useState, useEffect, useCallback, useRef } from "react"; type MonitorData = { timestamp: string; nginx?: { activeConnections?: number; requests?: number; reading?: number; writing?: number; waiting?: number; error?: string }; martin?: { status?: string; sources?: string[]; sourceCount?: number; error?: string }; pmtiles?: { url?: string; status?: string; size?: string; lastModified?: string; error?: string }; cacheTests?: { tile: string; status: string; cache: string }[]; config?: { martinUrl?: string; pmtilesUrl?: string; n8nWebhook?: string }; }; type EterraSessionStatus = { connected: boolean; username?: string; connectedAt?: string; activeJobCount: number; eterraAvailable?: boolean; eterraMaintenance?: boolean; eterraHealthMessage?: string; }; type GisStats = { totalUats: number; totalFeatures: number; totalTerenuri: number; totalCladiri: number; totalEnriched: number; totalNoGeom: number; countiesWithData: number; lastSyncAt: string | null; dbSizeMb: number | null; }; export default function MonitorPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(""); const [logs, setLogs] = useState<{ time: string; type: "info" | "ok" | "error" | "wait"; msg: string }[]>([]); const [counties, setCounties] = useState([]); const [selectedCounty, setSelectedCounty] = useState(""); const [eterraSession, setEterraSession] = useState({ connected: false, activeJobCount: 0 }); const [eterraConnecting, setEterraConnecting] = useState(false); const [showLoginForm, setShowLoginForm] = useState(false); const [eterraUser, setEterraUser] = useState(""); const [eterraPwd, setEterraPwd] = useState(""); const [gisStats, setGisStats] = useState(null); const rebuildPrevRef = useRef(null); const pollRef = useRef | null>(null); const refresh = useCallback(async () => { setLoading(true); try { const res = await fetch("/api/geoportal/monitor"); if (res.ok) setData(await res.json()); } catch { /* noop */ } setLoading(false); }, []); useEffect(() => { refresh(); }, [refresh]); // Auto-refresh every 30s useEffect(() => { const interval = setInterval(refresh, 30_000); return () => clearInterval(interval); }, [refresh]); const addLog = useCallback((type: "info" | "ok" | "error" | "wait", msg: string) => { setLogs((prev) => [{ time: new Date().toLocaleTimeString("ro-RO"), type, msg }, ...prev.slice(0, 49)]); }, []); // Fetch counties for sync selector useEffect(() => { fetch("/api/eterra/counties") .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d: { counties: string[] }) => setCounties(d.counties ?? [])) .catch(() => {}); }, []); // eTerra session status — poll every 30s const fetchEterraSession = useCallback(async () => { try { const res = await fetch("/api/eterra/session"); if (res.ok) setEterraSession(await res.json() as EterraSessionStatus); } catch { /* noop */ } }, []); useEffect(() => { void fetchEterraSession(); const interval = setInterval(() => void fetchEterraSession(), 30_000); return () => clearInterval(interval); }, [fetchEterraSession]); // GIS stats — poll every 30s const fetchGisStats = useCallback(async () => { try { const res = await fetch("/api/eterra/stats"); if (res.ok) setGisStats(await res.json() as GisStats); } catch { /* noop */ } }, []); useEffect(() => { void fetchGisStats(); const interval = setInterval(() => void fetchGisStats(), 30_000); return () => clearInterval(interval); }, [fetchGisStats]); const handleEterraConnect = async () => { setEterraConnecting(true); try { const payload: Record = { action: "connect" }; if (eterraUser.trim()) payload.username = eterraUser.trim(); if (eterraPwd.trim()) payload.password = eterraPwd.trim(); const res = await fetch("/api/eterra/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const d = await res.json() as { success?: boolean; error?: string }; if (d.success) { await fetchEterraSession(); addLog("ok", "eTerra conectat"); setShowLoginForm(false); setEterraPwd(""); } else { addLog("error", `eTerra: ${d.error ?? "Eroare conectare"}`); } } catch { addLog("error", "eTerra: eroare retea"); } setEterraConnecting(false); }; const handleEterraDisconnect = async () => { const res = await fetch("/api/eterra/session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "disconnect" }), }); const d = await res.json() as { success?: boolean; error?: string }; if (d.success) { setEterraSession({ connected: false, activeJobCount: 0 }); addLog("info", "eTerra deconectat"); } else { addLog("error", `Deconectare: ${d.error ?? "Eroare"}`); } }; // Cleanup poll on unmount useEffect(() => { return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, []); const triggerRebuild = async () => { setActionLoading("rebuild"); addLog("info", "Se trimite webhook la N8N..."); try { const res = await fetch("/api/geoportal/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "rebuild" }), }); const result = await res.json() as { ok?: boolean; error?: string; alreadyRunning?: boolean; previousPmtiles?: { lastModified: string } }; if (!result.ok) { addLog("error", result.error ?? "Eroare necunoscuta"); setActionLoading(""); return; } addLog("ok", result.alreadyRunning ? "Rebuild deja in curs. Se monitorizeaza..." : "Webhook trimis. Rebuild pornit..."); rebuildPrevRef.current = result.previousPmtiles?.lastModified ?? null; // Poll every 15s to check if PMTiles was updated if (pollRef.current) clearInterval(pollRef.current); pollRef.current = setInterval(async () => { try { const checkRes = await fetch("/api/geoportal/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "check-rebuild", previousLastModified: rebuildPrevRef.current }), }); const check = await checkRes.json() as { changed?: boolean; current?: { size: string; lastModified: string } }; if (check.changed) { addLog("ok", `Rebuild finalizat! PMTiles: ${check.current?.size}, actualizat: ${check.current?.lastModified}`); if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } setActionLoading(""); refresh(); } } catch { /* continue polling */ } }, 15_000); // Timeout after 90 min (z18 builds can take 45-60 min) setTimeout(() => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; addLog("error", "Timeout: rebuild nu s-a finalizat in 90 minute"); setActionLoading(""); } }, 90 * 60_000); } catch { addLog("error", "Nu s-a putut trimite webhook-ul"); setActionLoading(""); } }; const triggerWarmCache = async () => { setActionLoading("warm-cache"); addLog("info", "Se incarca tile-uri in cache..."); try { const res = await fetch("/api/geoportal/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "warm-cache" }), }); const result = await res.json() as { ok?: boolean; error?: string; total?: number; hits?: number; misses?: number; errors?: number; message?: string }; if (result.ok) { addLog("ok", result.message ?? "Cache warming finalizat"); } else { addLog("error", result.error ?? "Eroare"); } } catch { addLog("error", "Eroare la warm cache"); } setActionLoading(""); setTimeout(refresh, 2000); }; return (

Tile Infrastructure Monitor

{data?.timestamp ? `Ultima actualizare: ${new Date(data.timestamp).toLocaleTimeString("ro-RO")}` : "Se incarca..."}

{/* Status Cards */}
{/* Nginx Card */} {data?.nginx?.error ? ( ) : data?.nginx ? (
) : }
{/* Martin Card */} {data?.martin?.error ? ( ) : data?.martin ? (
{data.martin.sources?.map((s) => ( {s} ))}
) : }
{/* PMTiles Card */} {data?.pmtiles?.error ? ( ) : data?.pmtiles?.status === "not configured" ? ( ) : data?.pmtiles ? (
) : }
{/* Cache Test Results */} {data?.cacheTests ? (
{data.cacheTests.map((t, i) => ( ))}
Tile HTTP Cache
{t.tile} {t.status} {t.cache}
) : }
{/* eTerra Connection + Live Stats */}
{/* Connection card */}
{eterraSession.eterraMaintenance ? "Mentenanta" : eterraSession.connected ? (eterraSession.username || "Conectat") : "Deconectat"}
{eterraSession.connected && eterraSession.connectedAt && (
Conectat de la {new Date(eterraSession.connectedAt).toLocaleTimeString("ro-RO")}
)} {eterraSession.connected && eterraSession.activeJobCount > 0 && (
{eterraSession.activeJobCount} {eterraSession.activeJobCount === 1 ? "job activ" : "joburi active"}
)}
{eterraSession.connected ? ( ) : ( )}
{showLoginForm && !eterraSession.connected && (
setEterraUser(e.target.value)} className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm" placeholder="Utilizator eTerra" /> setEterraPwd(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleEterraConnect(); }} className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm" placeholder="Parola" />
)}
{/* Live stats cards */} = 1024 ? `${(gisStats.dbSizeMb / 1024).toFixed(1)} GB` : `${gisStats.dbSizeMb} MB`}` : undefined} />
{gisStats?.lastSyncAt && (
Ultimul sync: {new Date(gisStats.lastSyncAt).toLocaleString("ro-RO")} — auto-refresh 30s
)} {/* Actions */} {/* Tile infrastructure actions */}

Tile-uri

{/* Sync actions */}

Sincronizare eTerra

Gestioneaza reguli sync
{/* County sync */}

Sync pe judet

{/* Activity Log */} {logs.length > 0 && (
Log activitate
{logs.map((log, i) => (
{log.time} {log.type === "ok" ? "[OK]" : log.type === "error" ? "[ERR]" : log.type === "wait" ? "[...]" : "[i]"} {log.msg}
))}
)} {/* Config */} {data?.config ? (
MARTIN_URL: {data.config.martinUrl}
PMTILES_URL: {data.config.pmtilesUrl}
N8N_WEBHOOK: {data.config.n8nWebhook}
) : }
); } /* ---- Sub-components ---- */ function Card({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function StatusBadge({ status, label }: { status: "ok" | "error" | "warn"; label: string }) { const colors = { ok: "bg-green-500/20 text-green-400", error: "bg-red-500/20 text-red-400", warn: "bg-yellow-500/20 text-yellow-400", }; return ( {label} ); } function Stat({ label, value }: { label: string; value?: string | number | null }) { return (
{label}
{value ?? "-"}
); } function Skeleton() { return
; } function StatCard({ label, value, sub }: { label: string; value?: number | null; sub?: string }) { return (
{label}
{value != null ? value.toLocaleString("ro-RO") : --}
{sub &&
{sub}
}
); } function ActionButton({ label, description, loading, onClick }: { label: string; description: string; loading: boolean; onClick: () => void; }) { return ( ); } function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, actionKey, actionLoading, setActionLoading, addLog, pollRef, customEndpoint, customBody, disabled }: { label: string; description: string; siruta: string; mode: "base" | "magic"; includeNoGeometry: boolean; actionKey: string; actionLoading: string; setActionLoading: (v: string) => void; addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void; pollRef: React.MutableRefObject | null>; customEndpoint?: string; customBody?: Record; disabled?: boolean; }) { const startTimeRef = useRef(0); const formatElapsed = () => { if (!startTimeRef.current) return ""; const s = Math.round((Date.now() - startTimeRef.current) / 1000); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`; }; return ( ); }