fix(parcel-sync): fix ArcGIS 1000 server cap pagination + scan improvements

- 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.
This commit is contained in:
AI Assistant
2026-03-08 02:37:39 +02:00
parent d12f01fc02
commit 041bfd4138
3 changed files with 77 additions and 14 deletions
@@ -3068,6 +3068,30 @@ export function ParcelSyncModule() {
</span> </span>
</div> </div>
{/* Include no-geom toggle (works independently of scan) */}
{session.connected && (
<label className="flex items-center gap-2 cursor-pointer select-none ml-6">
<input
type="checkbox"
checked={includeNoGeom}
onChange={(e) => setIncludeNoGeom(e.target.checked)}
disabled={
exporting ||
(!!bgJobId && bgProgress?.status === "running")
}
className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600"
/>
<span className="text-xs">
Include și parcelele fără geometrie
</span>
{noGeomScanning && (
<span className="text-[10px] text-muted-foreground">
(scanare în curs)
</span>
)}
</label>
)}
{/* Row 2: Background sync buttons */} {/* Row 2: Background sync buttons */}
{session.connected && ( {session.connected && (
<div className="grid gap-2 sm:grid-cols-2"> <div className="grid gap-2 sm:grid-cols-2">
@@ -339,6 +339,11 @@ export class EterraClient {
let offset = 0; let offset = 0;
const all: EsriFeature[] = []; const all: EsriFeature[] = [];
// ArcGIS servers have a maxRecordCount (typically 1000).
// If we request 2000 but get exactly 1000, we hit the server cap.
// Track this so we continue paginating instead of stopping.
let serverMaxRecordCount: number | null = null;
while (true) { while (true) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("f", "json"); params.set("f", "json");
@@ -375,11 +380,28 @@ export class EterraClient {
break; break;
} }
// Detect server maxRecordCount cap:
// If we asked for more than we got AND the result is a round number
// (1000, 2000), the server likely capped us. Adjust pageSize to match.
if (
serverMaxRecordCount === null &&
features.length < pageSize &&
features.length > 0 &&
features.length % 500 === 0 // round cap: 500, 1000, 1500, 2000...
) {
serverMaxRecordCount = features.length;
pageSize = serverMaxRecordCount;
// Don't break — this is a full page at server's cap, continue
}
all.push(...features); all.push(...features);
offset += features.length; offset += features.length;
if (onProgress) onProgress(all.length, total); if (onProgress) onProgress(all.length, total);
if (total && all.length >= total) break; if (total && all.length >= total) break;
if (features.length < pageSize) {
// End of data: fewer features than the effective page size
const effectivePageSize = serverMaxRecordCount ?? pageSize;
if (features.length < effectivePageSize) {
const nextSize = PAGE_SIZE_FALLBACKS.find((s) => s < pageSize); const nextSize = PAGE_SIZE_FALLBACKS.find((s) => s < pageSize);
if (total && all.length < total && nextSize) { if (total && all.length < total && nextSize) {
pageSize = nextSize; pageSize = nextSize;
@@ -223,36 +223,53 @@ export async function scanNoGeometryParcels(
// 2. Fetch remote GIS cadastral refs (lightweight — no geometry) // 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
// This is the source of truth for "has geometry" regardless of local DB state. // This is the source of truth for "has geometry" regardless of local DB state.
// ~4 pages for 8k features with outFields only = very fast. // Count first so pagination knows the total and doesn't stop early.
const terenuriLayer = { const terenuriLayer = {
id: "TERENURI_ACTIVE", id: "TERENURI_ACTIVE",
name: "TERENURI_ACTIVE", name: "TERENURI_ACTIVE",
endpoint: "aut" as const, endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1", whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
}; };
const [terenuriCount, cladiriCount] = await Promise.all([
client.countLayer(terenuriLayer, siruta).catch(() => 0),
client
.countLayer(
{
id: "CLADIRI_ACTIVE",
name: "CLADIRI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
},
siruta,
)
.catch(() => 0),
]);
const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, { const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, {
returnGeometry: false, returnGeometry: false,
outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID", outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID",
pageSize: 2000, pageSize: 1000,
total: terenuriCount > 0 ? terenuriCount : undefined,
}); });
// 2b. Also fetch CLADIRI_ACTIVE count (lightweight, just OBJECTID) // 2b. Also fetch CLADIRI_ACTIVE features (lightweight, just OBJECTID)
const cladiriLayer = { const cladiriLayer = {
id: "CLADIRI_ACTIVE", id: "CLADIRI_ACTIVE",
name: "CLADIRI_ACTIVE", name: "CLADIRI_ACTIVE",
endpoint: "aut" as const, endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1", whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
}; };
let remoteCladiriCount = 0; let remoteCladiriCount = cladiriCount;
try { if (remoteCladiriCount === 0) {
const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, { try {
returnGeometry: false, const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, {
outFields: "OBJECTID", returnGeometry: false,
pageSize: 2000, outFields: "OBJECTID",
}); pageSize: 1000,
remoteCladiriCount = cladiriFeatures.length; });
} catch { remoteCladiriCount = cladiriFeatures.length;
// Non-fatal — just won't show clădiri count } catch {
// Non-fatal — just won't show clădiri count
}
} }
const remoteCadRefs = new Set<string>(); const remoteCadRefs = new Set<string>();