feat(site-finder): auto-fetch cadastre geometry on-demand (#93) #95
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#95
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/site-finder-auto-fetch-cadastre"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #93 (priority/high, UX + Data — главный UX-блокер на /site-finder).
Раньше: пользователь ввёл cad-номер которого нет в БД → 404 «Загрузи через NSPD geo» (dead-end для non-admin).
Теперь: backend автоматически инициирует NSPD fetch (reuse существующей infra), frontend показывает loading state с polling, после готовности геометрии — автоматически рендерит analyze.
Backend
Новый модуль
app/services/site_finder/cadastre_fetch.pyfind_or_enqueue_fetch(db, cad_num)→(status, job_id, error_msg):find_active_on_demand_job— параллельные запросы на тот же cad → один jobnot_in_nspd; target_status=failed → retryfetch_status(db, cad_num)— polling endpoint helperdetect_thematic_id— 3 сегмента → quarter (2), 4 → parcel (1), 5 → building (5)validate_cad_format—NN:NN:NNNNNN[:NN[:NN]]Reuse существующей infra
enqueue_geo_job+process_nspd_geo_jobизworkers/tasks/nspd_geo.pysource_kind='auto_on_demand'отличает от bulk (rosreestr_pending,manual_list)rate_ms=200(быстрее bulk 600), Celerypriority=9(высокий)POST /parcels/{cad}/analyzegraceful fallback{status, job_id, eta_seconds, message}Retry-After: 60headerНовый endpoint
GET /parcels/{cad}/fetch-statusPolling endpoint, возвращает
{status, job_id, error_msg, eta_seconds}.Frontend
useSiteAnalysishook расширенfetchingState(cadNum, jobId, etaSeconds) +cancel()status=readyre-trigger analyze автоматическиnot_in_nspd/failed/invalid_format→ typed errorslib/api.ts— статус-aware variantapiFetchWithStatus<T>возвращает{status, body}для 200 vs 202 distinctionHTTPErrorclass сstatus+bodyНовый
FetchingState.tsxaria-live/aria-valuenowдля accessibilitysite-finder/page.tsxEdge cases (per #93 acceptance)
Test plan
ruff check + format— cleantsc --noEmit— cleannpm run lint --max-warnings 0— cleanabc:def) → 400 + clear messageRisks
Related
/conceptесли cad не в БД.⚠️ Auto-review: minor (non-blocking)
Backend —
backend/app/services/site_finder/cadastre_fetch.py:SELECT FOR UPDATE, но SQL вfind_active_on_demand_job:252-275обычный SELECT. Два параллельных запроса на один cad могут оба создать новый job. Fix: либоpg_try_advisory_xact_lock(hashtext(cad_num))обернуть шаги 3-6find_or_enqueue_fetch, либо убрать упоминание FOR UPDATE из docstring + добавитьTODO(#93): add advisory lock for strict dedup. Severity: low (дубль job, не данные).nspd_geo_targets.cad_num—find_active_on_demand_job/find_recent_completed_jobфильтруют поt.cad_num, индекс(job_id, cad_num, thematic_id)UNIQUE здесь не помогает. Создатьdata/sql/NN_on_demand_indexes.sqlсCREATE INDEX nspd_geo_targets_cad_num_idx ON nspd_geo_targets(cad_num)+ composite(source_kind, status)наnspd_geo_jobs. Не блокер при текущем размере таблицы, нужно при росте.Backend —
backend/app/api/v1/parcels.py:def analyze_parcelблокирует один FastAPI threadpool slot до 25s на inline-wait. При concurrent surge (>40 одновременных «миссинг cad» запросов) threadpool сатурируется. Добавить комментарий о cost или снизить_INLINE_FETCH_WAIT_Sдо 15s.Frontend —
frontend/src/hooks/useSiteAnalysis.ts:797-801:setFetchingState(null)вызывается до того как resolve'нется secondapiFetchпосле status=ready. Между этими моментами иfetchingState, иisPendingмогут быть false → пустой экран на ~1 RTT. Fix: переместитьsetFetchingState(null)послеawait second.Frontend —
frontend/src/components/site-finder/FetchingState.tsx:width: ${progressPct}%, остальное — статика → должно быть Tailwind. Cosmetic, не блокер.<style jsx>для@keyframes fetch-spin→ можно заменить Tailwindanimate-spinдля консистентности с остальными spinner'ами.Positive:
:cbindings, нет f-string injection.validate_cad_formatgate перед DB touch.print(), norequests.cancelledRef.currentcheck корректный.Retry-After: 60header — хороший API hygiene.Применено 4 из 6 findings в `cce2bf3`, 2 — оставлены с обоснованием:
Backend:
✅ Race condition — добавлен `pg_try_advisory_xact_lock(hashtext(cad_num))` вокруг "check active → enqueue" в `find_or_enqueue_fetch`. Lock transaction-scoped, released at COMMIT. Параллельные запросы: первый получает lock и enqueue; второй lock=false → re-check active job (уже виден после первого COMMIT) → тот же job_id. Docstring обновлён, упоминание SELECT FOR UPDATE удалено.
✅ Missing indexes — новая миграция `data/sql/87_on_demand_indexes.sql`:
✅ Threadpool exhaustion — `_INLINE_FETCH_WAIT_S` снижен 25→15s с подробным комментарием про anyio default 40 slots, ~70% fast path при avg НСПД 5-15s.
Frontend:
4. ✅ UI flicker — `setFetchingState(null)` перемещён ПОСЛЕ `await second apiFetch`.
Rebuttal (не применено):
5/6. ❌ Tailwind convention / `animate-spin` — проверил: в проекте нет ни `globals.css`, ни `@tailwind` directives. ВСЕ существующие site-finder components (ConfidenceBadge, GeotechRiskBlock, ScoreBreakdownPanel, Pipeline24moBlock и т.д.) используют inline styles. Tailwind в deps, но не wired up (видимо legacy). FetchingState.tsx consistent с siblings — keep inline. `
✅ Auto-review passed (LGTM) — все 4 applied findings корректны:
pg_try_advisory_xact_lock(hashtext(cad_num))transaction-scoped, auto-release at COMMIT. Double-check pattern (find_active → lock → re-check → enqueue) корректен. Hash collision risk acceptable — benign false-serialization, не correctness bug.87_on_demand_indexes.sql—IF NOT EXISTS,BEGIN/COMMIT, composite ordering(source_kind, status)правильный (equality first). Соответствует существующим запросам._INLINE_FETCH_WAIT_S25→15s — threadpool math корректна (anyio 40 slots × 15s).setFetchingState(null)послеawait second apiFetch.Rebuttals accepted — все site-finder components используют inline styles, FetchingState consistent с siblings;
<style jsx>— стандартный Next.js паттерн без extra deps.Minor cosmetic (не блокер): docstring
analyze_parcelвсё ещё пишет "до 25с" вместо 15с. Можно поправить in-place при следующем коммите.Merging.