feat(geoportal-v2): manual fetch flag + friendlier pool-exhausted error
Two operational gaps observed after PR3 deep-enrich rollout:
1. Raw \"Eroare: no_available_account\" surfaced when the eTerra
account pool hit its hourly quota. Replace with a plain-language
note ("Pool-ul ANCPI e temporar epuizat — încearcă peste câteva
minute"). Same friendly treatment for the other common
orchestrator errors: no_immovable_match, parcel_not_found,
eterra_fetch_failed.
2. Marius wants the auto-trigger (fires on sparse-data load) and the
explicit "Citește din ANCPI" button to be separable on the
orchestrator side. Casual map browsing burns through the 500/h
quota with auto-triggers; a working session that needs 20-30
specific parcels shouldn't be starved.
refreshFromAncpi now takes { manual?: boolean }. The button passes
manual: true → request body includes manualOverride: true. The
auto-trigger useEffect calls it with no argument (manual defaults
to false). gis-api / orchestrator can later route manualOverride
to a separate-quota bucket or skip the per-hour check entirely.
Until then the flag is harmless (orchestrator ignores unknown
fields).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -409,7 +409,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
};
|
};
|
||||||
}, [isCladiri, feature.siruta, feature.cadastralRef, basic]);
|
}, [isCladiri, feature.siruta, feature.cadastralRef, basic]);
|
||||||
|
|
||||||
const refreshFromAncpi = useCallback(async () => {
|
const refreshFromAncpi = useCallback(async (opts: { manual?: boolean } = {}) => {
|
||||||
if (!feature.siruta || !feature.cadastralRef) {
|
if (!feature.siruta || !feature.cadastralRef) {
|
||||||
setError("missing_siruta_or_cad");
|
setError("missing_siruta_or_cad");
|
||||||
return;
|
return;
|
||||||
@@ -419,9 +419,16 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
try {
|
try {
|
||||||
// PR3 deep-enrich path: gis-api orchestrates the eTerra round-trip
|
// PR3 deep-enrich path: gis-api orchestrates the eTerra round-trip
|
||||||
// and persists NR_CF / ADRESA / PROPRIETARI + tech fields in gis_core
|
// and persists NR_CF / ADRESA / PROPRIETARI + tech fields in gis_core
|
||||||
// (30-day cache; force=true bypasses). After this returns the
|
// (30-day cache; force=true bypasses cache). After this returns the
|
||||||
// central record is canonical — we re-fetch it via parcela.get or
|
// central record is canonical — we re-fetch it via parcela.get or
|
||||||
// parcela.find so the panel sees what's actually in gis_core.
|
// parcela.find so the panel sees what's actually in gis_core.
|
||||||
|
//
|
||||||
|
// manualOverride=true is set when the user explicitly pressed the
|
||||||
|
// "Citește din ANCPI" button (vs the auto-trigger that fires on
|
||||||
|
// sparse-data load). gis-api/orchestrator can treat this as a
|
||||||
|
// separate-quota bucket so casual map browsing doesn't starve a
|
||||||
|
// user who needs to fetch 20-30 specific parcels in a working
|
||||||
|
// session. Until orchestrator supports it the flag is ignored.
|
||||||
const enrichResp = await fetch("/api/gis/parcel/enrich", {
|
const enrichResp = await fetch("/api/gis/parcel/enrich", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -429,6 +436,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
siruta: feature.siruta,
|
siruta: feature.siruta,
|
||||||
cadastralRef: feature.cadastralRef,
|
cadastralRef: feature.cadastralRef,
|
||||||
force: true,
|
force: true,
|
||||||
|
...(opts.manual ? { manualOverride: true } : {}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!enrichResp.ok) {
|
if (!enrichResp.ok) {
|
||||||
@@ -658,7 +666,17 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
{error && error !== "forbidden" && (
|
{error && error !== "forbidden" && (
|
||||||
<div className="m-3 flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-2 text-xs text-destructive">
|
<div className="m-3 flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 p-2 text-xs text-destructive">
|
||||||
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
<span>Eroare: {error}</span>
|
<span>
|
||||||
|
{error === "no_available_account"
|
||||||
|
? "Pool-ul ANCPI e temporar epuizat — încearcă din nou peste câteva minute (cota orară se resetează automat)."
|
||||||
|
: error === "no_immovable_match"
|
||||||
|
? "Parcela nu există în baza eTerra (cadref + SIRUTA nu se potrivesc)."
|
||||||
|
: error === "parcel_not_found"
|
||||||
|
? "Parcela nu există în baza centrală gis_core."
|
||||||
|
: error === "eterra_fetch_failed"
|
||||||
|
? "eTerra ANCPI nu răspunde momentan. Reîncearcă în 1-2 minute."
|
||||||
|
: `Eroare: ${error}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -920,7 +938,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-2">
|
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={refreshFromAncpi}
|
onClick={() => refreshFromAncpi({ manual: true })}
|
||||||
disabled={refreshing || basic}
|
disabled={refreshing || basic}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors",
|
"inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors",
|
||||||
|
|||||||
Reference in New Issue
Block a user