Adversarial review (9 agents) of f7f7c59..28c870f found 4 confirmed bugs in
the hardening itself; all fixed:
1. Parallel-download index race: two items with the SAME nrCadastral in one
batch both scanned MinIO, both computed index 1, the second putObject
silently overwrote the first paid extract. Pre-allocate per-cadastral
indices sequentially before the parallel block; storeCfExtract takes an
explicit index (epay-queue.ts, epay-storage.ts).
2. Metadata-fail orphan charge: on saveMetadata failure the row was popped
from cleanup tracking even when deleteCartItem was NOT confirmed, leaving
an undeletable metadata-less row in the global cart that submitOrder would
check out and charge. Now: pop only on confirmed delete; if unconfirmed,
mark cartDirty and ABORT before submit (epay-queue.ts).
3. Recover vs live queue race: the widened recover WHERE (orderId:null +
cart/ordering/... states) could scoop a concurrently-processing batch's
rows and re-stamp them with the wrong orderId. Block recover while
getQueueStatus().processing (recover/route.ts).
4. 'review' status leaked as 'done' in the geoportal CF-order modal (minioPath
short-circuit) — handed an unverified PDF as a finished extract. Check
review/failed BEFORE the minioPath fallback (cf-order-modal.tsx).
Plus 2 nits: download-zip excludes 'review' rows server-side; retry button
surfaces recover errors/results instead of swallowing them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live-path hardening from the 2026-06-04 deep-dive (11 confirmed criticals).
ArchiTools-only; the legacy queue is still the sole fulfiller.
Security:
- requireCfAccess() — staff-only, portal accounts blocked, fail-closed
in-route on download / download-zip / cf-status / orders (C4 IDOR/PII)
and order / recover (C3). order also enforces a daily credit cap
(ANCPI_DAILY_CREDIT_CAP, default 200) and stamps userId.
- /api/ancpi/test returns 404 in production — it was a GET that spends 2
real credits, CSRF-able (C5).
- drop the token-metadata debug blob from the session (QW8).
Correctness / robustness:
- cart hygiene (C1): build the ePay cart under an invariant — the Nth add
must report N items; any excess = pre-existing junk, so we wipe + abort
(never submit a cart we didn't fully build). Pre-submit failures clean
up our basket rows; post-submit we never touch the cart (recover owns
it). metadata-less rows are deleted from the cart.
- getOrderStatus fetches the whole order in ONE page (itemsPerPage, QW4);
navDir loop kept only as fallback. index-fallback matches are flagged
'review' instead of silently 'completed' with a possibly-wrong PDF (R4).
- downloadDocument asserts %PDF magic bytes — a login page returned mid
session no longer gets stored as a .pdf (R2). Session reuse TTL aligned
under ANCPI's ~10min expiry.
- recover accepts ?extractId= and pre-submit states; retry buttons in the
ePay tab re-run poll+download with no new charge (QW2/QW3).
Performance:
- parallel document downloads (V1, concurrency 4); poll writes only on
status change via updateMany (QW5); getNextFileIndex scans the cadastral
prefix instead of the whole bucket — and actually works now (it was
^-anchoring the full key, so every file got index 1) (V2); download-zip
streams instead of buffering the whole archive, capped at 100 (V3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Yesterday's pin to /api/ancpi exposed a real architecture split: there
are two CfExtract stores with no overlap and the previous pilot routing
only ever showed one at a time:
architools_postgres.CfExtract → ePay paid orders (type=epay)
gis_core.CfExtract via gis-api → CF intern (type=intern)
The pin made today's 50198 ePay visible but hid the 51 historic intern
rows; the pre-pin state was the opposite. Neither was right — users
think of "my CF extracts" as one timeline regardless of source.
Revert the pin and add client-side merge for pilot users (`useGisAc=true`):
fetchCfOrdersList now fans out to both /api/ancpi/orders and /api/cf/orders
in parallel, normalizes each row through a dedicated adapter (legacy or
gisApi), dedupes by id, and sorts by createdAt descending. fetchCfHas-
CompletedForCadastral checks both backends too (either a fresh intern
or a recent ePay row means "you already have one").
CfExtractRecord grows a required `type: 'epay' | 'intern'` field; the
existing rendering adds a small colored badge (sky=intern, emerald=ePay)
next to the status pill so users can tell where each row came from at
a glance. cfDownloadUrl is now type-aware — intern rows download via
/api/cf/:id/pdf, ePay rows via /api/ancpi/download regardless of pilot
flag, matching how each store keeps its files. Legacy (useGisAc, id)
signature still works for the few call sites that don't have the full
row in scope.
No data was deleted yesterday; the 51 intern rows in gis_core stayed
intact (verified via gis_superuser). The single edit was cancelling
the stuck 354686 pending row from 2026-05-19.
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>
Map tab: when UAT has no local data, shows a "Sincronizează terenuri,
clădiri și intravilan" button that triggers background base sync.
Sync background (base mode): now also syncs LIMITE_INTRAV_DYNAMIC layer
(intravilan boundaries) alongside TERENURI_ACTIVE + CLADIRI_ACTIVE.
Non-critical — if intravilan fails, the rest continues.
Also fixed remaining \u2192 unicode escapes in export/layers/epay tabs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Checkbox on each row (ordered selection → numbered files in ZIP)
- "Descarcă selecție (N)" button appears when items selected
- Tooltip shows position in ZIP: "#1 in ZIP", "#2 in ZIP"
- Select-all checkbox in header
- Tooltips on Descarcă tot + Descarcă selecție buttons
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>