diff --git a/src/app/(modules)/monitor/page.tsx b/src/app/(modules)/monitor/page.tsx index e7999c5..0ec04ac 100644 --- a/src/app/(modules)/monitor/page.tsx +++ b/src/app/(modules)/monitor/page.tsx @@ -235,6 +235,19 @@ export default function MonitorPage() { loading={actionLoading === "warm-cache"} onClick={triggerWarmCache} /> + void; addLog: (type: "info" | "ok" | "error" | "wait", msg: string) => void; pollRef: React.MutableRefObject | null>; + customEndpoint?: string; }) { const startTimeRef = useRef(0); const formatElapsed = () => { @@ -372,12 +386,14 @@ function SyncTestButton({ label, description, siruta, mode, includeNoGeometry, a onClick={async () => { setActionLoading(actionKey); startTimeRef.current = Date.now(); - addLog("info", `[${label}] Pornire sync (${mode}, noGeom=${includeNoGeometry})...`); + addLog("info", `[${label}] Pornire...`); try { - const res = await fetch("/api/eterra/sync-background", { + const endpoint = customEndpoint ?? "/api/eterra/sync-background"; + const body = customEndpoint ? {} : { siruta, mode, includeNoGeometry }; + const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ siruta, mode, includeNoGeometry }), + body: JSON.stringify(body), }); const d = await res.json() as { jobId?: string; error?: string }; if (!res.ok) { diff --git a/src/app/api/eterra/refresh-all/route.ts b/src/app/api/eterra/refresh-all/route.ts new file mode 100644 index 0000000..836af69 --- /dev/null +++ b/src/app/api/eterra/refresh-all/route.ts @@ -0,0 +1,153 @@ +/** + * POST /api/eterra/refresh-all + * + * Runs delta sync on ALL UATs in DB sequentially. + * UATs with >30% enrichment → magic mode (sync + enrichment). + * UATs with ≤30% enrichment → base mode (sync only). + * + * Returns immediately with jobId — progress via /api/eterra/progress. + */ + +import { PrismaClient } from "@prisma/client"; +import { + setProgress, + clearProgress, + type SyncProgress, +} from "@/modules/parcel-sync/services/progress-store"; +import { syncLayer } from "@/modules/parcel-sync/services/sync-service"; +import { enrichFeatures } from "@/modules/parcel-sync/services/enrich-service"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; +import { checkEterraHealthNow } from "@/modules/parcel-sync/services/eterra-health"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const prisma = new PrismaClient(); + +export async function POST() { + const username = process.env.ETERRA_USERNAME ?? ""; + const password = process.env.ETERRA_PASSWORD ?? ""; + if (!username || !password) { + return Response.json( + { error: "ETERRA_USERNAME / ETERRA_PASSWORD not configured" }, + { status: 500 }, + ); + } + + const jobId = crypto.randomUUID(); + setProgress({ + jobId, + downloaded: 0, + total: 100, + status: "running", + phase: "Pregătire refresh complet", + }); + + void runRefreshAll(jobId, username, password); + + return Response.json({ jobId, message: "Refresh complet pornit" }, { status: 202 }); +} + +async function runRefreshAll(jobId: string, username: string, password: string) { + const push = (p: Partial) => + setProgress({ jobId, downloaded: 0, total: 100, status: "running", ...p } as SyncProgress); + + try { + // Health check + const health = await checkEterraHealthNow(); + if (!health.available) { + setProgress({ jobId, downloaded: 0, total: 100, status: "error", phase: "eTerra indisponibil", message: health.message ?? "maintenance" }); + setTimeout(() => clearProgress(jobId), 3_600_000); + return; + } + + // Find all UATs with features + enrichment ratio + const uats = await prisma.$queryRawUnsafe< + Array<{ siruta: string; name: string | null; total: number; enriched: number }> + >( + `SELECT f.siruta, u.name, COUNT(*)::int as total, + COUNT(*) FILTER (WHERE f."enrichedAt" IS NOT NULL)::int as enriched + FROM "GisFeature" f LEFT JOIN "GisUat" u ON f.siruta = u.siruta + WHERE f."layerId" IN ('TERENURI_ACTIVE','CLADIRI_ACTIVE') AND f."objectId" > 0 + GROUP BY f.siruta, u.name ORDER BY total DESC`, + ); + + if (uats.length === 0) { + setProgress({ jobId, downloaded: 100, total: 100, status: "done", phase: "Niciun UAT in DB" }); + setTimeout(() => clearProgress(jobId), 3_600_000); + return; + } + + const results: Array<{ siruta: string; name: string; mode: string; duration: number; note: string }> = []; + let errors = 0; + + for (let i = 0; i < uats.length; i++) { + const uat = uats[i]!; + const uatName = uat.name ?? uat.siruta; + const ratio = uat.total > 0 ? uat.enriched / uat.total : 0; + const isMagic = ratio > 0.3; + const mode = isMagic ? "magic" : "base"; + const pct = Math.round(((i) / uats.length) * 100); + + push({ + downloaded: pct, + total: 100, + phase: `[${i + 1}/${uats.length}] ${uatName} (${mode})`, + note: results.length > 0 ? `Ultimul: ${results[results.length - 1]!.name} — ${results[results.length - 1]!.note}` : undefined, + }); + + const uatStart = Date.now(); + try { + // Sync TERENURI + CLADIRI (quick-count + VALID_FROM delta) + const tRes = await syncLayer(username, password, uat.siruta, "TERENURI_ACTIVE", { uatName }); + const cRes = await syncLayer(username, password, uat.siruta, "CLADIRI_ACTIVE", { uatName }); + + let enrichNote = ""; + if (isMagic) { + const client = await EterraClient.create(username, password, { timeoutMs: 120_000 }); + const eRes = await enrichFeatures(client, uat.siruta); + enrichNote = eRes.status === "done" + ? ` | enrich: ${eRes.enrichedCount}/${eRes.totalFeatures ?? "?"}` + : ` | enrich err: ${eRes.error}`; + } + + const dur = Math.round((Date.now() - uatStart) / 1000); + const parts = [ + tRes.newFeatures > 0 || (tRes.validFromUpdated ?? 0) > 0 + ? `T:+${tRes.newFeatures}/${tRes.validFromUpdated ?? 0}vf` + : "T:ok", + cRes.newFeatures > 0 || (cRes.validFromUpdated ?? 0) > 0 + ? `C:+${cRes.newFeatures}/${cRes.validFromUpdated ?? 0}vf` + : "C:ok", + ]; + const note = `${parts.join(", ")}${enrichNote} (${dur}s)`; + results.push({ siruta: uat.siruta, name: uatName, mode, duration: dur, note }); + console.log(`[refresh-all] ${i + 1}/${uats.length} ${uatName}: ${note}`); + } catch (err) { + errors++; + const dur = Math.round((Date.now() - uatStart) / 1000); + const msg = err instanceof Error ? err.message : "Unknown"; + results.push({ siruta: uat.siruta, name: uatName, mode, duration: dur, note: `ERR: ${msg}` }); + console.error(`[refresh-all] ${uatName}: ${msg}`); + } + } + + const totalDur = results.reduce((s, r) => s + r.duration, 0); + const summary = `${uats.length} UATs, ${errors} erori, ${totalDur}s total`; + setProgress({ + jobId, + downloaded: 100, + total: 100, + status: "done", + phase: "Refresh complet finalizat", + message: summary, + note: results.map((r) => `${r.name}: ${r.note}`).join("\n"), + }); + console.log(`[refresh-all] Done: ${summary}`); + setTimeout(() => clearProgress(jobId), 6 * 3_600_000); + } catch (err) { + const msg = err instanceof Error ? err.message : "Unknown"; + setProgress({ jobId, downloaded: 0, total: 100, status: "error", phase: "Eroare", message: msg }); + setTimeout(() => clearProgress(jobId), 3_600_000); + } +}