diff --git a/docs/plans/004-faza-h-runbook.md b/docs/plans/004-faza-h-runbook.md new file mode 100644 index 0000000..6fb6875 --- /dev/null +++ b/docs/plans/004-faza-h-runbook.md @@ -0,0 +1,518 @@ +# Plan 004 — Faza H runbook: 30-day read-only freeze + final DROP of legacy Gis*/CfExtract on architools_postgres + +**Date:** 2026-05-19 +**Author:** ArchiTools session (Claude, `/home/orchestrator/Code/ArchiTools`) +**Companion to:** `003-architools-cutover-execution-2026-05-17.md` §Faza H (see 003 line 108: "Do not drop Prisma tables … keep as dead columns 2-3 days. Drop in follow-up PR after prod validation.") + +This runbook formalizes the 30-day decommission of: +`public."GisFeature"`, `public."GisUat"`, `public."GisSyncRun"`, `public."GisSyncRule"`, `public."CfExtract"` +on `architools_postgres` (satra:5432). + +**Operator: Marius. This runbook is for review only — no commands here run themselves. Each step lists the command, a verify command, and a rollback if applicable.** + +--- + +## Pre-flight summary (measured 2026-05-19) + +| Table | Rows on `architools_postgres` | Rows on `postgres-gis` (paritate) | Notes | +|--------------------|------------------------------:|---------------------------------:|-------| +| `GisFeature` | 9,794,057 | 24,561,481 | gis-api ahead — full Romania sync in progress on shop | +| `GisUat` | 3,186 | 3,186 | identical count | +| `GisSyncRun` | 7,651 | n/a | local-only audit; lives or dies with satra | +| `GisSyncRule` | 9 | n/a | local-only schedule; lives or dies with satra | +| `CfExtract` | 8 | 47 (`gis_enrichment`) | **100% of architools 8 IDs already present** in gis_enrichment — see §H.3 | + +Disk: `architools_db` = **35 GB total**, of which `GisFeature` = **15 GB**, `GisUat` = **159 MB**, rest negligible. After DROP CASCADE the DB should shrink to ~20 GB (other heavy tables: KeyValueStore 16 MB, RegistryAudit 120 KB — and procurement/acquisition tables not listed but visible in pg_stat_activity). + +Last `GisFeature.updatedAt` = `2026-05-18 08:01:50` (4 rows touched in last 72 h, all `TERENURI_ACTIVE` on siruta 147358). Scheduler is officially disabled per memory `project_gis_db_overhaul.md`, so these writes are most likely from the enrichment path or a ghost cron — diagnose before §H.4. See **Risks §3**. + +References: +- `prisma/schema.prisma` lines 24–195: 5 models in scope. +- 003 plan TL;DR + Faza C (do-not-drop directive, line 108). +- Memory `feedback_gis_meta_default_acl.md` (gis-api): explicit REVOKE > rely on defaults. +- Memory `project_eterra_live_saas.md`: confirms eterra.live still reads the SAME satra Gis* tables — see Risks §2. + +--- + +## H.1 — Pre-flight inventory (read-only) + +### H.1.1 Row counts on architools_postgres +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT '\''GisFeature'\'' AS table_name, count(*) FROM \"GisFeature\" + UNION ALL SELECT '\''GisUat'\'', count(*) FROM \"GisUat\" + UNION ALL SELECT '\''GisSyncRun'\'', count(*) FROM \"GisSyncRun\" + UNION ALL SELECT '\''GisSyncRule'\'', count(*) FROM \"GisSyncRule\" + UNION ALL SELECT '\''CfExtract'\'', count(*) FROM \"CfExtract\";"' +``` +Expected (snapshot 2026-05-19): 9,794,057 / 3,186 / 7,651 / 9 / 8. + +### H.1.2 Disk size +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT pg_size_pretty(pg_database_size('\''architools_db'\'')) AS db_size; + SELECT relname, pg_size_pretty(pg_total_relation_size(quote_ident(schemaname)||'\''.\"'\''||relname||'\''\"'\'')) AS size + FROM pg_stat_user_tables + WHERE schemaname='\''public'\'' + ORDER BY pg_total_relation_size(quote_ident(schemaname)||'\''.\"'\''||relname||'\''\"'\'') DESC LIMIT 10;"' +``` +Expected: db_size = 35 GB; GisFeature dominates at 15 GB. + +### H.1.3 Paritate on postgres-gis (shop) — defense vs. orphan data +```bash +ssh shop 'docker exec postgres-gis psql -U gis_superuser -d gis -c " + SELECT '\''gis_core.GisFeature'\'' AS t, count(*) FROM gis_core.\"GisFeature\" + UNION ALL SELECT '\''gis_core.GisUat'\'', count(*) FROM gis_core.\"GisUat\" + UNION ALL SELECT '\''gis_enrichment.CfExtract'\'', count(*) FROM gis_enrichment.\"CfExtract\";"' +``` +**Gate**: postgres-gis `GisUat` must equal architools_postgres `GisUat` (= 3,186). If not, halt — the central DB is missing UATs that architools knows about, and parcel-sync hand-off is incomplete (Faza C/D not done). + +--- + +## H.2 — pg_dump → NAS + +**Goal:** one self-contained `-F c` archive of `architools_db`, separate from the rolling daily `architools-db.sql.gz`. Two reasons we need our own: (a) the daily is plain SQL gzipped → `pg_restore --list` won't work, can't selectively restore one table; (b) we want a pin file that survives daily rotation. + +NAS access (from satra): `ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10`. Backups land in `/HDD/satrabkp/` (per existing daily). For Faza H we'll use a dedicated subdir `/HDD/satrabkp/faza-h-pin/`. + +### H.2.1 Take the archive + +```bash +# On satra. Streams pg_dump → ssh → NAS file, no intermediate disk on satra. +ssh satra ' + DUMP_NAME="architools_db-pre-faza-h-$(date -u +%Y%m%dT%H%M%SZ).dump" + docker exec architools_postgres pg_dump \ + -U architools_user -d architools_db \ + -F c -Z 6 --no-owner --no-privileges \ + --verbose 2>/tmp/pg_dump.faza-h.log \ + | ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 \ + "mkdir -p ~/faza-h-pin && cat > ~/faza-h-pin/${DUMP_NAME}" + echo "DUMP_NAME=${DUMP_NAME}" + tail -5 /tmp/pg_dump.faza-h.log' +``` +- `-F c` = custom format (single binary file, supports `pg_restore --list` + selective `-t`). +- `-Z 6` = mid-tier compression. Estimated output ~5–7 GB (the existing daily plain-SQL gzip is 5.2 GB and `-F c -Z 6` is similar density on GIS GeoJSON blobs). +- Skipping parallel `-j` because we're streaming through one ssh pipe; parallel only helps when writing to disk on a multi-disk file system. + +**Verify upload arrived intact (run after pg_dump finishes):** +```bash +ssh satra 'ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 " + ls -lh ~/faza-h-pin/ + echo --- header check --- + head -c 5 ~/faza-h-pin/architools_db-pre-faza-h-*.dump | xxd"' +# Expected: first 5 bytes = "PGDMP" (custom-format magic). +``` + +### H.2.2 Verify dump contents with `pg_restore --list` + +```bash +# Stream the dump from NAS back through satra to do pg_restore --list, +# without copying to satra disk. +ssh satra ' + DUMP=$(ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 "ls ~/faza-h-pin/architools_db-pre-faza-h-*.dump | tail -1") + ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 "cat ~/faza-h-pin/$(basename "$DUMP")" \ + | docker exec -i architools_postgres pg_restore --list 2>/dev/null \ + | grep -E "TABLE DATA public (Gis|CfExtract)" ' +``` +Expected output: 5 TABLE DATA lines for `GisFeature`, `GisUat`, `GisSyncRun`, `GisSyncRule`, `CfExtract` (plus their indexes/constraints in other sections). + +### H.2.3 Disk + lifecycle + +NAS `/HDD` has 40 TB free (df 2026-05-19). 5–7 GB dump is rounding error. Pin folder `~/faza-h-pin/` is outside the daily-rotation tree — survives until manually deleted. **Keep this dump until at least 90 days post-DROP** — only restore path if Risk §1 materializes. + +--- + +## H.3 — CfExtract overlap check + +**Goal:** confirm gis_enrichment.CfExtract holds a superset of architools_postgres.CfExtract. Per memory `project_eterra_live_saas.md` + the migration history captured in gis-api memory `project_state_2026_05_17.md`, gis_enrichment.CfExtract was bootstrapped FROM architools_postgres. So overlap should be exact. + +Already verified manually 2026-05-19 — all 8 IDs match: + +```bash +# Architools side (8 rows): +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT id, \"nrCadastral\", \"userId\", \"createdAt\", status + FROM \"CfExtract\" ORDER BY \"createdAt\";"' > /tmp/arc-cf.txt + +# Gis-enrichment side — filter by the IDs from arc-cf.txt: +ssh shop 'docker exec postgres-gis psql -U gis_superuser -d gis -c " + SELECT id, \"nrCadastral\", \"userId\", \"createdAt\", status + FROM gis_enrichment.\"CfExtract\" + WHERE id IN ( + '\''c2ad0f24-f5bb-4586-947c-937254d8ed02'\'', + '\''48908c41-49de-459e-adef-6f0f8fcb8022'\'', + '\''3b4517a0-2d36-49ba-8da8-f3240c2fdf3f'\'', + '\''22aabf8b-610a-4525-a352-21dccf7da5fe'\'', + '\''676f0b3a-2148-414f-ab3a-69ef250509da'\'', + '\''9e3c9d61-d3c4-43dd-bc2c-f92dd9972bab'\'', + '\''8c6045cc-2f53-4c70-b4cb-a459e1fb94e1'\'', + '\''13abdd21-d60b-4b7e-a31f-8187e5840987'\'' + ) ORDER BY \"createdAt\";"' > /tmp/gis-cf.txt + +# Compare (must report 0 diff rows except whitespace): +diff <(sort /tmp/arc-cf.txt) <(sort /tmp/gis-cf.txt) +``` + +**Decision: SKIP migration.** All 8 architools rows exist on gis_enrichment with identical (id, nrCadastral, userId, createdAt, status). No divergent writes since architools-side CfExtract has been **read-only via app code since the cutover** (Faza C feature-flag flip). + +**Final safety check** before §H.4 — guard against new writes between now and freeze: +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT count(*) AS post_clone_rows + FROM \"CfExtract\" + WHERE \"createdAt\" > '\''2026-04-20 00:00:00'\''::timestamp;"' +``` +**Expected: 0** (gis_enrichment was cloned from architools on or around 2026-04-20 per gis-api memory `project_state_2026_05_17.md`). If > 0, halt: those rows are post-clone and must be exported + INSERTed into gis_enrichment with RLS-correct `userId` before freeze. + +--- + +## H.4 — REVOKE INSERT/UPDATE/DELETE + observe (30 days) + +**Goal:** make the 5 tables read-only at the database privilege level. Reads still work for any stale code path; writes raise `permission denied`, which we log and grep for. + +### H.4.1 Apply REVOKE + +```sql +-- Connect as superuser (the postgres role inside the container, not architools_user): +-- ssh satra 'docker exec -it architools_postgres psql -U postgres -d architools_db' + +BEGIN; + +REVOKE INSERT, UPDATE, DELETE, TRUNCATE + ON TABLE public."GisFeature", + public."GisUat", + public."GisSyncRun", + public."GisSyncRule", + public."CfExtract" + FROM architools_user; + +-- Reads must still succeed. Re-grant explicitly in case prior GRANTs are wider +-- than expected — this is idempotent. +GRANT SELECT + ON TABLE public."GisFeature", + public."GisUat", + public."GisSyncRun", + public."GisSyncRule", + public."CfExtract" + TO architools_user; + +-- Sanity print: +SELECT grantee, table_name, privilege_type + FROM information_schema.table_privileges + WHERE table_schema='public' + AND table_name IN ('GisFeature','GisUat','GisSyncRun','GisSyncRule','CfExtract') + AND grantee='architools_user' + ORDER BY table_name, privilege_type; + +COMMIT; +``` +Expected post-COMMIT: only `SELECT` rows for `architools_user` × 5 tables. + +### H.4.2 Verify write rejection + +```bash +# As architools_user — should fail with: ERROR: permission denied for table GisFeature +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + INSERT INTO \"GisFeature\"(id, \"layerId\", siruta, \"objectId\", attributes, \"createdAt\", \"updatedAt\") + VALUES (gen_random_uuid()::text, '\''TEST'\'', '\''999999'\'', -1, '\''{}'\''::jsonb, NOW(), NOW());"' +# Expected: ERROR: permission denied for table GisFeature +``` + +```bash +# Read must still succeed: +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT count(*) FROM \"GisFeature\";"' +# Expected: 9794057 (or whatever the live count is at freeze time). +``` + +### H.4.3 Rollback if observability breaks immediately + +If app errors flood logs within 30 min of REVOKE (active write path missed during code audit): + +```sql +BEGIN; +GRANT INSERT, UPDATE, DELETE, TRUNCATE + ON TABLE public."GisFeature", + public."GisUat", + public."GisSyncRun", + public."GisSyncRule", + public."CfExtract" + TO architools_user; +COMMIT; +``` +Then triage the offending code path, fix it (route to gis-api), redeploy, and re-attempt REVOKE. + +### H.4.4 Monitoring during 30-day window + +**Pattern to grep** in docker logs (architools container): +``` +permission denied for table (GisFeature|GisUat|GisSyncRun|GisSyncRule|CfExtract) +``` +Also catch ORM-side mirror errors: +``` +relation "(Gis|CfExtract) +``` + +**Cron snippet** (do NOT install yet — for review): +```bash +# /etc/cron.d/faza-h-freeze-watch (run as root on satra, every 15 min) +*/15 * * * * root \ + COUNT=$(docker logs --since 15m architools 2>&1 \ + | grep -cE 'permission denied for table (Gis|CfExtract)|relation "(Gis|CfExtract)') ; \ + if [ "$COUNT" -gt 0 ]; then \ + curl -fsS -X POST -H "Content-Type: application/json" \ + -d "{\"text\":\"Faza H: $COUNT write attempts hit revoked Gis/CfExtract tables in last 15min on architools\"}" \ + https://n8n.beletage.ro/webhook/; \ + fi +``` +The n8n webhook URL is a placeholder — Marius creates the workflow when ready. Alternative: pipe to `mail -s` to ops alias or set up an Uptime Kuma push monitor. + +**Manual probe** anytime during the window: +```bash +ssh satra 'docker logs --since 24h architools 2>&1 \ + | grep -E "permission denied for table (Gis|CfExtract)|relation \"(Gis|CfExtract)" \ + | tail -20' +``` + +**Exit criteria for §H.5**: 30 consecutive days with **zero** matches in the grep. + +--- + +## H.5 — Final DROP after 30 silent days + +### H.5.1 Final pre-flight (re-confirm gates from §H.1) + +```bash +# 1. NAS dump still exists: +ssh satra 'ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 \ + "ls -lh ~/faza-h-pin/architools_db-pre-faza-h-*.dump"' + +# 2. Re-take a fresh dump on the day of DROP (in case there's been +# enrichment-field drift we want a clean rollback target): +# (repeat §H.2.1 with a new timestamped filename → ~/faza-h-pin/) + +# 3. postgres-gis still at parity or ahead: +ssh shop 'docker exec postgres-gis psql -U gis_superuser -d gis -c " + SELECT count(*) FROM gis_core.\"GisUat\";"' # must be >= 3186 + +# 4. No code references left in src/ (sanity, should already be 0 from Faza C/D): +cd /home/orchestrator/Code/ArchiTools \ + && grep -rln "GisFeature\\|GisUat\\|GisSyncRun\\|GisSyncRule\\|CfExtract" src/ \ + | grep -v "\\.next\\|node_modules" +# Expected: empty. +``` + +### H.5.2 DROP TABLE CASCADE + +```sql +-- Connect as superuser: +-- ssh satra 'docker exec -it architools_postgres psql -U postgres -d architools_db' + +BEGIN; + +-- CASCADE because GisFeature has FK to GisSyncRun (onDelete: SetNull → safe but +-- DROP order otherwise matters). Order chosen to drop dependents first. +DROP TABLE IF EXISTS public."CfExtract" CASCADE; +DROP TABLE IF EXISTS public."GisFeature" CASCADE; +DROP TABLE IF EXISTS public."GisSyncRun" CASCADE; +DROP TABLE IF EXISTS public."GisUat" CASCADE; +DROP TABLE IF EXISTS public."GisSyncRule" CASCADE; + +-- Optional: drop the PostGIS trigger + 'geom' column infrastructure if it's +-- only used by GisFeature. See prisma/postgis-setup.sql — if all triggers ref +-- GisFeature, drop them now. If PostGIS itself is used by other tables, keep +-- the extension. +SELECT extname FROM pg_extension WHERE extname IN ('postgis','postgis_topology'); +-- If unused: DROP EXTENSION postgis CASCADE; -- only after verifying! + +COMMIT; + +-- Reclaim disk: +VACUUM FULL; -- alternative: pg_repack if downtime not acceptable +``` + +**Verify post-DROP:** +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT tablename FROM pg_tables + WHERE schemaname='\''public'\'' + AND tablename IN ('\''GisFeature'\'','\''GisUat'\'','\''GisSyncRun'\'','\''GisSyncRule'\'','\''CfExtract'\'');"' +# Expected: 0 rows. + +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT pg_size_pretty(pg_database_size('\''architools_db'\'')) AS db_size;"' +# Expected: ~20 GB (down from 35 GB). +``` + +### H.5.3 Prisma schema cleanup + +Remove from `prisma/schema.prisma` (lines 24–195): +- `model GisSyncRule` +- `model GisFeature` (and its `syncRun` relation field) +- `model GisSyncRun` (and its `features GisFeature[]` back-relation) +- `model GisUat` +- `model CfExtract` + +Also check `prisma/postgis-setup.sql` — if it exists and only configures the dropped tables, delete the file or reduce it to a no-op header. + +```bash +cd /home/orchestrator/Code/ArchiTools + +# Edit prisma/schema.prisma (remove the 5 models) +$EDITOR prisma/schema.prisma + +# Reformat + regenerate: +npx prisma format +npx prisma generate + +# Build clean: +npx next build +# Expected: zero errors. If any TS error references Gis*/CfExtract, that's +# left-over Faza C/D dead code — fix or delete the file. + +# Commit: +git add prisma/schema.prisma prisma/postgis-setup.sql +git commit -m "chore(prisma): drop legacy Gis*/CfExtract models — Faza H final" +git push origin main +``` + +### H.5.4 Container redeploy + +```bash +# Portainer API unreachable from Orchi (memory `feedback_portainer_api_unreachable.md`) +# → use Portainer UI (stack 8 → Editor → Update + Pull and redeploy) +# OR SSH on satra: +ssh satra 'cd /data/compose/8 && docker compose pull architools && docker compose up -d architools' + +# Verify: +ssh satra 'docker logs --tail 50 architools 2>&1 | grep -iE "error|prisma|gis"' +# Expected: no Prisma errors. Tile/map still works (sourced from tiles.gis.ac). +``` + +### H.5.5 Rollback (if a write attempt is observed during the 30-day window) + +If grep finds a "permission denied" hit: +1. Capture trace: `docker logs --since architools 2>&1 | grep -B2 -A20 "permission denied"`. +2. Identify route from stack, cross-check Faza C/D diff. +3. Either (a) delete dead Gis* code (proper fix) or (b) fix the ORM call to gis-api proxy. +4. **Do NOT GRANT writes back** unless fix takes > 24h and route is user-facing. If you must `GRANT INSERT, UPDATE, DELETE ON public."" TO architools_user;` — then **reset the 30-day clock**. + +### H.5.6 Hard rollback (if DROP turns out to break something — e.g. archi.* schema FK we missed) + +```bash +# 1. Restore the 5 tables from the pin dump (selective restore, no full DB): +ssh satra ' + DUMP=$(ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 \ + "ls ~/faza-h-pin/architools_db-pre-faza-h-*.dump | tail -1") + ssh -i ~/.ssh/satra_nas_backup satra-backup@10.10.10.10 \ + "cat ~/faza-h-pin/$(basename "$DUMP")" \ + | docker exec -i architools_postgres pg_restore \ + -U postgres -d architools_db \ + -t GisFeature -t GisUat -t GisSyncRun -t GisSyncRule -t CfExtract \ + --no-owner --no-privileges --verbose' + +# 2. Re-apply REVOKE (per §H.4.1) so we're back in read-only freeze, not write-active. +# 3. Restore the deleted Prisma models from git history: +cd /home/orchestrator/Code/ArchiTools +git revert +npx prisma generate && npx next build +git push origin main +# 4. Redeploy (per §H.5.4). +``` + +--- + +## Risks + open questions + +### Risk 1 (HIGH): eterra.live still reads architools_postgres Gis* tables + +Per memory `project_eterra_live_saas.md`: + +> "Shares same PostgreSQL+PostGIS geometry DB as ArchiTools (GisFeature, GisUat tables)" + +eterra.live is a separate Next.js app, separate repo (`gitadmin/eterra-live`), but its Prisma client connects to the same `architools_postgres`. If we DROP the Gis* tables before eterra.live is also cut over to api.gis.ac, eterra.live's `/harta` + `/rgi-search` routes break in prod. + +**Mitigation — must happen before §H.5 DROP** (not before §H.4 REVOKE, since REVOKE breaks eterra.live writes too, but eterra.live shouldn't be writing Gis* — verify): +- Audit `eterra-live` repo for `prisma.gisFeature.*`, `prisma.gisUat.*`, `prisma.cfExtract.*`. +- If write paths exist, those will fail at §H.4 REVOKE — schedule cutover of eterra.live FIRST. +- If read-only, §H.4 is safe, but §H.5 DROP still needs eterra.live migrated to api.gis.ac. + +**Check (run before §H.4):** +```bash +# On Orchi or wherever eterra-live is checked out: +cd /home/orchestrator/Code/eterra-live 2>/dev/null \ + && grep -rn "gisFeature\\|gisUat\\|cfExtract\\|GisFeature\\|GisUat\\|CfExtract" src/ prisma/ \ + | grep -v "\\.next\\|node_modules" +# If repo not local: ssh git.beletage.ro and clone, or check via Gitea web UI. +``` + +### Risk 2 (MEDIUM): unidentified writer touching GisFeature in last 72h + +Snapshot 2026-05-18: 4 GisFeature rows had `updatedAt` within 72h, all on `siruta=147358 (TERENURI_ACTIVE)`, all linked to `syncRunId=4fe614d6-...` from 2026-03-21. The scheduler is officially disabled (memory `project_gis_db_overhaul.md`), yet writes are happening. Candidates: +- A geoportal route that fills `enrichment` JSON on read-then-write — check `/api/geoportal/enrich/route.ts` (found earlier in src/). +- A leftover cron in n8n that hits `/api/eterra/*`. +- eterra.live writing enrichment to the shared DB (would also trip Risk 1). + +**Action before §H.4:** during a 24-h quiet window, monitor: +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT \"updatedAt\", \"layerId\", siruta, \"objectId\" + FROM \"GisFeature\" + WHERE \"updatedAt\" > NOW() - INTERVAL '\''24 hours'\'' + ORDER BY \"updatedAt\" DESC LIMIT 20;"' +``` +If non-empty after Faza C/D ship, find the writer (correlation with `pg_stat_activity` snapshots) before flipping REVOKE. + +### Risk 3 (LOW): pg_dump streaming may time out + +5.2 GB daily dump → expect 5–7 GB pin dump. NAS on 10G agg switch (`sw-agg-10g-01`) → 1–5 min. If stream times out: dump locally first (needs ~10 GB free at `/var/lib/docker/volumes/`), then `rsync` to NAS. + +### Risk 4 (LOW): orphaned PostGIS extension + +`GisFeature.geometry` is JSONB backed by PostGIS `geom` via trigger (see `prisma/postgis-setup.sql`). After DROP, if no other table uses PostGIS, the extension is orphaned but harmless (~7 MB `spatial_ref_sys`). Drop in §H.5.2 is optional — Marius's call. + +### Open question 1: external consumers of architools_postgres Gis*? + +Checked `pg_stat_activity` 2026-05-19 — only `architools_user` active. Point-in-time only. Re-run during freeze 3–4 times at different hours of day: +```bash +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT DISTINCT usename, application_name, client_addr FROM pg_stat_activity WHERE datname='\''architools_db'\'';"' +``` +Anything other than `architools_user` → investigate before §H.5. + +### Open question 2: archi.* schema FK to Gis*? + +Plan 003 §"Open questions" item 4 mentions `archi.*` schema (Canvas, Job, etc.) is DEFERRED. We don't know yet whether `archi.Canvas` or `archi.Job` carries an FK to `GisFeature.id`. **Check before §H.5:** +```sql +ssh satra 'docker exec architools_postgres psql -U architools_user -d architools_db -c " + SELECT conrelid::regclass AS from_table, + confrelid::regclass AS to_table, + conname + FROM pg_constraint + WHERE contype='\''f'\'' + AND confrelid::regclass::text IN + ('\''public.\"GisFeature\"'\'','\''public.\"GisUat\"'\'', + '\''public.\"GisSyncRun\"'\'','\''public.\"GisSyncRule\"'\'', + '\''public.\"CfExtract\"'\'');"' +``` +Empty result = safe. Non-empty = either drop the FK in a prior commit OR move the related data to gis-api before §H.5. + +### Random suspicious thing — schema drift + +Live `CfExtract` on satra has columns not in `prisma/schema.prisma` (`userId`, `pdfData bytea`, `type`, `adminOrderedBy`, `minioPath`). Same class of error as the 2026-05-17 gis RLS leak (memory `project_rls_leak_2026_05_17.md`). Before §H.5.3 audit other models with `npx prisma db pull --print` for deltas. **Never run `prisma db push`** (memory `feedback_prisma_migrate_drift.md`). + +--- + +## Citation map back to plan 003 + +| Section | Plan 003 reference | +|---|---| +| H.1 / H.2 | 003 §Faza C, line 108 ("Do not drop Prisma tables … keep as dead columns 2-3 days") — we extend "2-3 days" to "30 days" for safety. | +| H.3 (overlap) | 003 §Faza F + memory `project_eterra_live_saas.md` (gis_enrichment cloned from architools). | +| H.4 (REVOKE) | New for this runbook — not specified in 003. Justification: defense-in-depth, mirrors gis-api's "explicit REVOKE > rely on defaults" pattern (memory `feedback_gis_meta_default_acl.md`). | +| H.5 (DROP) | 003 §Faza G item 10 ("Follow-up PR drops dead Prisma tables") + §Faza C line 108. | +| Risks §1 (eterra.live) | 003 §Faza H header ("Planhub deferred … separate PR") — eterra.live is in the same "downstream consumer" bucket but not called out. Flagged here as a HARD prerequisite. | + +--- + +End of runbook. Total: 5 phases × ~1 hour of operator time spread across 30 days, +1 month wall-clock for the silent observation window. diff --git a/docs/plans/audit-2026-05-19.md b/docs/plans/audit-2026-05-19.md new file mode 100644 index 0000000..dd8d97c --- /dev/null +++ b/docs/plans/audit-2026-05-19.md @@ -0,0 +1,144 @@ +# ArchiTools → api.gis.ac cutover — audit 2026-05-19 + +**Scope:** every commit on `main` from `6b3d56e` (pre-cutover baseline) through `68355ef` (HEAD). +**Reviewer:** Claude Opus 4.7 — read-only audit; no code changes. +**Pilot user:** `m.tarau@beletage.ro` (gated via `GIS_AC_PILOT_USERS`). + +Commits in scope: + +``` +68355ef fix(geoportal-v2): UAT click deep-links to eterra.live + revert debug +1c6efb9 debug(auth): expose session.debug={hasRefreshToken, expiresIn} +3829401 feat(ops): VersionWatcher — toast prompt when a new deploy is live +64bccdb feat(ops): /api/version endpoint with git SHA + build time +6054d08 fix(faza-e): refresh dedup, fetch timeout, error surfacing +47ca366 fix(auth): Authentik access_token refresh flow +e0610b0 fix(geoportal-v2): handle PMTiles features without uuid id +7a22b11 debug(gis-search): log session presence + access token presence +99a673d feat(geoportal): Faza E v2 thin client (PMTiles + gis.ac) +fc2bdfb feat(gis-api): Faza D thin client lib (src/lib/gis-api-client.ts) +977db6d feat(cutover): Faza C feature-flag infra for api.gis.ac +403b6b3 feat(auth): Faza B NextAuth Authentik scope=enrichment + forward access_token +54b78c2 feat(deploy): Faza A Infisical runtime migration +``` + +Files changed (19): `Dockerfile`, `docker-compose.yml`, `docker-entrypoint.sh`, `src/middleware.ts`, `src/app/providers.tsx`, `src/core/auth/auth-options.ts`, `src/core/feature-flags/use-gis-ac.ts`, `src/core/version/version-watcher.tsx`, `src/lib/gis-api-client.ts`, `src/app/api/version/route.ts`, `src/app/api/gis/{search,parcela/[id],parcel/tech}/route.ts`, `src/modules/geoportal/components/geoportal-module.tsx`, `src/modules/geoportal/v2/*.tsx` (5 files). + +--- + +## 1. Production safety (flag=0 path) + +- `[ship]` Legacy `parcel-sync` / `eterra` / `ancpi` source trees are **untouched** by this session's commits. `git diff 6b3d56e..HEAD --name-only -- src/modules/parcel-sync/ src/modules/eterra/ src/modules/ancpi/ src/app/api/ancpi/ src/app/api/eterra/` returns empty. Non-pilot users see exactly the same code path as before. +- `[ship]` `src/config/{modules,navigation,flags,companies,external-tools,nas-paths}.ts` and `src/core/feature-flags/index.ts` are **unchanged**. No regression risk in module registration or sidebar/feature-flag wiring. +- `[ship]` `geoportal-module.tsx` is the **only** legacy module file touched: it imports `useSession`, reads `session.useGisAc`, branches to `GeoportalV2` only when true. The legacy renderer (`GeoportalV1Legacy`) is the verbatim previous body. For flag=0 users the render output is identical to baseline. +- `[ship]` `middleware.ts` adds `api/version` to the exclusion regex. No other matcher change. The version route is intentionally public (no auth), as is correct for a build-ID endpoint. +- `[ship]` `auth-options.ts` change is backwards-compatible: callbacks still mint the same `role` / `company` claims; new fields (`accessToken`, `error`, `useGisAc`) are additive on the session object. Users without `account.access_token` (rare, but possible on cached sessions) skip the refresh branch via the `token.accessToken && token.refreshToken` guard — no hot-loop risk. +- `[cosmetic]` `VersionWatcher` toast component is global (mounted in `providers.tsx`). It fires for **everyone**, not just pilot. For non-pilot users this is fine — they get a "new build available" nudge after each redeploy, which is desirable. + +## 2. Secret hygiene + +- `[ship]` No hard-coded secrets in any committed file. `AUTHENTIK_CLIENT_ID/SECRET/ISSUER`, `GIS_API_URL`, `NEXTAUTH_SECRET`, etc. are all `process.env.X` with safe `||` fallbacks (`""` for secrets, public URLs for non-secrets). +- `[ship]` `docker-entrypoint.sh` uses `--data` with `$INFISICAL_CLIENT_ID`/`$INFISICAL_CLIENT_SECRET` only in a JSON body sent to Infisical over HTTPS; nothing echoed. The `[infisical] exported env -` line only reports presence (`${VAR:+set}`), never values. Good. +- `[ship]` Hard-coded **URLs** present and acceptable: + - `https://api.gis.ac` (gis-api-client.ts:18, env-overridable) + - `https://pmtiles.gis.ac/overview.pmtiles` (map-viewer.tsx:39, env-overridable via `NEXT_PUBLIC_PMTILES_URL`) + - `https://eterra.live/harta?...` (geoportal-v2.tsx:39, feature-info-panel.tsx:157) — intentional cross-app deep link, fine. + - `https://tools.beletage.ro` fallback (middleware.ts) — pre-existing, not introduced this session. +- `[cosmetic]` `src/app/api/version/route.ts` exposes `useGisAcDefault` (boolean) and `pilotUsers` (**count only**) — no email leakage. Safe. +- `[ship]` `console.log("[auth] refresh OK expires_in=%d", body.expires_in)` logs only the integer (seconds-until-expiry), never the token itself. The `console.warn` paths log `res.status` + `body.error` (Authentik error code like `invalid_grant`), no token bodies. Safe. + +## 3. TypeScript correctness + +- `[ship]` `NODE_ENV=development npx tsc --noEmit` — **exits 0, zero errors, zero warnings**. + +## 4. Dead / orphan code + +- `[future]` `gisApi.parcel.unitsFetch`, `gisApi.parcel.immApps`, `gisApi.building.tech`, `gisApi.building.condoOwners`, `gisApi.enrichment.cf.{list,get,create,patch,uploadPdf,getPdf}`, `gisApi.enrichment.catalog`, `gisApi.me` — declared in `src/lib/gis-api-client.ts`, **no callers** under `src/`. These are Faza F surface (CF ordering + parcel detail tabs). Acceptable to land ahead of consumers since they share helper infrastructure. Bundle cost: lib is server-side only and tree-shaken in production builds. +- `[future]` `GisApiCallOpts.accessToken` override path: every public method accepts `opts.accessToken` but no caller passes it. Intended for future background jobs/service accounts. Keep. +- `[cosmetic]` `objectId` field on `ClickedFeatureLite` is computed from `p.object_id` in `map-viewer.tsx:248-254`, set on the panel feature, but **never read** by `feature-info-panel.tsx`. Minor — costs nothing but should be removed if no consumer materializes by Faza F. +- `[future]` `(session as any).error` is set when refresh fails; **no client component** currently reads `session.error` to force re-login. The map-viewer fetches will silently 401 and the user will see "Eroare: gis_api_401" in the panel without auto-redirect. Add a session-error watcher in Faza F polish. + +## 5. Faza E gaps vs plan 003 + +Plan 003 §Faza E specified five rewrites: `map-viewer.tsx`, `search-bar.tsx`, `feature-info-panel.tsx`, `basemap-switcher.tsx`, plus the v2 wrapper. All five exist under `src/modules/geoportal/v2/`. Gaps vs spec: + +- `[future]` **UAT bounds / flyTo:** Plan §Faza E implicitly assumed `gisApi.search` would return UAT bounds for client `fitBounds`. Today gis-api returns `{siruta, name, county}` only. Workaround in `geoportal-v2.tsx:34-41`: click on UAT opens `eterra.live/harta?siruta=…` in a new tab. Acceptable as documented; tracked as "add `GET /api/v1/uat/:siruta/bounds` to gis-api". Severity = future, not blocker. +- `[future]` **Layer panel removed entirely** from v2. v1 had `LayerPanel` + `getDefaultVisibility` controlling per-layer visibility (UATs/cladiri/terenuri toggles). v2 hard-codes all four layers on. Pilot user may complain about not being able to toggle building outlines. Minor UX regression vs v1. +- `[future]` **Selection toolbar removed entirely** from v2. v1 had `SelectionToolbar` + `selectionMode` (`off`/`point`/`rectangle`/`polygon`) for multi-select export workflows. v2 has no multi-select. Plan §Faza E was silent on this; reasonable to defer until usage is observed. +- `[future]` **`SetupBanner` removed.** Was a no-op in v1 once setup completed, so no regression. Skip. +- `[future]` **`boundary-check` / `cf-status` / `export` / `pad` / `piz` buttons** in the panel. Plan §Faza E: "defer scoping per 002." v2's `feature-info-panel.tsx` has only three actions: `Citește din ANCPI` (parcel/tech), `Export GeoPackage` (eterra.live deep-link), `Comandă CF` (disabled — "Va fi disponibil la Faza F"). Aligned with plan; not a gap. +- `[blocker for global rollout, ship for pilot]` **Map starts at Romania-wide zoom 6 with no auto-fit on click.** When user clicks an UAT in search, behavior = open eterra.live in new tab (not flyTo). Pilot probably aware; broader rollout needs the bounds API or a fallback `fitBounds` from the parcel polygon's bbox. +- `[cosmetic]` "gis.ac · v2" badge bottom-right (`geoportal-v2.tsx:83-85`) — intentional, fine. Remove on global rollout. + +## 6. Infisical drift (satra `.env` vs `/architools` prod) + +Cross-checked `ssh satra "grep ^[A-Z] /opt/architools/.env"` (41 keys) vs Infisical `/architools` prod (39 keys). + +**Drift category A — both places (31 keys, app-secret duplication):** + +`ADDRESSBOOK_API_KEY`, `ANCPI_*` (5), `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET`, `AUTHENTIK_ISSUER`, `BREVO_API_KEY`, `DATABASE_URL`, `DWG2DXF_URL`, `ENCRYPTION_SECRET`, `ETERRA_PASSWORD`, `ETERRA_USERNAME`, `MANICTIME_TAGS_PATH`, `MINIO_*` (6), `NEXTAUTH_SECRET`, `NEXTAUTH_URL`, `NOTIFICATION_*` (3), `PORTAL_ONLY_USERS`, `STIRLING_PDF_*` (2). + +- `[ship]` Behaviour is correct because the new `docker-compose.yml` no longer references these keys in the `environment:` block (only `INFISICAL_CLIENT_ID/SECRET`, `NODE_ENV`, `PORT`, `HOSTNAME` remain). `docker-entrypoint.sh` fetches them from Infisical at boot and `export`s them. The .env duplicates are **dead bytes** for the running container — never read by compose. +- `[future]` Drift is still a maintenance hazard. If a future operator rotates a secret in .env and forgets Infisical, nothing breaks (compose ignores the .env value), but reading `/opt/architools/.env` to "find the current value" gives a false answer. **Recommendation:** prune `.env` to bootstrap-only keys (`INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`, `NODE_ENV`, and the build-time `NEXT_PUBLIC_MARTIN_URL` / `NEXT_PUBLIC_PMTILES_URL`) after the cutover stabilizes. + +**Drift category B — only in `.env` (10 keys):** + +- `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET` — bootstrap, **must stay** in .env. +- `NODE_ENV` — compose reads `${NODE_ENV:-production}`, can stay or be removed (default kicks in). +- `DB_HOST`, `DB_NAME`, `DB_PASS`, `DB_PORT`, `DB_USER` — **only used by the `tippecanoe` service** (compose line 84-88), which has `profiles: ["tools"]` and is started ad-hoc. Belongs in .env. Acceptable. +- `NEXT_PUBLIC_MARTIN_URL`, `NEXT_PUBLIC_PMTILES_URL` — **build-time args** (Dockerfile lines 26-30) baked into the static client bundle. They must be in .env at `docker compose build` time. Cannot move to Infisical without entrypoint-side build (not viable for `NEXT_PUBLIC_*`). Acceptable. + +**Drift category C — only in Infisical (8 keys):** + +- `AI_PROVIDER`, `AI_MAX_TOKENS` — referenced in compose pre-refactor; no longer in compose `environment:` block but still fetched by entrypoint and present in Infisical. AI module reads them via `process.env.AI_*`. OK. +- `AUTHENTIK_JWKS_URL` — set in Infisical but **not consumed** by current code. NextAuth `AuthentikProvider` uses `issuer` to derive JWKS via discovery. Dead key (until/unless gis-api side validates with explicit JWKS URL — that's a gis-api concern, not architools). Cosmetic. +- `AUTHENTIK_SCOPES` = `"openid email profile enrichment offline_access"` — verified present; this is how `offline_access` is actually getting into the auth flow (overrides the code default `"openid email profile enrichment"`). Critical for refresh_token issuance from Authentik. **The fact that code default doesn't include `offline_access` is an audit finding** — if someone deletes `AUTHENTIK_SCOPES` from Infisical, refresh stops working silently. See §7 finding. +- `GIS_AC_PILOT_USERS` — Faza C pilot list. Live and used. +- `GIS_API_URL` — Faza D thin client target. Live. +- `N8N_WEBHOOK_URL` — PMTiles rebuild webhook (existing functionality, was on satra .env before). Compose no longer references it; entrypoint pulls from Infisical and `parcel-sync` reads from `process.env`. Working. +- `USE_GIS_AC` — Faza C cutover global toggle. Currently expected to be `""` (only pilot user enabled). + +## 7. Diagnostic logs left in code + +All emitted only on real errors (`.warn` / `.error`) except the one `.log`: + +- `[cosmetic]` `console.log("[auth] refresh OK expires_in=%d", ...)` at `auth-options.ts:69` — fires every ~5min for every active session. Will be high-volume in container logs. Suggest demote to `console.debug` or gate behind `process.env.AUTH_DEBUG === "1"`. Not a blocker — values are integers, not tokens. +- `[ship]` `console.warn("[auth] refresh failed: …")` / `console.warn("[auth] refresh error: …")` — fires on failure only. Keep as-is, this is real ops signal. +- `[ship]` `console.error("[gis-search]", "[gis-parcela]", "[gis-parcel-tech]" "internal error: …")` — error-path only, no token leakage (message truncated to 200 chars via `msg.slice(0, 200)`). Keep. +- `[ship]` `console.warn("[v2-click] tile props missing siruta/cadastral_ref:", p)` — fires only when PMTiles emits an unexpected feature shape. Useful diag, low volume. Keep. +- `[ship]` The earlier `7a22b11` debug log in `gis-search/route.ts` and `1c6efb9` `session.debug` exposure were both reverted in `6054d08` and `68355ef` respectively. Verified — neither leaks into HEAD. + +**Code default scope omits `offline_access`.** Plan 003 §Faza B specified `scope: "openid profile email enrichment"`. The code reads `process.env.AUTHENTIK_SCOPES || "openid email profile enrichment"`. The production-required `offline_access` (for refresh_token issuance) is only present because the Infisical value overrides the default. `[future]` Tighten the default to include `offline_access` so a missing Infisical key doesn't break refresh. + +## 8. UX gotchas the pilot user might hit + +- `[ship]` **Search-dropdown UAT click opens new tab to eterra.live** (`geoportal-v2.tsx:34-41`). Intentional, documented in code comment. Pilot has been told. Track: add `GET /api/v1/uat/:siruta/bounds` to gis-api so v2 can `fitBounds` in-place. +- `[ship]` **Pilot users (only) see V2; everyone else sees V1.** Confirmed in `geoportal-module.tsx:30-40`: `useSession()` → `Boolean(session.useGisAc)` → render V2 or V1Legacy. Server-side `useGisAcFlag` reads `USE_GIS_AC` env (currently unset = `""`) and `GIS_AC_PILOT_USERS` (only the pilot email). Non-pilot users land on V1 with byte-identical render output to pre-cutover. +- `[future]` **Search results dropdown shows UATs and parcele but does not snap the map.** UAT click → external tab (above). Parcel click → opens info panel immediately via `setClicked({…})` with `siruta: ""` (empty, line 47), so the **"Citește din ANCPI" button will fail with `missing_siruta_or_cad`** because `feature.siruta` is empty for search-originated features. The map-click path passes siruta correctly; the search-click path doesn't. **Severity: blocker for pilot productivity** if they expect to find parcels by cadastral search then enrich them. Test plan should cover this case. +- `[cosmetic]` "gis.ac · v2" badge in bottom-right corner. Aesthetic. Will be removed at global rollout. +- `[cosmetic]` "Comandă CF" button in v2 panel is disabled with tooltip "Va fi disponibil la Faza F". Pilot may try to click, see disabled state. Acceptable. +- `[future]` **No re-login UX** when `session.error === "RefreshAccessTokenError"`. The session keeps a stale token, every gis-api call returns 401, panel shows raw error code. Adding a session-error watcher in `providers.tsx` (or extending `VersionWatcher`) would close the loop. +- `[future]` **Map basemap switch tears down and rebuilds the whole MapLibre instance** (`map-viewer.tsx:198-283` — `useEffect` depends on `basemap`). PMTiles source has to re-fetch metadata each time. On slow networks this is noticeable. v1 had the same issue; not a regression. +- `[future]` **PMTiles overview tile coverage.** v2 uses a single `overview.pmtiles` from `pmtiles.gis.ac`. If that file's contents don't include UATs the pilot needs (e.g. a UAT not yet in the central DB), the map is silently empty for that area. v1 was backed by satra Martin and had different sync state. Worth a sanity check before broad rollout. + +--- + +## Verdict + +**Ready to enable for the pilot user broadly: YES (with one caveat).** + +The flag=0 path is byte-identical to pre-cutover; legacy code is untouched. TypeScript clean. Secret hygiene clean. No console leaks. The only **pilot-blocking** finding is **§8 third bullet** — clicking a parcel in the search dropdown sets `siruta: ""`, which breaks the "Citește din ANCPI" button. That should be patched before relying on cadastral-search workflows. Either: + (a) `gis-api`'s `/search` response carries `siruta` per feature (preferred), or + (b) `handleFeatureSelect` in `geoportal-v2.tsx` does a follow-up `parcela.get(f.id)` to hydrate `siruta` before showing the panel. + +**Ready for global rollout (flip `USE_GIS_AC=1`): NO.** Three blockers/gaps need to land first: +1. The siruta-empty-from-search bug above. +2. UAT-bounds endpoint in gis-api so search → flyTo works in-place (no eterra.live tab). +3. Faza F shipped (CF ordering parity) — currently the v2 panel's "Comandă CF" button is disabled. The work is half-done in the working tree (`src/app/api/cf/*`, `cf-api-base.ts`, modified `epay-tab.tsx`) but **not committed** to main. + +**Operational improvements (no rollout impact):** +- Demote `[auth] refresh OK` to debug-level. +- Add `offline_access` to the code-default `AUTHENTIK_SCOPES`. +- Prune duplicate keys from `/opt/architools/.env` after a few weeks of stable Infisical bootstrap. +- Wire `session.error === "RefreshAccessTokenError"` to a re-login UX. +- Remove unused `objectId` field on `ClickedFeatureLite` if nothing materializes by Faza F. diff --git a/docs/plans/prompt-handoff-2026-05-19.md b/docs/plans/prompt-handoff-2026-05-19.md new file mode 100644 index 0000000..23f3373 --- /dev/null +++ b/docs/plans/prompt-handoff-2026-05-19.md @@ -0,0 +1,85 @@ +# Handoff prompt — ArchiTools cutover, resume after 2026-05-19 session + +## Where we left off + +Faze A→F of plan 003 (`docs/plans/003-architools-cutover-execution-2026-05-17.md`) are committed and deployed. Pilot user `m.tarau@beletage.ro` is on the V2 / api.gis.ac path. Two outstanding bugs and the Faza G/H work remain. Full state in memory `[[architools-cutover-state-2026-05-19]]`. The Authentik token-endpoint root-cause is in memory `[[authentik-token-endpoint-shared]]`. + +## Suggested prompt for the next session + +``` +Sesiune nouă în ~/Code/ArchiTools după un session lung pe 2026-05-19. Continuăm +cutover-ul ArchiTools → api.gis.ac (Plan 003). + +Citește în ordine de prioritate: +1. docs/plans/prompt-handoff-2026-05-19.md (acest fișier) +2. ~/.claude/projects/-home-orchestrator-Code-ArchiTools/memory/MEMORY.md +3. docs/plans/audit-2026-05-19.md (audit complet după Faze A-E) +4. docs/plans/004-faza-h-runbook.md (Faza H runbook — NU executa, doar înțelege) +5. docs/plans/003-architools-cutover-execution-2026-05-17.md (planul original) + +Confirmă că înțelegi state-ul curent + cele 2 bug-uri outstanding, apoi cere-mi +direcție. NU re-rula Faze A-F — sunt deployate. Pilot user m.tarau e activ pe V2. + +Bug-uri prioritare de fixat: +- B1: /geoportal V2 panel nu randează enrichment-ul din DB după click parcelă. + Datele EXISTĂ în gis_core.GisFeature.enrichment (verificat: parcela + f9bf2ca4-d35c-4636-bd82-9caf09454166, 10 chei). api.gis.ac /parcela + întoarce datele cu scope=full. Dar panel-ul arată "Apasă „Citește din + ANCPI" pentru a încărca". Caut bug pe client între setDetail(data) și + enrichmentEntries render. Începe cu logs `[gis-parcela]` (deja există) + + DevTools Network response shape. +- B2: "Comandă CF" în V2 panel deschide /parcel-sync?tab=epay&cad=… dar + pagina destinație nu citește query params. Sau wire-up parametrii în + parcel-sync page, sau schimbă strategia de deep-link. + +Apoi: Faza G (24h grace + monitor), Faza H prereqs (eterra.live cutover, +identify writer la GisFeature, schema drift CfExtract). +``` + +Adaugă tu la prompt orice ai vrea schimbat sau extins. + +## Quick facts cheatsheet for the new session + +- **Live commit**: check `/api/version` for `commitShort` — should be `162c8ed` or later +- **Pilot user**: `m.tarau@beletage.ro` (in Infisical `/architools` `GIS_AC_PILOT_USERS`) +- **Flag global**: `USE_GIS_AC=0` (only pilot list active) +- **Authentik token endpoint**: `https://auth.beletage.ro/application/o/token/` (shared, NOT per-provider) +- **ePay pool**: 2 accounts on `gis_meta.eterra_accounts`, 500/h quota each — needs manual reset when exhausted: `UPDATE gis_meta.eterra_accounts SET usage_current_hour = 0 WHERE status = 'active';` +- **Trigram index on `gis_core."GisFeature"."cadastralRef"`**: `gisfeature_cadref_trgm_idx` exists (built 2026-05-18, search latency 30s → 150ms) +- **Connection limit** on gis-api Prisma: 10 (after the trigram-index fix, lower causes pool timeouts) +- **Pilot decision on CF**: Marius wants per-user ePay session + credit visibility — V2 panel deep-links to legacy `/parcel-sync` instead of using `/api/cf/*` + +## Files modified this session (for context only — DO NOT redo) + +``` +src/core/auth/auth-options.ts — refresh flow + cooldown + token endpoint fix +src/core/feature-flags/use-gis-ac.ts — flag helper +src/core/version/version-watcher.tsx — toast component +src/lib/gis-api-client.ts — thin client lib +src/app/api/version/route.ts — version endpoint +src/app/api/gis/{search,parcela/[id],parcel/tech}/route.ts — V2 proxy routes +src/app/api/cf/{order,orders,[id],[id]/pdf,catalog}/route.ts — Faza F routes (currently unused by V2 panel) +src/modules/geoportal/components/geoportal-module.tsx — branch on session.useGisAc +src/modules/geoportal/v2/* — V2 components +src/modules/parcel-sync/components/cf-api-base.ts — UI helper +src/modules/parcel-sync/components/epay-{tab,order-button}.tsx — flag-branched +src/middleware.ts — /api/version exclusion +Dockerfile, docker-compose.yml, docker-entrypoint.sh — Infisical runtime + GIT_COMMIT/BUILD_TIME +``` + +## How to verify the next session is healthy + +```bash +curl -s https://tools.beletage.ro/api/version | jq . +# expect commitShort = latest pushed commit +``` + +```bash +ssh satra "docker logs architools --tail 10 2>&1" +# expect: [infisical] fetched 39+ secrets, Next.js Ready in <500ms +``` + +```bash +# Pool state +ssh shop "docker exec postgres-gis psql -U gis_superuser -d gis -c \"SELECT username, usage_current_hour, quota_per_hour FROM gis_meta.eterra_accounts WHERE status='active';\"" +```