- DEFAULT_PAGE_SIZE: 2000 -> 1000 (matches eTerra maxRecordCount, avoids
requesting more than the server supports on first try)
- PAGE_SIZE_FALLBACKS: [500, 200] (removed 1000 since it's now the default)
- Add retry-once logic for 'Error performing query operation':
Wait 2s and retry same page before falling to smaller sizes.
These errors are often transient server-side timeouts.
- Longer delay (1s vs 0.5s) between page size fallback attempts
Fixes Feleacu (7951 features) background sync failure.
- eterra-client: detect server maxRecordCount cap in fetchAllLayerByWhere
When server returns exactly 1000 (or other round cap) but we asked for 2000,
recognize this as a server limit, adjust pageSize, and CONTINUE paginating.
Previously: 1000 < 2000 -> break (lost all data beyond page 1).
- no-geom-sync: count layers first, pass total to fetchAllLayer
Belt-and-suspenders: even if cap detection misses, known total prevents
early termination. Also use pageSize 1000 to match typical server cap.
Clădiri count uses countLayer instead of fetching all OBJECTIDs.
- UI: add include-no-geom checkbox in background sync section
Users can toggle it independently of scan status.
Shows '(scanare in curs)' hint when scan is still running.
- Server: Promise.race with 120s timeout on no-geom-scan API route
- Client: AbortController with 120s timeout on scan fetch
- UI: show 'max 2 min' during scanning + hint that buttons work without scan
- UI: timeout state shows retry button + explains no-geom won't be available
- Prevents indefinitely stuck 'Se scanează...' on slow eTerra responses
- New POST /api/eterra/sync-background: fire-and-forget server-side processing
Starts sync + optional enrichment in background, returns 202 immediately.
Progress tracked via existing /api/eterra/progress polling.
Work continues in Node.js event loop even if browser is closed.
Progress persists 1 hour for background jobs (vs 60s for normal).
- Enhanced POST /api/eterra/export-local: base/magic mode support
mode=base: ZIP with terenuri.gpkg + cladiri.gpkg from local DB
mode=magic: adds terenuri_magic.gpkg (enrichment merged, includes no-geom),
terenuri_complet.csv, raport_calitate.txt, export_report.json
All from PostgreSQL — zero eTerra API calls, instant download.
- UI: background sync section in Export tab
'Sync fundal Baza/Magic' buttons: start background processing
'Descarc─â din DB Baza/Magic' buttons: instant download from local DB
Background job progress card with indigo theme (distinct from export)
localStorage job recovery: resume polling after page refresh
'Descarc─â din DB' button shown on completion
- fetchImmovableParcelDetails called FIRST (1 call, no applicationId needed)
- app-based fetchParcelFolosinte only as fallback when direct returns nothing
- SOLICITANT skipped entirely (was always '-' for old CF records)
- Remove unused pickApplication helper
- Net savings: ~500+ API calls per UAT enrichment (50-65% reduction)
- copycf/get returns same data as list (no enrichment value, kept as utility)
QUALITY GATE TIGHTENED:
No-geometry import now requires hasLandbook=1 (imobil electronic).
This filters out immovables without carte funciara — they have no
CF data, no owners, no parcel details to extract. For Cosbuc this
reduces useful no-geom from ~1916 to ~468 (only IEs with real data).
Three-tier quality gate:
1. Active (status=1)
2. Has landbook (hasLandbook=1) — is electronic immovable [NEW]
3. Has identification (cadRef/paperLbNo/paperCadNo) OR area
CLEANUP also updated: DB cleanup now removes stale no-geom records
that don't pass the tightened gate (existing non-IE records will be
cleaned on next import run).
NEW API METHODS (eterra-client):
- checkIfIsIE(adminUnitId, paperCadNo, topNo, paperCfNo) → boolean
Calls /api/immovable/checkIfIsIE — verifies IE status per-parcel
Available for future per-item verification if needed
- getCfExtractUrl(immovablePk, workspaceId) → string
Returns URL for /api/cf/landbook/copycf/get/{pk}/{ws}/0/true
Downloads the CF extract as PDF blob (future enrichment)
UI updated: 'Filtrate' label now says 'fara CF/inactive/fara date'
to reflect the new hasLandbook filter.
PROBLEM:
For no-geometry parcels (and many geometry parcels without application
IDs), CATEGORIE_FOLOSINTA was always '-' because:
1. fetchImmAppsByImmovable returned no apps (no applicationId)
2. Without appId, fetchParcelFolosinte was skipped entirely
3. No fallback existed
DISCOVERY (from eTerra UI investigation):
The endpoint /api/immovable/details/parcels/list/{wp}/{pk}/{page}/{size}
returns parcel use categories DIRECTLY — no applicationId needed.
Example: [{useCategory:'arabil', intravilan:'Necunoscut', parcelPk:17753903}]
FIX:
- After the app-based CATEGORIE_FOLOSINTA attempt, if result is still '-',
fall back to fetchImmovableParcelDetails (the direct endpoint)
- formatCategories now handles both API formats:
- App-based: categorieFolosinta + suprafata fields
- Direct: useCategory field (no area — shows category name only)
- When direct endpoint provides area=0, format shows just the category
name without ':0' (e.g. 'arabil; faneata' instead of 'arabil:0; faneata:0')
- Also picks up intravilan from direct endpoint if app-based was empty
- Fixed fetchImmovableParcelDetails default size: 1 → 20 (one immovable
can have multiple parcels, e.g. IE 25332 has 2: arabil + faneata)
- Results are cached in folCache to avoid duplicate requests
LAYER FETCH:
- fetchAllLayerByWhere now falls back through 2000 → 1000 → 500 → 200
instead of just 2000 → 1000 before giving up
- 500ms delay between fallback attempts to let eTerra recover
- Error message now includes the original cause:
'Failed to fetch layer TERENURI_ACTIVE: Session expired (401)'
instead of just 'Failed to fetch layer TERENURI_ACTIVE'
DISPLAY:
- 505 terenuri count no longer green (was emerald-600, now neutral semibold)
It's just a data value, not a status indicator
ROOT CAUSE: The cross-reference between immovable list and GIS layer
produces wildly different matchedCount on each scan (320, 430, 629, 433)
because the eTerra immovable/list API with inscrisCF=-1 returns
inconsistent results across calls. The GIS layer count (505) is stable.
SCAN DISPLAY — now uses only stable numbers:
- Header shows 'Layer GIS: 505 terenuri + X cladiri' (stable ArcGIS count)
- Shows 'Lista imobile: 2.717 (estimat ~2.212 fara geometrie)' using
simple subtraction totalImmovables - remoteGisCount
- Cross-ref matchedCount kept internally for import logic, but NOT shown
as the primary number — eliminates visual instability
- hasNoGeomParcels now uses estimated count (stable)
WORKFLOW PREVIEW — now accurate:
- Step 1: 'Sync GIS — descarca 505 terenuri + X cladiri' (separate counts)
or 'skip (date proaspete in DB)' when fresh
- Step 2 (enrichment): Fixed 'deja imbogatite' bug when DB is empty.
Now correctly computes what WILL be in DB after sync completes:
geoAfterSync + noGeomAfterImport - localDbEnrichedComplete
- Steps 3-4 unchanged
CLADIRI COUNT:
- Scan now also fetches CLADIRI_ACTIVE layer count (lightweight, OBJECTID only)
- New field remoteCladiriCount in NoGeomScanResult
- Displayed in header and workflow step 1
- Non-fatal: if CLADIRI fetch fails, just shows 0
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
- UI: scan card now shows remoteGisCount instead of matchedCount (withGeometry)
as the primary 'cu geometrie' number — this is the true GIS layer feature count
- UI: workflow preview step 1 shows remoteGisCount for download count
- UI: mismatch note reworded as secondary detail about cross-reference matching
- Import: automatic cleanup step at start of syncNoGeometryParcels
- Builds valid immovablePk set from fresh list (active + identification/area)
- Deletes stale NO_GEOMETRY records not in the valid set
- Reports cleaned count in result + progress note
- NoGeomSyncResult type: added 'cleaned' field
- Gitignore: temp-db-check.cjs
BUGS FIXED:
- paperCfNo does NOT exist in eTerra API — field is paperLbNo
Renamed withPaperCf → withPaperLb everywhere (type, scan, UI)
- Area fields: only measuredArea and legalArea exist on immovable/list
Removed phantom area/areaValue/suprafata checks from import filter
FILTERING TIGHTENED:
- Quality gate now requires status=1 (active) in eTerra
- Items with status≠1 are filtered out before import
- Quality breakdown adds: withActiveStatus, withLandbook counters
- Import attributes now store MEASURED_AREA, LEGAL_AREA, HAS_LANDBOOK
- workspace.nomenPk used instead of workspacePk for accuracy
ENRICHMENT ROBUSTNESS:
- Area fallback: when AREA_VALUE is missing (no-geom), enrichment
now falls back to listItem.measuredArea/legalArea from immovable list
- Post-enrichment verification: logs 100% coverage or warns about gaps
- EnrichResult type extended with totalFeatures + unenrichedCount
UI UPDATES:
- Quality grid shows 6 stats: cadRef, CF/LB, paperCad, area, active, landbook
- Filter explanation updated: 'inactive sau fără date' instead of old text
- Magic GPKG (terenuri_magic.gpkg) now contains ALL records:
rows with geometry render as polygons, rows without have null geom
but still carry all attribute/enrichment data (QGIS shows them fine)
- Added HAS_GEOMETRY column to Magic GPKG fields (0 or 1)
- GPKG builder now supports includeNullGeometry option: splits features
into spatial-first (creates table), then appends null-geom rows
- Base terenuri.gpkg / cladiri.gpkg unchanged (spatial only)
- CSV still has all records as before
- GeoJsonFeature type now allows null geometry
- Reproject: null geometry guard added
- UI text updated: no longer says 'Nu apar in GPKG'
- NoGeomScanResult now includes: localDbTotal, localDbWithGeom, localDbNoGeom,
localDbEnriched, localSyncFresh (parallel DB queries, fast)
- Scan card shows 'Baza de date locala: X cu geometrie + Y fara + Z imbogatite'
- Workflow preview shows numbered steps with smart estimates:
step 1 shows 'skip (date proaspete)' when sync is fresh
step 2 shows '~N noi de importat' or 'deja importate' for no-geom
step 3 shows '~N de procesat (~M min)' or 'deja imbogatite' for enrichment
- All-geometry card also shows local DB summary
- User can see exactly what will happen before pressing Magic
- scanNoGeometryParcels now fetches TERENURI_ACTIVE features from remote
ArcGIS (lightweight, no geometry) to cross-reference with eTerra immovable list
- Cross-references by both NATIONAL_CADASTRAL_REFERENCE and IMMOVABLE_ID
- Works correctly regardless of whether user has synced to local DB
- Renamed totalInDb -> withGeometry in NoGeomScanResult, UI, and API
- Extended fetchAllLayer() to forward outFields/returnGeometry options
- resolveWorkspacePk chain: explicit param -> GisUat DB -> ArcGIS layer query
- UI passes workspacePk from UAT selection to scan API
- Fixes: FELEACU (Cluj, workspace!=65) returning 0 immovables
- Better messaging: shows X total, Y with geometry, Z without
- Shows warning when 0 immovables found (workspace resolution failed)
- handleNoGeomScan accepts optional targetSiruta parameter
- useRef tracks last auto-scanned siruta to prevent duplicate scans
- Show zero result on error instead of hiding card (null)
- Fixes: FELEACU scan disappearing after 2s while COSBUC worked
- Auto-scan triggers when UAT selected + connected (no manual click needed)
- Three states: scanning spinner, found N parcels (amber alert card), all OK (green check)
- Checkbox more prominent: only shown when no-geom parcels exist
- Re-scan button available, scan result cached per siruta
- AlertTriangle icon for visual warning
- Add geometrySource field to GisFeature (NO_GEOMETRY marker)
- New no-geom-sync service: scan + import parcels missing from GIS layer
- Uses negative immovablePk as objectId to avoid @@unique collision
- New /api/eterra/no-geom-scan endpoint for counting
- Export-bundle: includeNoGeometry flag, imports before enrich
- CSV export: new HAS_GEOMETRY column (0/1)
- GPKG: still geometry-only (unchanged)
- UI: checkbox + scan button on Export tab
- Baza de Date tab: shows no-geometry counts per UAT
- db-summary API: includes noGeomCount per layer
2 bugs:
1. After Magic/base download completes, progress bar stayed stuck at
77% because exportProgress was never updated to 'done' client-side.
Fix: set progress to 'Finalizat' + 100% after successful blob download.
2. syncLayer overwrote the export route's weighted percentages (0-100)
with raw feature counts (50/200), causing progress bar to jump.
Fix: when isSubStep=true, sync writes phase/note/phaseCurrent/phaseTotal
but preserves the parent route's downloaded/total weighted values.
3 bugs fixed:
- syncLayer was called without jobId -> user saw no progress duringSync
- syncLayer set status:'done' prematurely -> client stopped polling before GPKG phase
- syncLayer errors were silently ignored -> confusing 'no features in DB' error
Added isSubStep option to syncLayer: when true, keeps status as 'running'
and doesn't schedule clearProgress. Export routes now pass jobId + isSubStep
so the real sync progress (Descărcare features 50/200) is visible in the UI.
- Rewrite export-bundle to sync-first: check freshness -> sync layers -> enrich (magic) -> build GPKG/CSV from local DB
- Rewrite export-layer-gpkg to sync-first: sync if stale -> export from DB
- Create enrich-service.ts: extracted magic enrichment logic (CF, owners, addresses) with DB storage
- Add enrichment + enrichedAt columns to GisFeature schema
- Update PostGIS views to include enrichment data
- UI: update button labels for sync-first semantics, refresh sync status after exports
- Smart caching: skip sync if data is fresh (168h / 1 week default)
Layer catalog now has 3 actions per layer:
- Sync: downloads from eTerra, stores in PostgreSQL (GisFeature table),
incremental — only new OBJECTIDs fetched, removed ones deleted
- GPKG: direct download from eTerra (existing behavior)
- Local export: generates GPKG from local DB (no eTerra needed)
New features:
- /api/eterra/export-local endpoint — builds GPKG from DB, ZIP for multi-layer
- /api/eterra/sync now uses session-based auth (no credentials in request)
- Category headers show both remote + local feature counts
- Each layer shows local DB count (violet badge) + last sync timestamp
- 'Export local' button in action bar when any layer has local data
- Sync progress message with auto-dismiss
DB schema already had GisFeature + GisSyncRun tables from prior work.
- CSV export: all fields properly quoted to prevent column misalignment
when values contain commas (e.g. nrTopo with multiple topo numbers)
- Layer catalog: 'Numara toate' button fetches feature count per layer
via /api/eterra/layers/summary (now supports session auth)
- Feature counts displayed as badges on each layer and category total
- 'Drumul de azi' section: persists today's layer counts in localStorage
grouped by SIRUTA with timestamps
- Always build from structured fields first (street, postalNo, building, locality)
- Fall back to addressDescription ONLY when no structured fields exist
- Support multiple addresses per immovable (joined with |)
- Deduplicate identical addresses
- Handle addressDescription as last-resort fallback
- Address: use street.dictionaryItem.name (Strada/Alee/etc) + street.name,
postalNo as house number, buildingEntryNo/FloorNo/UnitNo/SectionNo
for apartment details, locality.name, county.name
- Area+intravilan: fetch from /api/immovable/details/parcels/list (direct
endpoint with area, intravilan, useCategory) before trying immApps
- Owners: remove strikethrough, use smaller neutral font (text-[11px]
text-muted-foreground/80), rename label to 'Proprietari anteriori'
- Area: use measuredArea/legalArea from immovable list and documentation
(actual fields from eTerra API, not area/areaValue which don't exist)
- Owners: detect radiated via nodeStatus === -1 on ancestor I (inscription)
nodes. Walk up parentId tree from P (person) I A C.
nodeStatus: -1=radiated, 0=active, 2=pending
- Remove debug logging (data structure now understood)
- Address: handle street/locality/county as objects (extract .name)
Fixes 'Str. [object Object], Feleacu' 'Str. X, Feleacu'
- Suprafata: fallback to total area from folosinte endpoint when
immovable list and documentation APIs return null
- Owners: use tree traversal (nodeId/parentNodeId) to detect radiated
inscriptions. Walk up parent chain to check radiationDate/cancelled/
isActive/closed/status on ancestor inscription nodes.
- Enhanced logging: first/last 3 partTwoRegs entries + node types
for debugging owner structure in Dozzle
- Proprietari split into proprietariActuali + proprietariVechi (radiati)
based on cancelled/isActive/radiat/status/radiationDate fields
- UI shows owners separated: actuali bold, vechi strikethrough
- CSV export has separate PROPRIETARI_ACTUALI / PROPRIETARI_VECHI columns
- Address: use addressDescription directly when present (>3 chars)
- Add county to address fallback
- Try area/areaValue/areaMP/suprafata fields for surface
- Debug logging: log immovable item keys + partTwoRegs sample on first search
- resolveWorkspace: use listLayer() instead of listLayerByWhere() with
hardcoded field names. Auto-discovers admin field (ADMIN_UNIT_ID/SIRUTA)
from ArcGIS layer metadata via buildWhere().
- resolveWorkspace: persist WORKSPACE_ID to DB on first resolution for
fast subsequent lookups.
- UATs POST: seed from uat.json (correct SIRUTA codes) instead of eTerra
nomenclature API (nomenPk != SIRUTA, county nomenPk != WORKSPACE_ID).
- Remove eTerra nomenclature dependency from UATs endpoint.
- Fix activeJobs Set iteration error on container restart.
- Remove unused enrichedUatsFetched ref.
- Add fetchNomenByPk() to EterraClient single nomen entry lookup
- resolveWorkspace() now tries fast path first: direct nomen lookup for
SIRUTA walk parentNomenPk chain to find COUNTY (1-3 calls vs 42+)
- Falls back to full county scan only if direct lookup fails
- Search route: DB lookup as middle fallback between workspacePk and resolve
- Debug logging to trace workspace resolution on production
- Fix: try all possible UAT identifier fields (nomenPk, siruta, code, pk)
- GisUat table now includes workspacePk column (created via raw SQL)
- GET /api/eterra/uats serves from PostgreSQL instant, no eTerra login needed
- POST /api/eterra/uats triggers sync check: compares county count with DB,
only does full eTerra fetch if data differs or DB is empty
- Frontend loads UATs from DB on mount (fast), falls back to uat.json if empty
- On eTerra connect, fires POST to sync-check; if data changed, reloads from DB
- Workspace cache populated from DB on GET for search route performance
- New /api/eterra/uats endpoint fetches all counties + UATs from eTerra,
caches server-side for 1 hour, returns enriched data with county name
and workspacePk for each UAT
- When eTerra is connected, auto-fetches enriched UAT list (replaces
static uat.json fallback) shows 'FELEACU (57582), CLUJ' format
- UAT autocomplete now searches both UAT name and county name
- Selected UAT stores workspacePk in state, passes it directly to
/api/eterra/search eliminates slow per-search county resolution
- Search route accepts optional workspacePk, falls back to resolveWorkspace()
- Dropdown shows UAT name, SIRUTA code, and county prominently
- Increased autocomplete results from 8 to 12 items
Search tab now uses eTerra application API (same as the web UI):
- POST /api/eterra/search queries /api/immovable/list with exact
identifierDetails filter + /api/documentation/data for full details
- Returns: nr cad, nr CF, CF vechi, nr topo, suprafata, intravilan,
categorii folosinta, adresa, proprietari, solicitant
- Automatic workspace (county) resolution from SIRUTA with cache
- Support for multiple cadastral numbers (comma separated)
UI changes:
- Detail cards instead of flat ArcGIS feature table
- Copy details to clipboard button per parcel
- Add parcels to list + CSV export
- Search list with summary table + CSV download
- No more layer filter or pagination (not needed for app API)
New EterraClient methods:
- searchImmovableByIdentifier (exact cadaster lookup)
- fetchCounties / fetchAdminUnitsByCounty (workspace resolution)
- Add /api/eterra/search queries eTerra ArcGIS REST API directly
by NATIONAL_CADASTRAL_REFERENCE, NATIONAL_CADNR, or INSPIRE_ID
across TERENURI_ACTIVE + CLADIRI_ACTIVE layers
- Search tab now queries eTerra live (not local DB) with 600ms debounce
- Requires session connected + UAT selected to search
- Updated placeholder and empty state messages in Romanian
- Add session-store.ts: global singleton for shared eTerra session state
with job tracking (registerJob/unregisterJob/getRunningJobs)
- Add GET/POST /api/eterra/session: connect/disconnect with job-running guard
- Export routes: credential fallback chain (body > session > env vars),
register/unregister active jobs for disconnect protection
- Login route: also creates server-side session
- ConnectionPill: session-aware display with job count, no credential form
- Auto-connect: triggers on first UAT keystroke via autoConnectAttempted ref
- Session polling: all clients poll GET /api/eterra/session every 30s
- Multi-client: any browser sees shared connection state