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) */
/* ══════════════════════════════════════════════════════════ */
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)}`,
@@ -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() {
</span>{" "}
imobile în eTerra:{" "}
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
{noGeomScan.withGeometry.toLocaleString("ro-RO")}
</span>{" "}
cu geometrie,{" "}
<span className="font-semibold text-amber-600 dark:text-amber-400">
@@ -2609,15 +2618,25 @@ export function ParcelSyncModule() {
</span>{" "}
<span className="font-medium">fără geometrie</span>
</p>
{noGeomScan.withGeometry <
noGeomScan.remoteGisCount && (
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
{noGeomScan.withGeometry.toLocaleString("ro-RO")}{" "}
din{" "}
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}{" "}
features GIS au corespondent în lista de imobile
</p>
)}
<p className="text-[10px] text-muted-foreground/70 mt-0.5">
Layer GIS:{" "}
<span className="font-medium">
{noGeomScan.remoteGisCount.toLocaleString("ro-RO")}
</span>
{" features (se descarcă toate)"}
{noGeomScan.remoteGisCount !== noGeomScan.withGeometry && (
<>
{" · "}
{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">
Cele fără geometrie există în baza de date eTerra dar
nu au contur desenat în layerul GIS.
@@ -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(),
};
}