Files
ArchiTools/docs/plans/004-faza-h-runbook.md
T
Claude VM 9847b4a070 docs(plans): session handoff + audit + Faza H runbook
End of 2026-05-19 cutover-debug session. Saves the full state +
2 outstanding bugs + Faza G/H plan into docs/plans/ so the next
session can resume without re-investigating.

- prompt-handoff-2026-05-19.md: short prompt for the next session
  to amend + resume.
- audit-2026-05-19.md: auditor-agent output (~30 findings).
- 004-faza-h-runbook.md: pg_dump + REVOKE + DROP runbook with
  prereqs (eterra.live shares DB, unidentified writer, CfExtract
  schema drift).

Memory entries also written:
- feedback/authentik-token-endpoint-shared
- project/architools-cutover-state-2026-05-19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:45 +03:00

519 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan 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 24195: 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 ~57 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). 57 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/<faza-h-alert>; \
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 24195):
- `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 <timestamp> 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."<table>" 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 <commit-from-H.5.3>
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 57 GB pin dump. NAS on 10G agg switch (`sw-agg-10g-01`) → 15 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 34 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.