From 531c3b08584f798d1dc99af1de5a3fec9aa14e4b Mon Sep 17 00:00:00 2001
From: AI Assistant
Date: Sat, 7 Mar 2026 21:22:29 +0200
Subject: [PATCH] fix: scan numbers always add up, match quality tracking,
pipeline audit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SCAN DISPLAY:
- Use matchedCount (withGeometry) for 'cu geometrie' — ALWAYS adds up
with noGeomCount to equal totalImmovables (ground truth arithmetic)
- Show remoteGisCount separately as 'Layer GIS: N features (se descarca toate)'
- When remoteGisCount != matchedCount, show matching detail with breakdown
(X potrivite + cadRef/ID split) so mismatches are transparent
- Workflow preview step 1 still uses remoteGisCount (correct: all GIS
features get downloaded regardless of matching)
MATCH QUALITY TRACKING:
- New fields: matchedByRef, matchedById in NoGeomScanResult
- Track how many immovables matched by cadastral ref vs by IMMOVABLE_ID
- Console log match quality for server-side debugging
- scannedAt timestamp for audit trail
PIPELINE AUDIT (export report):
- New 'pipeline' section in export_report.json with full trace:
syncedGis, noGeometry (imported/cleaned/skipped), enriched, finalDb
- raport_calitate.txt now has PIPELINE section before quality analysis
showing exactly what happened at each step
- Capture noGeomCleaned + noGeomSkipped in addition to noGeomImported
---
src/app/api/eterra/export-bundle/route.ts | 40 ++++++++++++++++++-
.../components/parcel-sync-module.tsx | 39 +++++++++++++-----
.../parcel-sync/services/no-geom-sync.ts | 30 +++++++++++++-
3 files changed, 96 insertions(+), 13 deletions(-)
diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts
index 0fa9180..f0aea75 100644
--- a/src/app/api/eterra/export-bundle/route.ts
+++ b/src/app/api/eterra/export-bundle/route.ts
@@ -256,6 +256,8 @@ export async function POST(req: Request) {
/* Phase 1b: Import no-geometry parcels (optional) */
/* ══════════════════════════════════════════════════════════ */
let noGeomImported = 0;
+ let noGeomCleaned = 0;
+ let noGeomSkipped = 0;
if (hasNoGeom && weights.noGeom > 0) {
setPhaseState("Import parcele fără geometrie", weights.noGeom, 1);
const noGeomClient = await EterraClient.create(
@@ -282,6 +284,8 @@ export async function POST(req: Request) {
pushProgress();
} else {
noGeomImported = noGeomResult.imported;
+ noGeomCleaned = noGeomResult.cleaned;
+ noGeomSkipped = noGeomResult.skipped;
const cleanedNote =
noGeomResult.cleaned > 0
? `, ${noGeomResult.cleaned} vechi șterse`
@@ -626,6 +630,26 @@ export async function POST(req: Request) {
siruta: validated.siruta,
generatedAt: new Date().toISOString(),
source: "local-db (sync-first)",
+ pipeline: {
+ syncedGis: {
+ terenuri: terenuriNeedsSync ? "descărcat" : "din cache",
+ cladiri: cladiriNeedsSync ? "descărcat" : "din cache",
+ },
+ noGeometry: hasNoGeom
+ ? {
+ imported: noGeomImported,
+ cleaned: noGeomCleaned,
+ skipped: noGeomSkipped,
+ }
+ : "dezactivat",
+ enriched: validated.mode === "magic" ? "da" : "nu",
+ finalDb: {
+ total: dbTerenuri.length,
+ withGeometry: withGeomRecords.length,
+ noGeometry: noGeomRecords.length,
+ cladiri: cladiriGeoFeatures.length,
+ },
+ },
terenuri: {
count: terenuriGeoFeatures.length,
totalInDb: dbTerenuri.length,
@@ -665,7 +689,21 @@ export async function POST(req: Request) {
` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`,
`══════════════════════════════════════════════════════════`,
``,
- `REZUMAT GENERAL`,
+ `PIPELINE — CE S-A ÎNTÂMPLAT`,
+ `─────────────────────────────────────────────────────────`,
+ ` 1. Sync GIS terenuri: ${terenuriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`,
+ ` 2. Sync GIS clădiri: ${cladiriNeedsSync ? "descărcat din eTerra" : "din cache local (date proaspete)"}`,
+ ...(hasNoGeom
+ ? [
+ ` 3. Import fără geometrie: ${fmt(noGeomImported)} noi importate` +
+ (noGeomCleaned > 0 ? `, ${fmt(noGeomCleaned)} vechi șterse` : "") +
+ (noGeomSkipped > 0 ? `, ${fmt(noGeomSkipped)} filtrate/skip` : ""),
+ ]
+ : [` 3. Import fără geometrie: dezactivat`]),
+ ` 4. Îmbogățire (CF, prop.): da`,
+ ` 5. Generare fișiere: GPKG + CSV + raport`,
+ ``,
+ `STARE FINALĂ BAZĂ DE DATE`,
`─────────────────────────────────────────────────────────`,
` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`,
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx
index 9bf8991..f33827c 100644
--- a/src/modules/parcel-sync/components/parcel-sync-module.tsx
+++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx
@@ -391,6 +391,8 @@ export function ParcelSyncModule() {
withGeometry: number;
remoteGisCount: number;
noGeomCount: number;
+ matchedByRef: number;
+ matchedById: number;
qualityBreakdown: {
withCadRef: number;
withPaperCad: number;
@@ -407,6 +409,7 @@ export function ParcelSyncModule() {
localDbEnriched: number;
localDbEnrichedComplete: number;
localSyncFresh: boolean;
+ scannedAt: string;
} | null>(null);
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
@@ -718,6 +721,8 @@ export function ParcelSyncModule() {
withGeometry: 0,
remoteGisCount: 0,
noGeomCount: 0,
+ matchedByRef: 0,
+ matchedById: 0,
qualityBreakdown: emptyQuality,
localDbTotal: 0,
localDbWithGeom: 0,
@@ -725,6 +730,7 @@ export function ParcelSyncModule() {
localDbEnriched: 0,
localDbEnrichedComplete: 0,
localSyncFresh: false,
+ scannedAt: "",
};
try {
const res = await fetch("/api/eterra/no-geom-scan", {
@@ -746,6 +752,8 @@ export function ParcelSyncModule() {
withGeometry: Number(data.withGeometry ?? 0),
remoteGisCount: Number(data.remoteGisCount ?? 0),
noGeomCount: Number(data.noGeomCount ?? 0),
+ matchedByRef: Number(data.matchedByRef ?? 0),
+ matchedById: Number(data.matchedById ?? 0),
qualityBreakdown: {
withCadRef: Number(qb.withCadRef ?? 0),
withPaperCad: Number(qb.withPaperCad ?? 0),
@@ -762,6 +770,7 @@ export function ParcelSyncModule() {
localDbEnriched: Number(data.localDbEnriched ?? 0),
localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0),
localSyncFresh: Boolean(data.localSyncFresh),
+ scannedAt: String(data.scannedAt ?? new Date().toISOString()),
});
}
} catch {
@@ -2601,7 +2610,7 @@ export function ParcelSyncModule() {
{" "}
imobile în eTerra:{" "}
- {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
+ {noGeomScan.withGeometry.toLocaleString("ro-RO")}
{" "}
cu geometrie,{" "}
@@ -2609,15 +2618,25 @@ export function ParcelSyncModule() {
{" "}
fără geometrie
- {noGeomScan.withGeometry <
- noGeomScan.remoteGisCount && (
-
- {noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
- din{" "}
- {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
- features GIS au corespondent în lista de imobile
-
- )}
+
+ Layer GIS:{" "}
+
+ {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
+
+ {" features (se descarcă toate)"}
+ {noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
+ <>
+ {" · "}
+ {noGeomScan.withGeometry.toLocaleString("ro-RO")} potrivite
+ cu lista de imobile
+ {noGeomScan.matchedByRef > 0 && noGeomScan.matchedById > 0 && (
+
+ {" "}({noGeomScan.matchedByRef} cadRef + {noGeomScan.matchedById} ID)
+
+ )}
+ >
+ )}
+
Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS.
diff --git a/src/modules/parcel-sync/services/no-geom-sync.ts b/src/modules/parcel-sync/services/no-geom-sync.ts
index 224bc1d..ac85ab7 100644
--- a/src/modules/parcel-sync/services/no-geom-sync.ts
+++ b/src/modules/parcel-sync/services/no-geom-sync.ts
@@ -118,6 +118,9 @@ export type NoGeomScanResult = {
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
remoteGisCount: number;
noGeomCount: number;
+ /** Match quality: how many matched by cadastral ref vs immovable ID */
+ matchedByRef: number;
+ matchedById: number;
/** Quality breakdown of no-geometry items */
qualityBreakdown: NoGeomQuality;
/** Sample of immovable identifiers without geometry */
@@ -142,6 +145,8 @@ export type NoGeomScanResult = {
localDbEnrichedComplete: number;
/** Whether local sync is fresh (< 7 days) */
localSyncFresh: boolean;
+ /** Timestamp of the scan (for audit trail) */
+ scannedAt: string;
/** Error message if workspace couldn't be resolved */
error?: string;
};
@@ -181,6 +186,8 @@ export async function scanNoGeometryParcels(
withGeometry: 0,
remoteGisCount: 0,
noGeomCount: 0,
+ matchedByRef: 0,
+ matchedById: 0,
qualityBreakdown: {
withCadRef: 0,
withPaperCad: 0,
@@ -198,6 +205,7 @@ export async function scanNoGeometryParcels(
localDbEnriched: 0,
localDbEnrichedComplete: 0,
localSyncFresh: false,
+ scannedAt: new Date().toISOString(),
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
};
}
@@ -248,16 +256,25 @@ export async function scanNoGeometryParcels(
legalArea?: number;
}> = [];
+ let matchedByRef = 0;
+ let matchedById = 0;
+
for (const item of allImmovables) {
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
const immPk = Number(item.immovablePk ?? 0);
const immId = normalizeId(item.immovablePk);
// Present in remote GIS layer by cadastral ref? → has geometry
- if (cadRef && remoteCadRefs.has(cadRef)) continue;
+ if (cadRef && remoteCadRefs.has(cadRef)) {
+ matchedByRef++;
+ continue;
+ }
// Present in remote GIS layer by IMMOVABLE_ID? → has geometry
- if (immId && remoteImmIds.has(immId)) continue;
+ if (immId && remoteImmIds.has(immId)) {
+ matchedById++;
+ continue;
+ }
noGeomItems.push({
immovablePk: immPk,
@@ -334,6 +351,12 @@ export async function scanNoGeometryParcels(
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
const matchedCount = allImmovables.length - noGeomItems.length;
+ console.log(
+ `[no-geom-scan] Match quality: ${matchedCount} total (${matchedByRef} by cadRef, ${matchedById} by immId)` +
+ ` | GIS layer: ${remoteFeatures.length} features | Immovables: ${allImmovables.length}` +
+ ` | Unmatched GIS: ${remoteFeatures.length - matchedCount}`,
+ );
+
// Quality analysis of no-geom items
let qWithCadRef = 0;
let qWithPaperCad = 0;
@@ -370,6 +393,8 @@ export async function scanNoGeometryParcels(
withGeometry: matchedCount,
remoteGisCount: remoteFeatures.length,
noGeomCount: noGeomItems.length,
+ matchedByRef,
+ matchedById,
qualityBreakdown: {
withCadRef: qWithCadRef,
withPaperCad: qWithPaperCad,
@@ -387,6 +412,7 @@ export async function scanNoGeometryParcels(
localDbEnriched: localEnriched,
localDbEnrichedComplete: localEnrichedComplete,
localSyncFresh: syncFresh,
+ scannedAt: new Date().toISOString(),
};
}