Site Finder UX: Auto-fetch cadastre geometry on-demand (404 → NSPD fallback) #93

Closed
opened 2026-05-12 05:12:46 +00:00 by lekss361 · 1 comment
lekss361 commented 2026-05-12 05:12:46 +00:00 (Migrated from github.com)

Эшелон: 🎨 UX + 🟠 Data — критичный UX-fix

Контекст / проблема

В /api/v1/parcels/{cad_num}/analyze (backend/app/api/v1/parcels.py:79-83):

if not row:
    raise HTTPException(
        status_code=404,
        detail=f"Геометрия для {cad_num} не найдена. Загрузи через NSPD geo.",
    )

Текущий UX: пользователь вводит cad-номер на https://gendsgn.ru/site-finder, видит красную ошибку «Загрузи через NSPD geo» — но он не админ, у него нет доступа к /admin/scrape/geo. Это полный dead-end для пользователя.

Coverage state:

  • cad_quarters_geom: ~9700 (только Свердл, bulk-импорт)
  • cad_buildings: 10590
  • cad_parcels_geom: 2 (почти пусто)
  • Любой 4-сегментный cad (участок) → почти гарантированно 404

Что сделать

Backend — graceful fallback flow

  1. Изменить логику в analyze_parcel:
    • Если row is None → НЕ возвращать 404 сразу
    • Вместо этого: проверить, есть ли уже in-flight NSPD job для этого cad (через nspd_geo_jobs.cad_num + status running|pending)
      • Если есть — return 202 Accepted + { status: 'fetching', job_id, eta_seconds: 30 }
      • Если нет — enqueue новый NSPD geo job (через существующую infra nspd_lite.py)
        • Auto-detect target type: 3 сегмента → quarter, 4 сегмента → parcel
        • source_kind = auto_on_demand
        • priority queue (выше чем bulk jobs)
      • Return 202 + job_id
  2. Новый endpoint GET /api/v1/parcels/{cad}/fetch-status:
    • Polling endpoint: { status: 'fetching'|'ready'|'failed'|'not_in_nspd', job_id, error_msg? }
  3. Опционально (если простой sync вариант): inline await до 25s через asyncio.wait_for:
    • Если NSPD ответил за 25s → сразу analyze + return 200
    • Если нет → 202 + job_id для polling

Frontend — loading state

  1. useSiteAnalysis hook расширить:
    • На 202 response → переход в состояние fetching, начать polling каждые 2с по /fetch-status
    • Loading UI: «Загружаем участок из НСПД, обычно 15-30 секунд…»
    • Progress indicator (animated bar)
    • Cancel button (отменить ожидание + job)
  2. Когда fetch-status = ready → автоматически re-trigger analyze + render
  3. Если not_in_nspd → понятная ошибка «Кадастровый номер не найден в НСПД. Проверьте формат: NN:NN:NNNNNN:NN»
  4. Если failed (rate-limit или WAF) → retry button + сообщение «НСПД временно недоступен, попробуйте через минуту»

Edge cases

Сценарий Behaviour
cad валидный, в НСПД есть, fetch <25s Inline await → 200 OK сразу
cad валидный, fetch >25s 202 + polling, потом render
cad валидный, в НСПД нет 404 + clear message
cad invalid format 400 + format hint
NSPD rate-limited (429) 429 + retry-after header
Параллельные запросы на тот же cad Dedupe — один job, оба клиента ждут
cad уже в БД но stale (>30 дней) Opt-in refresh button («Обновить из НСПД»)

Telemetry

  • Sentry: трекать % cad'ов которые потребовали on-demand fetch (для оценки coverage gap)
  • Log в nspd_geo_log каждый on-demand request (для capacity planning)
  • Metric: parcels_analyzed_auto_fetched для PostHog (после #81)

Rate-limiting

  • Глобальный rate limit на on-demand fetches: max 10/мин per IP (защита от спама)
  • В админ-режиме — без лимита

Acceptance

  • Ввод нового cad на site-finder → loading 15-30с → автоматический render score (НЕ ошибка «Загрузи через NSPD»)
  • Параллельные запросы на тот же cad — один NSPD job, оба ждут
  • Несуществующий cad → понятная ошибка с подсказкой формата
  • NSPD rate-limited → retry с backoff
  • Telemetry в Sentry + nspd_geo_log

Owner

backend-engineer (analyze fallback logic + new endpoint + dedupe) + frontend-engineer (loading state + polling hook)

Effort

~2 дня (backend 1 день + frontend 1 день)

Dependencies

  • Существующая NSPD infra: backend/app/services/scrapers/nspd_lite.py, nspd_geo_jobs, process_nspd_geo_job Celery task — переиспользуем
  • Опционально связано с #51 (E1 cad_parcels_geom массовый импорт) — on-demand fetch покрывает edge cases, bulk закрывает baseline

Risks / open

  • НСПД rate-limit / IP-block — основной риск. Митигация: глобальный rate-limit, retry-after, fallback на rosreestr2coord lib (есть в проекте)
  • WAF на НСПД — иногда блокирует. Мониторить через nspd_geo_log
  • Долгий fetch (>60с) — НСПД иногда залипает. Timeout 60с hard limit + clear UX message
  • Дедупликация одинаковых cad — нужна транзакционная блокировка либо Redis lock (SETNX cad_num)

Why high priority

Это главный UX-блокер на /site-finder. Любой первый посетитель (включая будущих пилотов) с большой вероятностью введёт cad которого нет в БД (parcels_geom = 2 записи всего). Видит ошибку «Загрузи через NSPD» — закрывает страницу. Терминальный bounce.

  • Foundation для on-demand UX, который тоже понадобится для Generative /concept если cad нет в БД
  • Cross-ref с #51 (bulk import) — bulk создаёт baseline, on-demand покрывает миссы
**Эшелон:** 🎨 UX + 🟠 Data — критичный UX-fix ## Контекст / проблема В `/api/v1/parcels/{cad_num}/analyze` ([backend/app/api/v1/parcels.py:79-83](backend/app/api/v1/parcels.py)): ```python if not row: raise HTTPException( status_code=404, detail=f"Геометрия для {cad_num} не найдена. Загрузи через NSPD geo.", ) ``` **Текущий UX:** пользователь вводит cad-номер на https://gendsgn.ru/site-finder, видит красную ошибку «Загрузи через NSPD geo» — но он **не админ**, у него нет доступа к `/admin/scrape/geo`. Это полный dead-end для пользователя. **Coverage state:** - `cad_quarters_geom`: ~9700 (только Свердл, bulk-импорт) - `cad_buildings`: 10590 - `cad_parcels_geom`: 2 (почти пусто) - Любой 4-сегментный cad (участок) → почти гарантированно 404 ## Что сделать ### Backend — graceful fallback flow 1. **Изменить логику в `analyze_parcel`:** - Если `row is None` → НЕ возвращать 404 сразу - Вместо этого: проверить, есть ли уже **in-flight** NSPD job для этого cad (через `nspd_geo_jobs.cad_num` + status `running|pending`) - Если есть — return 202 Accepted + `{ status: 'fetching', job_id, eta_seconds: 30 }` - Если нет — enqueue новый NSPD geo job (через существующую infra `nspd_lite.py`) - Auto-detect target type: 3 сегмента → quarter, 4 сегмента → parcel - source_kind = `auto_on_demand` - priority queue (выше чем bulk jobs) - Return 202 + job_id 2. **Новый endpoint `GET /api/v1/parcels/{cad}/fetch-status`:** - Polling endpoint: `{ status: 'fetching'|'ready'|'failed'|'not_in_nspd', job_id, error_msg? }` 3. **Опционально (если простой sync вариант):** inline await до 25s через `asyncio.wait_for`: - Если NSPD ответил за 25s → сразу analyze + return 200 - Если нет → 202 + job_id для polling ### Frontend — loading state 1. `useSiteAnalysis` hook расширить: - На 202 response → переход в состояние `fetching`, начать polling каждые 2с по `/fetch-status` - Loading UI: «Загружаем участок из НСПД, обычно 15-30 секунд…» - Progress indicator (animated bar) - Cancel button (отменить ожидание + job) 2. Когда `fetch-status` = `ready` → автоматически re-trigger analyze + render 3. Если `not_in_nspd` → понятная ошибка «Кадастровый номер не найден в НСПД. Проверьте формат: NN:NN:NNNNNN:NN» 4. Если `failed` (rate-limit или WAF) → retry button + сообщение «НСПД временно недоступен, попробуйте через минуту» ### Edge cases | Сценарий | Behaviour | |---|---| | cad валидный, в НСПД есть, fetch <25s | Inline await → 200 OK сразу | | cad валидный, fetch >25s | 202 + polling, потом render | | cad валидный, в НСПД нет | 404 + clear message | | cad invalid format | 400 + format hint | | NSPD rate-limited (429) | 429 + retry-after header | | Параллельные запросы на тот же cad | Dedupe — один job, оба клиента ждут | | cad уже в БД но stale (>30 дней) | Opt-in refresh button («Обновить из НСПД») | ### Telemetry - Sentry: трекать % cad'ов которые потребовали on-demand fetch (для оценки coverage gap) - Log в `nspd_geo_log` каждый on-demand request (для capacity planning) - Metric: `parcels_analyzed_auto_fetched` для PostHog (после #81) ### Rate-limiting - Глобальный rate limit на on-demand fetches: max 10/мин per IP (защита от спама) - В админ-режиме — без лимита ## Acceptance - [ ] Ввод нового cad на site-finder → loading 15-30с → автоматический render score (НЕ ошибка «Загрузи через NSPD») - [ ] Параллельные запросы на тот же cad — один NSPD job, оба ждут - [ ] Несуществующий cad → понятная ошибка с подсказкой формата - [ ] NSPD rate-limited → retry с backoff - [ ] Telemetry в Sentry + nspd_geo_log ## Owner `backend-engineer` (analyze fallback logic + new endpoint + dedupe) + `frontend-engineer` (loading state + polling hook) ## Effort ~2 дня (backend 1 день + frontend 1 день) ## Dependencies - Существующая NSPD infra: `backend/app/services/scrapers/nspd_lite.py`, `nspd_geo_jobs`, `process_nspd_geo_job` Celery task — переиспользуем - Опционально связано с #51 (E1 cad_parcels_geom массовый импорт) — on-demand fetch покрывает edge cases, bulk закрывает baseline ## Risks / open - **НСПД rate-limit / IP-block** — основной риск. Митигация: глобальный rate-limit, retry-after, fallback на rosreestr2coord lib (есть в проекте) - **WAF на НСПД** — иногда блокирует. Мониторить через nspd_geo_log - **Долгий fetch (>60с)** — НСПД иногда залипает. Timeout 60с hard limit + clear UX message - **Дедупликация одинаковых cad** — нужна транзакционная блокировка либо Redis lock (SETNX cad_num) ## Why high priority Это **главный UX-блокер на /site-finder**. Любой первый посетитель (включая будущих пилотов) с большой вероятностью введёт cad которого нет в БД (parcels_geom = 2 записи всего). Видит ошибку «Загрузи через NSPD» — закрывает страницу. Терминальный bounce. ## Related - Foundation для on-demand UX, который тоже понадобится для Generative `/concept` если cad нет в БД - Cross-ref с #51 (bulk import) — bulk создаёт baseline, on-demand покрывает миссы
lekss361 commented 2026-05-12 05:45:52 +00:00 (Migrated from github.com)

Foundation для этой задачи перенесена в #94 NSPD API client. Использовать REST endpoint GET /api/geoportal/v2/search/geoportal?query={cad} через единый клиент — он возвращает GeoJSON Polygon + properties (permitted_use, land_category, cost_value, readable_address) за один запрос. То есть #93 + G2 (#29) фактически закрываются одним call'ом.

Foundation для этой задачи перенесена в **#94 NSPD API client**. Использовать REST endpoint `GET /api/geoportal/v2/search/geoportal?query={cad}` через единый клиент — он возвращает GeoJSON Polygon + properties (permitted_use, land_category, cost_value, readable_address) **за один запрос**. То есть #93 + G2 (#29) фактически закрываются одним call'ом.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#93
No description provided.