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

28 KiB
Raw Permalink Blame History

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

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:
    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

{
  "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

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

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

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

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.