Commit graph

153 commits

Author SHA1 Message Date
0a72ef9491 merge main + renumber 163→167 + guard riasurt harvest (#108 review)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m33s
CI / backend-tests (pull_request) Successful in 10m10s
2026-06-17 22:52:15 +03:00
31e316e04e feat(site-finder): РИАСУРТ Свердл 14-layer harvest client+schema+task for ЕКБ agglomeration (#108)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
2026-06-17 21:49:04 +03:00
8022be6b2f Merge pull request 'fix(leads): replace ::interval cast with make_interval (psycopg v3 trap) (#1383 follow-up)' (#1707) from fix/admin-leads-interval-cast-1694 into main
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
2026-06-17 18:42:11 +00:00
145dc62e0b fix(admin-scrape): ge=1 on count param + enforce LLM finish_reason (#review-followup)
Some checks failed
CI / changes (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / changes (pull_request) Has been cancelled
- admin_scrape.py /failures limit: ge=0 → ge=1 (returning 0 rows is nonsensical for a list endpoint)
- orchestrator.py: check result.finish_reason after ok=True; 'length'/'content_filter' and any
  other non-stop reason triggers deterministic fallback with reason=finish_reason:<value>
  (LLMResult docstring mandated this but no consumer enforced it)
2026-06-17 21:23:13 +03:00
bcb348da40 fix(leads): replace ::interval cast with make_interval (psycopg v3 trap) (#1383 follow-up)
Some checks failed
CI / changes (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
(:bind || ' months')::interval is a psycopg v3 SyntaxError — the driver
sees '::' immediately after the bind placeholder token and chokes.
Replace all 6 occurrences (admin_leads.py + analytics_queries.py) with
make_interval(months => :bind) which is unambiguous to the parser and
semantically identical.
2026-06-17 21:22:53 +03:00
7fdbca2666 Merge pull request 'fix(leads): window revenue_total/deals_total to leads_window (#1383)' (#1694) from fix/leads-stats-window-1383 into main
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
2026-06-17 18:05:51 +00:00
64ed95271f fix(leads): window revenue_total/deals_total to leads_window (#1383)
Some checks failed
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m42s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m45s
CI / backend-tests (push) Failing after 8m48s
CI / backend-tests (pull_request) Failing after 8m46s
Both metrics were querying prinzip_deals without any date filter,
returning all-time figures while the surrounding stats (leads_window,
converted_window, conv_pct_window) were scoped to the last N months.
Now both subqueries restrict to deals linked to leads in window_leads
(via deal_id IN (...)), making all «за период» figures consistent.
2026-06-17 20:49:16 +03:00
d0a409f873 fix(parcels): _score_label honor documented «хорошо»=25 threshold, close band gap (#1357)
Some checks failed
CI / changes (push) Successful in 13s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m51s
CI / backend-tests (push) Failing after 9m0s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m17s
CI / backend-tests (pull_request) Failing after 9m13s
2026-06-17 20:30:19 +03:00
14f3ef2019 fix(week-review): backend-аудит v2 — 82 фиксов (#1660)
All checks were successful
Deploy / build-worker (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m20s
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m52s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-17 17:13:38 +00:00
50db2b82aa Merge pull request 'fix(site-finder): nearest_top3 metro from osm_poi_ekb (#1667)' (#1670) from fix/1667-metro-block into main
Some checks failed
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / changes (push) Successful in 9s
Deploy / deploy (push) Has been cancelled
2026-06-17 17:13:17 +00:00
d3a53ae475 docs(site-finder): clarify analyze confidence is by-design distinct from §15 (#1668)
All checks were successful
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m38s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m40s
CI / backend-tests (push) Successful in 8m42s
CI / backend-tests (pull_request) Successful in 8m40s
_compute_confidence docstring claimed "stub до G1/G2/D1/D2" — те эшелоны уже
отгружены, формулировка устарела. Это намеренно отдельная метрика: надёжность
site-finder скоринга (coverage POI/район/рынок), НЕ форсайтный §15
confidence_engine (надёжность прогноза спроса/цены над форсайт-входами,
которых в analyze hot-path нет). Поведение не меняется — резолв документацией.

Closes #1668
2026-06-17 18:47:07 +05:00
7072d7803e fix(site-finder): compute nearest_top3 metro from osm_poi_ekb (#1667)
All checks were successful
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m39s
CI / backend-tests (pull_request) Successful in 8m41s
CI / backend-tests (push) Successful in 8m40s
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / changes (push) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 1m41s
Replace bare metro_block placeholder ({"nearest_top3": None}, blocked on a
never-merged 22h metro scraper) with a direct KNN query against osm_poi_ekb
category='metro_stop' (loaded by poi_loader). Returns 3 nearest stations with
name + rounded distance_m; empty list when none nearby (vs None on error),
SAVEPOINT + try/except like adjacent SF-B5 blocks. No frontend consumer yet.

Closes #1667
2026-06-17 18:44:29 +05:00
479f01ae1a Merge remote-tracking branch 'forgejo/main' into HEAD
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (push) Successful in 1m7s
CI / openapi-codegen-check (push) Failing after 2m9s
CI / frontend-tests (pull_request) Successful in 1m4s
CI / openapi-codegen-check (pull_request) Failing after 1m37s
CI / backend-tests (pull_request) Failing after 9m7s
CI / backend-tests (push) Failing after 9m11s
# Conflicts:
#	backend/app/services/site_finder/gate_verdict.py
2026-06-15 21:07:05 +03:00
8279b0ee1f fix(week-review): cross-file добивка код-ревью (#1338, #1363, #1419, #1421, #1424, #1480)
- #1338 concepts.py: geometry.generate вынесен в run_in_threadpool (не блокирует event loop)
- #1363 house_imv_backfill.py + scrape_pipeline.py: периодический heartbeat в долгих IMV-фазах (reap_zombies не помечает живой sweep zombie)
- #1419 site-finder-api.ts: placeholderData keepPreviousData для bbox-запроса (KPI не мигает «0/0» при пане/зуме)
- #1421 site-finder-api.ts: area_ha nullable + адаптер не подставляет 0; null-гейт потребителей EntryMap.tsx, ParcelDrawer.tsx
- #1424/#1480 EstimateResult.tsx: блок «Параметры объекта» скрыт при восстановлении по shared-ссылке (нет мусора «0 м²/Студия/Этаж 0 из 0»)

Верификация: tsc --noEmit 0 ошибок; py_compile OK.
2026-06-15 20:29:00 +05:00
86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00
cab19020a9 perf(site-finder): pushdown obj_pricing CTE to nearby competitors (#70/#307) (#1335)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m40s
Deploy / build-worker (push) Successful in 2m38s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 16:58:43 +00:00
72e0b050f9 feat(site-finder): POI saturation per capita по районам ЕКБ (#42) (#1329)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m52s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 15:05:22 +00:00
5507bbc1a4 feat(site-finder): domrf→cad geom-match + parking_ratio конкурентов (#96) (#1327)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
2026-06-14 14:49:28 +00:00
d38417d8af feat(admin): freshness endpoint (#73) + README Forgejo refresh (#83) + migrations doc (#72)
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m39s
Deploy / build-worker (push) Successful in 7m20s
Deploy / deploy (push) Successful in 2m48s
#73: GET /admin/scrape/freshness — last_success/age/freshness/status для 5 источников
(kn/objective/nspd/nspd_geo/cadastre) + overall_status (worst по critical). Prod-verified
имена таблиц, FILTER/COALESCE корректны, CAST(:d AS interval). +4 теста.
#83: README — GitHub→Forgejo CI/CD, секция 6 subagents, ссылки перемаплены.
#72: docs/Schema_Migrations_vs_Alembic.md — _schema_migrations паттерн (не Alembic),
рекомендация снести пустой alembic-скелет (отдельный PR).

Closes #73
Closes #83
Closes #72
2026-06-13 18:25:32 +00:00
50555821fb feat(site_finder): routing POI decay (#41) + OSM utility distances (#44) + ЗОУИТ geom (#255bk)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m42s
CI / backend-tests (pull_request) Successful in 7m40s
Deploy / changes (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
#41: per-category routing-radius decay через reusable ORS /matrix client (foot-walk
время + per-category пороги + decay-curves), opt-in ?decay=routing, graceful straight-line
fallback (analyze не 500 при ORS down) + length-guard durations==destinations.
#44: +5 OSM-категорий (transformer/gas/water/sewerage/heat) via Overpass nwr;
nearest_{water_main,substation,gas,heat}_m в analyze.utilities.
#255bk: ST_AsGeoJSON(CAST(geom AS geometry)) в _get_cad_zouit_overlaps → geom_geojson.

Closes #41
Closes #44
2026-06-13 22:31:56 +05:00
658d724075 feat(analytics): velocity-anomaly alerts (#17) + ARN ДДУ price indicator (#99)
Some checks failed
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m37s
CI / backend-tests (pull_request) Successful in 7m42s
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (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
#17: detect_velocity_anomalies + GET /analytics/velocity-alerts — z-score drop-detection
на domrf_kn_sale_graph (double-gate z≤-2 AND drop≤-30%, starvation-guards). Snapshot=richest
(не MAX — prod новейшие частичные), lookback anchored на latest report_month (scrape лаг ~4мес).
Prod: ЖК Центральный Парк -69%, ~14ms.

#99: mv_ddu_price_indicator (миграция 152) + POST /market/ddu-indicator — ARN-mirror
ценовой индекс per quarter×area_bucket из rosreestr_deals (ДДУ регион 66). MVP: subject-level,
period Q, window 2025-Q2+, methods 1/2 (basis/previous, prev_period_value honesty). Q1-2026
headline 1.0185 vs ARN 1.03 (±5%). Method 3 blocked (нет pre-2025-Q2 данных) — задокументировано.

Closes #17
Closes #99
2026-06-13 22:04:49 +05:00
3b79533f9f feat(generative): движок концепций Stage 1a/1b/1c (#54 #55 #56)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (push) Has been cancelled
Заменяет generative stubs детерминированным end-to-end пайплайном:
- 1a geometry: WGS84-parse, метрическая AEQD-репроекция, setback-буфер, placement grid
- 1b placement: greedy section-fill + STRtree коллизии (full-gap), 3 стратегии, coverage cap
- 1c teap/financial: площади/квартиры/FAR/паркинг; выручка/затраты/маржа + IRR-proxy
- exporters: ezdxf DXF (геометрия) + WeasyPrint ТЭП/фин PDF (lazy import)
generate() заменил generate_stub в route (422 на degenerate). mypy-strict + ruff clean,
31 generative-тест + full suite 3078 passed. ConceptVariant заполняется реальными числами.

Closes #54
Closes #55
Closes #56
2026-06-13 21:32:32 +05:00
84d08940c6 fix(analytics): свежая ипотечная ставка + recommend last_updated + drop dead route (#236 #237 #251)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Successful in 3m9s
Deploy / build-backend (push) Successful in 4m12s
Deploy / build-worker (push) Successful in 5m31s
Deploy / deploy (push) Successful in 1m19s
#236: _current_mortgage_rate сортировал cbr_mortgage_series по TEXT-полю period
лексикографически ('Январь'>'Февраль') → возвращал stale-точку. Сортировка по
parsed (year, month) + regex-guard на malformed period-строки. Остаётся на
subsidized weighted series (НЕ domrf market — иная метрика). Verified prod:
Февраль 2026 (было Январь 2026).

#237: scope.last_updated = MAX(snapshot_date) domrf_kn_objects в recommend_mix +
подпись «данные на <дата>» в recommend page.

#251: удалён never-implemented GET /parcels/{parcel_id} (501) + ParcelDetail схема
(frontend bare 1-сегментный route не зовёт).

Closes #236
Closes #237
Closes #251
2026-06-13 15:22:22 +00:00
3cf9fad683 fix(backend): экранировать Excel formula-injection (#1244) + увести chat-чтение с event loop (#1245)
Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m38s
CI / backend-tests (pull_request) Successful in 6m37s
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#1244 (security): внешние/скрейпинг-строки (comm_name из DOM.РФ, headline/usp_text)
с ведущим = + - @ \t \r писались как есть → openpyxl сохранял как формулы
(data_type='f'), исполнялись при открытии в Excel/LibreOffice. _sanitize_formula
префиксует такие строки апострофом (OWASP CSV-injection escape); числа/даты/bool
не трогаются. _write_kv labels тоже санитизируются. Подтверждено на openpyxl 3.1.5.

#1245 (concurrency): async ask() вызывал sync get_report_for_chat() (sync SQLAlchemy
тянет крупный JSONB §22-отчёт) напрямую — блокировал event loop, в отличие от
LLM-ветки (run_in_threadpool). Обёрнуто в run_in_threadpool.

Closes #1244
Closes #1245
2026-06-13 18:10:21 +05:00
99c9a130bc feat(forecast): расширить горизонт прогноза до 24 мес (ТЗ §12.1)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Successful in 46s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 46s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m30s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m14s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / build-worker (push) Successful in 4m6s
Deploy / deploy (push) Successful in 1m40s
API отвергал ?horizon=24 (422), хотя ТЗ §12.1 называет 6/12/18/24, а движок
УЖЕ считает 24 на каждом ране: _DEFAULT_HORIZONS=(6,12,18,24) во всех 6 точках
стека (orchestrator/forecast-task/demand_supply_forecast/scenarios/
special_indices/report_assembler), PIPELINE_HORIZON_MONTHS=24.
_hidden_release_fraction клампит h/18→1.0 на 24 (без переполнения),
future_supply._horizon_weight расширяет окно чисто — скрытых ≤18 потолков нет.
Чистое расширение валидатора-enum, не новая математика.

Backend: _ALLOWED_FORECAST_HORIZONS → {6,12,18,24}, Query/docstring/error-msg.
Frontend: HorizonSelector HORIZONS=[6,12,18,24] (тип horizon=number, union не нужен;
прочие потребители data-driven через meta.horizons/forecasts_by_horizon).
Тесты: API принимает 24/отвергает 30; движок-тесты доказывают h=24 осмыслен
(поля посчитаны, demand(24)>demand(18), hidden созрел, индексы в диапазонах).

Closes #944 (Q1 горизонт 24)
2026-06-13 17:27:35 +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
2a68c2e202 fix(parcels): guard NULL geom + None lat/lon в isochrones/poi-score (#1201)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
`cad_parcels_geom` — VIEW над `cad_parcels` где `geom` nullable (~964 prod-
строк с NULL geom задокументировано в analyze:1504-1507). Без фильтра
`geom IS NOT NULL` в UNION-ветках:
- NULL-строка из ранней ветки (`cad_quarters_geom`) ПЕРЕКРЫВАЕТ валидную
  геометрию из поздней (`cad_parcels_geom`) — LIMIT 1 берёт первую попавшуюся.
- ST_Centroid(NULL) → NULL → coord_row.lat = NULL → float(None) → TypeError
  → 500 (вместо честного 404).

Guard `if not coord_row` НЕ ловил эту дыру: RowMapping с None-полями
truthy. Зеркало паттерна из analyze (parcels.py:1418-1438) для обоих
эндпоинтов (`/isochrones` + `/poi-score`):
1. `AND geom IS NOT NULL` во ВСЕХ ветках UNION.
2. Belt-and-suspenders: явная проверка `coord_row.get("lat") is None`
   на случай топологически битого polygon'а (NOT-NULL geom, NULL centroid).

26 analyze/isochrones/poi-score юнит-тестов зелёные. ruff clean.

Closes #1201
2026-06-13 05:24:25 +00:00
c8000d1089 test(parcels): integration EXPLAIN-gate для analyze hot-path SQL (#1198)
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 6m24s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m46s
Deploy / build-worker (push) Successful in 2m55s
CI / backend-tests (pull_request) Successful in 6m20s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 1m10s
PR #1195 ввёл CTE alias `overlaps` (PG keyword для time-period operator)
→ каждый POST /analyze падал с syntax error ~10ч до hotfix PR #1197. Mock-БД
в tests/api/v1/test_*.py не поймала: MagicMock на db.execute() не парсит SQL.

Расширяет существующий phantom-column gate (tests/integration/) на
_neighbors_summary и parcel_ird_overlaps SQL:

1. _NEIGHBORS_SUMMARY_SQL вынесен из inline-text в module-level constant
   в parcels.py (паттерн как в best_layouts/ird_overlay_lookup/krt_lookup).
   Заодно сконвертирован на canonical CAST(:wkt AS text) — backend.md rule.
2. EXPLAIN-тесты через phantom_check_session (SSH-туннель к prod-PG) —
   skip без TEST_DATABASE_URL, run в спец CI job.
3. Compile-time guards (всегда работают, без БД):
   - CTE-alias не должен быть PG-keyword (overlaps/user/current_date/select/where)
   - SQL не должен содержать :bind::type (psycopg v3 antipattern)

Поймал бы:
- #1195 (CTE alias overlaps) — оба EXPLAIN-тест + compile-time keyword guard
- :type recurring class — compile-time parametrized guard
- Phantom column / typo PostGIS function — EXPLAIN parses + plans

Refactor backward-compatible: _neighbors_summary использует ту же константу,
behavior идентичен. 26 unit-тестов parcels analyze продолжают зелёные.

Refs #1198
2026-06-13 09:43:49 +05:00
2b23a4b24b fix(parcels): rename CTE overlaps → overlap_rows (#1196 regression of #1195)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m12s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m28s
CI / backend-tests (pull_request) Successful in 6m25s
Deploy / build-backend (push) Successful in 1m45s
`OVERLAPS` — это PostgreSQL keyword (binary operator для time-periods —
`(start1,end1) OVERLAPS (start2,end2)`). Парсер ругается на `overlaps AS (...)`
в WITH-блоке с

  ERROR: syntax error at or near "overlaps"

→ exception → транзакция в aborted state → весь analyze пайплайн ниже
получает InFailedSqlTransaction (ird_overlays, persist, ird block) → фронт
видит data_available=False, neighbors=[]. Воспроизводится на КАЖДОМ
/analyze с момента мерджа PR #1195.

Mock-БД в pytest tests/api/v1/ синтаксис не проверяет — отсюда регрессия
прошла CI. Прямой psql-replay на проде:
  WITH neighbors AS (...), overlaps AS (...) → syntax error
  WITH neighbors AS (...), overlap_rows AS (...) → ok

Минимальная переименование CTE alias (4 строки: 2 в SQL + 1 в SELECT
column alias + 1 в Python `row["overlap_rows"]`). Семантика идентична PR #1195.

Memory: future-proofing — добавить интеграционный тест с реальной PG для
analyze hot-path (отдельный issue, не блокер этого hotfix).

Refs #1196
2026-06-13 08:59:45 +05:00
49b85ab1d6 perf(site-finder): air-quality TTL-кэш + neighbors_summary 2→1 SQL (#1130 Phase B)
All checks were successful
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 6m28s
CI / backend-tests (pull_request) Successful in 6m24s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m37s
CI / changes (push) Successful in 6s
Deploy / build-worker (push) Successful in 3m3s
Deploy / deploy (push) Successful in 1m24s
Phase B продолжает hot-path latency cut'ы analyze_parcel поверх Phase A (PR #1194):

1. get_air_quality_cached в weather_cache (третий TTL-слот рядом с forecast/climate):
   - hit-TTL 1h (hourly bucket Open-Meteo), negative-TTL 5min.
   - timeout 5s → 2s.
   - single-flight под per-cache threading.Lock.
   - Снимает 0.26с на cache-hit и до 5с при DNS-fail (тот же антипаттерн,
     что был у forecast/climate до Phase A).

2. _neighbors_summary: 2 SQL → 1 statement.
   - WITH neighbors AS (... LIMIT 30), overlaps AS (... LIMIT 5)
   - + COALESCE(json_agg(row_to_json(...)), '[]'::json) в основном SELECT.
   - Один round-trip вместо двух (~-47 ms на каждый analyze).
   - Семантика идентична: те же ST_DWithin(geom, point, 100m) для neighbors,
     ST_Intersects + ST_Area для overlaps. Anti-fakes guards (_COST_PER_M2_MIN/MAX,
     float() try/except, >50 m² overlap) сохранены.

Формат возвращаемых dict не меняется (frontend контракт). 5 новых юнит-тестов в
test_weather_cache.py (TestAirQualityCache: hot-hit, negative, empty-hourly,
separate-from-weather, ttl-expires). 7 файлов API-тестов обновили patch-цель
_fetch_air_quality_sync → get_air_quality_cached.

Refs #1130
2026-06-12 22:42:32 +05:00
97ff0c9e81 perf(site-finder): TTL-кэш Open-Meteo в analyze hot-path (#1130 Phase A)
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Successful in 6m28s
CI / backend-tests (pull_request) Successful in 6m26s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m41s
Deploy / build-worker (push) Successful in 4m51s
Deploy / deploy (push) Successful in 1m24s
Два внешних HTTP-вызова в analyze (_fetch_weather_sync + _fetch_seasonal_weather_sync)
сидели без кэша. На проде в private network с restricted egress DNS до *.open-meteo.com
фейлит и каждый replay висит полный timeout (5s + 15s = до 20с лишних на analyze).
Профиль cProfile одного replay: 2.38с в httpx + 1.73с в SSL recv.

Вынес логику в app/services/weather_cache.py:
- TTL hit: forecast 6h, climate normals 7d. Negative-cache: 5min (DNS-fail не повторяет
  timeout на каждый analyze, восстановление подхватывается через ~5 минут).
- Ключ — округлённые до 0.01° (~1 км) координаты.
- Single-flight: per-cache threading.Lock + check-then-fetch-then-store.
- Тайм-ауты сокращены: forecast 5s→2s, climate 15s→3s.

Контракт для caller'а не изменился: None по-прежнему допустим (env_ok-флаг в
_build_environmental_score). 7 файлов API-тестов обновили patch-цели на новые имена
get_weather_cached / get_seasonal_weather_cached. 11 новых юнит-тестов в
tests/services/test_weather_cache.py (single-flight через threading.Barrier(16), TTL
expire через monkeypatch _now, изоляция forecast vs climate).

Refs #1130
2026-06-12 19:58:07 +05:00
1a4778d006 feat(sf): cross-load ETL tradein→gendesign newbuilding_listings — вариант A (#976)
- docker-compose.prod.yml: backend + worker добавлены в сеть gendesign_shared
  (нужен прямой TCP к tradein-postgres для psycopg ETL)
- tradein-mvp/data/sql/101_gendesign_reader_role.sql: роль gendesign_reader
  (LOGIN, SELECT на houses/house_sources/houses_price_dynamics/
   house_reliability_checks/house_reviews; пароль через TRADEIN_READER_PASSWORD
   bootstrap в deploy-tradein.yml)
- .forgejo/workflows/deploy-tradein.yml: bootstrap ALTER ROLE gendesign_reader
  PASSWORD из env после миграций; warn (не exit 1) если переменная не задана
- backend/app/core/config.py: settings.tradein_database_url (env TRADEIN_DATABASE_URL)
- backend/app/services/etl/newbuilding_crossload.py: run_crossload() — psycopg
  server-side cursor, batch 500, UPSERT ON CONFLICT (source, ext_house_id),
  SAVEPOINT per-row; disabled-режим если url пуст
- backend/app/workers/tasks/etl_newbuilding_crossload.py: Celery task
- backend/app/workers/beat_schedule.py: nightly 03:30 МСК (00:30 UTC)
- backend/app/api/v1/admin_scrape.py: POST /admin/scrape/newbuilding-crossload
- backend/tests/services/test_newbuilding_crossload.py: 10 тестов без реальной БД

needs-human: TRADEIN_READER_PASSWORD → tradein .env.runtime;
             TRADEIN_DATABASE_URL → gendesign backend/.env.runtime
2026-06-12 11:18:47 +03:00
fceaaf9a2c feat(chat): LLM tool-loop + §19 redaction wiring for #957 (Step 2+3)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 5s
CI / frontend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m41s
CI / backend-tests (push) Successful in 6m24s
CI / backend-tests (pull_request) Successful in 6m23s
Deploy / build-worker (push) Successful in 3m26s
Deploy / deploy (push) Successful in 1m12s
Add the LLM prose-composition path for the parcel-forecast chat, layered
over the deterministic Step-1 fallback which stays the safety net.

- chat/tools.py: 5 read-only section tools (exec_summary, product_recommendation,
  forecast, risks, scenarios) — pure slices of the loaded report dict, no DB/
  recompute, graceful on missing sections. market_now (raw analyze blob) and meta
  are deliberately NOT exposed -> highest-PII data cannot reach the LLM.
- chat/safe_payload.py: the §19 gate — single place that builds the outbound
  SafePayload from a section-aggregate allowlist; honors is_confidential hard-block.
- chat/orchestrator.py: manual tool-call loop with call-cap/termination, real
  grounded_in provenance; any LLMResult.ok=False (disabled/timeout/rate_limited/
  redaction_refused/call_cap/provider_error/empty) degrades to the deterministic answer.
- llm/prompts.py: versioned chat_system@v1 — answer only from sections, never
  fabricate numbers, advisory tone, decline out-of-scope.
- api/v1/chat.py: branch on settings.llm_enabled; sync complete bridged via
  run_in_threadpool. Default-off -> deterministic path, no provider built.
- Tests: fake provider only (no network), planted-secret redaction-boundary +
  per-reason fallback + call-cap + numbers-from-report coverage.

Refs #957
2026-06-08 17:45:01 +05:00
4847a2eb01 feat(chat): deterministic §22 chat foundation — POST /api/v1/chat/ask (no LLM)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (pull_request) Successful in 6m23s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m25s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m43s
Deploy / deploy (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 7m39s
Step 1 of #957. Answers parcel questions by reading the already-persisted
§22 SiteFinderReport (latest_run_for, schema "1.0", read-only) and returning
templated RU answers with engine numbers verbatim (§16, never fabricated).
Intent routing (explicit or RU keyword match) -> per-section renderers;
graceful on partial/missing sections and pending (no run / DB error) without
500s. Works with llm_enabled=False (llm_used always False); LLM composition
is Step 2. Mounted off /api/v1/chat so rbac_guard auto-requires an authed
known user.

Refs #957
2026-06-08 17:21:51 +05: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
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
d9b92f19f4 feat(sf): вшить parcel_reservations в ИРД-блок analyze (#1127)
All checks were successful
Deploy / build-worker (push) Successful in 2m36s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m22s
Deploy / deploy (push) Successful in 1m17s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 11:09:26 +00:00
4cc0b6da8c feat(sf): включить enable_ird_analyze + latency-hardening (#1115)
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 2m31s
Deploy / deploy (push) Successful in 1m12s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 10:00:48 +00:00
40b1f3ad69 feat(export): add PPTX presentation export of §22 forecast report (#959)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m58s
Deploy / build-worker (push) Successful in 3m26s
Deploy / deploy (push) Successful in 1m26s
CI / backend-tests (pull_request) Successful in 6m7s
Completes EPIC #959 export formats (md/json/tg/docx/pptx; PDF/XLSX pre-existing).
render_report_pptx (python-pptx, pure + graceful, reuses report_pdf helpers,
local import) → 6-slide deck. Endpoint format=pptx (early-return, presentationml
media-type, .pptx attachment). Add python-pptx + regenerate uv.lock (python-pptx
+ xlsxwriter; uv sync --frozen passes).

Closes #959.
2026-06-07 14:09:35 +05:00
3a89287c6b feat(export): add DOCX export of §22 forecast report (#959)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Has been skipped
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m49s
Deploy / build-worker (push) Successful in 3m27s
Deploy / deploy (push) Successful in 1m13s
CI / backend-tests (pull_request) Successful in 6m4s
render_report_docx (python-docx) mirrors report_md/report_pdf section order &
content, reuses report_pdf pure helpers (DRY), graceful on thin/empty report.
Widen /forecast/export format Literal to include docx → Word attachment.
Add python-docx dep + regenerate uv.lock (uv sync --frozen passes). Part of #959.
2026-06-07 13:58:04 +05:00
7b40e7e480 feat(sf): wire ИРД-слой в analyze за флагом enable_ird_analyze (#1101)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m33s
Deploy / build-worker (push) Successful in 2m49s
Deploy / deploy (push) Successful in 1m11s
D9b capstone — поле `ird` в ответе analyze за флагом enable_ird_analyze (default OFF): parcel_ird_overlaps (м.132, incl opportunity) + функц.зона/КРТ (геопортал WFS) + ПЗЗ-регламент (C8b cache-first). Логика в self-contained ird_analyze.py; parcels.py +13 строк flag-gated, defense-in-depth ×3 graceful. Schema additive (ird Optional). Замыкает ИРД-эпик #1067 (#1078/#1090/#1092/#1099/#1100/#1058/#1060). 4 теста.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-06 19:50:57 +00:00
579ad54202 feat(export): compact Telegram summary export for §22 forecast (#959)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m30s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Has been cancelled
render_report_telegram_summary (pure, no new deps, DRY-reuses report_pdf str
helpers) + `tg` format on GET /{cad}/forecast/export → inline text/plain snippet
(no attachment, copy-paste-ready). md/json unchanged; no-run 404, bad format 422.
Graceful on thin/empty reports. Part of EPIC #959.
2026-06-06 18:55:47 +00:00
25f7052084 feat(export): Markdown + JSON download of §22 forecast report (#959)
All checks were successful
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 2m34s
Deploy / deploy (push) Successful in 1m12s
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
render_report_markdown (pure, no new runtime deps) reuses report_pdf's str
helpers (DRY), + GET /{cad_num}/forecast/export?format=md|json. No forecast run
→ 404; graceful on thin/empty reports; GFM-safe table escaping. PDF/XLSX already
existed; this adds the cheapest no-dep formats. Part of EPIC #959.
2026-06-06 18:39:51 +00:00
22fffb9878 feat(parcels): typed AnalyzeResponse + run-history read endpoints (#961)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m20s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Has been cancelled
#992: attach response_model=AnalyzeResponse to POST /{cad_num}/analyze. Model
uses extra="allow" so any result_payload key not explicitly modeled is preserved
in the 200 response (no silent drops that would break Site Finder), and ALL
fields are Optional so the #93 202 fetch-stub serializes without a 500.

#994: add GET /{cad_num}/runs (light summary list, empty 200 not 404) and
GET /runs/{run_id} (full row incl result, 404 if missing), backed by
list_runs_for/get_run in analysis_runs.repository (psycopg v3 CAST(:x AS type)).
Routes ordered before /{parcel_id} so /runs/{run_id} is not shadowed.

Closes #992. Closes #994. Refs #961.
2026-06-06 18:13:34 +00:00
a87a69af7c feat(analyze): expose competitor + pipeline lat/lon for map layers (#999)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m25s
Deploy / build-worker (push) Successful in 2m32s
Deploy / deploy (push) Successful in 1m9s
Add nullable lat/lon (EPSG:4326, 6 dp) to /analyze competitors[] and
pipeline_24mo.top_objects[] so the frontend can plot Leaflet markers. Coords
come from domrf_kn_objects.latitude/longitude (same source as distance_m).
Purely additive: no existing field/shape changed. Frontend map layers follow
in a separate PR. Part of EPIC #958 (958-B4).
2026-06-05 16:44:32 +05:00
19584e0249 feat(parcels): wire §22 forecast into analyze endpoint (3b-ii, #995)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m30s
Deploy / build-worker (push) Successful in 2m50s
Deploy / deploy (push) Successful in 1m14s
Final forecast wiring — §22 reaches the API.

- horizon query param on POST /{cad}/analyze ({6,12,18}, default 12; 422 before any
  DB/compute work otherwise).
- best-effort fire-and-forget forecast_site_finder_report.delay(cad, horizon,
  created_by) after persist (lazy import, no api↔workers cycle); Celery/Redis down →
  forecast.status='unavailable', analyze still 200 (never blocks/500s). Adds additive
  result_payload['forecast'] = {status: pending|unavailable, horizon}.
- new read-only GET /{cad}/forecast: latest schema_version='1.0' run → 200
  {status:ready, run_id, created_at, report} else 202 {status:pending}; graceful on DB
  error (202, never 500). Mirrors fetch-status RBAC/style.

Additive only — existing analyze response keys unchanged. code-review APPROVE
(analyze uncrashable + additive; 422 pre-flight; GET reads '1.0' not 'analyze-1.0').
96 api tests pass; ruff clean. Refs #995 #992 #961.
2026-06-05 09:22:57 +05:00
cdfa4b3ab4 fix(analyze): 500 on parcels with NULL geometry (random-parcel crash)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 3m12s
Deploy / deploy (push) Successful in 1m6s
Deploy / build-frontend (push) Has been skipped
POST /analyze 500'd (TypeError: float(None)) for ~964 cad_parcels_geom rows
that have a meta row but geom IS NULL: the resolution query returned the row
(no NULL filter), geom_wkt became NULL → ST_Centroid(NULL) → NULL lat/lon →
float(centroid_row["lat"]) crashed (the `if centroid_row else` guard checked
row existence, not NULL values). A user entering any such "random parcel" hit
a 500.

Fix:
- Add `AND geom IS NOT NULL` to all three geometry-resolution queries (initial
  UNION, inline-refetch UNION, geom_row WKT subquery) so a NULL-geometry parcel
  is treated as "no usable geometry" → falls into the #93 graceful fallback
  (enqueue NSPD on-demand fetch → 202 polling), exactly like a parcel absent
  from the DB. The fetch then populates real geometry and analyze succeeds.
- Defensive: centroid_lat/lon now check the VALUE is not None (not just row
  existence) before float(), falling back to EKB center.

Verified: badformat→400, not-in-DB→202(fetching), valid parcel→200 unaffected.
39 analyze/by-bbox tests green. Refs #944.
2026-06-03 19:50:06 +05:00
c8ab3e8be9 fix(analyze): district KeyError 500 in #994 persist + revive by-bbox tests
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m31s
Deploy / build-worker (push) Successful in 2m28s
Deploy / deploy (push) Successful in 1m44s
LIVE BUG (from #994, already merged): the analysis_runs persist call-site in
analyze_parcel extracted district via result_payload["district"]["district_name"]
— but a district dict without that key (real data: partial district lookup)
raised KeyError OUTSIDE the best-effort SAVEPOINT (extraction is at the call
site, not inside persist_analysis_run) → 500 on the LIVE /analyze endpoint.
Fix: .get("district_name") → None instead of raising. Caught by reviving the
analyze test suite (below).

Test-infra (the suite was un-runnable, which is why the bug shipped):
- Lazy-import WeasyPrint in layout_tz_pdf.py + trade_in_pdf.py (matches the
  existing report_pdf.py / snapshot_pdf.py pattern). The eager top-level imports
  made `app.main` (→ parcels/trade_in routers) un-importable on hosts without
  WeasyPrint native libs (e.g. macOS dev), breaking pytest COLLECTION of the
  whole api/v1 suite. WeasyPrint is still imported when a PDF is actually rendered.
- tests/conftest.py: autouse fixture clears app.dependency_overrides after each
  test (anti-leak — a leaked get_db override caused real-DB connection attempts).
- test_parcel_by_bbox.py: rewrite get_db mocking from patch("...get_db") (a no-op
  — FastAPI Depends holds the original ref) to app.dependency_overrides[get_db],
  add explicit X-Authenticated-User header (RBAC gate), patch latest_run_dates,
  + a new test asserting last_analysis_date from a latest run (#994). 5/5 green.

NOTE: reviving collectability exposes PRE-EXISTING rot in other api/v1 suites
(analyze/admin/best_layouts: RBAC-header + stale-assertion/mock drift) — those
are NOT regressions from this PR (they were uncollectable before) and are
tracked separately for a deliberate suite-rehab + CI test-gate effort.

Refs #994.
2026-06-03 18:04:37 +05:00
72eae3bc9b feat(parcels): persist analysis runs + real last_analysis_date (#994)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-worker (push) Successful in 2m31s
Deploy / deploy (push) Successful in 1m42s
Persist each successful POST /analyze into analysis_runs (migration 127):
best-effort SAVEPOINT-wrapped INSERT in a thin analysis_runs repository, then
explicit db.commit() (get_db has no commit-on-success; SAVEPOINT RELEASE alone
does not persist — without the commit the row rolls back on request teardown,
a silent no-op caught in code review). A persist failure never breaks the
response or poisons the session. Result serialized via jsonable_encoder→
json.dumps (handles embedded Pydantic models/dates); confidence/status
normalized to satisfy the 127 CHECK constraints (unknown→NULL/'complete').

Populate by-bbox last_analysis_date from v_analysis_runs_latest via a single
batch query (no N+1), replacing the #307 placeholder. The read is best-effort
wrapped too — a view-missing deploy window or future drift must not 500 the
map (falls back to last_analysis_date=None).

Additive only — analyze response shape unchanged. Tests: by-bbox suite patches
latest_run_dates (mock-db returns same rows for any query) + new test asserts
ISO last_analysis_date from a latest run. Analyze mock-suite unaffected
(graceful side_effect overflow + best-effort persist absorb the extra INSERT).
Incidental: ruff-format fixed one pre-existing f-string spacing (line ~2391)
the format hook flags on touch.

Closes #994. Refs #961.
2026-06-03 17:36:01 +05:00