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>
This commit is contained in:
@@ -0,0 +1,822 @@
|
||||
# Plan 001 v2 — DB GIS Central Multi-App
|
||||
|
||||
**Status:** FINAL APPROVED — 2026-04-20 (replaces v1 DRAFT)
|
||||
**Autor:** Marius Tarau + Claude Code
|
||||
**Scope:** Arhitectură unificată GIS pentru ArchiTools, eterra.live, pug.digital, planhub.ro + viitoare servicii
|
||||
**Execution trigger:** user sends "go" in new session → orchestrator begins Sprint 1
|
||||
|
||||
---
|
||||
|
||||
## 0. Cum recunoaște Claude "GO"
|
||||
|
||||
În sesiunea următoare, la cuvântul **"go"** singular:
|
||||
1. Re-read `/home/orchestrator/Code/ArchiTools/docs/plans/001-v2-central-gis-db-multi-app.md` complet
|
||||
2. Read `project_plan001_GO_sequence.md` din memorii
|
||||
3. Verifică secrete pre-loaded în Infisical (listă la §20)
|
||||
4. Pornește **Sprint 1 — Day 1** (§16)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
**Ce construim:** platformă GIS centralizată pe server shop (10.10.10.84) care servește 4 produse (ArchiTools intern, eterra.live SaaS topografi, pug.digital SaaS primării, planhub.ro SaaS arhitecți) + viitoare servicii.
|
||||
|
||||
**Durată:** 5 săptămâni dev + 1 weekend cutover + 1 săptămână cleanup = ~7 săptămâni total.
|
||||
|
||||
**Echipă:** 1 dev (Marius) + Claude Code pair.
|
||||
|
||||
**Rezultat:**
|
||||
- DB unică (PostgreSQL 18.3 + PostGIS 3.6.3) schemă split, RLS multi-tenant
|
||||
- Tile serving unificat (Martin + TiTiler + PMTiles + Cloudflare CDN)
|
||||
- Sync orchestrator cu multi-account ANCPI shuffle
|
||||
- Backup multi-tier (NAS + B2 + viitor Azure + viitor CJ2)
|
||||
- Dashboard unificat admin eterra.live
|
||||
- MDLPA export compliance pentru PUG/PUZ/PUD/PMUD
|
||||
|
||||
---
|
||||
|
||||
## 2. State curent (baseline)
|
||||
|
||||
### Infrastructură
|
||||
- **Shop** (10.10.10.84): Xeon Gold 6430, 128 vCPU, 251GB RAM, 3.3TB NVMe free
|
||||
- **Satra** (10.10.10.166): Ubuntu, 194GB disk (61% plin), architools_postgres curent
|
||||
- **Proxy** (10.10.10.199): Traefik v3.6.8
|
||||
- **NAS NewAmun** (10.10.10.10): NETGEAR ReadyNAS, 40.88TB free, btrfs
|
||||
|
||||
### App-uri curente
|
||||
- ArchiTools (tools.beletage.ro) — Next.js, Prisma, conectat architools_postgres satra
|
||||
- eterra.live — Next.js v5 NextAuth, per-user AES-256-GCM, același DB
|
||||
- Martin tile server v1.4.0 pe satra port 3010
|
||||
|
||||
### POC postgres-gis (2026-04-19)
|
||||
- Deployed pe shop la `/home/dnz/postgres-gis/`
|
||||
- PG 18.3 + PostGIS 3.6.3 + pgBouncer v1.24.1-p1
|
||||
- `architools_db` restored from satra: 16GB, 24 tabele, 100% paritate row counts
|
||||
- Performance test: **9.4× faster** ST_Intersects vs satra
|
||||
- Status: **RUNNING sandbox**, zero impact production
|
||||
|
||||
### Securitate rotate 2026-04-19
|
||||
- AUTHENTIK_CLIENT_SECRET ✅
|
||||
- NEXTAUTH_SECRET ✅
|
||||
- DB_PASS Postgres ✅
|
||||
- MINIO_ACCESS/SECRET_KEY ✅ (user nou gen-hex)
|
||||
- NOTIFICATION_CRON_SECRET ✅
|
||||
- ETERRA_USERNAME/PASSWORD ✅ (mutate Infisical)
|
||||
|
||||
### Backup infrastructure testată 2026-04-20
|
||||
- NAS rsync via SSH key RSYNC-ONLY flag, `gisbkp/` → `/HDD/gisbkp` (symlink), 50 MB/s LAN
|
||||
- Backblaze B2 bucket `beletage-gis-backups` (EU, Object Lock Compliance, SSE-B2), key scoped
|
||||
|
||||
---
|
||||
|
||||
## 3. Decizii arhitecturale (finalizate)
|
||||
|
||||
### Golden rule (aplicat întreg planul)
|
||||
Preferă investment upfront pentru câștiguri pe termen lung în: **safety, speed, resources, future-proof**.
|
||||
|
||||
### Tehnologii
|
||||
- **PostgreSQL 18.3** (async I/O, skip scans, temporal constraints, UUID v7)
|
||||
- **PostGIS 3.6.3** (ST_RemoveIrrelevantPointsForView, SFCGAL modern)
|
||||
- **pgBouncer 1.24.1-p1** (transaction mode, 1000 max clients, 50 default pool)
|
||||
- **Martin 1.5.0** tile server (vector MVT)
|
||||
- **TiTiler** (raster COG)
|
||||
- **Next.js 16 + Hono + Prisma + Zod** pentru gis-api
|
||||
- **pg-boss** queue (NU Redis)
|
||||
- **pgBackRest** backup engine
|
||||
- **Docker Compose** (NU Kubernetes)
|
||||
- **MinIO** object storage
|
||||
- **Authentik** OIDC (existent)
|
||||
|
||||
### Versioning policy
|
||||
- Latest stable ≥3 luni în producție + ≥1 patch
|
||||
- Pin exact în compose (NU `:latest`)
|
||||
- Manual upgrades după test pe snapshot
|
||||
- Periodic review trimestrial
|
||||
|
||||
### Domains (brand strategy)
|
||||
| Domain | Rol |
|
||||
|---|---|
|
||||
| `gis.ac` | Infra neutru — tiles, api, pmtiles, s3 |
|
||||
| `eterra.live` | SaaS topografi |
|
||||
| `pug.digital` | SaaS primării (fost SmartCity360) |
|
||||
| `planhub.ro` | SaaS multi-tenant arhitecți |
|
||||
| `app.beletage.ro` | Beletage tenant CNAME rebrand |
|
||||
| `vreau.digital` | Rezervă — portal cetățeni |
|
||||
| `buildini.ai` | Rezervă — AI design |
|
||||
| `puz.digital` | SEO redirect → pug.digital |
|
||||
| `beletage.ro` | Intern + admin |
|
||||
|
||||
---
|
||||
|
||||
## 4. Target stack shop (13 containere)
|
||||
|
||||
```
|
||||
shop (10.10.10.84) — /home/dnz/postgres-gis/ + /opt/gis-stack/:
|
||||
|
||||
├── postgres-gis PG 18.3 + PostGIS 3.6.3 [✅ POC]
|
||||
├── postgres-gis-pgbouncer transaction mode, 1000 clients [✅ POC]
|
||||
├── martin-public vector tiles publice, zero auth [Sprint 1]
|
||||
├── martin-private vector tiles auth via JWT ForwardAuth [Sprint 1]
|
||||
├── titiler raster COG + DEM proxy [Sprint 1]
|
||||
├── gis-api Next.js 16 + Hono + Prisma + Zod [Sprint 2]
|
||||
├── gis-sync-orchestrator pg-boss workers + session pool [Sprint 3]
|
||||
├── nginx-pmtiles static serve MinIO pmtiles [Sprint 1]
|
||||
├── minio-gis buckets: pmtiles, cog, cf-pdfs, dxf, backups [Sprint 1]
|
||||
├── pgadmin intern doar (pgadmin.beletage.ro) [Sprint 1]
|
||||
├── prometheus scrape metrics [Sprint 4]
|
||||
├── grafana dashboards [Sprint 4]
|
||||
└── alertmanager alerts → n8n webhook [Sprint 4]
|
||||
```
|
||||
|
||||
**Resource utilization estimat:** ~8% CPU shop, ~15% RAM shop. Loaded la plural, idle majority.
|
||||
|
||||
---
|
||||
|
||||
## 5. Schema DB split
|
||||
|
||||
```
|
||||
postgres-gis / gis (database):
|
||||
|
||||
├── schema: gis_core Cadastru ANCPI (public readable)
|
||||
│ ├── terenuri Parcele (25M estimate)
|
||||
│ ├── cladiri Clădiri cadastrale
|
||||
│ ├── uats 3,186 UAT România
|
||||
│ ├── administrativ Intravilan + arii protejate
|
||||
│ └── uats_z0/5/8/12 Simplified views per zoom
|
||||
│
|
||||
├── schema: gis_urban PUG/PUZ/PUD (multi-tenant RLS per siruta)
|
||||
│ ├── plan_spatial MDLPA-compliant root (SIRUTA, Judet, HCL, stadiu)
|
||||
│ ├── zf_existenta, zf_propusa Zone funcționale + HILUCS codes
|
||||
│ ├── zona_reglementare Zone protecție (monumente, sanitară, etc.)
|
||||
│ ├── utr Unități Teritoriale Referință + regulament JSONB
|
||||
│ ├── puz, pud Sub-plans
|
||||
│ ├── cai_comunicatie Drumuri
|
||||
│ ├── retele_edilitare Apă, canal, gaz, electric, telecom
|
||||
│ ├── echipare_edilitara Hidranți, stații (puncte)
|
||||
│ ├── regulament_local Text regulament
|
||||
│ ├── avize_acorduri PDF metadata
|
||||
│ ├── *_drafts Versiuni work-in-progress arhitecți
|
||||
│ └── puz_in_avizare Public preview în timpul avizării
|
||||
│
|
||||
├── schema: gis_enrichment GDPR sensitive (CF, proprietari, adrese)
|
||||
│ ├── cf_extracts Criptat, RLS strict
|
||||
│ ├── proprietari Nume + CNP
|
||||
│ ├── adrese Normalizare nomenclator
|
||||
│ └── shared_pool GDPR-safe shareable (nr_cad, suprafață, UTR)
|
||||
│
|
||||
├── schema: gis_meta Orchestrare + audit
|
||||
│ ├── sync_runs Log sync eTerra
|
||||
│ ├── sync_rules Planning sync
|
||||
│ ├── audit ENCRYPTED cu pgcrypto (audit metadata)
|
||||
│ ├── eterra_sessions Session pool persistent
|
||||
│ ├── eterra_accounts Multi-account ANCPI shuffle
|
||||
│ ├── eterra_account_usage_log Per-action audit
|
||||
│ └── raster_sources COG registry (upload metadata)
|
||||
│
|
||||
├── schema: tenants Control plane multi-tenancy
|
||||
│ ├── tenants id, name, tenant_type, is_beletage_group, siruta_scope[]
|
||||
│ ├── members user → tenant mapping
|
||||
│ └── enrichment_scopes per-user flag (none/basic/full)
|
||||
│
|
||||
├── schema: eterra App-specific eterra.live
|
||||
├── schema: pug App-specific pug.digital
|
||||
├── schema: archi App-specific ArchiTools
|
||||
├── schema: planhub App-specific planhub.ro (multi-tenant)
|
||||
├── schema: queue pg-boss queue
|
||||
└── schema: public Extensii (postgis, pg_stat_statements, pgcrypto, pg_trgm)
|
||||
```
|
||||
|
||||
### Super-tenant Beletage group
|
||||
Tabela `tenants.is_beletage_group BOOLEAN`. Tenants marked (Beletage SRL, Studii de Teren, Urban Switch, Cubitron, + viitoare firme asociate) au acces NERESTRICȚIONAT la toate datele. Controlat prin admin Beletage.
|
||||
|
||||
---
|
||||
|
||||
## 6. Roluri Postgres + RLS
|
||||
|
||||
### DB users
|
||||
- `gis_app_rw` — gis-api main
|
||||
- `gis_sync_rw` — orchestrator worker (UNICUL scriitor în gis_core)
|
||||
- `gis_public_ro` — Martin public (read-only)
|
||||
- `gis_private_ro` — Martin-private (read-only cu RLS)
|
||||
- `gis_titiler_ro` — TiTiler raster
|
||||
- `gis_admin_dba` — pgAdmin + Marius (NO BYPASSRLS — Golden)
|
||||
|
||||
### Session variables setate la fiecare tranzacție
|
||||
```sql
|
||||
SET LOCAL app.user_id = ...;
|
||||
SET LOCAL app.tenant_id = ...;
|
||||
SET LOCAL app.is_beletage_group = 'true'|'false';
|
||||
SET LOCAL app.enrichment_scope = 'none'|'basic'|'full';
|
||||
SET LOCAL app.allowed_sirutas = 'csv,list';
|
||||
SET LOCAL app.roles = 'csv,list';
|
||||
```
|
||||
|
||||
### RLS policy patterns
|
||||
- `gis_core`: zero RLS, public read
|
||||
- `gis_urban.*_drafts`: tenant-isolated (own tenant_id OR Beletage group)
|
||||
- `gis_urban.*_aprobate`: public read, write admin_primaria sau Beletage
|
||||
- `gis_enrichment.cf_extracts`:
|
||||
```sql
|
||||
USING (
|
||||
current_setting('app.is_beletage_group', true)::boolean = true
|
||||
OR fetched_by_user_id = current_setting('app.user_id', true)::uuid
|
||||
OR fetched_by_tenant_id = current_setting('app.tenant_id', true)::uuid
|
||||
OR (is_shareable = true AND current_setting('app.enrichment_scope', true) IN ('basic', 'full'))
|
||||
)
|
||||
```
|
||||
- `FORCE ROW LEVEL SECURITY` pe toate tabelele RLS (fallback deny)
|
||||
|
||||
---
|
||||
|
||||
## 7. Auth flow (Authentik OIDC)
|
||||
|
||||
### JWT custom claims
|
||||
```json
|
||||
{
|
||||
"sub": "user-uuid",
|
||||
"email": "...",
|
||||
"tenant_id": "tenant-uuid",
|
||||
"is_beletage_group": true,
|
||||
"roles": ["topograf", "firm_admin"],
|
||||
"enrichment_scope": "full",
|
||||
"allowed_sirutas": ["54975", "155243"]
|
||||
}
|
||||
```
|
||||
|
||||
### User attributes sync
|
||||
- **Webhook HMAC-signed** din planhub/eterra.live la user create/update
|
||||
- Idempotency key dedupe 24h
|
||||
- Exponential backoff retry (6 tentative: 1,2,4,8,16,32s)
|
||||
- Dead Letter Queue în pg-boss `auth-sync-dlq`
|
||||
|
||||
### Super-tenant check
|
||||
**Dynamic DB query** la fiecare JWT emit (Authentik custom expression), cache 60s.
|
||||
|
||||
### JWT expiry
|
||||
- Access: 1h
|
||||
- Refresh: 30d
|
||||
|
||||
---
|
||||
|
||||
## 8. Sync orchestrator + multi-account ANCPI
|
||||
|
||||
### Componente
|
||||
- **Queue:** pg-boss (schema `queue`), max 8 workers, retention 30d
|
||||
- **Workers:**
|
||||
- `eterra-sync-worker` (delta/full UAT sync)
|
||||
- `enrichment-worker` (CF fetch + GDPR pool split)
|
||||
- `pug-ingest-worker` (GPKG upload → gis_urban drafts)
|
||||
- `pmtiles-rebuild-worker` (tippecanoe → MinIO)
|
||||
- `cache-invalidate-worker` (Cloudflare purge API)
|
||||
|
||||
### Multi-account ANCPI shuffle
|
||||
```sql
|
||||
gis_meta.eterra_accounts (
|
||||
id, username, password_encrypted (AES-256-GCM, key GIS_SYNC_AES_KEY),
|
||||
account_type, quota_per_hour DEFAULT 500,
|
||||
usage_current_hour, last_reset_at,
|
||||
status, notes
|
||||
)
|
||||
```
|
||||
Round-robin pe conturi active cu quota disponibilă. Failover auto dacă blocked. Distribution load ANCPI = zero detection singular-account patterns.
|
||||
|
||||
### Session pool persistent (TTL 20 min)
|
||||
Înlocuiește in-memory din eterra-live + hardcoded din ArchiTools.
|
||||
|
||||
### LISTEN/NOTIFY events cross-app
|
||||
- `sync:uat:done` → apps refresh UI
|
||||
- `pmtiles:rebuild:done` → cache invalidate
|
||||
- `enrichment:new` → user UI notify
|
||||
|
||||
### Scheduler (pg-boss cron)
|
||||
- Delta sync: Lu-Vi 2:00
|
||||
- Deep sync: Weekend 23:00
|
||||
- PMTiles rebuild: Lu-Vi 4:00
|
||||
- Cleanup sessions: 6h
|
||||
|
||||
### ArchiTools/eterra-live post-migration
|
||||
- **ArchiTools:** DELETE eterra-client.ts + setInterval. Read-only consumer + trigger sync via API.
|
||||
- **eterra-live:** Păstrează UI eterra login (per-user), trigger sync prin orchestrator API.
|
||||
- **Orchestrator = UNICUL scriitor gis_core.**
|
||||
|
||||
---
|
||||
|
||||
## 9. Tile serving (4 subdomenii)
|
||||
|
||||
### `tiles.gis.ac` (PUBLIC)
|
||||
Martin 1 instanță, zero auth, CF rate limit 100 req/s/IP.
|
||||
|
||||
Layer groups:
|
||||
- `/cadastru`, `/uat`, `/pug-public`, `/zone-protectie`, `/drumuri`, `/retele-publice`
|
||||
- `/puz-aprobate`, `/pud-aprobate`, `/analize-publice`, `/puz-in-avizare`
|
||||
|
||||
### `tiles-private.gis.ac` (AUTH)
|
||||
Martin-private container + Traefik ForwardAuth JWT validation. Zero cache.
|
||||
|
||||
Layer groups:
|
||||
- `/pug-drafts/{tenant}`, `/puz-drafts/{tenant}`, `/pud-drafts/{tenant}`
|
||||
- `/observatii-primarie/{siruta}`, `/analize-interne/{tenant}`
|
||||
|
||||
### `raster.gis.ac` + `dem.gis.ac`
|
||||
TiTiler (NU Martin — Martin only vector).
|
||||
- `raster.gis.ac` serves COG din MinIO bucket
|
||||
- `dem.gis.ac` = **proxy cu cache CF** la ANCPI MNT (geoportal.ancpi.ro/maps/rest/services/ANCPI/MNT)
|
||||
- Hillshade + contour generate local din DEM
|
||||
|
||||
### `pmtiles.gis.ac`
|
||||
nginx static serve din MinIO bucket `pmtiles`. CF cache 7 zile.
|
||||
|
||||
### CF TTL config
|
||||
| Path | TTL |
|
||||
|---|---|
|
||||
| tiles.gis.ac/uat | 30 zile |
|
||||
| tiles.gis.ac/cadastru | 6h + auto-purge la sync |
|
||||
| tiles.gis.ac/pug-public | 24h |
|
||||
| tiles.gis.ac/puz-in-avizare | 1h |
|
||||
| tiles.gis.ac/puz-aprobate, pud | 24h |
|
||||
| pmtiles.gis.ac | 7 zile |
|
||||
| tiles-private.gis.ac | zero |
|
||||
| raster.gis.ac, dem.gis.ac | 30 zile |
|
||||
|
||||
Bot Fight Mode: Medium default.
|
||||
|
||||
### PUZ lifecycle visibility
|
||||
| Stadiu | Vizibil | Cache |
|
||||
|---|---|---|
|
||||
| Draft intern | tiles-private (tenant only) | zero |
|
||||
| Avizare publică | tiles.gis.ac/puz-in-avizare | 1h |
|
||||
| Aprobat | tiles.gis.ac/puz-aprobate (overlay pug-public) | 24h |
|
||||
|
||||
### Raster Library module (admin eterra.live)
|
||||
- Upload TIFF/GeoTIFF → background gdal_translate → COG + overviews
|
||||
- Preview thumbnail 512px
|
||||
- Public/private toggle
|
||||
- Delete/replace
|
||||
- DB: `gis_meta.raster_sources`
|
||||
|
||||
---
|
||||
|
||||
## 10. API layer (api.gis.ac)
|
||||
|
||||
### Stack
|
||||
Next.js 16 + Hono router intern + Prisma + Zod + Authentik JWT middleware.
|
||||
|
||||
### Endpoint exemple
|
||||
```
|
||||
POST /enrichment/parcela auth: tier≥basic
|
||||
GET /parcela/{id} auth: optional
|
||||
POST /pug/zone-functionala auth: admin_primaria
|
||||
POST /sync/uat auth: admin Beletage
|
||||
POST /raster/upload auth: architect cu scope
|
||||
GET /search?q=... auth: optional
|
||||
```
|
||||
|
||||
### Rate limiting (Redis per JWT sub)
|
||||
| Tier | Req/oră | Enrichments/zi |
|
||||
|---|---|---|
|
||||
| Free | 100 | 10 |
|
||||
| Basic | 1000 | 100 |
|
||||
| Pro | 5000 | unlimited |
|
||||
| Admin | nelimitat | nelimitat |
|
||||
|
||||
Managed prin admin eterra.live.
|
||||
|
||||
---
|
||||
|
||||
## 11. MDLPA compliance (Ordin 904/2023)
|
||||
|
||||
**Abordare Golden:** schema internă flexibilă + export layer separat conform MDLPA.
|
||||
|
||||
### Internal schema gis_urban.plan
|
||||
Include ALL MDLPA required fields + extras (history, drafts, workflows, comments).
|
||||
|
||||
### PG18 temporal constraint
|
||||
```sql
|
||||
ALTER TABLE gis_urban.plan_spatial ADD CONSTRAINT no_overlap_plan
|
||||
EXCLUDE USING gist (siruta WITH =, doc_type WITH =,
|
||||
daterange(data_aprob, data_exp, '[)') WITH &&);
|
||||
```
|
||||
Garantează zero PUG-uri suprapuse pe aceeași UAT.
|
||||
|
||||
### Export worker
|
||||
- `pug-export-worker` (pg-boss job)
|
||||
- Input: `{plan_id}`
|
||||
- Output: ZIP cu 5 subdirectoare + GPKG MDLPA-compliant + PDF-uri
|
||||
- Tooling: ogr2ogr (GDAL) pentru GPKG, custom orchestrator Node
|
||||
- Drop Z/M dimensions la export
|
||||
- Default HILUCS_N1 dacă missing intern
|
||||
- ZIP naming: `^[A-Z][A-Z]_.*?_\d{1,10}_(PUG|PUZ|PATJ|PMUD)_[0-9]{8}`
|
||||
|
||||
### Validator MDLPA
|
||||
**Faza 1:** manual pre-submit by user (download Validator_2.0.5, run local).
|
||||
**Faza 2:** Marius primește repo validator de la friends MDLPA → containerizăm.
|
||||
**Faza 3:** custom pre-validation 80% reguli (blocker-i obvious) înaintea ZIP download.
|
||||
|
||||
### Doc types în scope
|
||||
PUG (prio 1), PUZ (prio 1), PUD (prio 2), PMUD (da), PATJ (opțional, max 1 județ).
|
||||
|
||||
---
|
||||
|
||||
## 12. Backup strategy (multi-tier)
|
||||
|
||||
### Tier 1 — NAS local (LAN fast)
|
||||
- Path: `gis-backup@10.10.10.10:gisbkp/` (symlink → `/HDD/gisbkp`)
|
||||
- btrfs + COW + compression
|
||||
- Retention: 30 daily + 12 monthly + 5 yearly snapshots
|
||||
- Protocol: rsync over SSH, RSYNC-ONLY key flag
|
||||
|
||||
### Tier 2 — Backblaze B2 (offsite hot)
|
||||
- Bucket: `beletage-gis-backups` (region EU)
|
||||
- Object Lock Compliance mode 90d + SSE-B2
|
||||
- Weekly full + monthly archive encrypted AES-256 client-side
|
||||
|
||||
### Tier 3 — Azure (deferred, credit €2400/year)
|
||||
- Azure PostgreSQL Flexible Server (DR cloud fallback, ~50-80€/lună)
|
||||
- Azure CDN + Blob Hot (PMTiles secondary, ~20€/lună)
|
||||
- When: scale 10k+ users sau geo-redundanță
|
||||
|
||||
### Tier 4 — CJ2 standby (deferred ~oct 2026)
|
||||
- Streaming replication async, RPO~0, RTO<5min
|
||||
- Aștept workstation AI nou + disk dedicat
|
||||
|
||||
### pgBackRest setup
|
||||
- 2 repos: NAS + B2
|
||||
- PITR window: **30 zile**
|
||||
- Continuous WAL archive
|
||||
- Compress zstd + encrypt AES-256
|
||||
- Parallel restore workers
|
||||
|
||||
### DR drill
|
||||
- **Trimestrial** manual full recovery simulation
|
||||
- **Săptămânal** smoke test (read-only integrity check)
|
||||
- **Lunar** test restore automation (ephemeral container, 30 min, 20GB temp)
|
||||
|
||||
### Backup Dashboard
|
||||
Modul în admin.eterra.live, tab "Backup & Recovery":
|
||||
- Cards pentru fiecare tier (NAS, B2, Azure placeholder, CJ2 placeholder)
|
||||
- Plugin architecture `BackupTarget` interface — extensibil
|
||||
- Slot-uri viitoare: email archival, Gitea repos, workstations, configs
|
||||
- Alerting Golden low-noise (email doar la red)
|
||||
|
||||
---
|
||||
|
||||
## 13. Monitoring (Dashboard admin eterra.live)
|
||||
|
||||
### 6 cards simple (RO tooltip clar)
|
||||
1. Hărți (req/s)
|
||||
2. Baza de date (query latency)
|
||||
3. Spațiu disc (%)
|
||||
4. Topografi activi
|
||||
5. Sincronizare (ultim)
|
||||
6. Backup (ultim)
|
||||
|
||||
### Culori: verde/galben/roșu
|
||||
### Alert doar la roșu (zero spam)
|
||||
### Grafana full = collapsed "Detalii avansate"
|
||||
|
||||
### Prometheus metrics scraped
|
||||
- Martin (/metrics), Postgres (pg_stat_statements + exporter), pgBouncer, MinIO, gis-api, orchestrator
|
||||
|
||||
---
|
||||
|
||||
## 14. Securitate — posture final
|
||||
|
||||
### Authentication
|
||||
- Authentik OIDC pentru toate apps
|
||||
- JWT 1h access + 30d refresh
|
||||
- JWKS cache 1h
|
||||
|
||||
### Authorization
|
||||
- RLS multi-tenant (siruta + tenant_id + is_beletage_group)
|
||||
- Session variables via SET LOCAL (zero leak)
|
||||
- Zero Postgres role cu BYPASSRLS permanent
|
||||
|
||||
### Secrets management
|
||||
- Infisical single source of truth
|
||||
- Rotate schedule: annual pentru keys critice, automatic pentru JWT
|
||||
- Encryption keys: Infisical + paper print sealed Beletage safe (YubiKey viitor)
|
||||
|
||||
### Network
|
||||
- PG 5433 bind 127.0.0.1 (intern shop)
|
||||
- pgBouncer 6432 bind 127.0.0.1
|
||||
- Martin/TiTiler/gis-api prin Traefik only
|
||||
- Sophos DNAT doar 80/443 către proxy
|
||||
|
||||
### Audit
|
||||
- `gis_meta.audit` ENCRYPTED cu pgcrypto (key AUDIT_ENCRYPTION_KEY)
|
||||
- Retention: 1 an hot PG + 5 ani archive MinIO
|
||||
- Decryption doar la query admin explicit
|
||||
|
||||
### Data at rest
|
||||
- Backups encrypted (pgBackRest + client-side AES pentru B2)
|
||||
- ENCRYPTION_SECRET rotate pending (re-encrypt 15 vault entries)
|
||||
|
||||
---
|
||||
|
||||
## 15. Ce DISPARE post-cutover
|
||||
|
||||
- `setInterval` cron-in-cod din ArchiTools
|
||||
- `eterra-client.ts` din ArchiTools (mut în orchestrator)
|
||||
- In-memory session pool eterra-live (persistent DB)
|
||||
- PG 5432 exposed 0.0.0.0 satra (shop bind LAN)
|
||||
- PMTiles webhook broken (înlocuit de pmtiles-rebuild-worker)
|
||||
- systemd pmtiles-webhook parole în args (container nou)
|
||||
- Hardcoded ETERRA_USERNAME/PASSWORD ArchiTools (shared pool)
|
||||
- AES IV=16 eterra-live (upgrade 12)
|
||||
- Rate limit in-memory eterra-live (Redis)
|
||||
|
||||
---
|
||||
|
||||
## 16. Migration timeline (6 săptămâni)
|
||||
|
||||
### Sprint 1 — Foundation stack (săpt 1)
|
||||
**Day 1** (GO trigger):
|
||||
- Deploy `martin-public` container pe shop
|
||||
- Deploy `martin-private` container + Traefik ForwardAuth middleware
|
||||
- Deploy `titiler` container
|
||||
- Deploy `nginx-pmtiles` container
|
||||
- Deploy `minio-gis` container + bucket setup
|
||||
|
||||
**Day 2:**
|
||||
- Deploy `pgadmin` (intern pgadmin.beletage.ro)
|
||||
- Traefik routes pentru toate subdomeniile GIS
|
||||
|
||||
**Day 3-4:**
|
||||
- Cloudflare zones: add `gis.ac`, `eterra.live`, `pug.digital`, `planhub.ro`
|
||||
- DNS records + Page Rules TTL + Rate Limits + Bot Fight Medium
|
||||
- Test tile serving basic
|
||||
|
||||
**Day 5:**
|
||||
- Schema split pe shop:
|
||||
- `CREATE SCHEMA gis_core, gis_urban, gis_enrichment, gis_meta, tenants, eterra, pug, archi, planhub, queue`
|
||||
- `ALTER TABLE public.X SET SCHEMA gis_core` pentru tabele existente
|
||||
- Create views + roles
|
||||
|
||||
### Sprint 2 — Auth + data layer (săpt 2)
|
||||
**Day 1-2:**
|
||||
- Authentik custom property mappings (tenant_id, is_beletage_group, enrichment_scope, allowed_sirutas)
|
||||
- Webhook signed HMAC din eterra.live → Authentik sync
|
||||
- PG roluri aplicație + grants
|
||||
|
||||
**Day 3-4:**
|
||||
- RLS policies per tabel (gis_urban, gis_enrichment)
|
||||
- FORCE ROW LEVEL SECURITY pe toate
|
||||
- gis-api boilerplate (Next.js 16 + Hono + Prisma + Zod)
|
||||
- Auth middleware JWT verify + SET LOCAL
|
||||
|
||||
**Day 5:**
|
||||
- Cypress test suite RLS (6 scenarii obligatorii)
|
||||
|
||||
### Sprint 3 — Sync orchestrator (săpt 3)
|
||||
**Day 1-2:**
|
||||
- `gis-sync-orchestrator` container
|
||||
- pg-boss setup schema queue
|
||||
- eTerra client extract în shared lib
|
||||
|
||||
**Day 3:**
|
||||
- Session pool persistent `gis_meta.eterra_sessions` + multi-account `gis_meta.eterra_accounts`
|
||||
- Workers: eterra-sync, enrichment, pug-ingest
|
||||
|
||||
**Day 4:**
|
||||
- Workers: pmtiles-rebuild, cache-invalidate (CF API)
|
||||
- LISTEN/NOTIFY events implementation
|
||||
|
||||
**Day 5:**
|
||||
- Admin UI module eterra.live — sync jobs control panel
|
||||
|
||||
### Sprint 4 — Backup + Dashboard (săpt 4)
|
||||
**Day 1-2:**
|
||||
- pgBackRest deploy shop
|
||||
- 2 repos: NAS (rsync push via cron) + B2 (direct S3)
|
||||
- Scheduled jobs: daily incremental, weekly full, monthly archive
|
||||
- WAL archive continuous
|
||||
|
||||
**Day 3:**
|
||||
- Backup Dashboard UI (admin eterra.live tab "Backup & Recovery")
|
||||
- Plugin architecture `BackupTarget` interface
|
||||
- Cards: NAS, B2, Azure placeholder, CJ2 placeholder
|
||||
|
||||
**Day 4:**
|
||||
- DR runbook scripts
|
||||
- Monthly restore test automation (ephemeral container cron)
|
||||
- Prometheus + Grafana + Alertmanager deploy
|
||||
|
||||
**Day 5:**
|
||||
- Grafana dashboards (GIS Overview, Martin Perf, PG Health, API Rate Limits, Raster Library)
|
||||
- 6 simple cards admin eterra.live
|
||||
- Alertmanager → n8n webhook (email + Telegram on red)
|
||||
|
||||
### Sprint 5 — Test & parity (săpt 5)
|
||||
**Day 1-2:**
|
||||
- Shadow sync: orchestrator rulează în paralel cu satra setInterval (both read from ANCPI)
|
||||
- Compare row counts ambele, checksum-uri, latency
|
||||
|
||||
**Day 3:**
|
||||
- Load test: 1000 concurrent API requests, 500 QPS tile server
|
||||
- Performance comparisons satra vs shop end-to-end
|
||||
|
||||
**Day 4:**
|
||||
- Security audit final: TLS scan, dependency audit, pentest basic
|
||||
- CORS, CSP, rate limits verify
|
||||
|
||||
**Day 5:**
|
||||
- GO/NO-GO decision meeting
|
||||
- Final checklist review
|
||||
|
||||
### Weekend cutover (if GO)
|
||||
**Friday 22:00:**
|
||||
- Notify users (eterra.live + ArchiTools) maintenance 4h
|
||||
- Traefik maintenance page
|
||||
- Stop ArchiTools + eterra-live containers
|
||||
- pg_dump satra final snapshot → shop (delta apply)
|
||||
- Diff check paritate
|
||||
- Switch DATABASE_URL containere → shop
|
||||
- Deploy ArchiTools nou (cu eterra-client DELETED)
|
||||
- Cloudflare DNS update: tiles.gis.ac → shop Martin
|
||||
- Start orchestrator cron jobs
|
||||
- Smoke test full flow
|
||||
|
||||
**Saturday 06:00:**
|
||||
- Monitor dashboard
|
||||
- On-call standby
|
||||
|
||||
**Saturday 20:00:**
|
||||
- All green → GO production
|
||||
- satra architools_postgres READ-ONLY (fallback 30 zile safety)
|
||||
|
||||
**Sunday:**
|
||||
- Observation + bug bash
|
||||
|
||||
### Cleanup (săpt 6)
|
||||
- git filter-repo ArchiTools (rescrie istoria, notify re-clone)
|
||||
- Structural fix compose (env_file + stack.env in .gitignore)
|
||||
- Remove satra containers (după 30 zile)
|
||||
- ENCRYPTION_SECRET rotate + re-encrypt 15 vault entries
|
||||
- Documentation final în busc-infra
|
||||
|
||||
---
|
||||
|
||||
## 17. Runbooks (DR scenarios)
|
||||
|
||||
### R1 — Corruption parcela X la 14:30
|
||||
```bash
|
||||
pgbackrest --stanza=gis --type=time --target="2026-04-20 14:29:00" restore
|
||||
# Verify integrity + promote
|
||||
```
|
||||
|
||||
### R2 — Crash shop total → failover CJ2 (când activ)
|
||||
1. Promote CJ2 standby to primary
|
||||
2. Update DNS: tiles.gis.ac, api.gis.ac → CJ2 IP
|
||||
3. Restart apps pointing CJ2
|
||||
4. RTO target: <5 min
|
||||
|
||||
### R3 — Ransomware
|
||||
- B2 Object Lock Compliance = fișiere immutable
|
||||
- Restore latest clean snapshot pre-ransomware
|
||||
- Wipe compromised + rebuild from B2
|
||||
|
||||
### R4 — Accidental DROP TABLE
|
||||
```bash
|
||||
pgbackrest restore --type=time --target="<moment înainte de DROP>"
|
||||
# Replay WAL până exact înainte de comanda distructivă
|
||||
```
|
||||
|
||||
### R5 — Disaster regional (shop + CJ2 down)
|
||||
- Restore din B2 arhive lunar
|
||||
- Bootstrap new cluster pe alt host
|
||||
- RTO: 4-8 ore
|
||||
|
||||
---
|
||||
|
||||
## 18. Next features post-production
|
||||
|
||||
### Imediat după stable (month 1-2)
|
||||
- MDLPA validator containerizat (după repo de la friends MDLPA)
|
||||
- Geopackage export pipeline complet
|
||||
- Raster Library module + upload UI
|
||||
|
||||
### Medium term (month 3-6)
|
||||
- PMUD workflow (rețele transport, piste, parcare)
|
||||
- SmartCity360 / pug.digital launch (primul tenant primărie)
|
||||
- planhub.ro SaaS pivot multi-tenant (primii arhitecți externi)
|
||||
- CJ2 standby activation (după workstation AI nou)
|
||||
|
||||
### Long term (month 6-12)
|
||||
- PATJ suport (1 județ pilot)
|
||||
- Azure activation (Azure PostgreSQL DR + Blob Hot CDN)
|
||||
- Cross-region geo-redundanță dacă scale justifies
|
||||
|
||||
---
|
||||
|
||||
## 19. Risk register
|
||||
|
||||
| Risc | Probabilitate | Impact | Mitigare |
|
||||
|---|---|---|---|
|
||||
| Cutover fail | Low | High | Rollback DNS → satra, 30d safety window |
|
||||
| Data loss cutover window | Very Low | High | Shadow sync săpt 5, delta final |
|
||||
| Orchestrator bugs prod | Medium | Medium | Săpt 5 shadow prinde majoritatea |
|
||||
| ANCPI block multi-account | Low | High | Shuffle + quota + alerting |
|
||||
| B2 cloud outage | Very Low | Medium | NAS + CJ2 (când activ) alternative |
|
||||
| Marius unavailable | Medium | High | Claude Code has full plan + memory |
|
||||
|
||||
---
|
||||
|
||||
## 20. Secrete pre-loaded în Infisical (verificate 2026-04-20)
|
||||
|
||||
### Active (ArchiTools production)
|
||||
- AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET
|
||||
- NEXTAUTH_SECRET
|
||||
- DB_PASS (satra Postgres user, will rotate at cutover)
|
||||
- ETERRA_USERNAME, ETERRA_PASSWORD
|
||||
- NOTIFICATION_CRON_SECRET
|
||||
- BREVO_BELETAGE_API_KEY
|
||||
|
||||
### Shop stack (pre-loaded pentru sprint 1)
|
||||
- POSTGRES_GIS_SUPERUSER_PASSWORD
|
||||
- POSTGRES_GIS_ARCHITOOLS_PASS
|
||||
- POSTGRES_GIS_ETERRA_PASS
|
||||
- POSTGRES_GIS_MARTIN_PASS
|
||||
- POSTGRES_GIS_SYNC_PASS
|
||||
- POSTGRES_GIS_PUG_PASS
|
||||
- POSTGRES_GIS_PLANHUB_PASS
|
||||
- MINIO_GIS_ROOT_USER
|
||||
- MINIO_GIS_ROOT_PASSWORD
|
||||
- PGADMIN_DEFAULT_PASSWORD
|
||||
|
||||
### Security/Crypto
|
||||
- GIS_SYNC_AES_KEY
|
||||
- AUTHENTIK_WEBHOOK_HMAC_SECRET
|
||||
- AUDIT_ENCRYPTION_KEY
|
||||
- BACKUP_DASHBOARD_ADMIN_TOKEN
|
||||
|
||||
### Backup
|
||||
- BACKUP_ARCHIVE_KEY (AES-256 B2 client-side)
|
||||
- PGBACKREST_REPO1_CIPHER_PASS
|
||||
- STANDBY_REPLICATION_PASS (pentru CJ2 viitor)
|
||||
- B2_APPLICATION_KEY_ID
|
||||
- B2_APPLICATION_KEY
|
||||
- B2_BUCKET_NAME (beletage-gis-backups)
|
||||
- B2_ENDPOINT (s3.eu-central-003.backblazeb2.com)
|
||||
|
||||
### De generat la moment (viitor)
|
||||
- POSTGRES_GIS_TITILER_PASS (day 1 sprint 1)
|
||||
- POSTGRES_GIS_APP_RW_PASS (day 1 sprint 2)
|
||||
- GRAFANA_ADMIN_PASSWORD (sprint 4)
|
||||
- PROMETHEUS_BASIC_AUTH (sprint 4)
|
||||
- AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_SAS (când activăm)
|
||||
|
||||
---
|
||||
|
||||
## 21. Hardware/infrastructure check-list pre-GO
|
||||
|
||||
- ✅ Shop 10.10.10.84 reachable SSH (user dnz, groups docker+wheel)
|
||||
- ✅ Shop docker 29.2.1 instalat
|
||||
- ✅ Shop disk 3.3TB free
|
||||
- ✅ Shop RAM 225GB avail
|
||||
- ✅ Shop ports 5433, 6432 libere (POC ocupate)
|
||||
- ✅ Satra SSH (user bulibasa) reachable
|
||||
- ✅ Proxy Traefik v3.6.8 (10.10.10.199)
|
||||
- ✅ Sophos 80/443 only WAN (LAN internal safe)
|
||||
- ✅ NAS 10.10.10.10 rsync key deployed, symlink OK
|
||||
- ✅ Backblaze B2 bucket + scoped key tested
|
||||
- ✅ Authentik auth.beletage.ro v2025.2.4 operational
|
||||
- ✅ Infisical toate secretele pre-loaded
|
||||
- ✅ Cloudflare API token disponibil
|
||||
|
||||
---
|
||||
|
||||
## 22. Acceptance criteria (Definition of Done)
|
||||
|
||||
Production considerat stable când:
|
||||
- [ ] Toate 13 containere shop running + healthy
|
||||
- [ ] Cypress RLS suite 100% pass
|
||||
- [ ] Load test 500 QPS tiles + 100 QPS api sub 200ms p95
|
||||
- [ ] Backup test restore successful (monthly)
|
||||
- [ ] ArchiTools + eterra.live live pe shop DATABASE_URL
|
||||
- [ ] Orchestrator UNICUL scriitor gis_core
|
||||
- [ ] Zero error logs 72h continuous
|
||||
- [ ] Dashboard admin eterra.live arată toate verde
|
||||
- [ ] DR runbook validat (scenario R1 + R4 testat)
|
||||
- [ ] Grafana dashboards populated
|
||||
- [ ] Users notificați + feedback pozitiv
|
||||
|
||||
---
|
||||
|
||||
## 23. Contacts & ownership
|
||||
|
||||
- **Owner:** Marius Tarau (m.tarau@beletage.ro)
|
||||
- **Pair:** Claude Code (via Anthropic Opus 4.7 1M context)
|
||||
- **Escalation:** m.tarau@beletage.ro + (telefon Beletage)
|
||||
- **On-call during cutover weekend:** Marius primary, Claude assist
|
||||
|
||||
---
|
||||
|
||||
## 24. Changelog
|
||||
|
||||
- **v1 DRAFT** (2026-04-18) — initial propunere
|
||||
- **v2 FINAL** (2026-04-20) — approved, includes all decisions from Pas 1-7, POC results, security rotation status, multi-account ANCPI shuffle, super-tenant Beletage group, Azure credit alloc, NAS + B2 tested, Backup Dashboard plugin architecture
|
||||
|
||||
---
|
||||
|
||||
**END OF PLAN.**
|
||||
Next action: when Marius sends "go" → Sprint 1 Day 1 begins.
|
||||
Reference in New Issue
Block a user