From 87f9d72e4f2dc9b57407595ae799e850a5895e49 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Tue, 19 May 2026 19:52:00 +0300 Subject: [PATCH] feat(geoportal-v2): auto-fetch enrichment when DB only has tech keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parcel 328607 in Cluj-Napoca (and many others) is in gis_core with only 10 enrichment keys, all tech-level (PARCEL_HAS_LANDBOOK, PARCEL_IS_CONDOMINIUM, etc.) — no NR_CF, no ADRESA, no PROPRIETARI. The panel renders correctly but with nothing of substance shown. User had to manually click "Citește din ANCPI" to backfill. Now auto-fires the fetch when: - Panel mounts a fresh detail (or after parcel switch) - AND enrichment lacks ALL three of NR_CF / ADRESA / PROPRIETARI - AND we haven't already auto-fetched for this parcel this tab session (sessionStorage dedupe keyed by uuid OR siruta+cad+layerId) Visible feedback while it runs: a quiet "Se preiau date suplimentare din ANCPI…" strip below the loading area. The user can keep reading whatever is already on screen. Side fixes: - refreshFromAncpi → useCallback (stable deps) so it can sit in the auto-trigger useEffect's dep array without infinite loops. - Refresh path now uses /api/gis/parcela/find as the fallback when no uuid is available, matching the initial-load logic. The old orchestrator-shape projection still exists as a last-resort fallback but is rarely hit. Note: orchestrator's parcel/tech only re-populates PARCEL_* tech fields. Truly enriching rich PII (NR_CF/ADRESA/PROPRIETARI) needs the "deep enrich" orchestrator path which the gis-api proxy contract doesn't expose yet — separate gis-api task. So parcels that only ever got tech-level enrichment will stay at tech-level even after this auto-fetch. The visible improvement is: parcels that DO have rich data load it in the first second instead of needing a manual click. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../geoportal/v2/feature-info-panel.tsx | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/modules/geoportal/v2/feature-info-panel.tsx b/src/modules/geoportal/v2/feature-info-panel.tsx index cf67760..ef4ce0a 100644 --- a/src/modules/geoportal/v2/feature-info-panel.tsx +++ b/src/modules/geoportal/v2/feature-info-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { signIn } from "next-auth/react"; import { X, RefreshCw, Loader2, FileText, Download, AlertCircle, @@ -409,7 +409,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { }; }, [isCladiri, feature.siruta, feature.cadastralRef, basic]); - const refreshFromAncpi = async () => { + const refreshFromAncpi = useCallback(async () => { if (!feature.siruta || !feature.cadastralRef) { setError("missing_siruta_or_cad"); return; @@ -432,15 +432,26 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { return; } const techData = await res.json().catch(() => null); - // Refresh stored detail via parcela.get if uuid known. - if (detail?.id || feature.id) { - const id = detail?.id ?? feature.id; - const updated = await fetch( - `/api/gis/parcela/${encodeURIComponent(id)}`, + // After orchestrator updates the central DB, re-fetch via the + // server-side find/get path so we land on the canonical shape + // (and pick up rich enrichment that the tech response itself + // doesn't carry). + const id = detail?.id ?? feature.id; + let updated: Response | null = null; + if (id) { + updated = await fetch(`/api/gis/parcela/${encodeURIComponent(id)}`); + } else { + updated = await fetch( + `/api/gis/parcela/find?siruta=${encodeURIComponent(feature.siruta)}` + + `&cad=${encodeURIComponent(feature.cadastralRef)}` + + `&layerId=${encodeURIComponent(feature.layerId)}`, ); - if (updated.ok) setDetail(await updated.json()); + } + if (updated.ok) { + setDetail(await updated.json()); } else if (techData) { - // Project the orchestrator response into the panel directly. + // Final fallback — project the orchestrator response if we + // can't re-fetch the canonical record. const inner = (techData?.data as Record | undefined) ?? techData; setDetail({ @@ -456,7 +467,44 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { } finally { setRefreshing(false); } - }; + }, [ + feature.id, + feature.siruta, + feature.cadastralRef, + feature.layerId, + feature.areaValue, + detail?.id, + ]); + + // Auto-fetch from ANCPI when the central DB has only tech-level keys + // (no NR_CF / ADRESA / PROPRIETARI). The orchestrator caches its results + // for 30 days so repeat clicks on the same parcel don't re-spend quota. + // Dedupe per parcel-key via sessionStorage so a remount or React + // strict-mode double-fire doesn't double the call. + useEffect(() => { + if (basic || !detail || refreshing) return; + const e = (detail.enrichment ?? {}) as Record; + const richPresent = Boolean(e.NR_CF || e.ADRESA || e.PROPRIETARI); + if (richPresent) return; + + const parcelKey = + String(detail.id ?? "") || + `${feature.siruta}:${feature.cadastralRef}:${feature.layerId}`; + const ssKey = `gis_auto_enrich_${parcelKey}`; + if (typeof sessionStorage === "undefined") return; + if (sessionStorage.getItem(ssKey)) return; + sessionStorage.setItem(ssKey, "1"); + + void refreshFromAncpi(); + }, [ + basic, + detail, + refreshing, + feature.siruta, + feature.cadastralRef, + feature.layerId, + refreshFromAncpi, + ]); const exportGpkg = () => { const url = `https://eterra.live/harta?siruta=${encodeURIComponent( @@ -585,6 +633,14 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) { )} + {/* Auto-enrich is running in background (sparse DB record → fetched from ANCPI) */} + {refreshing && !loading && ( +
+ + Se preiau date suplimentare din ANCPI… +
+ )} + {/* Forbidden after a silent re-grant attempt already failed once — likely Authentik scope misconfig. Stays discreet (no call-to-action), only diag info for the operator. */}