Commit graph

160 commits

Author SHA1 Message Date
1883271908 fix(cadastre): NSPD 500-retry + per-layer skip (#252) + disable dead pzz-beat (#259) + geom backfill (#200)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m43s
CI / backend-tests (pull_request) Successful in 6m41s
Deploy / build-backend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#252: NspdBulkServerError на HTTP 5xx + WMS <ServiceException>-body → quarter-level
autoretry; _grid_walk_category флагует layer_failed только если ВСЕ cells 5xx (none ok),
network-errors исключены — пустой/флапающий слой не ложно-скипается. harvest_meta.layer_X_failed.

#259: pzz-sync-monthly beat отключён (PKK6 deprecated, pzz_zones_ekb=0, заменён
zone_regulation_cache+NSPD dumps). logger.exception→warning (стоп GlitchTip BACKEND-1B),
raise сохранён для admin-endpoint.

#200: geom_unavailable флаг (миграция 151) + backfill_parcel_geom task — grid-walk
layer 36368 per quarter лечит 930/964 NULL-geom участков (NSPD search вернул центроиды),
остаток marks unavailable (не ретраить вечно). SAVEPOINT per-cell, idempotent.

Closes #252
Closes #259
Closes #200
2026-06-13 20:45:05 +05:00
d1bc59a5dd chore(objective): fuzzy backfill objective_complex_mapping +115 dev-corroborated (#307 OBJ-2)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m31s
CI / backend-tests (pull_request) Successful in 6m29s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m16s
Deploy / build-worker (push) Successful in 3m34s
Deploy / deploy (push) Successful in 1m28s
P0 goldmine: objective_lots (1.12M, 81% цен) связывается с domrf для velocity/
layout-аналитики. Mapping было 148 → +115 (107 high + 8 mid) = 263.

Fuzzy-match objective(project_name) ↔ domrf_kn_objects(comm_name) LATEST snapshot
region_cd=66 через pg_trgm на НОРМАЛИЗОВАННЫХ именах (strip ЖК-boilerplate с
\m\M word-boundary). Правило приёмки dev-corroborated: nsim≥0.62 AND dsim≥0.5 —
precision over recall для P0-цен (generic-имена с разным застройщиком, напр.
Солнечный/Прекрасный/Стрелки dsim=0.0, отклонены). match_method='auto_fuzzy_v2',
is_reviewed=false (для human-review топ-кандидатов), confidence-tier в note.
Idempotent ON CONFLICT (objective_complex_name, objective_group). REFRESH
CONCURRENTLY mv_layout_velocity после COMMIT.

Refs #307
2026-06-13 18:50:57 +05:00
b211183940 feat(site-finder): детерминированная атрибуция застройщика в analyze (#1088)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m33s
CI / backend-tests (pull_request) Successful in 6m33s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m44s
Deploy / build-worker (push) Successful in 5m24s
Deploy / deploy (push) Successful in 1m30s
Сервис get_developer_attribution поверх fn_developer_for_parcel (миграция 149):
топ-1 застройщик участка + track-record (РНС/РВЭ + домрф) + nearby_developers.
Дедуп по норм-ИНН (лучший match_method первым). Wire в analyze additive без флага
(чистый DB-резолвер по индексам, graceful → None). Beat-refresh developer_registry
ежемесячно 05:30 МСК после ekburg-permits. Pydantic-схемы + 18 тестов.

Источники: ekburg_construction_permits (РНС, ИНН) ⋈ domrf_kn_objects по
нормализованному ИНН + spatial/quarter fallback. 68% domrf-застройщиков имеют РНС.
EXPLAIN: point-lookup 0.06ms, резолвер ~30ms (functional/GIST индексы в миграции).

Closes #1088
2026-06-13 16:13:45 +05:00
b85147a3a0 fix(db): dedupe domrf_kn_documents + UNIQUE NULLS NOT DISTINCT (#1206)
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
UNIQUE (obj_id, doc_type, doc_num, file_url) в м.112 без NULLS NOT
DISTINCT, doc_num nullable. В PG NULL != NULL в уникальном индексе →
для документов с docNum=null (документированный частый вход:
extract_documents:125-134 → doc_num=None) ON CONFLICT-арбитр в
scrapers/documents.py:59 НИКОГДА не матчился → каждый ежедневный sweep
domrf_kn.py:1903 молча вставлял новую строку-дубль вместо UPDATE.
Таблица росла ×N прогонов; дубли уходили в analytics_queries.py:1068.

Миграция 140:
1. Дедуп существующих NULL-дублей: ROW_NUMBER() PARTITION BY все 4 ключа
   ORDER BY scraped_at DESC, id DESC → оставляем самую свежую, удаляем
   остальные WHERE rn>1 AND doc_num IS NULL.
2. DROP старого autonamed constraint'а IF EXISTS.
3. ADD uq_domrf_kn_documents_logical UNIQUE NULLS NOT DISTINCT (...) с
   именованным guard по pg_constraint.conname (паттерн м.125 → idempotent).

Caller scrapers/documents.py НЕ меняется — после миграции ON CONFLICT-
арбитр начнёт матчить NULL-doc_num строки автоматически. Прецедент того
же класса: м.110 (uq_infra_dedupe), м.125 (uq_supply_layers_logical).

Closes #1206
2026-06-13 06:17:13 +00:00
df34e55ab4 feat(site-finder): own-portfolio data source for §25.3 cannibalization (#1169 PR1)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m22s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m31s
Deploy / build-worker (push) Successful in 2m39s
Deploy / deploy (push) Successful in 1m12s
Foundation PR: unified "our projects" source the §25.3 overlap engine (PR2)
will consume. Two origins normalized to OwnProject (class/timing/price/unit-mix):
- current <- domrf_kn_objects filtered by settings.own_developer_ids (numeric
  prefix of composite dev_id; empty -> [] graceful, no DB hit, no hardcoded id)
- future  <- new manual-entry own_planned_project entity (migration 148)

Adds OWN_DEVELOPER_IDS config (comma-sep -> list[int], default []),
own_planned_project table (range/unit_mix CHECKs via IMMUTABLE helper, generated
geom), /api/v1/own-projects CRUD (created_by from X-Authenticated-User), and
get_own_portfolio(db). Per-source graceful degradation; psycopg-v3 CAST clean.

Does not touch special_indices.py or parcels.py (out of scope).

Refs #1169
2026-06-08 16:21:53 +05:00
4b2e7d9af8 fix(db): sync location.demand_index comment to city-relative (#948)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m26s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 4m52s
Deploy / build-worker (push) Successful in 13m20s
Deploy / deploy (push) Successful in 1m18s
The demand_index DB column comment still said "насыщающее преобразование"
(saturating) after #1167 switched it to city-relative normalization —
exactly the misleading-comment class that hid the original bug. Migration
147 updates COMMENT ON COLUMN (idempotent, comment-only, no data DDL).

Refs #948
2026-06-08 14:04:51 +05:00
8da1c00138 feat(location): district-level Location entity + indices (#948 part B)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m11s
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m22s
Promote district to a first-class `location` entity (ТЗ §8.2), ADDITIVE — no
district->FK refactor. New `location` table keyed by district_name (joinable by
string), carrying 4 normalized [0,1] indices (NULL when no data, never 0):
- infra: _district_poi_score / _city_avg_poi_score (per-district POI aggregate)
- competition: market_metrics.overstock_index (available/stuck competing supply
  — orthogonal to demand; NOT sell-through, which is market-heat correlated w/ demand)
- demand: market_metrics.unit_velocity (saturating /50)
- future_supply: future_supply_pressure.index (passthrough, already 0..1)

- data/sql/146_location.sql: idempotent table + UNIQUE(district_name) + range
  CHECK + centroid GIST
- services/site_finder/locations.py: compute_location_indices (reuses forecast
  per-district fns) + refresh_locations (SAVEPOINT per-row, CAST, ON CONFLICT)
- workers/tasks/location_refresh.py + beat (Mon 07:00 MSK, after supply-layers)
- api/v1/locations.py: read-only GET list + GET by name (analyst+admin via rbac,
  frontend pilot-gated)
- tests: 34 (normalization 0..1/null, competition⊥demand orthogonality, idempotent
  upsert, read API list/by-name/404)

Part of #948 (Part A insight shipped #1164).
2026-06-08 13:28:19 +05:00
6400ebde24 feat(insights): manual-entry Insight entity CRUD + §19 audit (#948 part A)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m41s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m50s
Deploy / deploy (push) Successful in 1m22s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m32s
Insight = аналитик пишет free-form intel про локацию/участок с пометкой
«непублично» (is_confidential, §7.13/§8.9). CRUD под /api/v1/insights;
created_by из X-Authenticated-User (не из тела — спуфинг автора невозможен).
Зеркало custom_pois: raw-SQL service + Pydantic v2 schemas, sync def handlers
(threadpool, off event loop).

- data/sql/145_insight.sql: table insight (idempotent, geom GENERATED из lon/lat,
  индексы district/cad_num/is_confidential/created_by/GIST); аддитивно расширяет
  CHECK audit_log.action += insight_write (тот же constraint ck_audit_log_action,
  to_regclass-guarded, superset — не ломает #962-аудит; НЕ правит applied 144).
- audit_middleware.classify_path: method-aware insight_write для POST/PUT/DELETE
  /api/v1/insights (GET-чтения не аудируются); обратно совместимо с parcels.
- ACL: backend rbac_guard hard-блокирует только /api/v1/admin/*; pilot отсекается
  frontend RouteGuard + Caddy (как parcels/custom_pois). is_confidential — стораемая
  пометка без per-record backend-гейта (analyst видит, per #962-политике).
- tests: 28 insight (CRUD+filters+required+#261 commit) + 5 audit. 86 зелёных, ruff.

Part B (Location first-class, §8.2) — отдельный follow-up.

Refs #948.
2026-06-08 12:41:39 +05:00
86828d0388 feat(rbac): add analyst role + §19 audit-log middleware (#962)
Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy Trade-In / changes (push) Successful in 7s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 36s
Deploy / build-backend (push) Successful in 1m39s
Deploy / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 48s
Deploy / build-worker (push) Successful in 2m41s
Deploy / deploy (push) Failing after 4s
Deploy Trade-In / deploy (push) Successful in 47s
CI / backend-tests (push) Successful in 6m38s
CI / backend-tests (pull_request) Successful in 6m30s
EPIC18/§19. analyst sees everything (deals, insights, exports, site-finder,
analytics, concept) EXCEPT admin/data-management. Enforcement is backend-hard
(the existing rbac_guard already 403s any non-admin role on /api/v1/admin/*, so
adding the role auto-blocks it) + frontend (deny_paths via /me) + audit.

§19 audit: new best-effort HTTP middleware logs the sensitive actions
(analyze / forecast / forecast-export) to a new audit_log table after the
response. Audit failures never break or delay-fail the request (2-layer
try/except + finally close). Registered INNER to rbac_guard so only authorized
requests are audited (a 403 short-circuits before audit). classify_path matches
export before forecast (anchored).

- auth/roles.yaml: analyst role (paths /**, deny admin-mgmt) + analysttest QA user
- core/auth.py (+ tradein mirror): Role Literal += analyst
- core/audit_middleware.py (new) + main.py registration
- data/sql/144_audit_log.sql (idempotent; auto-applies)
- tests: analyst rbac (403 admin / 200 parcels) + 11 audit cases

No data-level ACL (analyst sees data per policy) -> no #948 dependency. No new
deps. parcels.py untouched. Real analyst logins still need adding to
caddy/users.caddy.snippet (devops).

Refs #962.
2026-06-08 12:16:19 +05:00
3921180d07 fix(sf): ST_MakeValid genplan geom + backfill 881 invalid polygons (#1157)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 17:59:04 +00:00
9736192359 feat(sf): подзоны ПАТ Кольцово → pat_subzones (#1150)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m25s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Successful in 1m11s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-07 14:43:07 +00:00
50fcba1ca0 feat(sf): ingest ОКН-объектов ЕКБ из АИС ЕГРКН (точки+категория) → okn_objects (#1141)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m23s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m21s
Deploy / deploy (push) Successful in 1m15s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 12:51:18 +00:00
86b9a5a197 perf(sf): КРТ-геометрия в БД, _krt_at из БД вместо живого WFS (#1139)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-backend (push) Successful in 1m21s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Successful in 1m13s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-07 12:14:35 +00:00
189ecc81f8 feat(sf): ingest функц.зон генплана ЕКБ-2045 из ГИСОГД-СО WFS → ekb_genplan_functional_zone (#1137)
All checks were successful
Deploy / build-backend (push) Successful in 1m18s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 2m28s
Deploy / deploy (push) Successful in 1m17s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 12:03:39 +00:00
1e53416277 feat(sf): ППТ/ПМТ ТЭП-парсер (Табл.11/12/13) → ekb_ppt_tep MVP (#1133)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m27s
Deploy / deploy (push) Successful in 1m21s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 11:54:40 +00:00
57c6d119f5 feat(sf): scraper реестра КРТ-площадок ЕКБ → ekb_krt_sites (#1126)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 2m40s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 3m22s
Deploy / deploy (push) Successful in 1m30s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 11:24:49 +00:00
59f2628e0b feat(sf): ПАГЕ-парсер изъятия/резервирования → land_reservation (#1118)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m41s
Deploy / build-worker (push) Successful in 3m25s
Deploy / deploy (push) Successful in 1m25s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 10:15:03 +00:00
f8aa1d7512 feat(sf): newbuilding_listings house-grain таблица для future-ЖК слоя (#1108)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m23s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m25s
Миграция 135_newbuilding_listings.sql — house-grain таблица под листинги новостроек (зеркало ключевых колонок tradein.houses: гео/класс/застройщик/очередность/кадастр/cross-ref). geometry(Point,4326)+GIST, UNIQUE (source, ext_house_id), partial-индексы. Отдельно от objective_lots (lot-grain ≠ house-grain). Идемпотентна, 0 INSERT (наполнение ETL #976).

Closes #975.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 21:48:51 +00:00
9e4348629b feat(sf): ППТ/ПМТ WFS ingest → planning_projects (future-supply, #1104)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m36s
Deploy / build-worker (push) Successful in 2m20s
Deploy / deploy (push) Successful in 1m14s
Миграция 134_planning_projects.sql + planning_harvest.py: ингест утверждённых ППТ/ПМТ ЕКБ из геопортал-WFS (один BBOX-запрос на слой, geom уже 4326 → SetSRID без Transform) → planning_projects (project_type/source_key UNIQUE, doc_status/год). SAVEPOINT per-row, beat monthly (1-е 06:00). Зависит от TLS-фикса #1103. Документ-центричная таблица отдельно от ird_overlays. 3 теста. ТЭП/PDF — follow-up.

Refs #1085.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 20:25:23 +00:00
e5b12d579a feat(sf): zone_regulation_cache — ПЗЗ-регламент по zone_index + числовой экстракт (#1099)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Миграция 133_zone_regulation_cache.sql + zone_regulation.py: кэш ПЗЗ-регламента по (city, zone_index) — 3 списка ВРИ + сырой текст предельных параметров + regex-экстракт числовых (max_far/застройка/этажность/площадь). get-or-fetch через urbanCard. Резолв один раз на зону (~100/город). Self-contained, wiring в D9b. 10 тестов на реальных geoportal-строках.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 19:31:28 +00:00
3120c902c6 feat(sql): ird_overlays durable overlay store for ИРД geometry (#1078)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 27s
Deploy / build-worker (push) Successful in 27s
Deploy / deploy (push) Successful in 58s
Миграция data/sql/132_ird_overlays.sql — durable overlay-store геометрии ИРД-ограничений участка (ЗОУИТ/ПЗЗ-границы/ОКН/ООПТ), снятых из НСПД WMS. geometry(Geometry,4326)+GIST, UPSERT-ключ (source_layer_id, geom_data_id), nullable content_* (резолв C8/D9). Идемпотентна (IF NOT EXISTS + guarded DO), пустая таблица. Foundation для #1067 B5 harvest-воркера.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 18:08:40 +00:00
b83ffedec3 feat(sql): add ekb_district_alias micro->admin vocab bridge (131)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 27s
Deploy / build-worker (push) Successful in 27s
Deploy / deploy (push) Successful in 58s
§9.x forecast metrics filter objective_lots.district (informal micro-districts)
but /analyze resolves the official admin district from ekb_districts → admin
name matched 0 rows → silent empty forecast (market_metrics('Кировский')→0).
Add a curated micro→admin alias table seeding all 35 micro values to their
primary admin (8 EKB admins). Curated not spatial: complex_id join hits only
~28% of rows and complexes is multi-city, so ST_Contains scatters micros
uniformly. 4 cross-city values (Н.Тагил/Каменск) contained in one noted bucket.
Coverage: 8/8 admins resolve, 35/35 mapped, 0 unmapped. Idempotent + additive
(CREATE TABLE IF NOT EXISTS + INSERT ON CONFLICT DO NOTHING); applied to prod
+ registered in _schema_migrations. Resolver/fallback documented in header;
market_metrics wiring is Step 2 (separate PR). Refs #969 #949.
2026-06-05 06:39:50 +05:00
51966b38c8 fix(db): default cad_parcels.source in trigger (on-demand ingest 2nd NOT NULL gap)
Some checks failed
Deploy / build-worker (push) Successful in 39s
Deploy / deploy (push) Has been cancelled
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 36s
After #129 fixed quarter_cad_number, the on-demand НСПД ingest of a real
not-pre-loaded parcel hit the NEXT cad_parcels NOT NULL column: source
(NotNullViolation) → geometry transaction rolled back → POST /analyze still
stuck. Repro 66:41:0610029:83 (job 57: quarter derived by #129, source null).

Migration 130: extend the #129 BEFORE INSERT/UPDATE trigger function to also
default source→'nspd' when NULL (column DEFAULT can't help — INSERT binds
explicit NULL; trigger overrides it). Explicit sources ("search"/"wms_grid_walk")
respected — only NULL is filled. cad_parcels NOT NULL surface now fully covered
(cad_num/quarter[#129]/category_id[literal]/source[#130]/fetched_at+updated_at[NOW]).
Dry-run-verified on prod: null quarter+source → 66:41:0610029 + nspd, INSERT ok.
2026-06-04 12:49:46 +05:00
45d5a338ea fix(db): auto-derive cad_parcels.quarter_cad_number trigger (on-demand ingest 500)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 29s
Deploy / build-worker (push) Successful in 29s
Deploy / deploy (push) Successful in 58s
PROD bug: on-demand НСПД ingest (bulk_harvest.upsert_parcel, for parcels NOT
pre-loaded) intermittently inserts quarter_cad_number=NULL at runtime →
NotNullViolation → geometry never persists → POST /analyze for a real new parcel
stuck "Геометрия загружается из НСПД…" forever (re-enqueues each call). Repro:
66:41:0610029:83 (real НСПД parcel; failing row had valid geom + props, only
quarter_cad_number null). Code derives quarter_cad but NULL still reaches the
INSERT via some runtime path (grid-walk).

Migration 129: BEFORE INSERT OR UPDATE trigger on cad_parcels — when
quarter_cad_number IS NULL and cad_num has >=3 colon-segments, derive it
(first 3 segments: 66:41:0610029:83 → 66:41:0610029). Fixes ALL write paths at
the DB level (quarter_cad_number is a pure derivation of cad_num). <3-segment
cad_num intentionally left NULL → NOT NULL still rejects genuine garbage (no
masking). Coexists with m.92 set_updated_at trigger. Idempotent.

Dry-run-verified on prod catalog: null-quarter insert → derived 66:41:0610029;
garbage <3seg still rejected. Verification = re-fetch 0610029:83 post-deploy.
2026-06-04 12:19:21 +05:00
ed3b302d57 fix(supply-layers): thread dev_group_name into L3 upsert key (#970 CRITICAL)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m22s
Deploy / build-worker (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m4s
REOPENED. L3 future-supply rows are computed per (district_name, dev_group_name)
but dev_group_name was never a key column — only embedded in method text. With
complex_id/obj_class NULL for L3, every dev_group of a district collapsed to one
upsert key → ~95.6% loss. Ground-truth (Академический, prod): should be 13,808
units / 15 dev_groups / 54 objects; only 1 row / 607 units survived.

Migration 128: ADD COLUMN supply_layers.dev_group_name TEXT + rebuild
uq_supply_layers_logical to (layer, district_name, complex_id, obj_class,
dev_group_name, source, snapshot_date) NULLS NOT DISTINCT (L1/L2 dev_group_name
NULL stays transparent → their dedup unchanged; L3 distinct groups no longer
collapse). Dry-run-verified vs prod catalog (applies clean, ROLLBACK clean).

Worker: SupplyLayerRow gains dev_group_name (L1/L2=None, L3=group); _UPSERT_SQL
adds it to INSERT/VALUES (CAST(:dev_group_name AS text)) + ON CONFLICT (key col,
not in DO UPDATE SET). Service+worker regression tests assert same-district/
different-dev_group → distinct keys (no collapse). 234 supply tests pass.

Deploy applies migration before container restart; collapsed data self-heals on
next supply_layers_refresh. Verification = prod re-measure post-deploy.

Refs #970
2026-06-04 10:06:30 +05:00
0f194de069 feat(db): analysis_runs versioned store + latest-view §22 (#993)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 38s
Deploy / build-backend (push) Successful in 42s
Deploy / deploy (push) Successful in 1m0s
Migration 127: durable, versioned, re-openable persistence for Site Finder
analysis runs (the §22 API result + forecast/recommendation/report snapshot).
One row = one completed analysis of a parcel (frozen result jsonb =
SiteFinderReport.as_dict() #987, schema_version for versioned re-open).
Append-mostly (history/versioning — never overwrites prior runs → TABLE not MV).

Replaces the last_analysis_date=None placeholder (parcels.py:1026,1092):
"date of last analysis" + "current analysis of parcel" now come from
v_analysis_runs_latest (DISTINCT ON cad_num, max created_at, deterministic
id-DESC tiebreak — mirrors v_supply_layers_latest m.125).

created_by = X-Authenticated-User identity, nullable TEXT, NO FK (no users
table; precedent m.90/m.119; background/Celery runs have no author). result/
params/segment as JSONB (report shape evolves); district/confidence/status
denormalized out for filter/sort. status + confidence CHECK-guarded.
Idempotent (CREATE TABLE/INDEX IF NOT EXISTS + CREATE OR REPLACE VIEW).

Dry-run-verified vs prod catalog (DDL applies clean in rolled-back txn;
DISTINCT ON latest-wins confirmed). Foundation for #994 persist + #992
contract. Closes #993. Refs #961.
2026-06-03 17:14:47 +05:00
c06dc0f5d6 feat(db): Dom.РФ housing/escrow → macro_indicator §7.10 (#946, partial)
All checks were successful
Deploy / build-backend (push) Successful in 34s
Deploy / build-worker (push) Successful in 41s
Deploy / deploy (push) Successful in 59s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Migration 126: aggregate already-scraped domrf_kn_sale_graph + domrf_kn_objects
into monthly macro_indicator series (source=domrf): housing_price_per_m2
(area-weighted, latest-snapshot deduped — naive SUM double-counts 2.7x),
new_contracts_volume (Σ contracted ДДУ/mo), escrow_objects_count (escrow FLAG,
not balances — caveat documented). Regions sverdl (region_cd=66) + rf (all
scraped — currently = sverdl, domrf_kn covers region 66 only). Idempotent
ON CONFLICT DO UPDATE; dry-run-verified vs prod (price ~156k ₽/m², dedup
45792→16853, escrow 460). Pure SQL, data already scraped.

EMISS CPI/income (§7.11) DEFERRED: fedstat.ru WAF-blocks non-browser clients
(JA3) — needs stealth.py browser-fetch generalization (separate effort, low
priority while §9.6 forecast is advisory). Refs #946.
2026-06-03 17:05:33 +05:00
33ecbda689 feat(db): supply_layers store + latest view (#950 PR B) (#1003)
All checks were successful
Deploy / build-worker (push) Successful in 28s
Deploy / deploy (push) Successful in 59s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 28s
2026-06-02 20:21:04 +00:00
420cda2831 feat(domrf): persist domrf_kn_objects.free_flats (#950 PR A) (#1002)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m26s
Deploy / build-worker (push) Successful in 2m27s
Deploy / deploy (push) Successful in 1m7s
Deploy / changes (push) Successful in 5s
2026-06-02 20:06:29 +00:00
dbae4b0bda feat(site-finder): macro_indicator table + backfill + reader (#945 PR A) (#963)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m20s
Deploy / build-backend (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m14s
Deploy / changes (push) Successful in 6s
2026-06-02 18:41:28 +00:00
87a5de0cae feat(db): quarter_price_index FDW foreign table + monthly refresh (Refs #762) (#797)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 26s
Deploy Trade-In / build-backend (push) Successful in 25s
Deploy / build-backend (push) Successful in 1m30s
Deploy Trade-In / deploy (push) Successful in 37s
Deploy / build-worker (push) Successful in 2m54s
Deploy / deploy (push) Successful in 1m8s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-05-30 17:16:28 +00:00
a4057a5b28 feat(db): mv_quarter_price_index — quarter price-index из ДКП (Refs #760) (#792)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 26s
Deploy / build-worker (push) Successful in 26s
Deploy / deploy (push) Successful in 57s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-05-30 17:05:51 +00:00
e72cf87e35 feat(tradein): автоматизация cian-backfill + rosreestr-import через scheduler (#560 #563) (#630)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 29s
Deploy / build-worker (push) Successful in 30s
Deploy Trade-In / build-backend (push) Successful in 47s
Deploy Trade-In / deploy (push) Successful in 43s
Deploy / deploy (push) Successful in 58s
2026-05-28 15:31:24 +00:00
4e4762f234 fix(tradein): backfill kn obj_class (parser + 3-pass) (#572) (#628)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m27s
Deploy / build-worker (push) Successful in 3m9s
Deploy / deploy (push) Successful in 1m9s
2026-05-28 15:04:32 +00:00
609ac8ea9d fix(tradein): backfill kn price_per_m2 via sale_graph + peer median (#573) (#625)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m43s
Deploy / build-worker (push) Successful in 2m41s
Deploy / deploy (push) Successful in 1m11s
2026-05-28 13:50:45 +00:00
94cf199276 feat(tradein): postgres_fdw live read of gendesign.cad_buildings (replaces snapshot) (#493)
Some checks failed
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 26s
Deploy Trade-In / build-backend (push) Successful in 47s
Deploy / build-frontend (push) Successful in 29s
Deploy / build-backend (push) Successful in 1m24s
Deploy Trade-In / deploy (push) Successful in 40s
Deploy / build-worker (push) Successful in 2m57s
Deploy / deploy (push) Failing after 37s
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.
2026-05-24 08:57:30 +00:00
0567ad2130 feat(22d): domrf_catalog_object scraper — fill ~25 NULL kn_objects cols from SSR __NEXT_DATA__ (#335)
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m43s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m49s
Deploy / deploy (push) Failing after 44s
2026-05-17 21:26:22 +00:00
c61c8c86af feat(sf-b1): GET /parcels/by-bbox — map entry endpoint + parcel_user_status table (#336)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-17 21:25:48 +00:00
lekss361
99e55c0bc2 feat(sf-b3): POST /pilot/request + pilot_requests table
Добавляет endpoint для приёма заявок на пилот (lead-gen).
INSERT в pilot_requests, response {id, created_at, status}.
Telegram-уведомление — TODO (creds не настроены, #307 SF-B3).

Migration: data/sql/118_pilot_requests.sql
2026-05-18 00:01:35 +03:00
0c4d7f1ffb feat(obj-2): mapping backfill via pre-materialized temp tables (migration 117) (#327)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 27s
Deploy / build-worker (push) Successful in 2m7s
Deploy / deploy (push) Successful in 51s
2026-05-17 20:34:42 +00:00
2bd80c5719 fix(obj-2): trim migration 116 to GIST index only — INSERT CTE timed out in deploy (#326)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 27s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 28s
Deploy / deploy (push) Successful in 47s
2026-05-17 20:21:58 +00:00
79c9a8a034 feat(obj-2): multi-feature mapping backfill (name+dev+district) — 142 → 200+ (#323)
Some checks failed
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Successful in 26s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 27s
Deploy / deploy (push) Failing after 10m3s
2026-05-17 19:53:50 +00:00
c80d79e2d4 fix(objective parser): use_float=True in ijson + default=str in json.dumps (#321)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 27s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 28s
Deploy / deploy (push) Successful in 39s
2026-05-17 19:11:25 +00:00
bb9a003080 fix(objective parser): wrap bytes-iter in file-like reader for ijson (#318) 2026-05-17 17:52:55 +00:00
29dbbee2ac feat(trade-in): TI-1 mock /trade-in/estimate endpoint + Pydantic + SQL migration (#316)
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Failing after 10m58s
Deploy / build-backend (push) Failing after 11m0s
2026-05-17 16:45:06 +00:00
389e141ec9 fix(scrape-objective): stream-parse lots_pf via ijson to avoid OOM (#315)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 3m0s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 5m56s
Deploy / deploy (push) Successful in 1m19s
2026-05-17 16:37:30 +00:00
b37e011aaa feat(22c): domrf_kn_flat_plans new table + parser stub (#308)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-17 15:39:34 +00:00
6612c1cd2b feat(22begh): schema extension — flats+6 / objects+30 / developers+4 / metro_top3 (#306)
All checks were successful
Deploy / build-backend (push) Successful in 1m32s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 3m32s
Deploy / deploy (push) Successful in 56s
2026-05-17 15:26:59 +00:00
171a029997 feat(22i): domrf_kn_documents new table + parser (#305)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m34s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 59s
2026-05-17 15:21:31 +00:00
3183abe41b fix(22j): DEDUP domrf_kn_infrastructure + ON CONFLICT (issue #297) (#304)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m34s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m45s
Deploy / deploy (push) Successful in 1m4s
2026-05-17 15:15:03 +00:00