Files
ArchiTools/docs/plans/005-gis-api-export-endpoints.md
T
Claude VM 372a9c55ea chore: clean .gitignore (utf-16 noise) + mark plan 005 shipped
- .gitignore had 2 lines saved as UTF-16-LE (temp-db-check.cjs and
  .playwright-mcp), so the patterns weren't actually ignoring those
  files. Rewrote in plain UTF-8.
- Plan 005 (gis-api export endpoints) was marked "not yet built" but
  gis-api commit bbd6e7c shipped all five endpoints on 2026-05-21 with
  38 new tests; update the status block to reflect that, including the
  one open caveat (PIZ basemap=orto still 501).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:32:33 +03:00

353 lines
12 KiB
Markdown

# Plan 005 — gis-api export endpoints (PIZ / PAD / Coord / DXF / CF intern)
**Date:** 2026-05-21 (updated 2026-05-27)
**Author:** ArchiTools session (V2 panel toolbar)
**Owner of execution:** gis-api session (`/home/orchestrator/Code/gis-api`)
**Status:** SHIPPED — gis-api commit `bbd6e7c` (2026-05-21) added all five
endpoints (`/parcel/:id/{piz,pad,coords-xlsx,dxf}` + `/enrichment/cf-intern`)
with 38 new tests. PIZ basemap=orto still returns 501 awaiting an
orchestrator basemap proxy; arch UI disables that button (commit `5cfa6c8`).
Architools toolbar is fully functional for all four exports via the default
basemap. CF intern works end-to-end (verified live with curl + DB rows from
2026-05-23/24).
---
## Why this exists
The V2 panel on ArchiTools now exposes a 4-button export toolbar
(PIZ / Plan situație / Coord. / DXF) plus a 2-button CF row (CF intern /
Extras CF). Every button except "Extras CF" routes through gis-api so a
future eterra.live migration or new app (planhub) consumes the same
endpoints — same code path, same auth, same audit log.
Architots ships the UI + a thin proxy under `/api/gis/parcel/[id]/{piz,
pad,coords,dxf}` and `/api/cf-intern/order`. Each route forwards to
gis-api with the user's Authentik JWT, then streams the response back.
Until these gis-api endpoints exist, those routes surface a 404 from
gis-api as a friendly Romanian message in the modal ("…urmează —
endpoint-ul gis-api se livrează în sesiunea următoare").
Source for the heavy lifting (PDF renderers, basemap fetchers, DXF
writer, ExcelJS layout) is `~/Code/eterra-live/src/`. The relevant
files are listed below per endpoint. Port them into gis-api; keep them
provider-agnostic (no eterra.live user table, no usage tracking).
---
## Shared contract
All endpoints below take the **GisFeature uuid** in the path. Architots
already resolves the uuid via the find chain (uuid → by-ref → search)
and passes it in. The endpoints look up geometry + enrichment via
`gis_core."GisFeature"` (or the orchestrator's enrichment path when a
fresh fetch is needed).
### Auth
- Bearer JWT (Authentik) — same as every other v1 endpoint.
- `enrichment_scope` claim must be at least `basic` for CF intern;
`none` is allowed for the geometry-only exports (PIZ/PAD/Coord/DXF).
- Per-tenant rate limit applies (existing `GIS_API_TENANT_RATE_LIMITS`).
### Errors
| Status | Code | Architots renders… |
|--------|-------------------------|------------------------------------------------------|
| 400 | `signer_required` | "Selectează un semnatar." |
| 400 | `invalid_geometry` | "Geometria parcelei e invalidă." |
| 401 | `unauthorized` | "Autentificare expirată. Reîncarcă pagina." |
| 403 | `enrichment_scope_*` | "Nu ai permisiunea pentru acest export." |
| 404 | `parcel_not_found` | "Parcela nu există în baza gis_core." |
| 502 | `eterra_fetch_failed` | "ANCPI nu răspunde momentan." |
| 503 | `no_available_account` | "Pool-ul ANCPI e temporar epuizat." |
| 504 | `upstream_timeout` | "Timeout la generare. Reîncearcă." |
Architots already handles each of these (see `feature-info-panel.tsx`
error mapping for the deep-enrich endpoint — same shape).
### Signer payload shape
```ts
type Signer = {
kind: "user" | "org"; // PFA vs PJA
displayName: string; // "Dan-Gheorghe Tiurbe" or "Studii de teren SRL"
authClass: string | null; // "Cat. D" / "Clasa III"
authNumber: string; // "RO-B-F/3183"
};
```
gis-api never resolves signers — architools picks them client-side
(merged DB rows + ENV defaults) and sends the final string-only struct
in the request body. gis-api renders verbatim into the PDF footer /
PIZ cartouche. Co-signer rendering follows the same shape.
---
## Endpoint 1: `POST /api/v1/parcel/:id/piz`
PDF — Plan de încadrare în zonă, A4 portrait, scara 1:5000 (1:10000
peste 400 m), parcela conturată cu roșu peste fundal satelit / orto.
### Request body
```ts
{
signer: Signer, // required
coSigner?: Signer | null, // optional second name in footer
basemap?: "google" | "orto", // default "google"
layerId?: "TERENURI_ACTIVE" | "CLADIRI_ACTIVE", // disambiguator
}
```
### Response
`200 application/pdf` with `Content-Disposition: attachment; filename="PZ_<cad>_<UAT>_<YYYY-MM-DD>.pdf"`.
### Port from eterra.live
- `src/app/api/geoportal/piz/route.ts` (logic + filename + content-disposition)
- `src/lib/piz/renderer.ts` (PDF layout)
- `src/lib/piz/google-tiles.ts` (Google satellite tile composite, EPSG:3857)
- `src/lib/piz/overlay-tiles.ts` (Harta 1970 overlay — optional, can ship later)
- `src/lib/eterra-basemap.ts` (ANCPI ortofoto WMS)
- `src/lib/pad/geometry.ts` (`extractVertices`, `computeBBox`)
- `src/lib/auth-display.ts` (`formatSignerOneLine`)
- `src/lib/categorie-folosinta.ts`
### Replace in port
- Drop `getEffectiveSession` → use gis-api's bearer-token + claims context.
- Drop `prisma.eterraUser.findUnique` → use the signer struct from body.
- Drop `resolveSignerIdentity` → trust body.signer (architools-side validated).
- Drop `trackUsage` → optional; can keep a thin audit row in gis_meta if
desired, but skip credit accounting.
- `enrichFeatureIfNeeded` → call the existing
orchestrator path (or auto-trigger `/api/v1/parcel/enrich` internally).
Best-effort: render with whatever's in DB on failure (filename + footer
okay even without NR_CF).
### Special considerations
- Whichever PDF lib eterra.live uses (skia-canvas, pdf-lib, etc.) needs
to be added to gis-api's package.json. Memory says eterra.live runs
Next 16 with skia-canvas — should port cleanly.
- Google tiles require an HTTP fetch grid. Cache per-bbox (Redis or
filesystem) to avoid hammering Google when users repeat exports.
- Ortofoto requires ANCPI WMS auth — needs a service account in
Infisical (`/gis-api/ANCPI_ORTOFOTO_USER` + `_PASS`?).
---
## Endpoint 2: `POST /api/v1/parcel/:id/pad`
PDF — Plan de amplasament și delimitare. Auto-picks scale + paper +
orientation based on geometry; rasters neighbors (parcels +
buildings) within the drawing bbox; renders a cartouche with CF / topo
/ categorie / adresă / coords table.
### Request body
```ts
{
signer: Signer, // required (single signer)
layerId?: "TERENURI_ACTIVE" | "CLADIRI_ACTIVE",
scale?: number, // override autoSelectLayout
paper?: string, // "A4" | "A3" | …
}
```
### Response
`200 application/pdf`, `PS_<cad>_<UAT>_<YYYY-MM-DD>.pdf`.
### Port from eterra.live
- `src/app/api/geoportal/pad/route.ts`
- `src/lib/pad/renderer.ts`
- `src/lib/pad/geometry.ts` (`computeSegments`, `computeArea`,
`computePerimeter`, `autoSelectLayout`, `fillBboxToDrawable`)
- Plus same shared libs as PIZ.
### Special considerations
- Neighbor SQL stays in raw `$queryRaw` (already PostGIS-aware,
intersection with the draw bbox, `isSubject` flag for buildings).
Just point at gis-api's gis_core connection.
- No basemap fetch — PAD is line-drawing only.
---
## Endpoint 3: `GET /api/v1/parcel/:id/coords-xlsx`
XLSX — coordonate Stereo70 per vârf, lungimi segmente, suprafață
calculată Shoelace. 3 sheets (Sumar / Coordonate / Segmente).
### Request
No body. The parcel id in the path is sufficient.
### Response
`200 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,
filename `coordonate_<cad>.xlsx`.
### Port from eterra.live
- `src/app/api/geoportal/coords-xlsx/route.ts` (~280 LOC, mostly
ExcelJS scaffolding). One file, minimal dependencies (`exceljs` +
Prisma) — easiest port; recommend starting here to validate the
contract end-to-end.
### Replace in port
- `prisma.gisFeature.findMany` already targets gis_core in eterra.live
— same shape on gis-api side, just swap the client import.
- Drop `enrichment` and `attributes` reads if scope doesn't permit
(Coord works fine with geometry alone).
---
## Endpoint 4: `POST /api/v1/parcel/:id/dxf`
DXF — parcela ca polilinie închisă pe layer-ul `PARCELA_<cad>`, plus
vecini opționali pe layer `VECINI` și clădiri pe `CLADIRI`. Coordonate
Stereo70.
### Request body
```ts
{
layerId?: "TERENURI_ACTIVE" | "CLADIRI_ACTIVE",
includeNeighbors?: boolean, // default true for TERENURI, false for CLADIRI
}
```
### Response
`200 application/dxf`, filename `<cad>.dxf`.
### Port from eterra.live
- `src/app/api/geoportal/export/route.ts` (single-parcel branch when
`format=dxf`)
- `src/lib/dxf-writer.ts` (the pure-JS DXF emitter — no native deps)
### Replace in port
- Strip the `geojson` + `gpkg` branches — gis-api endpoint is DXF-only
(architools GeoJSON export already exists via the gis-api search path,
and GPKG is rarely useful for single-parcel workflows).
- The neighbor query is similar to PAD's — share the helper.
---
## Endpoint 5: `POST /api/v1/enrichment/cf-intern`
PDF — extras CF din circuitul intern eTerra (`copycf`). Gratuit (nu
consumă credit ePay). Streams the eTerra response directly.
### Request body
```ts
{
nrCadastral: string, // "354686"
siruta: string, // "54975"
}
```
### Response
`200 application/pdf` with the eTerra-generated filename in
`Content-Disposition`.
### Port from eterra.live
- `src/app/api/eterra/cf-intern/route.ts`
- `src/lib/eterra-session.ts` (account pool — likely already partially
shared with `/parcel/enrich` orchestrator path)
- Rate limit: keep eterra.live's `RATE_LIMITS.docDownload` window
(default 10/h per user — tune in gis-api env).
### Replace in port
- Drop usage tracking + per-user eterra session caching tied to
`eterraUser` — use the account pool that already serves enrichment.
- Persist the CF row via gis-api's existing
`prisma.cfExtract` writer (CfExtract table on gis_core or
architools_postgres depending on architecture decision — likely
gis_core for consistency).
### Special considerations
- Already-cached CF (catalog hit) should short-circuit and return the
cached PDF — saves an eTerra round-trip. The orchestrator's
`enrichment/catalog/:nrCadastral` endpoint can drive this.
---
## Implementation order recommendation
1. **Coord-XLSX (Endpoint 3)** — simplest, validates auth + contract.
2. **DXF (Endpoint 4)** — also simple, no PDF lib needed.
3. **PAD (Endpoint 2)** — first PDF endpoint, no basemap; isolates the
skia-canvas / pdf lib selection question.
4. **CF intern (Endpoint 5)** — depends on eTerra account pool; orth-
ogonal to the others.
5. **PIZ (Endpoint 1)** — hardest, needs basemap fetcher + tile cache.
Architots already speaks all five via the thin client
(`gisApi.exports.{piz,pad,coordsXlsx,dxf,cfIntern}` in
`src/lib/gis-api-client.ts`). No architools change required as each
gis-api endpoint ships.
---
## Verification per endpoint
After each ships:
```bash
# From architools cwd, on the live deploy (assumes test cad 354686 in UAT Cluj-Napoca, siruta 54975):
# (Replace <PARCEL_UUID> with the GisFeature uuid for that cadref.)
# Coord
curl -fsS https://tools.beletage.ro/api/gis/parcel/<PARCEL_UUID>/coords \
-H "Cookie: $(...your session cookie...)" -o coord.xlsx
# DXF
curl -fsS https://tools.beletage.ro/api/gis/parcel/<PARCEL_UUID>/dxf \
-X POST -H "Content-Type: application/json" -d '{}' \
-H "Cookie: $(...)" -o test.dxf
# PIZ (substitute signer)
curl -fsS https://tools.beletage.ro/api/gis/parcel/<PARCEL_UUID>/piz \
-X POST -H "Content-Type: application/json" \
-d '{"signer":{"kind":"user","displayName":"Test","authClass":"Cat. D","authNumber":"RO-B-F/3183"},"basemap":"google"}' \
-H "Cookie: $(...)" -o test_piz.pdf
```
Open each in its native viewer (Excel, AutoCAD/QCAD, PDF reader)
to confirm rendering parity with eterra.live's output.
---
## Out of scope for this plan
- Bulk-export multi-parcel (UAT-level) — separate flow, will live at
`/api/v1/uat/:siruta/export` once Coord + DXF singles are stable.
- Mobile-first signer UI — current picker is desktop layout only.
- PIZ overlay raster (Harta 1970) — port last, low priority.
- Cache eviction policy for the Google tile composites — initial impl
can be filesystem with daily rotation.
---
## Related memory
- [[architools-cutover-state-2026-05-20]] — V2 panel state
- [[v2-panel-contract]] — panel section layout
- [[parcel-enrich-contract]] — gis-api /parcel/enrich auto vs manual
- [[gis-api-parcela-by-ref]] — find chain endpoint
- [[architools-auto-deploy-webhook]] — push → live in ~90s