Two issues from Marius's cladiri screenshots:
1. APARTAMENTE "se încarcă…" sat for ~10s then vanished
— useEffect fired for every CLADIRI click regardless of whether
the building was actually a condo. Orchestrator's
/api/v1/building/condo-owners hit eTerra live, got back an empty
list for non-condos, returned [], section auto-hid → user saw the
spinner blink and disappear.
New gate: useEffect waits for `detail` to land, then reads
IS_CONDOMINIUM / PARCEL_IS_CONDOMINIUM from enrichment. If neither
is `1`, skip the fetch entirely. Non-condos no longer pay the 10s
eTerra round-trip just to show nothing.
2. EMPTY CONDO LISTS WERE HIDING SILENTLY
— for buildings flagged condo where ANCPI hasn't registered units
yet, the section would still vanish (`condoOwners.length > 0`
check). Now: if the fetch returns [] AND the building is a
condo, render the section with "Fără apartamente înregistrate la
ANCPI." That's the truthful UX. Same fallback when the fetch
errors — treat as empty rather than swallow.
Render trigger flipped from
(condoLoading || (condoOwners && condoOwners.length > 0))
to
(condoLoading || condoOwners != null)
so the section shows whenever the gate decided to fetch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Orchestrator team shipped CLADIRI support in
/api/v1/parcel/enrich (PR4):
- DeepEnrichLayerId enum + optional layerId in input
- inferLayerIdFromCadref() regex /-C\d+(-U\d+)?$/ — covers apartments too
- loadFeature() now layer-aware (no more hardcoded
layerId='TERENURI_ACTIVE')
- fetchBuildingsForParcel skipped when running for a building
- HTTP layer accepts layerId in body
Confirmed live via gis-api container logs — manual-override calls
on "304629-C2" / "304629-C3" (CLADIRI) reach orchestrator and complete.
Architots side:
1. Re-enabled the "Actualizează" / "Încarcă din ANCPI" button for
cladiri. The disabled+tooltip gate I added last commit was the
right thing while the orchestrator path didn't accept buildings —
no longer needed.
2. refreshFromAncpi now forwards feature.layerId in the request body.
Skips the orchestrator's dash-suffix auto-detect — more reliable
than parsing "-C3" out of cadref each time.
3. ParcelRefBody type gained optional manualOverride + layerId fields
so callers can pass both through the thin client.
Note from orchestrator team: extractEnrichment populates only the
generic NR_CF / ADRESA / PROPRIETARI / PROPRIETARI_VECHI / SOLICITANT
keys today. The CLADIRE_TYPE / CLADIRE_DESTINATIE / CLADIRE_OBSERVATII
/ CLADIRE_NIVELURI etc. fields the V2 building panel renders come
from a different orchestrator pipeline (building-tech), already wired
+ populating those rows when the orchestrator's bulk-enrich runs.
Single-parcel deep-enrich for a building updates NR_CF/ADRESA/
PROPRIETARI but leaves CLADIRE_* alone unless that pipeline runs in
parallel — separate iteration if needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marius hit "Parcela nu există în baza centrală gis_core." on building
304629-C3 (siruta=54975) — feature exists in gis_core (verified, id
70fd7485-fa39-4c38-9074-cfad154ed288) but orchestrator's
parcel-deep-enrich hardcodes layerId='TERENURI_ACTIVE' in its
pre-flight lookup:
// gis-sync-orchestrator src/lib/parcel-deep-enrich/fetch.ts L232–240
SELECT id, attributes, enrichment, "enrichedAt"
FROM gis_core."GisFeature"
WHERE "layerId" = 'TERENURI_ACTIVE'
AND siruta = $1 AND "cadastralRef" = $2
LIMIT 1
→ throws ParcelNotFoundError for any CLADIRI cadref. Architots
mapped that to a misleading "doesn't exist" message even though
by-ref correctly retrieved the building row up-front.
Architots-side fix (the proper fix is on orchestrator):
1. Error message for `parcel_not_found` now branches on
feature.layerId. For CLADIRI_ACTIVE the user sees:
"Deep-enrich nu suportă încă construcțiile — datele clădirii
vin via parcela părinte (gis-api orchestrator side)."
No more "parcela nu există" confusion when the parcel obviously
does.
2. "Actualizează" / "Încarcă din ANCPI" button in the Date eTerra
header is now disabled when isCladiri. Tooltip explains why.
The button is the one that triggers refreshFromAncpi →
/parcel/enrich → the very orchestrator path that doesn't handle
buildings.
Result: building panels show whatever's in gis_core (CLADIRE_TYPE,
CLADIRE_DESTINATIE, etc. if previously enriched via eterra.live's own
flow) and don't pretend they can be re-fetched.
Next step (orchestrator session): teach loadFeature() to accept an
optional layerId param OR auto-detect from cadref-suffix pattern
(/-C\\d+$/) so /parcel/enrich works for both layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iteration on the info panel per Marius's feedback.
1. UAT NAME IN HEADER
New uat-lookup.ts hook loads public/uat.json (3,186 rows, ~95 KB,
one-shot fetch + Map cache + subscribers) and exposes
useUatName(siruta). Header reads:
Terenuri · 2.400 m² · FELEACU · 57582
instead of just "SIRUTA 57582". The localitate name lives in front
of the bare siruta number (muted, smaller weight) — siruta is
still there for ops + tooltip, just not the primary signal.
2. SOLICITANT MOVED INTO ÎNSCRIERE
Was rendered as a prominent User-icon line right above PROPRIETARI,
which led to "BOJAN ELENA = current owner?" confusion. The two
fields semantically differ: SOLICITANT is the person who filed the
most recent ANCPI application (e.g. the new buyer initiating a
transfer), PROPRIETARI is who's currently registered as owner. Now
SOLICITANT is collapsed into the existing Înscriere <details> next
to TIP_INSCRIERE / DATA_CERERE / ACT_PROPRIETATE — the
registration-metadata bucket where it belongs.
3. GOOGLE MAPS INLINE WITH ADDRESS
When ADRESA exists, the Google Maps text-link sits right of the
address (using feature.lat/lng for the query). One-tap go-to-map
without a separate Localizare section.
4. LOCALIZARE → COLLAPSIBLE
Bottom Localizare card becomes a closed-by-default <details>.
Inside: WGS84 lat/lng, SIRUTA, and a separate Google Maps link.
ID (objectId) shows in the summary line. Mirrors eterra.live's
approach. The redundant Feleacu/coords echo at the bottom is
gone — coords are still one click away when needed.
NOT in this commit (parked for follow-up):
- PIZ / Plan situație / Coord. / DXF actions — would mean porting
eterra.live's three /api/geoportal/{piz,pad,coords-xlsx} document
generators. Substantial work (mapbox-static-image render +
server-side PDF layout); needs its own session.
- CF intern (gratuit) vs Extras CF (1 credit) split — current
"Comandă CF" modal already handles both pool/connection states,
but the two-button visual split mirroring eterra.live's catalog-
hit fast path is a smaller follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marius hit "Date ne-încărcate" / "Parcela nu există" on Feleacu parcels
(SIRUTA 57582, cadref 61745 / 61746) even though gis_core has 28 rich
enrichment keys for them. Root cause: 232 features in gis_core share
cadref `61745` across different UATs. Our find proxy was doing:
1. gisApi.search(cad, limit=20)
2. for each candidate (up to 20): parcela.get(id), check siruta
Feleacu's parcel sat past position 20 in the search ranking, so we
never tried parcela.get on it — fallback returned a sibling parcel
with 0 keys (the "Date ne-încărcate" UI) or no readable candidate at
all (the "nu există în DB centrală" 404 UI).
This was wrong on two counts:
1. WE WERE DOING N+1: gis-api's /api/v1/search already returns siruta
per feature (see gis-api src/routes/search.ts:41). One round-trip
would have given us the answer; we just weren't reading the field.
Updated src/lib/gis-api-client.ts to declare siruta in the
response type + bumped default limit from 20 → 50 (gis-api's
server-side cap).
2. WE WERE FAILING SILENTLY: when search-cap was the actual bottleneck
the proxy returned 404 with no hint that gis-api had more
data we just couldn't reach. New find proxy:
- First pass: direct match on cadref + layerId + siruta from the
search response. Single follow-up parcela.get to fetch full
detail. No more sequential probing.
- If no direct match: log + report distinctively. When the search
returned MAX_LIMIT (50) features all with the same cadref, we
return 422 search_limit_exceeded with a hint about the missing
siruta filter. Otherwise 404 (genuinely not in gis_core).
3. Panel surfaces the 422 with a plain-language explanation rather
than the raw "Eroare: ..." dump.
For the long-term fix: gis-api needs either a `siruta` query param on
/api/v1/search OR a dedicated /api/v1/parcela/by-ref?siruta&cad&layerId
endpoint that does a single indexed lookup. Today's patch handles the
top-50 case (was top-20); the 422 surfaces the residual cases for
follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user clicks a parcel or building, render a subtle overlay so
they can tell at a glance which feature corresponds to the open
info panel. Four new MapLibre layers:
v2-terenuri-selected-fill — green tint (#15803d/0.25)
v2-terenuri-selected-line — darker green stroke (#14532d/2.5px)
v2-cladiri-selected-fill — strong blue (#1d4ed8/0.55)
v2-cladiri-selected-line — navy stroke (#0c2050/1.6px)
All four start with a filter that matches nothing (==,object_id,-1).
A new useEffect in MapViewer watches `selectedFeature` (passed down
from GeoportalV2's `clicked` state) and updates the filter on the
matching layer pair via map.setFilter on every change. Switching
TERENURI ↔ CLADIRI clears the other layer pair, so the highlight
never doubles up.
selectedFeature.objectId is the filter key — comes straight from
PMTiles' object_id property and is reliable across both layers. If
the value isn't numeric (search-dropdown features sometimes lack
it), the filter falls back to no-match — the panel still works,
just no on-map glow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marius didn't like the previous map palette + missing labels.
Re-paint to match eterra.live's V2 map:
UATs:
Was: #9ca3af / #6b7280 (two-tone gray)
Now: #7c3aed violet, width ramps 0.3 → 0.6 by zoom band
Plus new v2-uats-z8-label symbol layer that prints the UAT name
at z9–12 (Noto Sans 10px, violet text + white halo).
Parcele (terenuri):
Was: #0ea5e9 sky-blue line, transparent 0.001 fill (hit-only)
Now: #22c55e green fill at 0.15 opacity (so the click area is
visible at the same time it acts as hit-target) + #15803d
darker green outline at 0.8 / 0.9 opacity. Cadastral-ref label
layer renders at z17+ (small enough zoom that the polygon can
carry a 10px label without overlapping neighbours).
Cladiri (buildings):
Was: #fb923c orange / #ea580c dark orange
Now: #3b82f6 blue fill at 0.5 / #1e3a5f navy line — same blue
eterra.live uses, distinct from the green parcel underlay and
from the violet UATs.
Cladiri „fără acte" (build_legal == 0):
NEW. Two amber overlay layers, filtered on the tile's
build_legal property:
v2-cladiri-unreg-fill — #f59e0b at 0.6 opacity
v2-cladiri-unreg-line — #b45309 at width 1
Renders on top of the default cladiri layers so illegal builds
visibly pop. If a PMTiles tile doesn't carry build_legal yet
the layer is empty — no regression on legal buildings.
Building suffix label (C1, C2, C3…):
NEW. v2-cladiri-label symbol at z16+. Mirrors eterra.live's
expression: extract the slice after the last "-" of
cadastral_ref ("354686-C1" → "C1"). Noto Sans 9px, navy text +
white halo so it reads on both light and dark basemaps.
Click handler unchanged — v2-cladiri-fill covers ALL buildings
(no filter), so legal vs unreg both route through the same query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related issues with the modal when the user kept clicking
around the map while in CF order mode:
1. LAYOUT BREAK (Marius screenshot — modal header clipped above
viewport): The V2 panel wrapper uses `backdrop-blur-md`. Per CSS
spec, an element with non-none backdrop-filter establishes a
containing block for `fixed`-positioned descendants. So
`fixed inset-0` on the modal was relative to the panel
(top-right, ~50px tall at min) instead of the viewport — the
modal anchored to the panel and overflowed up. Fix: render via
React's createPortal to document.body. The modal now escapes the
panel's stacking context entirely and centers in the viewport.
Also bumped z-index from 50 to 100 so the modal stays above the
MapLibre canvas + panel itself.
2. STATE CARRY-OVER: clicking a different parcel while the modal
was open silently re-targeted the modal at the new parcel — same
modal showing different cadref/sold mid-flow could mislead the
user about which parcel they were buying CF for. Fix:
FeatureInfoPanel now has a useEffect that closes the modal when
feature.cadastralRef / siruta / layerId changes. Modal stays
scoped to a single decision.
SSR guard: if (typeof document === "undefined") return null; before
the portal call so the modal doesn't blow up during server-side
render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marius: don't kick the user out to /parcel-sync just to connect ePay,
do everything inside the modal. The parcel-sync page also wasn't
helpful when reached (UAT selector empty), so the redirect was a
dead-end anyway.
State machine rewrite:
loading-status → GET /api/ancpi/session
not-connected → DELETED (replaced by transparent flow)
connecting → POST /api/ancpi/session with {} — server picks up
ANCPI_USERNAME/ANCPI_PASSWORD from env (Infisical
has them in /architools), connects silently
need-credentials → only if env creds are missing OR invalid: shows
an inline form (username / password / Conectează
button + privacy note "nu sunt păstrate la noi
după sfârșitul sesiunii")
no-credits / ready / placing / processing / done / error — as before
Flow for the happy path (Marius's case): user clicks "Comandă CF" →
modal shows "Conectare la ePay ANCPI…" for ~1s → "Verificare credite"
done → "Ești sigur? 1 credit, mai ai X" → confirm → animated steps
→ done. Zero page navigations.
Flow for the no-env case (other tenants or first-run): user sees
inline form, types credentials, presses Conectează → server stores
them in the in-memory session for the lifetime of the request,
modal continues straight to "ready".
Removed:
- goToParcelSync() handler + "Conectează ePay" deep-link button
- "not-connected" UI panel
- Phase value "not-connected" (no longer reachable)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marius: click "Comandă CF" from the card itself, no new-tab to
parcel-sync. Show "Ești sigur? Costă 1 credit, mai ai X" first.
Animate the order through its phases until done.
New component cf-order-modal.tsx — a 7-state machine over a single
shadcn-style dialog:
loading-status — checks /api/ancpi/session for connection + credits
not-connected — ePay session offline → prompt to connect via
parcel-sync (the only place credentials live)
no-credits — 0 credits, can't proceed
ready — confirmation: 1 credit cost, current balance,
projected balance after the order, all in
rounded chips with Coins icon
placing — POST /api/ancpi/order, spinner on step 1
processing — poll /api/ancpi/orders every 3s until status
becomes completed/done/minioPath populated.
Shows live elapsed seconds; 90s timeout falls
through to error with "verifică din nou peste
câteva minute".
done — checkmark anim + "Descarcă PDF" if document URL
came back
error — destructive panel + Reîncearcă button
Animations (tailwindcss-animate utilities):
- Modal backdrop: fade-in 200ms
- Modal card: zoom-in-95 + slide-in-from-bottom 200ms
- Step rows: active row gets primary-tinted bg + Loader2 spin,
done rows turn emerald + Check icon zooms in 300ms
- Success/error final state: rounded badge + icon zooms in 500ms
Footer adapts per phase: Anulează+Confirmă (ready), Conectează ePay
(not-connected), Închide (loading/no-credits), Închide fereastra
(placing/processing — order continues in bg), Gata (done), Închide+
Reîncearcă (error).
Wires into feature-info-panel by replacing the "open /parcel-sync"
click handler with setCfModalOpen(true). Modal mounts at the
panel's root with fixed positioning + z-50 so it overlays the map.
Backdrop click dismisses except during placing/processing.
Uses the legacy /api/ancpi/* endpoints (not /api/cf/* gis-ac route)
per Marius's earlier decision to keep credit tracking on his own
ePay session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Marius's feedback: eterra.live is a separate product, ArchiTools
shouldn't link out to it. Removed both touch-points:
1. The "eterra.live" button I'd added beside "Actualizează" in the
Date eTerra header — gone. This was also breaking layout (the
second button forced "acum câteva secunde" to wrap into "acum /
câteva / secunde" stacked above the button text).
2. The "Export GPKG" action in the toolbar — gone. It used to deep-
link to eterra.live/harta?…&autoexport=geopackage. Toolbar now
holds just "Comandă CF" which stays internal (/parcel-sync).
While in there:
- Actualizează button: `whitespace-nowrap` + `shrink-0` so it stops
wrapping when the panel is at min-width.
- Dropped the inline " · acum X min" beside the button label —
cleaner button, less truncation risk.
- Resurfaced the relative time as a small line at the bottom of the
Date eTerra body ("Actualizat din ANCPI · acum 3 min"). Same info,
no layout pressure.
- Cleaned unused lucide imports (Download, ExternalLink, Hash,
Layers, CalendarDays) that were leftover from the removed
eterra.live button + Cladire icon experiments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Marius's iteration on the panel:
1. AUTO-ENRICH DISABLED — the panel no longer fires /parcel/enrich on
sparse-data load. Refresh only via the explicit "Actualizează"
button in the Date eTerra header. Keeps the eTerra account pool
safe from browse-spam (was draining 500/h on rapid clicks).
2. eterra.live link button — sits beside "Actualizează" in the Date
eTerra header. Opens https://eterra.live/harta?siruta=...&cad=...
in a new tab so the user can cross-check the full eterra.live
panel.
3. ÎNSCRIERE collapsible — Tip înscriere / Data cererii / Act
proprietate now hide inside a <details> closed by default (per
the highlighted-block screenshot). Keeps the "above the fold"
info trimmed to what matters at a glance.
4. CARACTERISTICI CORP — new section, only rendered for
CLADIRI_ACTIVE clicks. Shows the cladire-specific enrichment
fields the orchestrator populates after a deep-enrich:
- Chip row: tip / destinație / subtype (chips) + Condominium
chip with unit count + Cu/Fără acte status pill
- 3-col metric strip: Regim înălțime / Niveluri / An construire
- Suprafață CF, CF IE, Clasă energetică, Părți comune (rows)
- Observații (multi-line InfoBlock)
Fields wired: CLADIRE_TYPE, CLADIRE_DESTINATIE, CLADIRE_SUBTYPE,
CLADIRE_REGIM, CLADIRE_NIVELURI, CLADIRE_AN_CONSTRUIRE,
CLADIRE_AREA_CF, CLADIRE_OBSERVATII, CLADIRE_LANDBOOK_IE,
CLADIRE_COMMON_PARTS, CLADIRE_UNITS_NO_ANCPI,
CLADIRE_ENERGETIC_CLASS, IS_LEGAL_BUILDING, IS_CONDOMINIUM.
All show only when populated (no empty "-" rows). LABEL map
extended with Romanian translations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Significant restructure of the parcel info panel based on Marius's
side-by-side comparison with eterra.live. Same data + same workflow,
much denser layout.
Layout changes:
1. HEADER — status dot + cadref + small uat/area/SIRUTA line. Removed
the redundant "Activ"/"Inactiv" chip (the dot is the signal).
Building suffixes (C1, C2…) still resolve via the search-by-cadref
path; header shows the full cadref ("354686-C1") so the user sees
both parent and suffix.
2. CARACTERISTICI — chips row only; tighter padding (px-2 py-1.5
vs px-3 py-2.5). Same intravilan / categorie / corpuri.
3. METRIC STRIP — new. Three cells in a single divided pill: GIS /
2D eTerra / Legală. Same pattern eterra.live uses. Saves a whole
section worth of vertical space.
4. DATE ETERRA CARD — wrapped in a bordered subtle-bg container with
the refresh button INLINE in the section header (vs at the bottom
of the panel). Shows "acum X min" relative time when enriched.
Two-column NR. CF + Nr. topo. Adresă with pin icon. Solicitant
with user icon. Proprietari as InfoBlock (multi-line preserved).
Foști proprietari as <details> collapsible (closed by default).
Înscriere group (tip / data / act) as a small subsection.
5. CONSTRUCȚII LIST — new. For TERENURI parcels, fetches the
building siblings via gisApi.search(parentCadref) + filter on
"<parent>-" prefix. Renders BuildingRow per cladire:
- Icon (Home / Building2 / Factory / Warehouse from destinatie)
- C1/C2/C3… suffix (mono, font-semibold)
- Area
- "Cu acte" (green) / "Fără acte" (amber) / "Necunoscut"
pill from BUILD_LEGAL / PARCEL_HAS_LANDBOOK enrichment
Click row → onSelectFeature switches panel to that building.
Lazy isLegal hydration: row first shows "Necunoscut", then
parallel parcela.get for each building fills the pill (5ms per
cache hit, no blocking).
6. APARTAMENTE — same content as before (for CLADIRI clicks), now
sits beside Construcții in the same flow. Header consistent with
the other section labels.
7. LOCALIZARE — moved to a single tight strip (lat/lng + Google
Maps link). Removed the SIRUTA repetition since it's already in
the header.
8. ACTIONS toolbar — compressed. Removed the in-toolbar "Citește din
ANCPI" button since the refresh button is now inside the Date
eTerra card header (where it belongs contextually). Kept Export
GPKG + Comandă CF.
GeoportalV2 wires onSelectFeature={setClicked} so building rows
propagate. Building clicks reuse the same panel + same auto-enrich
flow — the second feature.layerId === CLADIRI_ACTIVE branch in the
condo-owners useEffect kicks in for buildings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
gis-api session shipped PR3 (gis-api 09f1ab8 + gis-sync-orchestrator
0371d81): new POST /api/v1/parcel/enrich does the full eTerra
round-trip (searchImmovableByIdentifier → fetchDocumentationData
→ fetchImmovableParcelDetails) and merges NR_CF / ADRESA / PROPRIETARI
+ 20-plus fields into gis_core.GisFeature.enrichment with a 30-day
cache. Verified on 266888 + 328607 → 27 keys with full PII.
Wired in three places:
1. src/lib/gis-api-client.ts — gisApi.parcel.enrich({siruta,
cadastralRef, force?}) thin wrapper.
2. src/app/api/gis/parcel/enrich/route.ts — architots-side proxy,
matches the parcel/tech pattern (auth check → forward → bubble up
GisApiError status codes).
3. src/modules/geoportal/v2/feature-info-panel.tsx — refreshFromAncpi
now POSTs to /api/gis/parcel/enrich instead of /api/gis/parcel/tech.
After the orchestrator returns, the panel re-fetches the canonical
record via parcela.get (when uuid known) or parcela.find (when
not), so it sees exactly what gis_core stores rather than the
orchestrator response shape.
The existing auto-trigger (fires when detail has no NR_CF/ADRESA/
PROPRIETARI) now actually fills those fields. Subsequent clicks on the
same parcel hit gis-api's 30-day cache (5ms vs 1-2s live fetch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Marius's feedback: "datele arată foarte ciudat" — the flat dl renderer
showed enrichment keys in whatever order gis-api returned them, with
raw key names (PARCEL_POSTAL_NO instead of "Nr. poștal") and no visual
hierarchy. With 20+ keys the important things (NR_CF, ADRESA,
PROPRIETARI) ended up below the fold under PARCEL_* tech metadata.
Restructure to mirror eterra.live's parcel-info layout:
1. HERO BLOCK at the top of the data area
- "Nr. Carte Funciară" big mono number (NR_CF)
- NR_CF_VECHI shown below when different
- Adresă with pin icon and proper text wrapping
2. NAMED SECTIONS (rendered in fixed order, only when populated)
- Proprietari — splits comma/semicolon-separated names into a list
- Cadastru — NR_CAD, NR_TOPO, PARCEL_TOPO_NO
- Suprafețe — SUPRAFATA_R, SUPRAFATA_2D, PARCEL_LEGAL_AREA, SUPRAFATA
(values parsed + " m²" suffix)
- Înscriere — SOLICITANT, TIP_INSCRIERE, DATA_CERERE, DOC
- (CF/Adresă removed from the list because they're in the hero)
3. CARACTERISTICI CHIPS (existing) — Intravilan / Categorie / nr corpuri /
status stay at the top. CARACTERISTICI_KEYS excludes them from the
sections below so nothing is shown twice.
4. DETALII TEHNICE (collapsed by default) — anything not in a named
section: PARCEL_HAS_LANDBOOK, PARCEL_IS_CONDOMINIUM,
PARCEL_TECH_ENRICHED_AT, etc. Renders with friendly labels (Da/Nu
for flags, dd/mm/yyyy hh:mm for dates) instead of "1" / ISO strings.
5. Value renderers per key type:
- formatAreaValue("456" | "456.06" | "456 mp") → "456 m²"
- formatFlag(0/1, da/nu) → "Da" / "Nu"
- formatDate(ISO) → "19.05.2026, 04:40" (Romanian locale)
- parseOwners("MATHBOUT MOHAMED MAHER, DR.") → list items
6. enrichedAt moved to a single small line at the bottom of the data
area instead of a per-section caption.
LABEL map covers all 25 enrichment keys observed in DB. Anything new
falls back to raw key name (visible — easy to spot and add).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the "Re-loghează-te" button + scope-mismatch warning prose.
On 403 from /api/gis/parcela/find the panel now:
1. Checks sessionStorage flag — false on first 403 of the tab
2. Sets the flag, fires signIn("authentik", { callbackUrl: current
URL }) silently. For an SSO'd user this is a sub-second Authentik
redirect cycle that mints a fresh access_token with the right
scope claims, lands the user back on the same panel, and the
re-mount fetches successfully — no visible message, no prompt.
3. If another 403 happens after the retry (i.e., Authentik genuinely
can't grant the scope — config issue, not a stale-token issue),
falls through to a discreet "Datele detaliate nu pot fi încărcate
momentan." note. No call-to-action, no jargon.
4. On any successful 200 fetch, clears the sessionStorage flag so a
future 403 in the same tab can re-trigger the silent retry.
Per Marius: "vreau doar să meargă, safe și fix" — no auth-flow
chrome shown to the user. The recovery is part of the system's
correctness contract, not a feature for the user to manage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The other-session's gis-api investigation found that gis-api is
working correctly — full/basic/none scopes all behave per spec.
The bug was in our /api/gis/parcela/find proxy: when EVERY candidate
returned 403 from gis-api (because the caller's JWT carried no
enrichment_scope claim), the proxy swallowed the 403s and returned
silent 404. The panel then rendered the "not in central DB" empty
state instead of prompting re-login.
This was the case for Marius today — his pre-refresh-fix session
held a token without the enrichment claim. After the auth self-heal
fix (commit 8ff67d1) the next gis-api call would have re-authed
correctly, but the panel never gave him that signal because find
hid the 403.
Fix in two places:
1. /api/gis/parcela/find:
- Count 403s seen during candidate iteration
- If forbiddenCount > 0 && forbiddenCount === candidates.length,
return 403 { error: "scope_insufficient", ... } with a log line
[gis-parcela-find] all_candidates_forbidden siruta=X cad=Y N
- Otherwise log [gis-parcela-find] no_match (so we never go silent)
2. feature-info-panel: when fetch returns 403, the existing
"forbidden" UI was a passive warning. Now it shows an actionable
"Re-loghează-te" button that fires signIn("authentik", {
callbackUrl: current }) — same path SessionErrorWatcher uses for
RefreshAccessTokenError.
Reference: gis-api session report 2026-05-19 (Marius forwarded
analysis); the gis-api repo is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix searched by cadastralRef and picked the first
layerId-matching result. But cadastral refs collide across UATs:
"354686" exists in multiple counties. The Cluj-Napoca f9bf2ca4-...
parcel with full enrichment got passed over for a same-cad parcel
in another UAT that has no enrichment → panel rendered header +
"Caracteristici" with empty Intravilan, no "Date eTerra" section.
New server-side /api/gis/parcela/find?siruta&cad&layerId proxy:
- gisApi.search(cad) → filter by layerId → up to ~20 candidates
- For each candidate, parcela.get and check stored siruta
- Return the siruta-matching detail
- Fallback: first readable candidate (so the panel still has data
even if siruta mismatch — better than empty)
Panel useEffect simplified: fast path = parcela.get by uuid when the
tile has one, slow path = parcela/find when not. 404 from find sets
the "not in central DB yet" empty state (user can hit Citește din
ANCPI to trigger orchestrator live-fetch).
Diagnostic logs: [gis-parcela-find] siruta=… cad=… layerId=…
candidates=N + per-hit "has_enrich=true keys=N" so we can tell from
container logs whether the right parcel resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of B1 (panel showed "Apasă din ANCPI" even with full enrichment
in DB): PMTiles overview tiles don't carry the GisFeature uuid, only
siruta/cadastral_ref/object_id. The panel's useEffect bailed out at
`!feature.id` and never fetched. So the data was there, the UI just
refused to ask for it.
Fix: when the click feature has no uuid, the panel now calls
`/api/gis/search?q=<cadref>`, filters by layerId match, and uses the
returned id to do `parcela.get(id)`. One extra round trip (~50ms with
the trigram-idx fix from 2026-05-18). For features arriving from the
search dropdown the uuid is already known — that path is unchanged.
Panel redesign — same data shape as eterra.live, ArchiTools styling
(shadcn instead of HeroUI), single-file:
- Header: cadref + layer + area + status chip + close
- Caracteristici: intravilan + categorie folosință + nr corpuri (chips)
- Date eTerra: all enrichment fields, PII passes through gis-api scope
redaction (scope=basic → PROPRIETARI/NR_CF/DOC already null)
- Apartamente (condominium): for CLADIRI_ACTIVE clicks, fetches
/api/gis/building/condo-owners and renders units with owners + cf + area
- Localizare: click lat/lng + Google Maps link + SIRUTA echo
Two new proxy routes (thin wrappers over gis-api):
- POST /api/gis/parcel/units-fetch
- POST /api/gis/building/condo-owners
Basic-panel mode for restricted users (per Marius: "for users I don't
want to give full access to"):
- New env BASIC_PANEL_USERS (csv emails) → session.basicPanel flag
- Optional PANEL_BASIC_GLOBAL=1 to force-basic everyone
- When true, panel renders only header + cadref + suprafață + a
restriction notice; all sections + condo fetch are skipped
- Defaults to off; pilot user Marius gets full panel as before
map-viewer now forwards lngLat on click so the Localizare section has
coordinates without a second lookup.
Type-check clean. Production build (NODE_ENV=production npx next build)
passes. The dev-mode prerender error on / page is pre-existing (Next 16
useContext-null on client component during static export, unrelated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User pushback on the pool-based CF flow: he wants his own ePay account
(per-user creds, visible credit balance, decrement per order) — not the
shared orchestrator pool which hides cost attribution.
V2 panel "Comandă CF" now opens /parcel-sync?tab=epay&cad=<ref> in a
new tab where the legacy UI handles ordering with credits. The
/api/cf/* gis-api routes stay (used elsewhere + future SaaS consumers)
but the V2 button doesn't hit them.
Plus diagnostic on /api/gis/parcela showing enrichment presence + key
count to debug "data nu raman" — should reveal whether Marius's session
is getting full enrichment back from gis-api.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was a disabled placeholder ("Va fi disponibil la Faza F"). Now
POSTs to /api/cf/order with nrCadastral + siruta + gisFeatureId.
Forwards 409 catalog_hit as "Extras CF valid deja există".
Spinner during request, result text shown below the toolbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-flagged blocker: search-dropdown feature select → setClicked
with siruta:"" because gis-api /search response doesn't include
siruta per feature. If user clicks "Citește din ANCPI" before the
panel's auto-fetch of parcela.get completes, refresh fails with
missing_siruta_or_cad.
Fix: refresh handler now awaits parcela.get inline when siruta is
empty + feature.id is present, then hydrates detail before posting
to /api/gis/parcel/tech.
Proper followup: extend gis-api /search response with siruta per
feature so the race goes away entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UAT click previously console.logged only. gis-api search response
doesn't include bbox/centroid, so ArchiTools can't fitBounds locally.
Reuse the deep-link pattern (already used by Export GeoPackage) →
open eterra.live/harta?siruta=X in a new tab. eterra.live has its own
/api/geoportal/uat-bounds + flyTo wired.
Future: add GET /api/v1/uat/:siruta/bounds to gis-api so ArchiTools
can fitBounds inline without leaving the page.
Also reverts the session.debug diagnostic (Marius confirmed
hasRefreshToken=true + expiresIn=293 after attaching offline_access
scope mapping to Authentik provider pk=6 — root cause fixed,
diagnostic no longer needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PMTiles overview tiles may carry only object_id + siruta + cadastral_ref
(not the gis_core.GisFeature uuid). Old click handler required `id` and
silently dropped clicks. Now:
- map-viewer click handler: extract objectId AND id; require only
siruta+cadastral_ref to dispatch. Logs the props when fields are
missing for further diagnosis.
- feature-info-panel: skip auto-fetch when feature.id is empty; show
basic info from tile properties + nudge user to "Citește din ANCPI"
to populate enrichment.
- Refresh button: project orchestrator response straight into the
panel when no gis-uuid available (no second parcela.get roundtrip).
Falls back to detail.siruta when click came from search (which only
returns id + cadastralRef, no siruta).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changed bg-background/95 to bg-background (opaque) and added
text-foreground to ensure input text is visible on dark theme.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search results now JOIN GisUat to display UAT name prominently instead
of just SIRUTA codes. Map flyTo uses imperative handle instead of
stateful props that re-triggered on re-renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PMTiles was loaded via HTTP from MinIO (10.10.10.166:9002) on an HTTPS page,
causing browser mixed-content blocking — parcels invisible on geoportal.
Fixes:
- tile-cache nginx proxies /pmtiles/ → MinIO with Range header support
- PMTILES_URL changed to relative path (resolves to HTTPS automatically)
- clickableLayers includes PMTiles fill layers (click on parcels works)
- Selection highlight uses PMTiles source at z13+ (was Martin z17+ only)
- tippecanoe per-layer zoom ranges (terenuri z13-z18, cladiri z14-z18)
skips processing millions of features at z0-z12 — faster rebuild
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PMTiles now covers z0-z18 (full zoom range). Martin sources kept only for
selection highlight and fallback when PMTiles not configured.
All terenuri/cladiri fill/line/label layers served from PMTiles when active.
Zero PostGIS load for tile rendering at any zoom level.
File will be ~1-2 GB but eliminates all cold-load latency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PMTiles now covers z0-z16 (was z0-z14) for terenuri + cladiri
- Martin only serves z17-z18 (very close zoom, few features per tile)
- map-viewer: PMTiles layers serve z13-z16 for terenuri, z14-z16 for cladiri
- Labels at z16 now from PMTiles (cadastral_ref included in tiles)
- Remove failed compound index from postgis-setup.sql
This eliminates PostgreSQL as bottleneck for 99% of tile requests.
PMTiles file will be ~300-500MB (vs 104MB at z14).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extend PMTiles to include simplified terenuri (5m tolerance) and cladiri (3m)
- map-viewer: terenuri z13 from PMTiles, z14+ from Martin (live detail)
- map-viewer: cladiri z14 from PMTiles, z15+ from Martin
- Martin sources start at higher minzoom when PMTiles active (less DB load)
- Add warm-tile-cache.sh: pre-populate nginx cache for major cities
- Rebuild script now includes cache warming step after PMTiles upload
- Remove deprecated docker-compose version: "3.8"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip parcel number prefix from building labels — "291479-C1" now displays as "C1".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add nginx reverse proxy cache in front of Martin (2GB, 1h TTL, stale serving, CORS)
- Martin no longer exposes host port — all traffic routed through tile-cache on :3010
- Add PMTiles support in map-viewer.tsx (conditional: NEXT_PUBLIC_PMTILES_URL env var)
- When set: single PMTiles source for UAT + administrativ layers (z0-z14, ~5ms/tile)
- When empty: fallback to Martin tile sources (existing behavior, zero breaking change)
- Add tippecanoe Docker service (profiles: tools) for on-demand PMTiles generation
- Add rebuild-overview-tiles.sh: ogr2ogr export → tippecanoe → MinIO atomic upload
- Install pmtiles npm package for MapLibre protocol registration
Performance impact:
- nginx cache: 10-100x faster on repeat tile requests, zero PostGIS load on cache hit
- PMTiles: sub-10ms overview tiles, zero PostGIS load for z0-z14
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: martin.yaml was never mounted in docker-compose.yml — Martin ran
in auto-discovery mode which dropped cadastral_ref from gis_cladiri tiles.
Changes:
- docker-compose: mount martin.yaml, upgrade Martin v0.15→v1.4.0, use --config
- map-viewer: add cladiriLabel layer (cadastral_ref at z16+), wire into visibility
- martin.yaml: update version comment
- geoportal/: tile server evaluation doc + 3 skill files (vector tiles, PMTiles, MapLibre perf)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Building labels (C1/C2/C3) disabled — Martin MVT tiles don't include
cadastral_ref as a property despite the PostgreSQL view exposing it.
Root cause needs investigation (Martin config or alternative tile server).
Removed temporary debug endpoints:
- /api/eterra/debug-tile-props
- /api/eterra/debug-tile-sample
Kept /api/eterra/debug-fields (useful long-term diagnostic).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed wrapper function/setTimeout approach. Now uses exact same
inline addLayer pattern as terenuriLabel which is proven to work.
Same source, same font, same coalesce pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Labels were hidden by MapLibre collision detection with terrain
labels. Now using text-allow-overlap + text-ignore-placement to
force visibility. Also added retry with setTimeout in case source
isn't ready when layer is first added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous index-of/slice expression wasn't rendering. Simplified to
just show the full cadastral_ref (e.g. "77102-C1") as-is. MapLibre
auto-hides overlapping labels. This is a diagnostic step to verify
the tile property is accessible.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous index-of expression wasn't rendering. Simplified to use
filter with index-of on dash + slice from dash position.
Also lowered minzoom from 17 to 16.
Added diagnostic log in enrichment for building cross-ref count
to debug HAS_BUILDING=0 cases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extracts body suffix from cadastral_ref (e.g. "77102-C1" → "C1") and
displays as centered label on each building polygon. Only visible at
zoom 17+ to avoid clutter at lower zooms.
Applied to both geoportal map-viewer and parcel-sync map tab.
Uses siruta filter in parcel-sync tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export fix:
- Replaced DropdownMenu with direct DXF/GPKG buttons in SelectionToolbar.
Radix dropdown portals don't work inside fixed z-[110] containers.
Direct buttons work reliably on all platforms.
Mobile RGI cards:
- Single-row compact layout: icon + nr cerere + solicitant + termen + status
- Smaller icons (3.5), tighter spacing, shorter status labels
- No Card wrapper — lightweight border div for less visual weight
Mobile filters:
- Tighter spacing, smaller labels
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Freehand drawing fix:
- Disable dragPan when in freehand mode (was only disabling dblclick
zoom). Without this, clicks were interpreted as pan gestures.
- Re-enable dragPan when exiting freehand mode.
Click highlight:
- Clicking a parcel in "off" mode now highlights it with the selection
layer (amber fill + orange outline). Clicking empty space clears it.
- Provides visual feedback for which parcel was clicked.
Mobile toolbar:
- Moved selection toolbar higher (bottom-12 on mobile) with z-20
to ensure it's above MapLibre attribution bar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SelectionToolbar: new hideEnrichment prop hides the Enrichment button.
Portal uses it to show only Export + Clear in selection toolbar.
Portal feature panel: added disabled "Solicita extras CF" button with
tooltip "Sectiune platita — contacteaza administratorul".
Initial map zoom: starts at zoom 15 (close-up) instead of 7 (Romania
overview). Prevents the UAT boundaries flash before fitBounds runs.
Applied to both ParcelSync Harta tab and Portal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Basemaps: added Google Satellite option
ANCPI ortofoto: fixed bbox conversion (all 4 corners, not just SW/NE)
Selection: ESC key and right-click exit selection mode, tooltips updated
UAT layers: removed fill (only lines + labels), less visual clutter
Proprietari vechi: greyed out (opacity-50) so current owners stand out
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Layer toggle fix: removed isStyleLoaded() check that silently blocked
visibility changes when OpenFreeMap style has pending sprite/font loads
2. Enrichment: "Actualizeaza" button always visible (re-fetch from eTerra)
replaces "Enrichment" button when data already exists
3. Panel updates with returned enrichment data immediately
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Enrichment: panel now updates immediately with returned data (was only showing message)
2. Layers: ALL data layers set to visibility:none immediately after creation,
then only enabled ones are shown. Fixes cladiri appearing when only terenuri toggled.
3. OpenFreeMap boundaries: also filter by source-layer="boundary" (more reliable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>