Replaces tradein.cad_buildings snapshot with live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings. Fixes /trade-in/api/v1/geocode/reverse 500 (Nominatim ban) and address_not_geocoded for cadastre addresses (e.g. Хохрякова 81).
Security (deep-review fixes):
- 100_tradein_fdw_role.sql: passwordless CREATE ROLE; password set by deploy.yml ALTER ROLE bootstrap reading GENDESIGN_FDW_PASSWORD from backend/.env.runtime (via psql :'pw' var → format %L — injection-safe).
- core/fdw.py: regex whitelist [A-Za-z0-9_-]{32,256} on password, ValueError without echoing value, try/rollback on commit.
- 060_postgres_fdw_extension.sql: connect_timeout='3' on FOREIGN SERVER + ALTER ADD/SET fallback.
- geocoder.py: _cadastral_forward_sync / _cadastral_reverse_sync wrapped in asyncio.to_thread.
- 100_*.sql: REVOKE ALL ON ALL TABLES/SEQUENCES/FUNCTIONS IN SCHEMA public; only GRANT SELECT on v_tradein_cad_buildings.
- pg_user_mappings query handles PUBLIC mapping (usename IS NULL).
Tests: 3 SQL-injection guards on ensure_fdw_user_mapping + rewritten cadastral suite.
Добавляет endpoint для приёма заявок на пилот (lead-gen).
INSERT в pilot_requests, response {id, created_at, status}.
Telegram-уведомление — TODO (creds не настроены, #307 SF-B3).
Migration: data/sql/118_pilot_requests.sql
Add _extract_obj_class_from_ai() with regex patterns for Комфорт/Бизнес/Премиум/Элит/Стандарт.
_norm_object() uses it as fallback; objClass from API still takes priority if ever returned.
Add backfill SQL 105 to retroactively populate existing rows from raw JSON payloads.
Coverage: ~1806/4548 rows will be filled; remainder NULL is expected (Нежилое/no AI desc).
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
(Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
emoji star removed (ui-ux.md), empty-state text min 30→15
Audit 2026-05-17: objective_corpus_room_month содержал активные сделки
для Малевич (98/12m) и Ньютон парк (30/12m), но записей в
objective_complex_mapping не было → mv_layout_velocity пропускал оба
проекта в velocity calculations.
Добавлены маппинги:
- Малевич → domrf_obj_id 64701 (Строящиеся, 244кв, сдача 2027-09)
- Ньютон парк → domrf_obj_id 47390 (Сданные, 411кв, крупнейший corpus)
ON CONFLICT DO NOTHING — идемпотентно. REFRESH mv_layout_velocity
выполняется вручную post-deploy (runbooks/Refresh_MV_Layout_Velocity.md).
Closes (epic part) #271 item 4
Add SQL migration 100_user_weight_profiles_default_seed.sql with system
presets Эконом/Комфорт/Бизнес (user_id='__system__'). Migration is
idempotent via ON CONFLICT DO UPDATE.
Backend:
- weight_profiles.py: add SYSTEM_USER_ID constant + list_profiles_with_system()
- admin_weight_profiles.py: add include_system query param to GET list endpoint
Tests: 3 new tests covering include_system flag and service sentinel behaviour.
PR #222 deploy failed на 99_nspd_entities_denorm.sql:
ERROR: syntax error at or near "а"
LINE 2: '3-сегментный квартал последнего harvest'а. Используется...
Unescaped apostrophe в COMMENT string closes literal early.
Fix: harvest'а → harvest''а (SQL escape).
Других unescaped апострофов в SQL литералах нет (другие 3 — в SQL комментариях
'--' , безопасно).
PR #173 fixed cad_buildings_pkey but missed 5 other indexes. Same root cause:
PG does NOT auto-rename indexes on table rename → all 6 backing indexes
remain on cad_buildings_old_apr26 with original names.
Add 5 more `ALTER INDEX IF EXISTS ... RENAME TO ..._old_apr26_*` statements
for: geom_gist, quarter_idx, objdoc_idx, complex_idx, purpose_idx.
Migration 92 still NOT in _schema_migrations (4× rollback now). Next deploy
will re-apply cleanly.
Co-authored-by: lekss361 <claudestars@proton.me>
Migration 92_cad_bulk_layers.sql fails on deploy with:
ERROR: relation "cad_buildings_pkey" already exists
CONTEXT: ALTER INDEX cad_buildings_new_pkey RENAME TO cad_buildings_pkey
Root cause: K.1 renamed cad_buildings → cad_buildings_old_apr26, but PG
does NOT auto-rename the backing PK index on table rename. The legacy
cad_buildings_pkey index is still attached to cad_buildings_old_apr26.
K.3 then tries to rename cad_buildings_new_pkey to the same name → collision.
Fix: add an explicit `ALTER INDEX IF EXISTS cad_buildings_pkey RENAME TO
cad_buildings_old_apr26_pkey` BEFORE the new rename. Idempotent (IF EXISTS).
Migration 92 was rolled back in last 3 deploy attempts (not in
_schema_migrations), so this edit will re-apply cleanly on next deploy.
Co-authored-by: lekss361 <claudestars@proton.me>
* fix(frontend): apiFetchWithStatus body stream double-read crash
Per user report 2026-05-14: 'Failed to execute text on Response: body stream
already read' при analyze с non-JSON error response.
apiFetchWithStatus делал .json() (consumes stream), потом .text() в catch
(FAILS — stream already consumed). Fetch API: body — ReadableStream,
consumable один раз.
Fix: read text() once → JSON.parse on string. На fail (HTML / non-JSON)
оборачиваем в {detail: rawText}. Никаких double-reads.
Site-finder cad search больше не крашится на error pages.
* feat(infra): auto-apply data/sql/*.sql migrations in deploy pipeline (#150)
Закрывает root cause production 500 на /api/v1/admin/site-finder/weight-profiles
(а также 2 unapplied migrations: #89/#91).
## Why это нужно
После audit обнаружили что:
- backend/alembic/versions/ пустой (Alembic configured но не используется)
- deploy.yml не имеет migration step
- 47+ raw SQL files накопилось без auto-apply
- 3 файла unapplied: 87 (engineering), 89 (drop brin), 90 (weight profiles)
User report: GET weight-profiles → 500 потому что user_weight_profiles
table не существует на prod.
## Changes
### deploy.yml +39 lines
- Path trigger расширен на data/sql/*.sql
- New step 'Apply DB migrations' между compose pull и compose up -d:
- Idempotent CREATE TABLE _schema_migrations
- Loop по data/sql/*.sql sorted → skip applied → psql with ON_ERROR_STOP=on
- Record applied filename в tracking table
- Exit 1 на failure → containers НЕ обновляются (no partial state)
### NN collision fix
- 87_engineering_networks_191fz.sql → 91_engineering_networks_191fz.sql
(collision с 87_on_demand_indexes.sql, мой renumber 85→87 был неудачным)
### CLAUDE.md +32 lines
- Subsection 'Auto-apply' под Migration pattern
- Bootstrap procedure docs
- Idempotency requirement
- Failure recovery
### scripts/bootstrap_schema_migrations.sh +100 lines
- One-time seed _schema_migrations с 48 already-applied files
- 3 файла intentionally NOT seeded: 89/90/91 (auto-apply на first deploy)
- Usage docs + verification query
## Vault
domains/infra/Runbook_Auto_Apply_Migrations.md NEW
infra-MOC.md updated
## Required manual steps ДО merge
1. SSH gendesign (tunnel)
2. POSTGRES creds export
3. bash scripts/bootstrap_schema_migrations.sh — seed 48 already-applied files
4. Verify: SELECT COUNT(*) FROM _schema_migrations → 48
5. После merge → first deploy auto-apply #89/#90/#91 → 500 closes
Closes#150
---------
Co-authored-by: lekss361 <claudestars@proton.me>
Per code review audit (May 14): файл без transaction wrapper — backfill CTE
INSERTs + view recreations executed как individual auto-commit statements.
Mid-file crash → orphan rows в complexes/complex_sources с no rollback path.
## Fix
`BEGIN;` после header comments (line 31, перед section 1).
`COMMIT;` в конце файла после последнего COMMENT ON VIEW.
Wraps все 6 секций atomically:
1-2: CREATE TABLE + CREATE INDEX для complexes + complex_sources
3-5: 3 backfill CTE INSERTs с ON CONFLICT DO NOTHING
6: DROP VIEW IF EXISTS + CREATE VIEW + COMMENT для v_complex_full
## No CONCURRENTLY split needed
Проверил — все 10 indexes используют plain `CREATE INDEX IF NOT EXISTS` без
CONCURRENTLY keyword. Безопасно обернуть в одну tx.
## Idempotency preserved
Все existing guards остаются:
- CREATE TABLE/INDEX IF NOT EXISTS
- ON CONFLICT (source, source_id) DO NOTHING
- DROP VIEW IF EXISTS
Re-running file двукратно — safe.
## Vault
`fixes/Migration_73_Complexes_Atomic_May14.md` — created (status: resolved).
Co-authored-by: lekss361 <claudestars@proton.me>
Per code review audit (May 14): BRIN index на partitioned parent table dead
weight. PG16 НЕ propagates такие индексы на child partitions; pg_stat
показывает idx_scan=0 → никогда не использовался.
## Why dead
rosreestr_deals — yearly partitioned по period_start_date. Queries делают
partition pruning через partition key. BRIN index на parent существует, но:
1. На child partitions его нет (PG16 не reflects)
2. Parent сам не имеет данных (partitioned out)
3. Queries никогда не trigger BRIN scan
Maintenance overhead (storage, autoanalyze) без value.
## Migration
`data/sql/89_drop_dead_brin_rosreestr_deals.sql` — атомарный DROP с idempotent
`IF EXISTS`. Apply order: после 88_*.
## Cleanup
`data/sql/01_schema_rosreestr_deals.sql:65-67` — закомментировал оригинальное
CREATE с note: fresh DB bootstrap НЕ создаст index заново.
## Rollback path
Если в будущем нужен range scan optimization — re-create per-partition:
```sql
CREATE INDEX rosreestr_deals_2024q1_period_brin
ON rosreestr_deals_2024q1 USING BRIN (period_start_date);
-- per-partition × N partitions
```
Но обычно partition pruning достаточно. Измерь EXPLAIN перед re-creation.
## Vault
`fixes/Migration_Drop_Dead_Brin_May14.md` создан (status: ready_to_apply).
Apply на prod: `psql ... -f data/sql/89_drop_dead_brin_rosreestr_deals.sql`
после merge.
Co-authored-by: lekss361 <claudestars@proton.me>
* fix(sql): renumber engineering_networks 85→87, dedup classid, add SRID
Three critical fixes на untracked 85_engineering_networks_191fz.sql per
code review (May 14) — до первого применения на prod.
1. **NN collision fix**: renumber 85_engineering_networks_191fz.sql → 87_
85_pzz_zones_ekb.sql уже занимает 85_ prefix. При alphabetic deploy
order `85_e*` применился бы ДО `85_p*` — но 87_ ставит нас после
86_v_bucket_success_score.sql без зависимости на 85_pzz.
2. **Duplicate classid '907015504'**:
- Был дважды: 'Опора металлическая' + 'Опора железобетонная'.
- Второй ON CONFLICT DO UPDATE молча перетёр бы первый → NSI код
металлической опоры терялся бы.
- Fix: 907015504 остаётся за металлической (lower-numbered subtype
per Минстрой NSI source), железобетонная переезжает на 907015507
(заполняет gap 906→508). Помечено comment-меткой 'verify with
Минстрой NSI table' до prod deploy.
3. **Untyped geometry**:
- Было: `geom GEOMETRY` без SRID constraint → разрешало бы insert
mix MSK-66 (srid=100000) и WGS84 (srid=4326) → silent ST_DWithin
corruption.
- Fix: `geom GEOMETRY(Geometry, 4326)`. Capital G позволяет
mixed Point/LineString/Polygon, SRID 4326 жёстко фиксирует WGS84.
ETL loader должен делать ST_Transform(msk66_raw, 4326) перед
insert (см. backend/app/core/crs.py для CRS utility).
Idempotency: BEGIN...COMMIT, IF NOT EXISTS, ON CONFLICT DO UPDATE
паттерн — уже корректные в файле.
Closes Day-1 critical #2 from code review audit (May 14).
Pre-deploy: verify 907015507 classid assignment against Минстрой
official NSI table; reassign and re-run migration if needed.
* fix(sql): remove provisional 907015507, fix Фонати→Фонари typo
Addresses review blocker on PR #118:
- Provisional classid 907015507 ('Опора железобетонная (столб)') убран
из seed insert. Commented out с TODO(NSI-verify) — следующий шаг
верифицировать против официальной NSI-таблицы Минстроя ДО восстановления
строки. Без верификации ON CONFLICT DO UPDATE name перетёр бы корректное
значение в eng_ref_classid.
- Опечатка 'Фонати электрические' → 'Фонари электрические' (907015602).
- Sort: 904015504/505/506/508 теперь в численном порядке (после удаления
провизорной 507).
Live seed теперь покрывает 504 (металл столб), 505 (металл ферма),
506 (металл фермовый столб), 508 (железобет ферма). Строка 507
restored after Минстрой NSI verification (см. follow-up).
---------
Co-authored-by: lekss361 <claudestars@proton.me>