Commit Graph

536 Commits

Author SHA1 Message Date
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 8ff67d19fb fix(auth): self-heal + auto re-login on refresh failure
Three-layer fix for the "session keeps dying with invalid_grant" pain:

1. Authentik provider config (separate change via API):
   access_token_validity bumped 5min → 60min so refreshes are 12x less
   frequent. Refresh-token rotation collisions only happen during the
   refresh, so a longer access_token TTL means far fewer windows.

2. jwt callback (auth-options.ts): when Authentik responds 400
   invalid_grant on refresh, the stored refresh_token is permanently
   dead — Authentik rotated it on a previous successful refresh and the
   old value can't be reused. Clear it (and the access_token) from the
   JWT so subsequent session checks see a clean RefreshAccessTokenError
   instead of looping into the same 400 every 5 minutes.

3. SessionErrorWatcher (new client component, mounted in providers
   tree): listens for session.error === "RefreshAccessTokenError" and
   calls signIn("authentik") with the current URL as callback. The
   cleared JWT cookie means Authentik runs a full OIDC flow, mints fresh
   tokens, and the user lands back where they were. No manual logout.

Net effect: refresh storms become invisible — at worst there's a single
redirect to Authentik (silent if the user is still SSO'd) instead of a
broken session that 401s every API call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:23:50 +03:00
Claude VM 1786c254d5 diag(gis): /api/gis/me proxy → surface Authentik claims for scope debugging
f9bf2ca4 has 25 enrichment keys in gis_core.GisFeature but
parcela.get returns only 10 — all PII (NR_CF, ADRESA, PROPRIETARI)
redacted. Symptom of enrichment_scope=basic. Plan 003 §Faza B says
Arhitecti LDAP group should get full. Need to verify the mapping.

Calls gisApi.me() and returns the claims. Logs them server-side
(truncated to 500 chars). Marius hits the URL once, we see what
enrichment_scope his JWT actually carries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:02:36 +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 ac193128d9 test(deploy): verify webhook id=5 signs request 2026-05-19 11:38:26 +03:00
Claude VM f8ae0f02ff test(deploy): pcap capture 2026-05-19 11:30:39 +03:00
Claude VM fcb788ebdf test(deploy): tcpdump capture headers 2026-05-19 11:30:22 +03:00
Claude VM a3904a8960 test(deploy): verify webhook chain (round 2 — after PATCH secret) 2026-05-19 11:29:52 +03:00
Claude VM 9e1c2e7ac0 test(deploy): verify webhook auto-deploy chain 2026-05-19 11:28:11 +03:00
Claude VM b957de77b9 feat(faza-c.2): gate legacy GisFeature writes under USE_GIS_AC
Adds gateLegacyGisWrite() helper that returns 410 when the caller is on
the api.gis.ac path (global USE_GIS_AC=1 or per-user GIS_AC_PILOT_USERS).
Wired into 13 routes covering every entry point that touches Gis*
tables on architools_postgres — directly or via parcel-sync services.

Why: yesterday 4 GisFeature rows were updated on architools_postgres
even though the scheduler is officially disabled. Root cause: pilot
user opened the legacy /geoportal UI in a stale tab and clicked
parcels; POST /api/geoportal/enrich wrote directly to the local DB.
Without a write gate, Faza H (pg_dump + REVOKE + DROP) is unsafe —
any stale tab in any user's browser can still trip writes between
freeze and DROP.

Gated routes (writes only — reads stay open for rollback ergonomics):
- /api/geoportal/enrich (POST) — the writer of the 4 rows
- /api/eterra/sync-rules (POST), /api/eterra/sync-rules/[id] (PATCH+DELETE)
- /api/eterra/sync-rules/bulk (POST)
- /api/eterra/uats (POST+PATCH)
- /api/eterra/sync (POST), /api/eterra/sync-county (POST)
- /api/eterra/sync-background (POST), /api/eterra/sync-all-counties (POST)
- /api/eterra/auto-refresh (POST), /api/eterra/refresh-all (POST)
- /api/eterra/export-layer-gpkg (POST), /api/eterra/export-bundle (POST)
  (last two trigger syncLayer write-first-then-export)

Read-only routes intentionally NOT gated: sync-status, no-geom-scan
(scanNoGeometryParcels is read-only), export-local, db-summary,
counties, search, features (GET), stats, uat-dashboard, sync-rules
(GET), sync-rules/scheduler.

Operations: after redeploy, flip USE_GIS_AC=1 in Infisical /architools
prod env and restart container. Then monitor docker logs for ~30 min:
grep "deprecated" + "/api/geoportal/enrich|/api/eterra/sync*" lines
indicate stale-tab clients that need a refresh. pg_stat_user_tables
write count on GisFeature should hit 0 within one hour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:00:16 +03:00
Claude VM 9847b4a070 docs(plans): session handoff + audit + Faza H runbook
End of 2026-05-19 cutover-debug session. Saves the full state +
2 outstanding bugs + Faza G/H plan into docs/plans/ so the next
session can resume without re-investigating.

- prompt-handoff-2026-05-19.md: short prompt for the next session
  to amend + resume.
- audit-2026-05-19.md: auditor-agent output (~30 findings).
- 004-faza-h-runbook.md: pg_dump + REVOKE + DROP runbook with
  prereqs (eterra.live shares DB, unidentified writer, CfExtract
  schema drift).

Memory entries also written:
- feedback/authentik-token-endpoint-shared
- project/architools-cutover-state-2026-05-19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:45 +03:00
Claude VM 162c8ed257 fix(auth): Authentik token endpoint is /application/o/token/ (shared)
THE bug behind every "data nu raman" / invalid_token incident this
session: refresh POSTed to `{issuer}/token/` = /application/o/architools/token/
which returns HTTP 405 + empty body. JSON.parse on the empty body
threw "Unexpected end of JSON input" → catch fired → token marked
RefreshAccessTokenError → 60s cooldown later, retry hit the same
broken URL → loop.

OIDC discovery at {issuer}/.well-known/openid-configuration confirms:
  "token_endpoint": "https://auth.beletage.ro/application/o/token/"

This is the SHARED endpoint, not per-provider. Hard-fix the URL by
constructing it from the issuer's origin.

Marius's currently-stuck session will auto-recover on next request
(cooldown expires, refresh fires against the corrected URL,
refresh_token still valid 30d).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:23:43 +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 293d15edf2 fix(auth): refresh cooldown 60s — auto-recover from sticky errors
Previous logic set token.error=RefreshAccessTokenError and never
retried — once a refresh failed (likely a race during the early
parallel-storm period), Marius's JWT cookie carried that error
forever. New jwt calls all saw "blocked" → kept using the stale
accessToken → api.gis.ac returned invalid_token on every call.

Fix: store errorAt timestamp alongside the error flag. Block refresh
attempts for 60s after a failure (avoids hot-loop on persistent
Authentik issues), then unblock and retry. On the next failure, the
60s cooldown re-arms.

For Marius's currently-stuck session: as soon as this deploys, his
next jwt callback will pass the cooldown check (errorAt is hours ago)
and trigger a fresh refresh. If Authentik is happy with his
refresh_token, the error flag is cleared and he's back to normal —
no relogin needed.

Logs now show "blocked=true/false" alongside secLeft for visibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 07:59:53 +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 8024ad0421 fix(faza-f): skip local ePay connected/credits gate on gis-ac path
epay-order-button.tsx was disabling the "Comanda CF" button on
!epayStatus.connected even for gis.ac pilot users. The new path
dispatches ePay calls through the orchestrator's shared account
pool — no per-user ePay session or credit balance applies.

Legacy path keeps the original gates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 07:42:07 +03:00
Claude VM afef778612 debug(auth): log jwt callback state + re-expose session.debug
Still hitting invalid_token. Need to see jwt callback behavior live —
why is refresh not firing for Marius?
2026-05-19 07:31:06 +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 21a058b429 feat(faza-f): ePay/CF backend swap — /api/cf/* proxies to gis-api
Plan 003 Faza F. Pilot users (session.useGisAc=true) get their CF
extract flow routed through api.gis.ac (RLS-filtered, RLS-owned
writes); everyone else keeps the legacy /api/ancpi/* path
unchanged. Feature-flag preserves rollback.

New routes (5):
- POST /api/cf/order        → gisApi.enrichment.cf.create. Forwards
  409 catalog_hit verbatim.
- GET  /api/cf/orders       → gisApi.enrichment.cf.list (limit, offset, status).
- GET  /api/cf/[id]         → gisApi.enrichment.cf.get.
- PATCH /api/cf/[id]        → gisApi.enrichment.cf.patch.
- GET  /api/cf/[id]/pdf     → streams gisApi.enrichment.cf.getPdf
  through to browser. Filename from documentName via cf.get; falls
  back to cf-<id>.pdf.
- GET  /api/cf/catalog      → gisApi.enrichment.catalog.

All use getAuthSession() → 401 on no session, forward GisApiError
status+code+body, fallback {error:"internal_error", hint} at 500.
runtime=nodejs, dynamic=force-dynamic.

Helper module `cf-api-base.ts`:
- cfApiBase(useGisAc) → "/api/cf" | "/api/ancpi"
- adaptCfRow(row) → maps gisApi.CfExtractRow into the UI shape
  expected by epay-tab.tsx (CfExtractRecord). Fields not in gis-api
  (siruta, judetName, uatName, errorMessage, etc.) default to
  empty/zero — filter-by-judet/uat on the pilot path is reduced
  until gis-api enriches the response.
- fetchCfOrdersList, fetchCfHasCompletedForCadastral, placeCfOrder,
  cfDownloadUrl — used by components.

UI changes:
- epay-tab.tsx: reads session.useGisAc; list fetch, reorder, single
  + bulk download routed via helpers. UI shape unchanged.
- epay-order-button.tsx: existence check uses catalog endpoint on
  gis-ac path; order placement uses placeCfOrder which treats 409
  catalog_hit as a soft success ("Extras CF valid").

Known gaps (followups):
- /api/ancpi/session still serves ePay session/credits — no gis-api
  equivalent today. epay-connect.tsx untouched.
- ZIP bulk download has no gis-api analog; "Descarcă tot" falls back
  to N tabs on gis-ac path.
- src/app/api/geoportal/cf-status returns hardcoded /api/ancpi/download
  URL — separate followup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:11:55 +03:00
Claude VM 3d389bf10a debug(gis-search): re-add minimal diagnostic for intermittent failures
Logs session + token + duration on every search. Will revert after
cause identified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:07:19 +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 1c6efb9d78 debug(auth): expose session.debug={hasRefreshToken, expiresIn}
Temporary — verify whether Marius's JWT has a refresh_token. Will
revert once cause is identified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:46:47 +03:00
Claude VM 382940112f feat(ops): VersionWatcher — toast prompt when a new deploy is live
Client-side polling component mounted in providers.tsx. At mount,
captures the initial commit from /api/version. Every 60s, re-checks.
If commit differs from the captured one → renders a dismissible toast
in the bottom-right offering a hard reload.

Useful because Next.js bundles cache per commit hash → after a deploy
users would otherwise keep running the old client until they manually
refresh. Now they get a discoverable nudge.

Banner UX:
- "Versiune nouă disponibilă: <shortSha> · apasă pentru reîncărcare"
- [Reîncarcă] button (window.location.reload)
- [X] dismiss for current page life
- Tailwind animate-in fade slide-from-bottom

Polling interval 60s is fine for our deploy frequency; cheap (one
GET per minute, ~150 bytes). Cache-busted with cache: "no-store".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:16:18 +03:00
Claude VM 64bccdb4b0 feat(ops): /api/version endpoint with git SHA + build time
Adds a public, no-auth endpoint at /api/version that returns:
  { commit, commitShort, buildTime, nodeEnv, cutover, nextVersion }

Build-time injection via GIT_COMMIT + BUILD_TIME ARG/ENV propagated
from compose build.args through Dockerfile builder + runner stages.
Excluded from middleware auth gating.

Deploy command (run on satra after git pull):
  GIT_COMMIT=$(git rev-parse HEAD) \
    BUILD_TIME=$(date -u +%FT%TZ) \
    docker compose build architools

Without these env vars, falls back to "unknown" so the build never
fails; only the endpoint shows reduced info.

Useful for: confirming what's actually deployed after CI, cross-app
deploy correlation (api.gis.ac, eterra.live, orchestrator), uptime
monitors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:06:35 +03:00
Claude VM 6054d083b5 fix(faza-e): refresh dedup, fetch timeout, error surfacing
4 fixes for the symptoms Marius hit on Faza E pilot — search returning
"Eroare la căutare" after sessions got stale, even after relogin:

1. Refresh deduplication
   Authentik rotates refresh_tokens — exchange-once. Parallel map +
   search + parcela.get all hit jwt callback concurrently, each fires
   its own refresh, the first wins, the rest get invalid_grant and
   poison the JWT with token.error=RefreshAccessTokenError → user
   appears logged out for no good reason. Cache the inflight refresh
   promise in-memory keyed by refresh_token so concurrent callers
   share one Authentik exchange.

2. Fetch timeout in gis-api-client
   AbortSignal.timeout(30s) on every api.gis.ac call. Without it, a
   slow upstream (ANCPI scrape, orchestrator hiccup) hangs the route
   for the full Next.js default → Marius saw 10s gaps with no
   feedback. Throws GisApiError(504, upstream_timeout) instead.

3. Better error surfacing
   /api/gis/* routes return { error, hint: <first 200 chars> } on
   non-GisApiError throws instead of a bare "internal_error". Easier
   to triage from browser DevTools without paging through container
   logs.

4. Remove diagnostic [gis-search] logs
   Diagnostic served its purpose (identified the stale-token cause
   pre-refresh-fix). Now noise; keep only [auth] refresh success/fail
   + per-route internal_error.

Also adds AbortSignal.timeout(8s) on the Authentik refresh fetch
itself to keep the jwt callback bounded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:58:16 +03:00
Claude VM 47ca366984 fix(auth): Authentik access_token refresh flow
Authentik provider pk=6 issues access tokens with 5-minute TTL but
NextAuth's JWT cookie lives 30 days. Without refresh, every api.gis.ac
call after the first 5 minutes returned 401 invalid_token — the exact
failure Marius hit on first Faza E pilot test.

Implementation:
- jwt callback captures account.refresh_token + account.expires_at on
  first sign-in alongside access_token.
- Before each jwt issuance, if access_token is within 30s of expiry
  and a refresh_token exists, POST to {issuer}/token/ with
  grant_type=refresh_token + client_id + client_secret. Update token
  with the new access_token + expiry + (rotated) refresh_token.
- On failure, set token.error="RefreshAccessTokenError" and stop
  trying (avoid hot-loop). Surfaced via session.error so client UI
  can prompt re-login.

AUTHENTIK_SCOPES updated in Infisical to include `offline_access` so
Authentik issues a refresh_token on first sign-in (standard OAuth2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:13:19 +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 7a22b11b54 debug(gis-search): log session presence + access token presence
Temporary diagnostic for Faza E debugging — Marius reports search
returning "Eroare la căutare" after relogin. Need to confirm whether
session.accessToken is reaching the route.

Will revert/clean up once cause identified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:30:24 +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 fc2bdfb2b4 feat(gis-api): Faza D thin client lib (src/lib/gis-api-client.ts)
Server-side wrapper around api.gis.ac. Auto-extracts Authentik
access_token from NextAuth session via getServerSession. Full surface
covering 15 endpoints + RateLimit / GisApiError types:

- me, parcela.get, search
- parcel.{tech, unitsFetch, immApps}
- building.{tech, condoOwners}
- enrichment.cf.{list, get, create, patch, uploadPdf, getPdf}
- enrichment.catalog

Streaming PDF endpoint (getPdf) returns raw Response so caller can
forward to browser without buffering. uploadPdf accepts ArrayBuffer/
Uint8Array/Blob with optional X-Document-Name header.

Rate-limit headers (X-RateLimit-{Limit,Remaining,Reset}) parsed and
attached to GisApiError on 429. No correlationId in requests — gis-api
overwrites server-side (per project_audit_correlation_echo memory).

GIS_API_URL env (Infisical /architools) defaults to https://api.gis.ac.

Dormant until Faza E geoportal + Faza F ePay backend swap import it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:22:05 +03:00
Claude VM 977db6d63a feat(cutover): Faza C feature-flag infra for api.gis.ac
Server-side helper useGisAcFlag(email) → boolean, gated by:
- USE_GIS_AC=1 (global rollout switch), OR
- GIS_AC_PILOT_USERS=a@x,b@y (per-email staged rollout)

Both defaults are off (USE_GIS_AC=0, pilot list empty) in Infisical
/architools — this PR is dormant; no call sites consume the flag yet.
Future Faza D/E call sites in src/lib/gis-api-client.ts and
src/modules/geoportal/* will branch on it.

Exposed on session.useGisAc so client components can branch identically
to server routes without a separate API roundtrip. Re-evaluated per
request → flag flip via Infisical + container restart, no rebuild.

Per-user override (PILOT_USERS) is the rollout vehicle:
1. Deploy with flag=0 (default) → nothing changes
2. Set GIS_AC_PILOT_USERS=marius@... → Marius sees new code path
3. Watch 24-48h → set USE_GIS_AC=1 → global cutover
4. Rollback = unset USE_GIS_AC

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:16:43 +03:00
Claude VM 403b6b37f1 feat(auth): Faza B NextAuth Authentik scope=enrichment + forward access_token
Authentik OIDC provider now requests `openid email profile enrichment`
(from AUTHENTIK_SCOPES env, Infisical-fetched at boot). The enrichment
scope triggers Authentik scope mapping pk=41b23bc3-bdd8-4a61-b975-
6e0eff56df72 which emits enrichment_scope + is_beletage_group claims
based on LDAP group membership (Arhitecti/Administrators/Domain Admins
→ scope=full + is_beletage_group=true).

jwt callback captures account.access_token on first sign-in; session
callback exposes it as session.accessToken so api.gis.ac calls can
forward it. Used by Faza D thin client (src/lib/gis-api-client.ts,
pending) to authenticate against api.gis.ac.

Without scope=enrichment, every architools user falls through to
scope=none on api.gis.ac → 403 on every parcel/enrichment read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:01:16 +03:00
Claude VM 54b78c2dcf feat(deploy): Faza A Infisical runtime migration
Stripped 35-var environment block from docker-compose.yml to 5 bootstrap
vars (INFISICAL_CLIENT_ID/SECRET, NODE_ENV, PORT, HOSTNAME). All app
secrets now fetched from Infisical /architools at container boot via
docker-entrypoint.sh (modeled on gis-api's pattern, INFISICAL_APP_PATH
=/architools).

- docker-entrypoint.sh: universal-auth login, fetch /architools + /
  root, expand ${/VAR} refs, export, exec CMD. Fails loud on Infisical
  unreachable (exit 2/3).
- Dockerfile runner: added curl+jq, COPY entrypoint + chmod +x,
  ENTRYPOINT ["/app/docker-entrypoint.sh"]
- compose: build args (NEXT_PUBLIC_*) preserved — build-time inlining
  into JS bundle. martin/tile-cache/tippecanoe service env blocks
  untouched (legacy, removed in Faza E).

Rotation workflow now: Infisical UI -> ssh satra "cd /opt/architools && docker compose up -d --force-recreate architools". Never docker compose restart (does not refetch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:41:22 +03:00
Claude VM 6b3d56e1e8 refactor(deploy): externalize all secrets to .env, migrate Brevo SMTP → REST API
- docker-compose.yml: replace 43 hardcoded env values with ${VAR} references.
  Operators must provide /opt/architools/.env (chmod 600, gitignored) with the
  matching keys. Removes the historical leak surface where every edit risked
  echoing secrets.
- email-service.ts: drop nodemailer SMTP transport; use Brevo REST API
  (POST https://api.brevo.com/v3/smtp/email) with BREVO_API_KEY header.
  Brevo SMTP relay credentials have been deleted upstream.
- package.json: remove nodemailer + @types/nodemailer.

NOTE: legacy hardcoded credentials present in git history must still be
rotated separately (DB password, Authentik client secret, ENCRYPTION_SECRET,
ANCPI password, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:49:08 +03:00
Claude VM 265e1c934b chore(parcel-sync): disable auto-refresh scheduler during GIS DB overhaul
Prevents nightly delta sync (Mon-Fri 01-05) and weekend deep sync
(Fri-Sun 23-04) from writing to GisFeature/GisUat while the schema
is being reworked. Re-enable by uncommenting the import in
src/instrumentation.ts once the new DB layout is stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:34:47 +03:00
Claude VM ddf27d9b17 fix(webhook): treat HTTP 409 (rebuild already running) as success, not error
The pmtiles-webhook returns 409 when a rebuild is already in progress.
Previously this was treated as a failure, showing 'Webhook PMTiles
indisponibil' error to the user. Now 409 is handled as a valid state
with appropriate messaging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:29:01 +03:00
Claude VM 377b88c48d feat(sync): auto-trigger PMTiles rebuild after sync + fix progress display
- Add pmtiles-webhook.ts shared helper for triggering PMTiles rebuild
- sync-county: trigger rebuild when new features synced, pass jobId to
  syncLayer for sub-progress, update % after UAT completion (not before)
- sync-all-counties: same progress fix + rebuild trigger at end
- geoportal monitor: use shared helper instead of raw fetch
- weekend-deep-sync + auto-refresh: consolidate webhook code via helper
- docker-compose: default N8N_WEBHOOK_URL to pmtiles-webhook on satra:9876

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:59:18 +03:00
Claude VM b356e70148 fix(session-store): rename globalThis key collision between session-store and eterra-client
Both session-store.ts and eterra-client.ts used globalThis.__eterraSessionStore
but for completely different purposes (EterraSession vs Map<string, SessionEntry>).
The Map from eterra-client made getSessionStatus() report connected: true on
server start (Map is truthy), while getSessionCredentials() returned undefined
username/password — causing "Credentiale lipsa" on sync attempts despite the
UI showing a green "Conectat" dot.

Renamed eterra-client's global keys to __eterraClientCache and
__eterraClientCleanupTimer to eliminate the collision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:36:33 +03:00
Claude VM 708e550d06 fix(parcel-sync): allow DB download regardless of layer freshness
The top export buttons required all primary layers to be "fresh" (<7 days)
before using the DB download path. When stale, they fell through to live
eTerra sync which requires credentials — blocking users who just want to
download existing data. Now any UAT with data in DB uses the local export
path directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:16:41 +03:00