Files
ArchiTools/docs/plans/002-architools-thin-client-review.md
T
Claude VM 28c870fb12 harden(epay): cart-hygiene invariant uses confirmed cart count + add service architecture plan
- cartCount tracks actual cart rows (decrement only on confirmed delete) so a
  failed cleanup delete can't trigger a false dirty-cart abort.
- docs/plans/006: the multi-tenant CF-service architecture (DB-backed
  fulfiller, account pool, catalog dedup, per-tenant credential model,
  reversible flag flip) — the executable next phase. The Phase-F flag flip is
  gated on the orchestrator fulfiller existing (Plan 003 Faza F was wrong).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:06:06 +03:00

12 KiB

Plan 002 — ArchiTools thin-client migration: review feedback from eterra.live side

Date: 2026-05-17 Reviewer: Claude session in /home/orchestrator/Code/eterra-live (Marius) Context: Reviews the "Faza 0..7" plan proposed by the architools claude that routes architools enrichment through eterra.live.


TL;DR — change of direction

The original plan made eterra.live the gateway for architools/planhub enrichment via POST /api/internal/enrich. This is the wrong boundary. The correct gateway is api.gis.ac (the gis-api service on shop), which is already designed for cross-app consumption (JWT + RLS + scope filtering, Hono on 10.10.10.84:3100, fronted by Traefik).

eterra.live is a single-instance Next.js end-user product. Putting architools' geoportal behind it makes eterra.live a SPOF — any deploy/restart breaks architools cadastre. The right model treats both architools AND eterra.live as peer consumers of api.gis.ac.

eterra.live already migrated to api.gis.ac for CfExtract (Sprint 2 Day 3-4, 2026-04-20). Same migration pattern applies to architools, just bigger surface area.


What's already in place (don't reinvent)

api.gis.ac exists in production:

  • Repo: gitadmin/gis-api, local clone ~/Code/gis-api
  • Stack: Next.js 16.1 + Hono 4 + Prisma 6.19 + Zod 4 + jose 6
  • Auth: HS256 JWT today (GIS_API_DEV_JWT_SECRET) — migrating to Authentik OIDC before architools cutover (decision locked 2026-05-17)
  • Claims: { sub, org_ids[], is_beletage_group, enrichment_scope: "none"|"basic"|"full", email? }tenant claim being added
  • RLS: withUserContext() sets app.user_id / org_ids / is_beletage_group / enrichment_scope per Postgres tx
  • Live endpoints (under /api/v1/):
    • GET /me — claims echo
    • GET /parcela/:id — full GisFeature read
    • GET /search?q=&limit= — UAT + cadastralRef text search
    • GET/POST/PATCH /enrichment/cf* — CfExtract CRUD with RLS
    • GET /enrichment/catalog/:nrCadastral — catalog metadata
    • GET/POST /enrichment/cf/:id/pdf — MinIO PDF stream
  • RLS test suite: 6 scenarios, asserts BOTH HTTP and direct-SQL paths (CI: .gitea/workflows/rls-test.yml)

What it does NOT have yet (gaps to close before architools cutover):

  1. Authentik OIDC (HS256 still in use)
  2. Live-fetch proxy endpoints to orchestrator (parcel/tech, parcel/units, building/tech, building/condo-owners, imm-apps)
  3. Scope-based field filtering on /parcela/:id (currently leaks enrichment.PROPRIETARI regardless of scope)
  4. tenant claim in JWT
  5. CORS allowlist for cross-origin app calls
  6. Per-tenant rate limiting
  7. Audit log table (gis_meta.api_audit)

These will be added in a dedicated gis-api session before architools starts consuming. Full spec is in gis-api project memory (~/.claude/projects/-home-orchestrator-Code-gis-api/memory/):

  • project_architools_planhub_plan.md — endpoints to add, in priority order
  • feedback_jwt_authentik_decision.md — Authentik OIDC spec (scope mapping from LDAP groups)
  • reference_orchestrator_endpoints.md — exact contracts for the 5 LAN endpoints to proxy
  • reference_cross_app_architecture.md — the diagram + consumer responsibility matrix

Revised plan (replaces architools claude's Faza 0..7)

Faza A — Wait for gis-api gaps to close (managed in gis-api repo, ~3 days)

Driven from ~/Code/gis-api in a separate session. NOT architools' work to do, but architools depends on it. Deliverables:

  1. PR1 (gis-api): Authentik OIDC support alongside HS256 (src/lib/auth.ts issuer-based dual path). Add tenant claim. Eterra.live keeps working on HS256 during the overlap. Provisioning needed in Authentik:
    • New OAuth2 application + provider with slug gis-api
    • Property mappings for enrichment_scope derived from LDAP groups:
      • beletage-staffenrichment_scope=full, is_beletage_group=true
      • planhub-profull
      • planhub-free / no match → basic
      • missing claim → REJECT
    • tenant claim from aud (application slug)
  2. PR2 (gis-api): 5 proxy endpoints + scope filter on /parcela/:id + CORS allowlist:
    POST /api/v1/parcel/tech              → orchestrator POST /api/v1/parcel/tech
    POST /api/v1/parcel/units/fetch       → orchestrator POST /api/v1/parcel-units/fetch
    POST /api/v1/parcel/imm-apps          → orchestrator POST /api/v1/imm-apps
    POST /api/v1/building/tech            → orchestrator POST /api/v1/building/tech
    POST /api/v1/building/condo-owners    → orchestrator POST /api/v1/building/condo-owners
    
    All require enrichment_scope >= basic. Rewrite correlationId server-side to ${tenant}:${user_sub[:8]}:${requestId}.
  3. PR3 (gis-api): Per-tenant rate limit + gis_meta.api_audit migration.

When PR1+PR2 are green and deployed, signal back to this plan and proceed with Faza B.

Faza B — ArchiTools secrets + Authentik client (½ day)

  • In Authentik: create OAuth2 client for architools (slug architools-app). Configure redirect URI for its NextAuth flow.
  • Infisical adds (prod env, path /):
    • AUTHENTIK_CLIENT_ID / AUTHENTIK_CLIENT_SECRET / AUTHENTIK_ISSUER / AUTHENTIK_JWKS_URL for architools
    • GIS_API_URL=https://api.gis.ac
    • NEXT_PUBLIC_MARTIN_URL=https://tiles.gis.ac
    • NEXT_PUBLIC_PMTILES_URL=https://pmtiles.gis.ac/overview.pmtiles
  • NextAuth on architools: add Authentik OIDC provider. Store access_token in session. ⚠️ DO NOT mint HS256 JWTs server-side anymore — pass the Authentik access_token directly as Bearer to gis-api.

Faza C — Rip-out (1 day, ONE PR, feature-flagged)

NEXT_PUBLIC_USE_GIS_AC=1 flag controls cutover. With flag on:

  • Geoportal map sources flip to pmtiles.gis.ac + tiles.gis.ac
  • All src/app/api/eterra/* and src/app/api/ancpi/* route handlers replaced by thin wrappers that forward to api.gis.ac with the user's access_token
  • All src/modules/parcel-sync/** and parcel-sync UI removed
  • src/config/{modules,navigation,flags}.ts cleanup

With flag off: old behavior retained. Do not drop Prisma tables (GisFeature, GisUat, GisSyncRun, GisSyncRule, CfExtract) in this PR. Keep them as dead columns for 2-3 days post-cutover so rollback stays cheap. Drop in a follow-up PR after validation in prod.

Stop martin container on satra ONLY after flag-on is verified working in prod. The PMTiles webhook on satra:9876 — does not exist; the architools claude's plan had this wrong. The actual PMTiles trigger is the orchestrator cron at 03:00 EEST + admin button in eterra.live, hitting gis-tippecanoe-builder on shop at port 9876.

Faza D — Thin client (1-2 days)

  • src/lib/gis-api-client.ts — fetch wrapper with the Authentik access_token from session
    • gisApi.parcela.get(id)GET /api/v1/parcela/:id
    • gisApi.search(q, limit)GET /api/v1/search
    • gisApi.parcel.tech({siruta, cadastralRef, force})POST /api/v1/parcel/tech
    • gisApi.parcel.unitsFetch(...)POST /api/v1/parcel/units/fetch
    • gisApi.parcel.immApps(...)POST /api/v1/parcel/imm-apps
    • gisApi.building.tech(...)POST /api/v1/building/tech
    • gisApi.building.condoOwners(...)POST /api/v1/building/condo-owners
    • gisApi.enrichment.cf.list/get/create/patch/uploadPdf/getPdf/getCatalog (mirror eterra.live's existing usage)
  • NO separate scope-mapper.ts — gis-api resolves scope from Authentik claims itself.
  • NO separate gis-api-token.ts — Authentik access_token is the JWT, NextAuth session already has it.

Faza E — Geoportal rewrite (2 days)

  • map-viewer.tsx: sources pmtiles://pmtiles.gis.ac/overview.pmtiles + Martin tiles.gis.ac/{view}/{z}/{x}/{y}. Drop satra Martin entirely.
  • search-bar.tsxgisApi.search().
  • feature-info-panel.tsxgisApi.parcela.get() + (on missing enrichment) gisApi.parcel.tech() + gisApi.parcel.unitsFetch() for buildings.
  • basemap-switcher.tsx: same config as eterra.live (liberty/dark/satellite/orto/topo50). Source files are public CSS/tile URLs; literal copy is OK.
  • Decision for boundary-check, cf-status, export, pad, piz: if architools keeps them, rewrite as thin proxies to api.gis.ac equivalents (which may need new endpoints — defer that scoping until each is actually needed).

Faza F — ePay / CF ordering (½ day)

  • Architools UI for "Comandă extras CF" calls gisApi.enrichment.cf.create({nrCadastral, type, ...}) directly.
  • Status + download list: gisApi.enrichment.cf.list({...}) and getPdf(id).
  • The tenant=architools claim in the access_token tells gis-api which billing account to attribute the order to (future-work for billing routing, but the field is in place from Faza A).
  • Delete src/app/api/ancpi/* entirely.

Faza G — Test E2E + deploy (½ day)

  • /geoportal in architools renders tiles from gis.ac, click parcel → enrichment via api.gis.ac → live-fetch fallback triggers orchestrator.
  • Verify zero references to architools_postgres.GisFeature remain in code AND that no DB query goes to the deprecated tables.
  • Type-check + lint + build clean.
  • Commit + push + Portainer redeploy.
  • 24h grace period: old stack (Martin satra, deprecated parcel-sync code) kept around. After validation, follow-up PR drops Prisma tables + stops martin satra container.

Faza H — Planhub (separate PR, post-architools)

  • Same pattern. Different scope mapping: planhub-freebasic, planhub-profull.
  • Already covered by gis-api's tenant claim from Faza A.

Specific corrections to the original plan

  1. POST /api/internal/enrich on eterra.live — REMOVE. Use api.gis.ac directly.
  2. Bearer $ARCHITOOLS_INTERNAL_TOKEN static token — REMOVE. Use Authentik OIDC access_token.
  3. GIS_API_JWT_SECRET (shared) in Infisical — REMOVE. With Authentik OIDC, gis-api fetches JWKS from https://auth.beletage.ro/application/o/gis-api/jwks/ — no shared secrets between apps.
  4. scope-mapper.ts in architools — REMOVE. Scope is in the JWT claims; architools doesn't compute it.
  5. PMTiles webhook satra:9876 — does not exist. The builder is gis-tippecanoe-builder on shop:9876, fired by orchestrator cron + eterra.live admin button.
  6. Single-PR rip-out — replace with feature-flagged migration (NEXT_PUBLIC_USE_GIS_AC=1), 2-3 day overlap, drop dead code in a separate follow-up PR after prod validation.
  7. eterralive-coordinator terminology — drop. The pool is "owned" by gis-sync-orchestrator (LAN-only on shop:3101). gis-api is the auth/scope/audit gateway in front of it.

Open questions

  1. Authentik OIDC client provisioning — needs to be done first; until then, no architools migration. Marius or a sys-admin session needs to set up the OAuth2 provider in Authentik UI.
  2. Audit table schema — does gis_meta already have an api_audit table? Probably no. Migration needed in same PR as proxy endpoints.
  3. Rate-limit defaults — what's the cap per tenant? 100 req/min seems safe; configurable per tenant via Infisical JSON env. Confirm with Marius.
  4. Architools' archi.* schema writes — currently architools writes Canvas, Job, RegistryAudit, RgiSearchTemplate directly via its own Prisma. Migration here is OPTIONAL for the cadastre work; could be done in a later sprint. Don't bundle it.

Reference docs

  • gis-api project memory: ~/.claude/projects/-home-orchestrator-Code-gis-api/memory/MEMORY.md
  • Plan 001 v2 master: ~/Code/ArchiTools/docs/plans/001-v2-central-gis-db-multi-app.md
  • Tile data sources / cache / gis.ac rule: ~/Code/busc-infra/references/TILE-DATA-SOURCES.md
  • Orchestrator workers + schema: ~/Code/busc-infra/references/SPRINT3.md
  • GIS stack infrastructure: ~/Code/busc-infra/references/GIS-STACK.md
  • Authentik configuration: ~/Code/busc-infra/references/AUTHENTIK.md
  • Sprint 2 (gis-api delivery): ~/Code/busc-infra/references/SPRINT2.md