372a9c55ea
- .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>
353 lines
12 KiB
Markdown
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
|