Commit graph

213 commits

Author SHA1 Message Date
028c6a1356 Merge pull request 'fix(scrape-kn): real documents endpoints (5x) + skip obj_checks + sem 3→8' (#325) from fix/domrf-real-doc-endpoints-skip-checks-sem6 into main
Some checks failed
Deploy / build-backend (push) Successful in 1m21s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m24s
Deploy / deploy (push) Has been cancelled
2026-05-17 20:08:50 +00:00
7a8d59beb4 feat(obj-3): SF backend migrate to objective_lots ground truth (price 0.3% -> 81%) (#324)
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-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
2026-05-17 20:08:17 +00:00
lekss361
2aa33e0eb8 fix(stealth): bump Semaphore 6->8 for higher concurrency in gather
Per-object 7 endpoints через asyncio.gather упираются в semaphore limit.
6 -> 8 — ещё +33% concurrency для batch fetch'а.
2026-05-17 23:04:51 +03:00
lekss361
0f289c05b1 fix(scrape-kn): real documents endpoints (5x) + skip obj_checks 404 + sem 3->6
PR #322 used placeholder URLs:
- /api/object/{id}/documents -> 404 (Playwright showed: 5 real endpoints:
  document/rpd / developer/report / project/documentation /
  documentation/other / document/permits). Replaced with 5 parallel
  fetches + merge into single upsert_documents call.
- /api/object/{id}/checks -> 404. 6 checkboxes likely inline in bulk
  kn/object payload; endpoint /checks does not exist. Commented out
  _fetch_obj_checks_safe with TODO for separate investigation PR.
- BrowserSession Semaphore(3) -> Semaphore(6) — double concurrency
  for intra-object gather.

GlitchTip: kn_scrape_failures run #19 showed 40 failures each for
both placeholder endpoints across first 30 objects of 1532.
2026-05-17 22:59:44 +03:00
f2983c86d7 feat(scrape-kn): parallel Phase B/C + wire obj_checks + documents + is_ekb auto-derive (#322)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m19s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m51s
Deploy / deploy (push) Successful in 55s
2026-05-17 19:40:16 +00:00
3978aa5905 feat(trade-in): TI-2 PDF export 4-page + frontend enable button (#319)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 2m40s
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-frontend (push) Successful in 2m58s
Deploy / deploy (push) Successful in 51s
2026-05-17 17:16:48 +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
fce48a8924 fix(scrape-objective): drop redundant self arg in sync_all_groups loop (#313)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m28s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m17s
Deploy / deploy (push) Successful in 1m5s
2026-05-17 16:13:41 +00:00
fd2b09b806 fix(22k): quartirography — use f.flat_type not f.type (column doesn't exist) (#312)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m38s
Deploy / deploy (push) Successful in 1m0s
2026-05-17 16:09:00 +00:00
f8c382f885 feat(22k): object page — full exposure 9 blocks (sales/quartirography/metro/photos/docs/specs/checks) (#310)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-worker (push) Successful in 2m37s
Deploy / build-frontend (push) Successful in 3m2s
Deploy / deploy (push) Successful in 56s
2026-05-17 15:56:39 +00:00
4bed58b43c feat(22d): domrf_catalog.py scraper for per-flat price/status from SSR HTML (#309)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m15s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m14s
Deploy / deploy (push) Successful in 1m0s
2026-05-17 15:40:01 +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
0b12a55eb6 fix(bug-22a): map missing kn-API fields (flat_number/living_area/wall_type/energy_eff) + backfill (#302)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m46s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 58s
Deploy / build-worker (push) Successful in 2m50s
2026-05-17 14:59:02 +00:00
bfe16ae71b fix(sf-16): derive price_per_m2 from price_rub in _norm_flat + backfill SQL (#298)
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 2m33s
Deploy / deploy (push) Successful in 58s
2026-05-17 14:32:23 +00:00
e054395464 Merge pull request 'fix(sf-15): extract obj_class from aiDescription (DOM.РФ list API never returns objClass)' (#293) from fix/sf-15-obj-class-scraping into main
All checks were successful
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 9s
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-worker (push) Successful in 2m24s
Deploy / deploy (push) Successful in 1m1s
2026-05-17 14:18:49 +00:00
lekss361
3daf2a1fb1 fix(sf-15): extract obj_class from aiDescription (DOM.RF list API never returns objClass field)
Add _extract_obj_class_from_ai() with regex patterns for Комфорт/Бизнес/Премиум/Элит/Стандарт.
_norm_object() uses it as fallback; objClass from API still takes priority if ever returned.
Add backfill SQL 105 to retroactively populate existing rows from raw JSON payloads.
Coverage: ~1806/4548 rows will be filled; remainder NULL is expected (Нежилое/no AI desc).
2026-05-17 17:02:07 +03:00
lekss361
2104ed879b fix(sf-21b): weighted AVG in _INLINE_VELOCITY_SQL (best_layouts.py)
Replace unweighted AVG(deals_total_avg_area_m2) and AVG(deals_total_avg_price_thousand_rub_per_m2)
with SUM(x * count) / NULLIF(SUM(count), 0) pattern in _INLINE_VELOCITY_SQL.
Months with zero deals no longer dilute the weighted mean 2-4x.
P0 follow-up to PR #290 (mv fix, issue #21).
2026-05-17 16:56:22 +03:00
lekss361
e1a8ad9395 fix(sf-17): rosreestr velocity fallback via cad_quarter (~100% EKB coverage)
When Objective mapping coverage falls below 50% of competitors in the radius,
fall back to rosreestr_deals JOIN on the parcel's cadastral quarter. Audit shows
237/237 EKB quarters (100%) have rosreestr data for the last 12 months, compared
to <20% Objective coverage before bulk mapping.

- velocity.py: add _compute_rosreestr_fallback(), _OBJECTIVE_COVERAGE_MIN_RATIO
  constant, velocity_source field on VelocityResult (objective/rosreestr_fallback/none)
- parcels.py: extract cad_quarter from cad_num, pass to compute_velocity
- site-finder.ts: add velocity_source field to Velocity interface
- VelocityBlock.tsx: badge "Источник: квартальные сделки" when rosreestr_fallback

Epic #271 item #17
2026-05-17 16:50:19 +03:00
9eed55cd30 Merge pull request 'fix(sf-20): adaptive district threshold 30→15 with data_confidence flag' (#288) from fix/sf-20-district-threshold-adaptive into main
All checks were successful
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Successful in 1m49s
Deploy / deploy (push) Successful in 1m0s
Deploy / build-worker (push) Successful in 2m41s
Deploy / build-frontend (push) Successful in 3m9s
2026-05-17 13:39:52 +00:00
lekss361
989d4e5b7d fix(sf-20): adaptive district threshold 30→15 with data_confidence flag
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
  (Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
  data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
  emoji star removed (ui-ux.md), empty-state text min 30→15
2026-05-17 16:34:52 +03:00
lekss361
2694e3180b feat(etl): fuzzy matcher v2 — pruned threshold 0.85→0.80 for objective mapping coverage
- Add AUTO_ACCEPT_THRESHOLD_V2 = 0.80 constant to objective_backfill.py
- Add min_threshold param to find_match_candidates() (default REVIEW_THRESHOLD=0.6)
- Add match_method param to auto_apply_matches() (default 'fuzzy_trgm')
- Add ?v2=true query param to POST /api/v1/admin/etl/objective-backfill
  - v2 mode: threshold=0.80, method='fuzzy_v2', search from 0.80 (not 0.60)
- Return type widened to dict[str,object] to include threshold_used + match_method_used

DB audit: 1068 unmapped EKB objs; v1 adds ~133 rows, v2 adds ~47 more.
Expected coverage: 8.5% → ~20% after sequential v1+v2 runs on prod.
Run: POST /objective-backfill (v1) then POST /objective-backfill?v2=true (v2).
Part of task #44 Part A, epic #271.
2026-05-17 16:28:20 +03:00
lekss361
b7ce4a589c fix(sf-10): velocity LEFT JOIN + velocity_data_available flag
Replace INNER JOIN on objective_complex_mapping with LEFT JOIN approach:
competitors without mapping now return velocity=0 with
velocity_data_available=False instead of being silently dropped.

UI: VelocityBlock shows 'нет данных velocity' badge when flag is False
and hides the gauge (meaningless zero). TS type updated (additive optional
field, backward compat).

Audit (EKB, region_cd=66): mapped=129, unmapped=1387, total=1516.

Closes #271 item #10
2026-05-17 15:51:43 +03:00
lekss361
689327909c fix(sf-09): capacity-aware redistribute (round 2) — bounded + correct invariant
Round 1 (commit bcd7dc8) был broken: на 2-bucket входах surplus уходил в free
полностью без учёта capacity → free превышал cap → следующая итерация
clamp'ировала его и наоборот. Infinite oscillation в FastAPI handler.

Round 2 fix per review BLOCK (#282 comment):
- Surplus распределяется пропорционально available capacity (cap - v),
  не текущему v. Free никогда не вылетит выше cap.
- free = строго < cap (не <=) — иначе деление на 0 capacity.
- Hard guard `for _ in range(N+1)` — гарантированно завершается.
- Pathological (surplus > total_capacity): возвращаем оригинальный pct_map
  + cap_skipped=True (sum=100 invariant сохранён).
- Hamilton round вынесен в _hamilton_round() helper.

Tests:
- 2-bucket cases (90/10, 70/30, 99/1) expected cap_skipped=True
- test_cap_iteration_count_bounded — все pathological завершаются < 100ms
- All 13 cases verified standalone (3 fast-path + 7 reproduced + 3 pathological)
2026-05-17 15:30:50 +03:00
lekss361
bcd7dc8f75 fix(sf-09): iterative cap algorithm + cap_skipped flag + unit tests
Review verdict MINOR (PR #282) — single-pass cap_and_redistribute
не гарантировал invariant max ≤ MAX_BUCKET_SHARE_PCT. Surplus от clamped
поднимал free выше cap (например {1k:75, studio:15, 2k:10} → studio=39).

- backend/app/services/site_finder/best_layouts.py
  - _cap_and_redistribute → iterative while-loop до сходимости
  - Returns (result_map, cap_skipped) — флаг для pathological all-clamped
  - Float work map, Hamilton финальный pass для invariant sum=100
- backend/app/schemas/parcel.py
  - LayoutTzRecommendation.cap_skipped: bool = False
- backend/tests/services/site_finder/test_best_layouts.py
  - 6 parametrized tests: invariants, 7 failing cases from review,
    no-dominant unchanged, empty, cap_skipped propagation, normal-case
- frontend/src/types/best-layouts.ts — cap_skipped: boolean
- frontend/src/components/site-finder/BestLayoutsBlock.tsx
  - banner условие сменилось с maxPct > 60 на rec.cap_skipped (honest signal)
2026-05-17 15:21:51 +03:00
lekss361
0d11f426b2 fix(sf-09): MAX_BUCKET_SHARE 35% cap + frontend warning banner
_build_recommendation зеркалило рынок через чистый Hamilton apportionment.
Добавлен _cap_and_redistribute: bucket >35% clamp к 35%, surplus
пропорционально перераспределяется в остальные bucket'ы (Hamilton финальный
pass гарантирует sum=100). Pathological case (все bucket'ы >35%) — warning
в лог, cap пропускается. Frontend: warning banner при maxPct>60 в
RecommendationCard (Tailwind amber utilities, без inline hex).
2026-05-17 14:49:56 +03:00
lekss361
b9259cd8ed fix(sf-08): add euro-1/euro-2 room_bucket for small-format rooms=2 flats
DOM.РФ API classifies euro-format flats (26-50м²) as rooms=2.
_SUPPLY_BATCH_SQL now maps:
  rooms=2 + area<35 → 'euro-1'
  rooms=2 + area<50 → 'euro-2'
before the generic rooms IN (1,2,3) branch.

Audit: 4 140 units rooms=2 + area<35 in domrf_kn_flats were inflating
the supply count for genuine 2-room apartments (median 57м²).
euro buckets have no velocity counterpart in objective_corpus_room_month
so they are filtered out of top_layouts by min_velocity.

Also extends RoomBucket Literal and room_bucket_from_flat() in
layout_signature.py with the new euro-1/euro-2 values.

Closes #271 item 8
2026-05-17 14:38:48 +03:00
lekss361
4d39ae5b5d fix(sf-07): use median_12m from mv_quarter_price_per_m2 for district price
ekb_districts.median_price_per_m2 was populated from a 24-month window.
LEFT JOIN mv_quarter_price_per_m2 on the cad quarter and COALESCE(median_12m,
fallback) so the API returns the 12-month median where available.
Frontend OverviewTab adds a "(12 мес)" hint next to the value.

Closes #271 item 7
2026-05-17 14:28:32 +03:00
lekss361
1799e1fb30 fix(sf-02): добавить site_status/ready_dt в /analyze competitors + sort строящиеся first
SQL в /analyze не выбирал site_status, ready_dt из domrf_kn_objects;
ORDER BY flat_count DESC ставил сданные ЖК в топ.

Closes (epic part) #271 item 2
2026-05-17 13:29:59 +03:00
lekss361
c701636515 Merge forgejo/main into fix/sf-01-velocity-time-window
Resolve conflict in best_layouts.py: keep SF-01 inline velocity
(deals_window numerator) + add SF-05 sold_pct clamp at 100% and
is_oversold flag from main.
2026-05-17 12:15:40 +03:00
lekss361
5a4408e0ab fix(sf-01): time_window honest velocity — inline SQL с реальным фильтром report_month
Раньше _VELOCITY_DIVISORS делил агрегаты mv_layout_velocity (24 мес)
на 4/12 для quarter/year, не меняя реальное окно данных. Теперь
inline SQL из objective_corpus_room_month с CAST(:window_interval AS interval).

velocity_per_month = deals_window / months_in_window (1.0/3.0/12.0).
Разные time_window → разные строки из БД → разный mix/velocity/jk_count.

Closes (epic part) #271 item 1
2026-05-17 12:12:19 +03:00
lekss361
247c252b04 fix(sf-05): clamp sold_pct_of_supply at 100% + add is_oversold flag
Symptom: sold_pct_of_supply = sum_deals_24mo / supply_count_snapshot * 100
yields >100% (e.g. 199%) for fast-selling small formats due to incompatible
time windows (24-month deals vs point-in-time supply snapshot).

Option A (hotfix): clamp to 100.0, expose is_oversold=True for UI badge.

- best_layouts.py: compute sold_pct_raw, clamp with min(..., 100.0),
  set is_oversold = raw > 100%
- parcel.py TopLayoutRow: add is_oversold: bool field
- best-layouts.ts TopLayoutRow: add is_oversold: boolean
- BestLayoutsBlock.tsx: show warn badge ">100%" when is_oversold=True
- tests: two new cases — raw 199% clamps + is_oversold=True;
  raw 50% passes through + is_oversold=False

Closes (epic part) #271 item 5
2026-05-17 11:57:29 +03:00
ee0bad445d Merge pull request 'fix(#231): re-enable nspd-harvest-stale-quarters beat schedule (Sub-PR D)' (#267) from fix/231-nspd-beat-reenable into main
All checks were successful
Deploy / build-worker (push) Successful in 2m38s
Deploy / deploy (push) Successful in 47s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m33s
Deploy / build-frontend (push) Has been skipped
2026-05-17 07:50:34 +00:00
f160c5efaf Merge pull request 'feat(#263 sub-A): cad_* overlay schemas + cadastre territorial_zones (ПЗЗ)' (#265) from feat/263-sub-a-cad-nspd-overlay-schemas-and-pzz into main
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
2026-05-17 07:50:08 +00:00
lekss361
8e377059a4 fix(#265 review): CRS 3857→4326 + stable zone_id hash + rename 102 + tx comment
- #1 (BLOCK): ST_GeogFromGeoJSON → ST_Transform(ST_SetSRID(..., 3857), 4326)::geography
  so EPSG:3857 features from get_territorial_zones_in_bbox are correctly
  stored in GEOGRAPHY(POLYGON, 4326) column (matches cad_parcels/cad_buildings pattern)
- #2: zone_id fallback changed from counter-based f"{quarter_cad}_{inserted}"
  to md5(json(props))[:12] — stable between runs, prevents wrong-row ON CONFLICT
- #3: rename 101_cad_nspd_overlay_tables.sql → 102_* (101_user_custom_pois.sql exists)
- #4: add comment clarifying SQLAlchemy autobegin guarantees outer tx before begin_nested()
- Tests: updated test_feature_without_zone_id_uses_fallback to assert md5 format + idempotency
2026-05-17 10:45:52 +03:00
lekss361
d4da1b26c6 fix(#231): re-enable nspd-harvest-stale-quarters beat schedule (Sub-PR D)
После Sub-PR B (#260, merged 8ebcd15) переключения search_by_quarter на
grid-walk dispatch — beat schedule безопасно re-enable'ить:

- harvest_quarter возвращает полноценные dumps (territorial_zones + ЗОУИТ +
  risk zones + engineering structures) вместо 0-3 features
- per-instance semaphore (PR #260) защищает от cross-loop binding bug
- soft/hard time_limit 600/900s — задача успевает завершиться

Schedule: Mon 01:00 UTC (04:00 МСК), batch_size=50 ограничивает WAF burst,
max_age_days=90 для регулярного refresh + новые quarters (fetched_at IS NULL).

Полный ЕКБ back-fill (11k+ quarters в cad_quarters_geom) займёт несколько
недель при batch_size=50/week; в production будет ускорено manual trigger
admin endpoint'ом если нужно.

Closes #231.
2026-05-17 10:37:07 +03:00
lekss361
f08db43145 feat(#263 sub-A): cad_* overlay schemas + cadastre territorial_zones (ПЗЗ)
Phase 1 (migration 101): создаёт 5 новых таблиц cad_territorial_zones,
cad_red_lines, cad_engineering_structures, cad_risk_zones,
cad_opportunity_parcels. Idempotent (IF NOT EXISTS), GIST-индексы.

Phase 2 partial (ПЗЗ only):
- NSPDBulkClient.get_features_in_bbox_grid — async grid-walk WMS GetFeatureInfo
- NSPDBulkClient.get_territorial_zones_in_bbox — convenience wrapper layer 875838
- harvest_quarter Phase 2.5 — вызов get_territorial_zones_in_bbox после grid-walk
- _save_territorial_zones — UPSERT loop с SAVEPOINT per-row, ON CONFLICT zone_id

9 unit tests (mock DB): insert/upsert/no-geom/no-id/error-isolation/savepoint pattern.
2026-05-17 10:28:22 +03:00
26ee3b015c Merge pull request 'fix(#261): custom_pois — regression tests для db.commit() в create/update/delete (P0 persistence)' (#262) from fix/261-custom-pois-missing-commit into main
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Successful in 2m23s
Deploy / deploy (push) Successful in 49s
Deploy / build-backend (push) Successful in 27s
Deploy / build-frontend (push) Has been skipped
2026-05-17 07:26:54 +00:00
8ebcd15ed6 Merge pull request 'fix(#231): switch search_by_quarter area layers to grid-walk (Sub-PR B)' (#260) from fix/231-nspd-search-by-quarter-grid into main
Some checks failed
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been cancelled
2026-05-17 07:26:08 +00:00
lekss361
570c987e78 fix(#231): repair test_rate_limit_semaphore regression + docstring sync
Re-review feedback (PR #260 head 8191a85):

1. tests/scrapers/test_nspd_bulk_client.py:360 — test referenced bulk_mod._SEMAPHORE
   which was renamed to _SEMAPHORE_LIMIT in previous commit. AttributeError на
   полном pytest run. Заменено: assert bulk_mod._SEMAPHORE_LIMIT == 3 (smoke).
   max_concurrent <= 3 уже валидирует throttling — capacity check был лишним.

2. app/services/scrapers/nspd_client.py docstrings (lines 483, 506) — обновлены
   ссылки NSPDBulkClient._SEMAPHORE(3) → NSPDBulkClient._sem (per-instance,
   capacity=3) чтобы соответствовать новой архитектуре.
2026-05-17 10:22:16 +03:00
lekss361
c94545f0eb fix(#261): add db.commit() regression tests для custom_pois service
Проблема: POST /custom-pois возвращал 201 с id=N, но row не сохранялась
в БД — user_custom_pois оставалась пустой после request close.

Симптом возникает если service-функция не вызывает db.commit() явно:
get_db.finally → db.close() без commit → psycopg3 rollbacks pending tx.

Код (#257) содержит db.commit() во всех 3 мутациях, но тесты полностью
мокировали service layer через patch() — regression в commit не ловился.

Fix: добавлены 6 service-level тестов с паттерном "два сеанса":
- MagicMock(db) + db.commit.assert_called_once() для create/update/delete
- db.commit.assert_not_called() для not-found и empty-payload случаев

Closes #261
2026-05-17 10:16:36 +03:00
lekss361
8191a85102 fix(#231): address re-review blockers — per-instance semaphore + time_limit + test mocks
PR #260 second-opinion review 🔴 BLOCK → fixes:

1. nspd_bulk_client.py: _SEMAPHORE module-level → per-instance self._sem
   создаётся в __aenter__ под текущий event loop. Sequential asyncio.run()
   в search_by_quarter (11 layers × asyncio.run() per layer) bind'ил
   module-level semaphore к first loop'у → RuntimeError на втором вызове →
   все cells return_exceptions=True проглатывали → grid возвращал 0 features.

2. nspd_sync.py: harvest_quarter добавлен time_limit=900 (hard) рядом с
   soft_time_limit=600. Safety net если task игнорит SoftTimeLimitExceeded.

3. tests/test_nspd_client.py: добавлен _make_fake_grid_walk helper +
   monkeypatch NSPDClient.get_features_in_bbox_grid в 3 тестах
   (search_by_quarter_core_only / _with_zouit / _layers_fetched_with_risks).
   Без этого area layers били бы по живому NSPD API → CI flaky.

Tests: 37 passed, 1 skipped (3 area-dispatch + 34 grid-walk).
2026-05-17 10:13:34 +03:00
lekss361
fdced94626 fix(#231): switch search_by_quarter area layers to grid-walk dispatch (Sub-PR B)
- Add _GRID_WALK_LAYERS frozenset (territorial_zones, red_lines,
  engineering_structures, zouit_*, risk_*)
- _fetch_layer dispatch: area layers → get_features_in_bbox_grid(grid_n=7,
  step_m=50) [foundation из PR #247], EGRN layers (parcels, buildings) →
  legacy get_features_in_bbox
- Enrich engineering_structures features с classified_kind через
  classify_engineering_kind (PR #247 classifier)
- Per-layer logger.info heartbeat (method + count) для прогресса
- Bump harvest_quarter soft_time_limit 120 → 600s
  (grid-walk: 11 layers × 49 cells × ~70ms ≈ 40s + retries margin)
- TestFetchLayerDispatch (3 tests): 34 passed, 1 skipped

Pre-push deep-code-reviewer: APPROVE (1 medium nit — opportunity_oopt не в
grid-walk, защищено через include_opportunity=False default; follow-up issue
рекомендован).
2026-05-17 09:46:42 +03:00
8cc94472f7 Merge pull request 'feat(#254): user_custom_pois — backend schema + CRUD + scoring integration' (#257) from feat/254-custom-pois-backend into main
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m40s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m49s
Deploy / deploy (push) Successful in 50s
2026-05-17 06:42:29 +00:00
lekss361
485f489bd2 feat(#254): user_custom_pois — backend schema + CRUD + scoring integration
- SQL migration 101_user_custom_pois.sql: table with GEOGRAPHY(POINT) + GIST index
- Pydantic schemas: CustomPoiCreate / CustomPoiUpdate / CustomPoiOut
- Service custom_pois.py: CRUD + get_overlaps_for_scoring (1km radius, psycopg v3 CAST)
- API /api/v1/custom-pois: POST(201)/GET/PATCH/DELETE, X-Session-Id header auth
- Scoring in analyze_parcel: custom POI block after OSM POI loop,
  decay weight * max(0, 1 - dist/1000), custom_poi_score_items in response
- Tests: CRUD + validation (weight/lon/lat 422) + scoring integration (mock-based)
- Vault: Schema_User_Custom_Pois.md + Endpoint_Parcel_Analyze v3.9 section
2026-05-17 09:34:33 +03:00
lekss361
81cf274e80 fix(#234): address re-review nits — Settings singleton + docstring sync
1. _acquire_harvest_lock: Settings() instance → singleton 'settings' import
   from app.core.config — соответствует convention (scrape_kn.py, objective.py,
   celery_app.py все используют singleton).
2. _trigger_harvest docstring: убрана incorrect фраза «остальные ... видят
   harvest_eta_seconds от первого запроса» (код возвращает eta=None для
   denied-lock callers). Заменена на actual behavior: graceful no-data UI
   с ПКК fallback на FE.

PR #250 re-review followup (issues #3 + docstring sync).
2026-05-17 09:31:43 +03:00
lekss361
b99ac76463 Merge remote-tracking branch 'forgejo/main' into fix/234-nspd-harvest-eta-badge
# Conflicts:
#	backend/app/services/site_finder/quarter_dump_lookup.py
2026-05-17 09:19:12 +03:00