Commit Graph

80 Commits

Author SHA1 Message Date
Claude VM 36840f31f6 fix(geoportal-v2): gate condo-owners on IS_CONDOMINIUM + visible empty state
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>
2026-05-20 22:03:18 +03:00
Claude VM c7cf1aee49 feat(geoportal-v2): re-enable deep-enrich for cladiri + forward layerId
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>
2026-05-20 21:41:12 +03:00
Claude VM 49dcdadc44 fix(geoportal-v2): cladiri-aware deep-enrich button + clearer error
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>
2026-05-20 19:18:08 +03:00
Claude VM 52c31e3c4d feat(geoportal-v2): UAT name + SOLICITANT into Înscriere + Google Maps inline
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>
2026-05-20 17:49:46 +03:00
Claude VM 653cffeee3 fix(geoportal-v2): use siruta from search response — no more N+1 misses
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>
2026-05-20 12:57:31 +03:00
Claude VM 7b01744fad feat(geoportal-v2): on-map selection highlight
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>
2026-05-20 09:10:39 +03:00
Claude VM 8d5316dd1b feat(geoportal-v2): map styling parity with eterra.live
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>
2026-05-20 09:08:41 +03:00
Claude VM 52e16e7807 fix(cf-modal): portal to body + auto-close on parcel switch
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>
2026-05-20 08:30:26 +03:00
Claude VM ad89da690f fix(cf-modal): inline auto-connect + credential form — no parcel-sync hop
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>
2026-05-20 08:10:33 +03:00
Claude VM 5e4618b309 feat(geoportal-v2): inline CF order modal — confirmation + animated steps
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>
2026-05-20 07:53:15 +03:00
Claude VM 8f86bab337 fix(geoportal-v2): remove eterra.live links + fix Actualizează wrap
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>
2026-05-20 07:44:12 +03:00
Claude VM 3004790ad2 feat(geoportal-v2): cladire characteristics + eterra.live link + collapsible Înscriere
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>
2026-05-20 07:36:01 +03:00
Claude VM 4f38fd1070 feat(geoportal-v2): compact eterra.live-style layout + buildings list
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>
2026-05-20 06:57:27 +03:00
Claude VM a4f61bf3d8 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>
2026-05-19 22:44:25 +03:00
Claude VM 02a466ccaa feat(geoportal-v2): swap refresh path to /parcel/enrich (deep-enrich)
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>
2026-05-19 22:24:02 +03:00
Claude VM 87f9d72e4f feat(geoportal-v2): auto-fetch enrichment when DB only has tech keys
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>
2026-05-19 19:52:00 +03:00
Claude VM 342bdca648 fix(geoportal-v2): structured panel sections + readable labels (back to basics)
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>
2026-05-19 17:17:48 +03:00
Claude VM a23ba1957f fix(geoportal-v2): silent auto re-grant on scope-missing 403
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>
2026-05-19 16:57:42 +03:00
Claude VM 71df1ee9ec fix(geoportal-v2): surface scope-insufficient instead of silent 404
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>
2026-05-19 16:52:47 +03:00
Claude VM 7afba6e1a9 fix(geoportal-v2): siruta-aware parcela lookup (B1 round 2)
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>
2026-05-19 15:26:49 +03:00
Claude VM b5eff5acc1 fix(geoportal-v2): rewrite info panel — auto-fetch + sections + condo + basic mode
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>
2026-05-19 12:35:09 +03:00
Claude VM 77da69e29f fix(geoportal-v2): CF button → deep-link to parcel-sync ePay tab
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>
2026-05-19 08:16:55 +03:00
Claude VM b85e074e3a feat(geoportal-v2): wire Comanda CF button to /api/cf/order
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>
2026-05-19 07:53:11 +03:00
Claude VM e6432b13f0 fix(geoportal-v2): hydrate siruta when refresh fires before parcela.get
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>
2026-05-19 00:15:07 +03:00
Claude VM 68355efbba fix(geoportal-v2): UAT click deep-links to eterra.live + revert debug
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>
2026-05-18 23:59:47 +03:00
Claude VM e0610b0573 fix(geoportal-v2): handle PMTiles features without uuid id
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>
2026-05-18 21:42:40 +03:00
Claude VM 99a673de3d feat(geoportal): Faza E v2 thin client (PMTiles + gis.ac)
New geoportal module flag-gated by session.useGisAc. Legacy code path
preserved as GeoportalV1Legacy (rename only — same logic). When
session.useGisAc=true (Infisical USE_GIS_AC=1 OR email in
GIS_AC_PILOT_USERS), the page renders GeoportalV2 instead.

V2 layout (851 LOC across 5 files):
- map-viewer.tsx (~295 LOC): MapLibre + PMTiles src `pmtiles://pmtiles.gis.ac/overview.pmtiles`. Layers: UAT boundaries (z5, z8), parcels (gis_terenuri line + invisible hit-test fill), buildings (gis_cladiri fill+line). Click → resolves layerId from sourceLayer, emits ClickedFeatureLite (id, siruta, cadastralRef, layerId).
- search-bar.tsx (~160 LOC): debounced 300ms, calls /api/gis/search, dropdown grouped by UATs / Parcele.
- feature-info-panel.tsx (~270 LOC): fetches /api/gis/parcela/[id], renders enrichment block (scope-aware — 403 shown explicitly as "permisiuni insuficiente"). Buttons: "Citește din ANCPI" (POST /api/gis/parcel/tech force:true), "Export GeoPackage" (deep-link to eterra.live/harta?…&autoexport=geopackage), "Comandă CF" placeholder pending Faza F.
- basemap-switcher.tsx (~40 LOC): liberty / dark / satellite / google. Dropped orto + topo50/25 (require ANCPI session — orto/topo via raster.gis.ac is TBD Sprint 2).
- geoportal-v2.tsx (~85 LOC): wraps MapViewer + SearchBar + BasemapSwitcher + FeatureInfoPanel.

API routes (90 LOC across 3 files):
- GET /api/gis/search → gisApi.search
- GET /api/gis/parcela/[id] → gisApi.parcela.get
- POST /api/gis/parcel/tech → gisApi.parcel.tech (refresh ANCPI)

All routes 401 if no NextAuth session, forward GisApiError status+code,
hit api.gis.ac with the Authentik access_token from session. Per
project_audit_correlation_echo memory: no correlationId set on client
side (gis-api overwrites server-side).

Cutover-bottom-right badge "gis.ac · v2" visible until full rollout for
ops visibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:32:36 +03:00
Claude VM 8222be2f0e fix(geoportal): search input text invisible in dark mode
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>
2026-04-07 22:11:46 +03:00
Claude VM 177f2104c1 fix(geoportal): show UAT name in search results + fix map snap-back
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>
2026-04-07 21:21:09 +03:00
AI Assistant 9bf79a15ed fix(geoportal): proxy PMTiles through HTTPS + fix click/selection + optimize rebuild
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>
2026-03-29 14:56:49 +03:00
AI Assistant 870e1bd4c2 perf(geoportal): extend PMTiles to z18 — eliminate Martin for terenuri/cladiri entirely
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>
2026-03-28 17:23:04 +02:00
AI Assistant f5c8cf5fdc perf(geoportal): extend PMTiles to z16 — near-zero PostGIS load for tile serving
- 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>
2026-03-28 15:40:03 +02:00
AI Assistant 0d5fcf909c feat(geoportal): PMTiles for terenuri/cladiri overview + cache warming + cleanup
- 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>
2026-03-27 21:46:47 +02:00
AI Assistant 236635fbf4 fix(geoportal): show only building body suffix (C1, C2) instead of full cadastral_ref
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>
2026-03-27 21:32:38 +02:00
AI Assistant 536b3659bb feat(geoportal): nginx tile cache + PMTiles overview layers + tippecanoe pipeline
- 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>
2026-03-27 20:28:49 +02:00
AI Assistant a75d0e1adc fix(geoportal): mount Martin config + upgrade v1.4 + enable building labels
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>
2026-03-27 10:28:20 +02:00
AI Assistant 9d45799900 revert: disable building labels + remove debug endpoints
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>
2026-03-27 08:43:20 +02:00
AI Assistant 946723197e debug: red building labels with template string syntax
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:30:13 +02:00
AI Assistant 3ea57f00b6 debug: try cladiri labels at minzoom 15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:23:48 +02:00
AI Assistant 1d233fdc19 fix(geoportal): building labels — inline addLayer like terenuriLabel
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>
2026-03-27 08:13:40 +02:00
AI Assistant c6eb1a9450 fix(geoportal): building labels — force overlap + delayed init
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>
2026-03-27 08:07:38 +02:00
AI Assistant 49a239006d fix(geoportal): simplify building labels — show full cadastral_ref
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>
2026-03-27 07:57:45 +02:00
AI Assistant 4c1ffe3d01 fix(geoportal): building labels C1/C2 — simpler expression + minzoom 16
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>
2026-03-27 07:11:56 +02:00
AI Assistant acb9be8345 feat(geoportal): building body labels (C1, C2, C3...) on map at zoom 17+
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>
2026-03-27 06:53:45 +02:00
AI Assistant c012adaa77 fix: export buttons direct (no dropdown), compact mobile cards
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>
2026-03-25 12:03:24 +02:00
AI Assistant 8acafe958b fix: freehand drawing, click highlight, mobile toolbar visibility
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>
2026-03-25 07:11:16 +02:00
AI Assistant 45d4d1bf40 fix: hide enrichment in portal, CF disabled button, no UAT flash, fix overlaps
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>
2026-03-25 06:58:01 +02:00
AI Assistant 3fcf7e2a67 fix(geoportal): Google satellite, ESC/right-click exit, no UAT fill, ANCPI bbox fix
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>
2026-03-24 14:19:02 +02:00
AI Assistant 024ee0f21a fix(geoportal): layer toggle + enrichment update + refresh button
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>
2026-03-24 13:53:18 +02:00
AI Assistant 19bed6724b fix(geoportal): enrichment panel update + force-hide all layers + boundary filter
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>
2026-03-24 13:45:29 +02:00