From 7a93a2805564c71b6d76cbeb03d701e342df0527 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Mon, 30 Mar 2026 23:25:23 +0300 Subject: [PATCH] fix(parcel-sync): always run syncLayer for delta detection + no-geom freshness - Always call syncLayer for TERENURI/CLADIRI (not gated by isFresh) so that quick-count + VALID_FROM delta actually run on daily syncs - syncLayer handles efficiency internally via quick-count match - Add 48h freshness check for no-geom import (skip if recent) - Admin layers: skip if synced within 24h - Log sync summary (new features, updated features) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/eterra/sync-background/route.ts | 124 ++++++++++++-------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/src/app/api/eterra/sync-background/route.ts b/src/app/api/eterra/sync-background/route.ts index 8b22a34..abfa26c 100644 --- a/src/app/api/eterra/sync-background/route.ts +++ b/src/app/api/eterra/sync-background/route.ts @@ -187,42 +187,38 @@ async function runBackground(params: { getLayerFreshness(siruta, "CLADIRI_ACTIVE"), ]); - const terenuriNeedsSync = - forceSync || - !isFresh(terenuriStatus.lastSynced) || - terenuriStatus.featureCount === 0; - const cladiriNeedsSync = - forceSync || - !isFresh(cladiriStatus.lastSynced) || - cladiriStatus.featureCount === 0; + const terenuriNeedsFullSync = + forceSync || terenuriStatus.featureCount === 0; + const cladiriNeedsFullSync = + forceSync || cladiriStatus.featureCount === 0; - if (terenuriNeedsSync) { - phase = "Sincronizare terenuri"; - push({}); - const r = await syncLayer(username, password, siruta, "TERENURI_ACTIVE", { - forceFullSync: forceSync, - jobId, - isSubStep: true, - }); - if (r.status === "error") - throw new Error(r.error ?? "Sync terenuri failed"); - } + // Always call syncLayer — it handles quick-count + VALID_FROM delta internally. + // Only force full download when no local data or explicit forceSync. + phase = "Sincronizare terenuri"; + push({}); + const terenuriResult = await syncLayer(username, password, siruta, "TERENURI_ACTIVE", { + forceFullSync: terenuriNeedsFullSync, + jobId, + isSubStep: true, + }); + if (terenuriResult.status === "error") + throw new Error(terenuriResult.error ?? "Sync terenuri failed"); updateOverall(0.5); - if (cladiriNeedsSync) { - phase = "Sincronizare clădiri"; - push({}); - const r = await syncLayer(username, password, siruta, "CLADIRI_ACTIVE", { - forceFullSync: forceSync, - jobId, - isSubStep: true, - }); - if (r.status === "error") - throw new Error(r.error ?? "Sync clădiri failed"); - } + phase = "Sincronizare clădiri"; + push({}); + const cladiriResult = await syncLayer(username, password, siruta, "CLADIRI_ACTIVE", { + forceFullSync: cladiriNeedsFullSync, + jobId, + isSubStep: true, + }); + if (cladiriResult.status === "error") + throw new Error(cladiriResult.error ?? "Sync clădiri failed"); - // Sync admin layers (always, lightweight) + // Sync admin layers — skip if synced within 24h for (const adminLayer of ["LIMITE_INTRAV_DYNAMIC", "LIMITE_UAT"]) { + const adminStatus = await getLayerFreshness(siruta, adminLayer); + if (!forceSync && isFresh(adminStatus.lastSynced, 24)) continue; phase = `Sincronizare ${adminLayer === "LIMITE_UAT" ? "limite UAT" : "limite intravilan"}`; push({}); try { @@ -232,37 +228,65 @@ async function runBackground(params: { isSubStep: true, }); } catch { - // Non-critical — don't fail the whole job note = `Avertisment: ${adminLayer} nu s-a sincronizat`; push({}); } } - if (!terenuriNeedsSync && !cladiriNeedsSync) { - note = "Date proaspete — sync skip"; - } + const syncSummary = [ + terenuriResult.newFeatures > 0 ? `${terenuriResult.newFeatures} terenuri noi` : null, + terenuriResult.validFromUpdated ? `${terenuriResult.validFromUpdated} terenuri actualizate` : null, + cladiriResult.newFeatures > 0 ? `${cladiriResult.newFeatures} cladiri noi` : null, + cladiriResult.validFromUpdated ? `${cladiriResult.validFromUpdated} cladiri actualizate` : null, + ].filter(Boolean); + note = syncSummary.length > 0 ? syncSummary.join(", ") : "Fără schimbări"; finishPhase(); /* ── Phase 2: No-geometry import (optional) ──────── */ if (hasNoGeom && weights.noGeom > 0) { - setPhase("Import parcele fără geometrie", weights.noGeom); - const noGeomClient = await EterraClient.create(username, password, { - timeoutMs: 120_000, - }); - const res = await syncNoGeometryParcels(noGeomClient, siruta, { - onProgress: (done, tot, ph) => { - phase = ph; - push({}); - }, - }); - if (res.status === "error") { - note = `Avertisment no-geom: ${res.error}`; + setPhase("Verificare parcele fără geometrie", weights.noGeom); + // Skip no-geom import if recently done (within 48h) and not forced + const { PrismaClient } = await import("@prisma/client"); + const _prisma = new PrismaClient(); + let skipNoGeom = false; + try { + const recentNoGeom = await _prisma.gisFeature.findFirst({ + where: { + layerId: "TERENURI_ACTIVE", + siruta, + geometrySource: "NO_GEOMETRY", + updatedAt: { gte: new Date(Date.now() - 48 * 60 * 60 * 1000) }, + }, + select: { id: true }, + }); + skipNoGeom = !forceSync && recentNoGeom != null; + } catch { /* proceed with import */ } + await _prisma.$disconnect(); + + if (skipNoGeom) { + note = "Parcele fără geometrie — actualizate recent, skip"; push({}); } else { - const cleanNote = - res.cleaned > 0 ? `, ${res.cleaned} vechi șterse` : ""; - note = `${res.imported} parcele noi importate${cleanNote}`; + phase = "Import parcele fără geometrie"; push({}); + const noGeomClient = await EterraClient.create(username, password, { + timeoutMs: 120_000, + }); + const res = await syncNoGeometryParcels(noGeomClient, siruta, { + onProgress: (done, tot, ph) => { + phase = ph; + push({}); + }, + }); + if (res.status === "error") { + note = `Avertisment no-geom: ${res.error}`; + push({}); + } else { + const cleanNote = + res.cleaned > 0 ? `, ${res.cleaned} vechi șterse` : ""; + note = `${res.imported} parcele noi importate${cleanNote}`; + push({}); + } } finishPhase(); }