Files
ArchiTools/docs/plans/001-central-gis-db-multi-app.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

286 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan 001 — DB GIS Central Multi-App
**Status:** DRAFT v1 — neaprobat, de discutat
**Autor:** Claude + Marius Tarau
**Data:** 2026-04-18
**Scope:** Arhitectură unificată GIS pentru ArchiTools, eTerra.live, SmartCity360, Planhub ERP + servicii viitoare
---
## 1. Context & motivație
Date GIS (cadastru ANCPI, enrichment, PUG/PUZ, UTR) sunt folosite de multiple platforme:
- **ArchiTools** — portal intern Beletage (există, producție)
- **eTerra.live** — SaaS topografi (proxy eTerra, repo nou gitadmin/eterra-live)
- **SmartCity360** — portal GIS primării (în arhitectură)
- **Planhub ERP** — ERP arhitecți cu geoportal (în arhitectură)
- Viitoare servicii
Nevoie: **single source of truth GIS** cu acces controlat, sync centralizat, cache/CDN partajat, izolare multi-tenant.
---
## 2. Topologie
**All on-prem.** Nimic în cloud public.
- Servere: satra (10.10.10.166), shop (10.10.10.84), proxy (10.10.10.199), orchi (10.10.40.60 — CJ2 via VPN)
- Gateway public: Traefik pe proxy, Sophos DNAT 80/443
- Cloudflare doar pentru domenii SaaS public-facing (eTerra.live, SmartCity360)
---
## 3. DB Central — Shop server (10.10.10.84)
**Resurse disponibile (verificat 2026-04-18):**
- Xeon Gold 6430, 128 vCPU, 251GB RAM, 3.5TB NVMe (1% ocupat)
- Rulează deja: Supabase stack, WordPress, Tegola
- Load avg ~0.5, headroom masiv
**Cluster Postgres nou, dedicat:**
- Container `postgres-gis` (separat de Supabase existent — izolare critică)
- Postgres 16 + PostGIS 3.4 + pg_cron
- SRID nativ 3844 (Stereo70)
- Port intern, expus doar pe 10.10.10.0/24 + VPN CJ2 (10.10.40.0/24)
- NICIODATĂ expus direct pe internet
### Schema split
| Schema | Conținut | Access |
|---|---|---|
| `gis_core` | Cadastru național ANCPI: UAT, terenuri, clădiri, administrativ | Public read, write doar sync daemon |
| `gis_urban` | PUG, PUZ, PUD, UTR, zone protecție, zone reglementate | Read public (layer public), write per tenant primărie (RLS) |
| `gis_enrichment` | CF, proprietari, adrese, categorii folosință — **GDPR sensibil** | Auth obligatoriu, RLS pe rol |
| `gis_meta` | Sync runs, rules, status, audit | Internal only |
| `tenants` | Primării, firme arh, clienți SaaS | Control plane |
### Roluri Postgres
- `gis_sync_rw` — write pe `gis_core` + `gis_meta` (sync daemons)
- `gis_public_ro` — read pe `gis_core` + layer public din `gis_urban` (Martin public tiles)
- `gis_app_ro` — read authenticated cu RLS pe `gis_urban` privat + `gis_enrichment`
- `gis_urban_rw` — write pe `gis_urban` doar pentru tenant-ul propriu
### RLS pe multi-tenant
```sql
-- exemplu pentru gis_urban tabele private
CREATE POLICY tenant_isolation ON gis_urban.puz_private
USING (siruta = ANY(current_setting('app.allowed_sirutas', true)::text[]));
```
---
## 4. Layer acces — public/auth endpoints
| Subdomain | Serviciu | Auth | Scop |
|---|---|---|---|
| `tiles.beletage.ro` | Martin (instanță dedicată) | Niciun auth pe layer public | Tile server public cadastru + PUG public |
| `pmtiles.beletage.ro` | nginx static | Niciun auth | Overview Romania, PMTiles pre-generated |
| `gis-api.beletage.ro` | Next.js API sau PostgREST | JWT Authentik | Enrichment, query spațial, mutations |
| `sync.beletage.ro` | Orchestrator UI | Authentik admin | Control panel sync central |
| `eterra.live` | SaaS topografi | Authentik | Portal topografi |
| `planhub.beletage.ro` | ERP arhitecți | Authentik | Geoportal + PM |
| `<primarie>.smartcity360.ro` | Portal primărie | Cetățeni anonim + admin Authentik | Subdomain per tenant |
**Traefik** — reverse proxy unic, SSL auto. **Cloudflare** în față pe domenii SaaS public (eTerra.live, SmartCity360) cu rate limit + CDN cache tile-uri (TTL 24h pentru UAT).
---
## 5. ACL Enrichment (per rol)
| Rol | Vede enrichment |
|---|---|
| `topograf` (eTerra.live) | Full — CF, proprietari, adrese |
| `architect` (Planhub ERP) | Full + PUG complet |
| `admin_primaria` (SmartCity360) | Full pentru UAT-ul primăriei + date PUG private |
| `cetatean` (SmartCity360 public) | Doar nr. cadastral + suprafață + UTR |
| `admin` (ArchiTools) | Tot |
Implementat prin RLS + `current_setting('app.role')`.
---
## 6. Shared enrichment pool — insight cheie
**De-duplicare globală.** Dacă topograf A cere enrichment pe parcela X:
1. Salvat în `gis_enrichment` central
2. Topograf B cere aceeași parcelă → **hit instant**, zero request la eTerra
3. TTL default 90 zile → după expire auto-refresh
Beneficii:
- Economii masive de sesiuni eTerra
- Viteză maximă pentru request-uri repetate
- Baza de date crește cu datele "cele mai cerute" de topografi (natural prioritization)
**TBD:** TTL fix 90 zile vs decizie manuală a topografului pentru "force refresh"?
---
## 7. Sync workers — centralizat
**Extrage din ArchiTools într-un serviciu standalone** `gis-sync-orchestrator`:
- Un singur scheduler (nu duplicat per app)
- Workers:
- `eterra-sync` — delta sync cadastru (nocturn automat + on-demand per UAT)
- `enrichment-worker` — pulls CF/owners pe cerere
- `pug-ingest` (viitor) — import PUG din shapefiles/PDFs
- **Queue:** `pg-boss` (Postgres-backed, zero infra extra) sau BullMQ+Redis
- **Delta sync** folosește `gis_uats.last_updated_dtm` deja existent
- **Notificare apps** la sync complete: `Postgres LISTEN/NOTIFY` + webhook → fiecare app se abonează
- **PMTiles rebuild** trigger-uit de orchestrator după sync major (webhook existent satra:9876)
### Session pool eTerra per-user
- Tabel `eterra_sessions` keyed by `user_id` (fiecare topograf = sesiune proprie)
- Sync on-demand eTerra.live folosește credențialele topografului logat
- Sync nocturn automat folosește cont service (actualul hardcoded)
- Avantaj: toți topografii + sistemul automat contribuie la menținerea DB-ului actual
---
## 8. Consumers
| App | Schemas | Write | DB user | Deploy |
|---|---|---|---|---|
| ArchiTools | core + urban + enrichment | enrichment | `archi_rw` | satra |
| eTerra.live | core + enrichment (ACL topograf) | enrichment via sync | `eterra_ro` + `eterra_sync` | satra/shop TBD |
| SmartCity360 | core + urban (filter siruta) | urban (PUG/PUZ) | `sc_rw` | TBD |
| Planhub ERP | core + urban | proiecte proprii (schemă `erp`) | `erp_rw` | TBD |
Fiecare app:
- Own Authentik OIDC app
- Own DB user + permisiuni minimale
- Own MinIO bucket
- Own Infisical project pentru secrete
---
## 9. Securitate
- **DB niciodată expus direct pe internet** → tot prin Martin / API cu auth
- **Enrichment** (CF, proprietari) → NU în tile-uri publice, doar via API auth'd
- **Rate limit Cloudflare** pe `tiles.*` (ex: 100 req/s per IP)
- **Audit log** pe `gis_urban` în `gis_meta.audit` — cine modifică PUG primăriei
- **Secrete** în Infisical, proiecte separate per app (zero reuse)
- **Backup:** `pg_basebackup` + WAL archiving zilnic → MinIO satra + replică async CJ2 (PITR 14 zile)
---
## 10. Performanță
- Tabele simplificate pe zoom (deja există pentru UAT: z0/5/8/12) — extinde pe `gis_terenuri` dacă nevoie
- **Materialized views** pentru query-uri grele cross-tenant (refresh nocturn)
- **Indexe GIST** pe toate `geom`, **BRIN** pe `created_at` pentru audit
- Martin pool: 16 conexiuni per instanță, 2 instanțe behind Traefik LB
- **Read replica Postgres** când QPS > 500/s (streaming replication)
- **PMTiles** pentru tot ce nu se schimbă zilnic (UAT, administrativ) — mult mai rapid decât Martin live
---
## 11. Scale target an 1
- ~50 arhitecți (Planhub ERP)
- ~100 topografi (eTerra.live)
- ~2 primării × 5 admin = 10 admin_primaria (SmartCity360)
- Citizen public access (SmartCity360) — nedeterminat
**Concluzie:** Shop server (128 vCPU, 251GB RAM) = zero probleme la scale-ul ăsta. Rezerve pentru 10x creștere.
---
## 12. Migrare graduală — fără downtime ArchiTools
1. Setup `postgres-gis` container pe shop, config de bază
2. Replicare initial snapshot `architools_db``postgres-gis` (pg_dump + restore)
3. Creează schemas noi (`gis_urban`, `tenants`), mută tabele existente (`ALTER TABLE SET SCHEMA`)
4. Creează roluri + RLS policies, testează cu user nou
5. Extrage sync în serviciu standalone — paritate funcțională cu ArchiTools
6. Lansează Martin dedicat pe `tiles.beletage.ro`
7. ArchiTools trece pe endpoint nou (feature flag)
8. Verify paritate + performanță → cutover final
9. eTerra.live + SmartCity360 + Planhub ERP conectate la DB central
---
## 13. Date GIS existente (autoritative)
| Tabel | Conținut | Features aprox |
|---|---|---|
| `gis_uats` | Limite UAT Romania (+ views z0/5/8/12 simplified) | 3181 UAT-uri |
| `gis_administrativ` | Intravilan + arii speciale ANCPI | — |
| `gis_terenuri` | Parcele cadastrale eTerra | Cluj 774K (extrapolare ~25M total Romania) |
| `gis_cladiri` | Clădiri cadastrale eTerra | Cluj — (similar magnitude) |
| `gis_terenuri_status` / `gis_cladiri_status` | Parcele/clădiri + flag enrichment/legal | — |
| `gis_features` | Raw eTerra (attributes JSON + enrichment JSON) | — |
| `gis_sync_rules`, `gis_sync_runs` | Orchestrare sync | — |
Status actual: Cluj complet sincronizat (81 UAT, ~8.3h), restul României de populat (estimare 15-25h inițial, 1-2h delta).
---
## 14. Date GIS de adăugat pentru SmartCity360 + ERP
**Urbanism PUG:**
- `gis_urban.utr` — Unități Teritoriale Referință (code, zone_type, regulation JSON cu POT/CUT/H_max, geom)
- `gis_urban.zone_reglementate` — zone funcționale PUG
- `gis_urban.zone_protectie` — monumente, arheologie, peisaj, sanitară, hidrologică
**PUZ/PUD:**
- `gis_urban.puz` / `gis_urban.pud` — doc_ref, HCL_number, approval_date, status, geom perimetru
**Administrativ suplimentar:**
- `gis_urban.strazi`
- `gis_urban.numerotare`
- `gis_urban.dotari_publice` (școli, spitale, parcuri)
**Fază 2 (opțional):**
- Rețele utilități (apă, canal, gaz, electric, telecom)
- Cadastru verde (arbori, scuaruri)
---
## 15. Întrebări deschise — TBD
### Tehnice
1. **Supabase vs Postgres raw pe shop** — schemă nouă în Supabase existent (câștig: RLS auto, PostgREST gratis, auth) sau container Postgres separat (izolare totală, simplu)? **Recomandare curentă:** separat.
2. **Domain SaaS**`eterra.live` deja deținut? `smartcity360.ro` de cumpărat? Subdomain per primărie (`cluj-napoca.smartcity360.ro`) vs path-based (`smartcity360.ro/cluj-napoca`)?
3. **Portal cetățeni SmartCity** — acces complet anonim (zero login, doar rate limit) vs email/captcha pentru abuse prevention?
4. **TTL enrichment** — 90 zile auto-refresh implicit? Topograf poate forța refresh manual?
5. **Backup offsite** — replică Postgres async la CJ2 (10.10.40.x) via VPN? Sau doar MinIO snapshot zilnic?
6. **Migrare ArchiTools** — DB-ul actual rămâne paralel cu noul central, apoi cutover? Sau migrare directă cu downtime controlat?
7. **Martin shared vs per-app** — o singură instanță Martin publică pentru toate app-urile, sau instanțe separate per app (branding URL + izolare)?
### Business / organizaționale
8. **Billing / metering** — eTerra.live SaaS plătit: how measured? Per sync on-demand? Per enrichment request? Per user/lună?
9. **Data ownership** — dacă topograf X face enrichment pe parcela Y, datele sunt ale lui privat sau contribuie automat la pool-ul central public? (răspuns parțial: shared pool, dar GDPR?)
10. **PUG private pentru primării** — ce date specific sunt "private" vs "public"? (ex: observații interne, documentație anexă)
11. **SLA public tiles** — uptime target? Recovery time după outage?
---
## 16. Concluzii curente
- Arhitectura e fezabilă pe infrastructura existentă fără hardware nou
- Shop server = candidat ideal DB central
- Shared enrichment pool = insight major cu ROI imediat
- Migrarea se poate face gradual fără downtime
- Securitate + multi-tenancy rezolvate cu RLS + Authentik per-app
**Next steps când e aprobat:**
1. Răspuns la întrebările deschise (secțiunea 15)
2. Design detaliat schema `gis_urban` (DDL concret)
3. Design API `gis-api.beletage.ro` (endpoints + auth flow)
4. Plan migrare ArchiTools pas-cu-pas
5. POC postgres-gis pe shop + test Martin + 1 app consumer