Добавляет endpoint для приёма заявок на пилот (lead-gen).
INSERT в pilot_requests, response {id, created_at, status}.
Telegram-уведомление — TODO (creds не настроены, #307 SF-B3).
Migration: data/sql/118_pilot_requests.sql
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
- 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
- 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.
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
TASK A (#29 G2): add parcel_meta to analyze response
- New ParcelMeta Pydantic schema in app/schemas/parcel.py
- SELECT from cad_parcels WHERE cad_num=:c in analyze_parcel() (step 9f)
- Returns permitted_use_established_by_document, land_record_category_type,
land_record_subtype, cost_value; None when row absent
- Tests: test_analyze_parcel_meta.py (found + not-found cases)
TASK B (#232 G3): cad_zouit fallback in _get_zouit_overlaps
- When nspd_quarter_dumps has zouit_count==0, fall back to ST_Intersects
query on cad_zouit (3483 rows, GIST indexed)
- Overlaps tagged with source='cad_zouit'; format compatible with NSPD path
- gate_verdict.py: BLOCKER_TYPE_ZONE_KEYWORDS tuple for keyword-based
classification (охранная зона / трубопровод / электр / газ -> blocker;
СЗЗ -> warning); NSPD subcategory path preserved backward-compat
- Tests: 6 new test cases in test_gate_verdict.py covering cad_zouit path
and backward-compat for NSPD subcategory path
Updated db.execute call sequence in test_analyze_*.py (index shift +1 at pos 10).
Wraps refresh_all / refresh_year Celery tasks behind the existing
AdminTokenAuth gate so the table can be populated on demand after
first deploy instead of waiting for the monthly beat (1st of month).
- TriggerEkburgPermitsRequest: year int|None, ge=2022 le=2030
- year=None -> refresh_all.apply_async() (scope all_years_2022_2026)
- year=N -> refresh_year.apply_async(args=[N]) (scope year_N)
- 4 smoke tests: all/year/invalid_year/no_token
Add SQL migration 100_user_weight_profiles_default_seed.sql with system
presets Эконом/Комфорт/Бизнес (user_id='__system__'). Migration is
idempotent via ON CONFLICT DO UPDATE.
Backend:
- weight_profiles.py: add SYSTEM_USER_ID constant + list_profiles_with_system()
- admin_weight_profiles.py: add include_system query param to GET list endpoint
Tests: 3 new tests covering include_system flag and service sentinel behaviour.
* fix(parcels): _parse_floors handle int (post-migration #169 schema change)
After PR #169 cad_buildings schema migration, `floors` column is INT
(was TEXT in legacy schema). Existing `_parse_floors(r.get("floors"))`
call in analyze_parcel → _neighbors_summary crashes with:
AttributeError: 'int' object has no attribute 'strip'
Fix: type union str | int | None. If int → return directly (no strip).
Preserve TEXT range parsing ("5-7" → 7) for backwards-compat with
any legacy data still in cad_buildings_old_apr26.
* test(smoke): production smoke tests for post-deploy regressions (#168)
Add tests/smoke/test_prod_smoke.py covering known regression surfaces.
Marks: prod_smoke + slow. Env: PROD_SMOKE_BASE_URL, PROD_SMOKE_ADMIN_TOKEN.
Run manually: cd backend && uv run pytest tests/smoke/ -m prod_smoke -v
* fix(cadastre): exact-match headers to nspd_lite to bypass NSPD WAF (#168)
Pilot v2 (job_id=2) failed 50/50 with HTTP 403 WAF block. Comparing
nspd_bulk_client.DEFAULT_HEADERS vs legacy nspd_lite.HEADERS (which works
on VPS IP since April 2026):
PascalCase keys → lowercase keys
Chrome/148 UA → Chrome/144 UA
No cache-control / pragma → "no-cache" both
accept-language ru first → en first
No origin → "https://nspd.gov.ru"
referer "/map" → "/map?thematic=PKK"
NSPD WAF (BotShield-class) likely fingerprints на header order + values
combined with TLS fingerprint. Matching legacy exactly minimizes deltas.
Test plan: retry pilot job after deploy, expect 0 WAF blocks for first
5 quarters.
* fix(test): exclude prod_smoke tests by default (#168)
CI ran tests/smoke/test_prod_smoke.py and they hit production
https://gendsgn.ru/api/v1/parcels/.../analyze which currently returns 500
(parse_floors regression — exactly what this PR fixes). Catch-22: PR can't
merge because smoke tests fail against pre-merge prod.
Fix: add `addopts = ["-m", "not prod_smoke"]` so default pytest excludes
them. Run manually post-deploy with: pytest -m prod_smoke -v
---------
Co-authored-by: lekss361 <claudestars@proton.me>
Production POST /parcels/{cad}/analyze падает с 500:
sqlalchemy.exc.InternalError: (psycopg.errors.InFailedSqlTransaction)
current transaction is aborted, commands ignored until end of transaction block
## Root cause
`compute_velocity()` SQL fails (probably на cad's без conkurrentов / sparse
sale_graph data) → exception caught в try/except → НО db.rollback() отсутствует
→ transaction остаётся в aborted state.
Следующая query (_geotech_risk на line 828) пытается выполниться → крашится
с InFailedSqlTransaction.
## Fix
Wrap velocity в `with db.begin_nested()` — SAVEPOINT pattern (consistent
with PR #124 pzz_loader fix). Failure внутри savepoint:
- Rollbacks ТОЛЬКО savepoint
- Outer transaction остаётся clean
- Subsequent queries (_geotech_risk и пр.) работают
Pattern matches feedback_subagent_delegation audit recommendation для
in-loop / per-section exception handling.
## Impact
POST /parcels/{cad}/analyze больше не 500 при velocity failure. Возвращает
`velocity: null` + остальные fields normal.
Refs: user report 2026-05-15 InFailedSqlTransaction
Co-authored-by: lekss361 <claudestars@proton.me>