Previous fix searched by cadastralRef and picked the first
layerId-matching result. But cadastral refs collide across UATs:
"354686" exists in multiple counties. The Cluj-Napoca f9bf2ca4-...
parcel with full enrichment got passed over for a same-cad parcel
in another UAT that has no enrichment → panel rendered header +
"Caracteristici" with empty Intravilan, no "Date eTerra" section.
New server-side /api/gis/parcela/find?siruta&cad&layerId proxy:
- gisApi.search(cad) → filter by layerId → up to ~20 candidates
- For each candidate, parcela.get and check stored siruta
- Return the siruta-matching detail
- Fallback: first readable candidate (so the panel still has data
even if siruta mismatch — better than empty)
Panel useEffect simplified: fast path = parcela.get by uuid when the
tile has one, slow path = parcela/find when not. 404 from find sets
the "not in central DB yet" empty state (user can hit Citește din
ANCPI to trigger orchestrator live-fetch).
Diagnostic logs: [gis-parcela-find] siruta=… cad=… layerId=…
candidates=N + per-hit "has_enrich=true keys=N" so we can tell from
container logs whether the right parcel resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of B1 (panel showed "Apasă din ANCPI" even with full enrichment
in DB): PMTiles overview tiles don't carry the GisFeature uuid, only
siruta/cadastral_ref/object_id. The panel's useEffect bailed out at
`!feature.id` and never fetched. So the data was there, the UI just
refused to ask for it.
Fix: when the click feature has no uuid, the panel now calls
`/api/gis/search?q=<cadref>`, filters by layerId match, and uses the
returned id to do `parcela.get(id)`. One extra round trip (~50ms with
the trigram-idx fix from 2026-05-18). For features arriving from the
search dropdown the uuid is already known — that path is unchanged.
Panel redesign — same data shape as eterra.live, ArchiTools styling
(shadcn instead of HeroUI), single-file:
- Header: cadref + layer + area + status chip + close
- Caracteristici: intravilan + categorie folosință + nr corpuri (chips)
- Date eTerra: all enrichment fields, PII passes through gis-api scope
redaction (scope=basic → PROPRIETARI/NR_CF/DOC already null)
- Apartamente (condominium): for CLADIRI_ACTIVE clicks, fetches
/api/gis/building/condo-owners and renders units with owners + cf + area
- Localizare: click lat/lng + Google Maps link + SIRUTA echo
Two new proxy routes (thin wrappers over gis-api):
- POST /api/gis/parcel/units-fetch
- POST /api/gis/building/condo-owners
Basic-panel mode for restricted users (per Marius: "for users I don't
want to give full access to"):
- New env BASIC_PANEL_USERS (csv emails) → session.basicPanel flag
- Optional PANEL_BASIC_GLOBAL=1 to force-basic everyone
- When true, panel renders only header + cadref + suprafață + a
restriction notice; all sections + condo fetch are skipped
- Defaults to off; pilot user Marius gets full panel as before
map-viewer now forwards lngLat on click so the Localizare section has
coordinates without a second lookup.
Type-check clean. Production build (NODE_ENV=production npx next build)
passes. The dev-mode prerender error on / page is pre-existing (Next 16
useContext-null on client component during static export, unrelated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
Logs session + token + duration on every search. Will revert after
cause identified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UAT click previously console.logged only. gis-api search response
doesn't include bbox/centroid, so ArchiTools can't fitBounds locally.
Reuse the deep-link pattern (already used by Export GeoPackage) →
open eterra.live/harta?siruta=X in a new tab. eterra.live has its own
/api/geoportal/uat-bounds + flyTo wired.
Future: add GET /api/v1/uat/:siruta/bounds to gis-api so ArchiTools
can fitBounds inline without leaving the page.
Also reverts the session.debug diagnostic (Marius confirmed
hasRefreshToken=true + expiresIn=293 after attaching offline_access
scope mapping to Authentik provider pk=6 — root cause fixed,
diagnostic no longer needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
Phase 1 of unified sync scheduler:
- New Prisma model GisSyncRule: per-UAT or per-county sync frequency
rules with priority, time windows, step selection (T/C/N/E)
- CRUD API: /api/eterra/sync-rules (list, create, update, delete, bulk)
- Global default frequency via KeyValueStore
- /sync-management page with 3 tabs:
- Reguli: table with filters, add dialog (UAT search + county select)
- Status: stats cards, frequency distribution, coverage overview
- Judete: quick county-level frequency assignment
- Monitor page: link to sync management from eTerra actions section
Rule resolution: UAT-specific > county default > global default.
Scheduler engine (Phase 2) will read these rules to automate syncs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /api/eterra/stats: lightweight polling endpoint (8 parallel Prisma queries, 30s poll)
- /api/eterra/sync-all-counties: iterates all counties in DB sequentially,
syncs TERENURI + CLADIRI + INTRAVILAN + enrichment (magic mode) per UAT
- Monitor page: live stat cards (UATs, parcels, buildings, DB size),
Sync All Romania button with progress tracking at county+UAT level
- Concurrency guard: blocks county sync while all-Romania sync runs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The eTerra connect/disconnect UI and session status were missing from the
deployed monitor page. Also, since ETERRA env vars are empty in the
container, the connect flow now accepts username/password from the UI.
This unblocks county sync which requires an active eTerra session.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same pattern as sync-background: session credentials from eTerra login
take priority, env vars are fallback only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GET /api/eterra/counties — distinct county list from GisUat
- POST /api/eterra/sync-county — background sync all UATs in a county
(TERENURI + CLADIRI + INTRAVILAN), magic mode for enriched UATs,
concurrency guard, creates notification on completion
- In-app notification service (KeyValueStore, CRUD, unread count)
- GET/PATCH /api/notifications/app — list and mark-read endpoints
- NotificationBell component in header with popover + polling
- Monitor page: county select dropdown + SyncTestButton with customBody
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed bg-background/95 to bg-background (opaque) and added
text-foreground to ensure input text is visible on dark theme.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search results now JOIN GisUat to display UAT name prominently instead
of just SIRUTA codes. Map flyTo uses imperative handle instead of
stateful props that re-triggered on re-renders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace old approach (5 stale UATs, 7-day freshness gate, no enrichment)
with new delta engine on ALL UATs:
- Quick-count + VALID_FROM delta for sync (~7s/UAT)
- Rolling doc check + early bailout for magic UATs (~10s/UAT)
- All 43 UATs in ~5-7 minutes instead of 5 UATs in 30+ minutes
- Runs Mon-Fri 1:00-5:00 AM, weekend deep sync unchanged
- Small delays (3-10s) between UATs to be gentle on eTerra
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First-run magic enrichment on partially-enriched UATs can take
30+ minutes per UAT. After first complete run, subsequent runs
will be seconds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoint POST /api/eterra/refresh-all processes all 43 UATs
sequentially. UATs with >30% enrichment get magic mode, others
get base sync only. Each UAT uses the new delta engine (quick-count
+ VALID_FROM + rolling doc check). Progress tracked via progress store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of marking features enrichedAt=null and falling through to the
full enrichment flow (which downloads the entire immovable list ~5min),
the rolling doc check now merges updated PROPRIETARI/DATA_CERERE directly
into existing enrichment and returns immediately.
Also touches enrichedAt on all checked features to rotate the batch,
ensuring different features are checked on each daily run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Always call syncLayer for TERENURI/CLADIRI (not gated by isFresh)
so that quick-count + VALID_FROM delta actually run on daily syncs
- syncLayer handles efficiency internally via quick-count match
- Add 48h freshness check for no-geom import (skip if recent)
- Admin layers: skip if synced within 24h
- Log sync summary (new features, updated features)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cluj-Napoca (54975): base mode, parcele+cladiri only (no magic)
- Feleacu (57582): magic + no-geom (full enrichment test)
- Both with elapsed timer and phase-change logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Quick test button on /monitor page to trigger smart delta sync
(magic mode) on Cluj-Napoca and track progress via polling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix: geoportal/enrich endpoint now looks up CLADIRI_ACTIVE from DB
instead of hardcoding HAS_BUILDING=0, BUILD_LEGAL=0
- Quick-count check: skip OBJECTID comparison when remote==local count
- VALID_FROM delta: detect attribute changes on existing parcels and
mark them for re-enrichment (catches spatial validity changes)
- Early bailout: skip all eTerra API calls when 0 features need enrichment
- Rolling doc check: probe 200 oldest-enriched parcels for new
documentation activity (catches ownership/CF changes VALID_FROM misses)
- Targeted doc fetch: only fetch documentation for immovable PKs that
actually need enrichment instead of all 10k+
Daily sync cost reduced from ~300+ API calls / 1-2h to ~6-10 calls / 10-15s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-deactivate monitoring when status is 'solutionat' or 'respins'
- 'Verifica acum' shows result inline (no page reload/close)
- 'Modifica' button opens edit dialog to fix petitioner/regNumber/date
- Show monitoring section even when inactive (final status visible)
- Display tracking config info (nr, date, petitioner) in detail view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>