fix: scan numbers always add up, match quality tracking, pipeline audit

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
This commit is contained in:
AI Assistant
2026-03-07 21:22:29 +02:00
parent 1e6888a32a
commit 531c3b0858
3 changed files with 96 additions and 13 deletions
+39 -1
View File
@@ -256,6 +256,8 @@ export async function POST(req: Request) {
/* Phase 1b: Import no-geometry parcels (optional) */ /* Phase 1b: Import no-geometry parcels (optional) */
/* ══════════════════════════════════════════════════════════ */ /* ══════════════════════════════════════════════════════════ */
let noGeomImported = 0; let noGeomImported = 0;
let noGeomCleaned = 0;
let noGeomSkipped = 0;
if (hasNoGeom && weights.noGeom > 0) { if (hasNoGeom && weights.noGeom > 0) {
setPhaseState("Import parcele fără geometrie", weights.noGeom, 1); setPhaseState("Import parcele fără geometrie", weights.noGeom, 1);
const noGeomClient = await EterraClient.create( const noGeomClient = await EterraClient.create(
@@ -282,6 +284,8 @@ export async function POST(req: Request) {
pushProgress(); pushProgress();
} else { } else {
noGeomImported = noGeomResult.imported; noGeomImported = noGeomResult.imported;
noGeomCleaned = noGeomResult.cleaned;
noGeomSkipped = noGeomResult.skipped;
const cleanedNote = const cleanedNote =
noGeomResult.cleaned > 0 noGeomResult.cleaned > 0
? `, ${noGeomResult.cleaned} vechi șterse` ? `, ${noGeomResult.cleaned} vechi șterse`
@@ -626,6 +630,26 @@ export async function POST(req: Request) {
siruta: validated.siruta, siruta: validated.siruta,
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
source: "local-db (sync-first)", 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: { terenuri: {
count: terenuriGeoFeatures.length, count: terenuriGeoFeatures.length,
totalInDb: dbTerenuri.length, totalInDb: dbTerenuri.length,
@@ -665,7 +689,21 @@ export async function POST(req: Request) {
` Generat: ${new Date().toISOString().replace("T", " ").slice(0, 19)}`, ` 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)}`, ` Total parcele în baza de date: ${fmt(dbTerenuri.length)}`,
` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`, ` • Cu geometrie (contur GIS): ${fmt(withGeomRecords.length)}`,
@@ -391,6 +391,8 @@ export function ParcelSyncModule() {
withGeometry: number; withGeometry: number;
remoteGisCount: number; remoteGisCount: number;
noGeomCount: number; noGeomCount: number;
matchedByRef: number;
matchedById: number;
qualityBreakdown: { qualityBreakdown: {
withCadRef: number; withCadRef: number;
withPaperCad: number; withPaperCad: number;
@@ -407,6 +409,7 @@ export function ParcelSyncModule() {
localDbEnriched: number; localDbEnriched: number;
localDbEnrichedComplete: number; localDbEnrichedComplete: number;
localSyncFresh: boolean; localSyncFresh: boolean;
scannedAt: string;
} | null>(null); } | null>(null);
const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); // siruta for which scan was done
@@ -718,6 +721,8 @@ export function ParcelSyncModule() {
withGeometry: 0, withGeometry: 0,
remoteGisCount: 0, remoteGisCount: 0,
noGeomCount: 0, noGeomCount: 0,
matchedByRef: 0,
matchedById: 0,
qualityBreakdown: emptyQuality, qualityBreakdown: emptyQuality,
localDbTotal: 0, localDbTotal: 0,
localDbWithGeom: 0, localDbWithGeom: 0,
@@ -725,6 +730,7 @@ export function ParcelSyncModule() {
localDbEnriched: 0, localDbEnriched: 0,
localDbEnrichedComplete: 0, localDbEnrichedComplete: 0,
localSyncFresh: false, localSyncFresh: false,
scannedAt: "",
}; };
try { try {
const res = await fetch("/api/eterra/no-geom-scan", { const res = await fetch("/api/eterra/no-geom-scan", {
@@ -746,6 +752,8 @@ export function ParcelSyncModule() {
withGeometry: Number(data.withGeometry ?? 0), withGeometry: Number(data.withGeometry ?? 0),
remoteGisCount: Number(data.remoteGisCount ?? 0), remoteGisCount: Number(data.remoteGisCount ?? 0),
noGeomCount: Number(data.noGeomCount ?? 0), noGeomCount: Number(data.noGeomCount ?? 0),
matchedByRef: Number(data.matchedByRef ?? 0),
matchedById: Number(data.matchedById ?? 0),
qualityBreakdown: { qualityBreakdown: {
withCadRef: Number(qb.withCadRef ?? 0), withCadRef: Number(qb.withCadRef ?? 0),
withPaperCad: Number(qb.withPaperCad ?? 0), withPaperCad: Number(qb.withPaperCad ?? 0),
@@ -762,6 +770,7 @@ export function ParcelSyncModule() {
localDbEnriched: Number(data.localDbEnriched ?? 0), localDbEnriched: Number(data.localDbEnriched ?? 0),
localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0), localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0),
localSyncFresh: Boolean(data.localSyncFresh), localSyncFresh: Boolean(data.localSyncFresh),
scannedAt: String(data.scannedAt ?? new Date().toISOString()),
}); });
} }
} catch { } catch {
@@ -2601,7 +2610,7 @@ export function ParcelSyncModule() {
</span>{" "} </span>{" "}
imobile în eTerra:{" "} imobile în eTerra:{" "}
<span className="text-emerald-600 dark:text-emerald-400 font-medium"> <span className="text-emerald-600 dark:text-emerald-400 font-medium">
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")} {noGeomScan.withGeometry.toLocaleString("ro-RO")}
</span>{" "} </span>{" "}
cu geometrie,{" "} cu geometrie,{" "}
<span className="font-semibold text-amber-600 dark:text-amber-400"> <span className="font-semibold text-amber-600 dark:text-amber-400">
@@ -2609,15 +2618,25 @@ export function ParcelSyncModule() {
</span>{" "} </span>{" "}
<span className="font-medium">fără geometrie</span> <span className="font-medium">fără geometrie</span>
</p> </p>
{noGeomScan.withGeometry < <p className="text-[10px] text-muted-foreground/70 mt-0.5">
noGeomScan.remoteGisCount && ( Layer GIS:{" "}
<p className="text-[10px] text-muted-foreground/70 mt-0.5"> <span className="font-medium">
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "} {noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
din{" "} </span>
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "} {" features (se descarcă toate)"}
features GIS au corespondent în lista de imobile {noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
</p> <>
)} {" · "}
{noGeomScan.withGeometry.toLocaleString("ro-RO")} potrivite
cu lista de imobile
{noGeomScan.matchedByRef > 0 && noGeomScan.matchedById > 0 && (
<span className="text-muted-foreground/50">
{" "}({noGeomScan.matchedByRef} cadRef + {noGeomScan.matchedById} ID)
</span>
)}
</>
)}
</p>
<p className="text-[11px] text-muted-foreground mt-0.5"> <p className="text-[11px] text-muted-foreground mt-0.5">
Cele fără geometrie există în baza de date eTerra dar Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS. nu au contur desenat în layerul GIS.
@@ -118,6 +118,9 @@ export type NoGeomScanResult = {
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer */ /** Total features in the remote ArcGIS TERENURI_ACTIVE layer */
remoteGisCount: number; remoteGisCount: number;
noGeomCount: number; noGeomCount: number;
/** Match quality: how many matched by cadastral ref vs immovable ID */
matchedByRef: number;
matchedById: number;
/** Quality breakdown of no-geometry items */ /** Quality breakdown of no-geometry items */
qualityBreakdown: NoGeomQuality; qualityBreakdown: NoGeomQuality;
/** Sample of immovable identifiers without geometry */ /** Sample of immovable identifiers without geometry */
@@ -142,6 +145,8 @@ export type NoGeomScanResult = {
localDbEnrichedComplete: number; localDbEnrichedComplete: number;
/** Whether local sync is fresh (< 7 days) */ /** Whether local sync is fresh (< 7 days) */
localSyncFresh: boolean; localSyncFresh: boolean;
/** Timestamp of the scan (for audit trail) */
scannedAt: string;
/** Error message if workspace couldn't be resolved */ /** Error message if workspace couldn't be resolved */
error?: string; error?: string;
}; };
@@ -181,6 +186,8 @@ export async function scanNoGeometryParcels(
withGeometry: 0, withGeometry: 0,
remoteGisCount: 0, remoteGisCount: 0,
noGeomCount: 0, noGeomCount: 0,
matchedByRef: 0,
matchedById: 0,
qualityBreakdown: { qualityBreakdown: {
withCadRef: 0, withCadRef: 0,
withPaperCad: 0, withPaperCad: 0,
@@ -198,6 +205,7 @@ export async function scanNoGeometryParcels(
localDbEnriched: 0, localDbEnriched: 0,
localDbEnrichedComplete: 0, localDbEnrichedComplete: 0,
localSyncFresh: false, localSyncFresh: false,
scannedAt: new Date().toISOString(),
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`, error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
}; };
} }
@@ -248,16 +256,25 @@ export async function scanNoGeometryParcels(
legalArea?: number; legalArea?: number;
}> = []; }> = [];
let matchedByRef = 0;
let matchedById = 0;
for (const item of allImmovables) { for (const item of allImmovables) {
const cadRef = normalizeCadRef(item.identifierDetails ?? ""); const cadRef = normalizeCadRef(item.identifierDetails ?? "");
const immPk = Number(item.immovablePk ?? 0); const immPk = Number(item.immovablePk ?? 0);
const immId = normalizeId(item.immovablePk); const immId = normalizeId(item.immovablePk);
// Present in remote GIS layer by cadastral ref? → has geometry // 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 // 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({ noGeomItems.push({
immovablePk: immPk, immovablePk: immPk,
@@ -334,6 +351,12 @@ export async function scanNoGeometryParcels(
// withGeometry = immovables that MATCHED a GIS feature (always adds up) // withGeometry = immovables that MATCHED a GIS feature (always adds up)
const matchedCount = allImmovables.length - noGeomItems.length; 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 // Quality analysis of no-geom items
let qWithCadRef = 0; let qWithCadRef = 0;
let qWithPaperCad = 0; let qWithPaperCad = 0;
@@ -370,6 +393,8 @@ export async function scanNoGeometryParcels(
withGeometry: matchedCount, withGeometry: matchedCount,
remoteGisCount: remoteFeatures.length, remoteGisCount: remoteFeatures.length,
noGeomCount: noGeomItems.length, noGeomCount: noGeomItems.length,
matchedByRef,
matchedById,
qualityBreakdown: { qualityBreakdown: {
withCadRef: qWithCadRef, withCadRef: qWithCadRef,
withPaperCad: qWithPaperCad, withPaperCad: qWithPaperCad,
@@ -387,6 +412,7 @@ export async function scanNoGeometryParcels(
localDbEnriched: localEnriched, localDbEnriched: localEnriched,
localDbEnrichedComplete: localEnrichedComplete, localDbEnrichedComplete: localEnrichedComplete,
localSyncFresh: syncFresh, localSyncFresh: syncFresh,
scannedAt: new Date().toISOString(),
}; };
} }