* feat(site-finder): auto-fetch cadastre geometry on-demand (#93)
Когда пользователь вводит cad-номер которого нет в БД (cad_quarters_geom /
cad_buildings / cad_parcels_geom), вместо 404 «Загрузи через NSPD geo» (dead-
end для non-admin) — теперь backend автоматически инициирует NSPD fetch,
frontend показывает loading state с polling /fetch-status каждые 2с.
Backend:
- Новый модуль app/services/site_finder/cadastre_fetch.py с helpers:
find_or_enqueue_fetch (atomic check + enqueue с дедупликацией по cad),
fetch_status (smart polling endpoint — отличает not_in_nspd от failed),
detect_thematic_id (3-сегм quarter / 4-сегм parcel / 5-сегм building),
validate_cad_format.
- Reuse: enqueue_geo_job + process_nspd_geo_job (workers/tasks/nspd_geo).
source_kind='auto_on_demand' отличает от bulk; rate_ms=200, priority=9.
- POST /parcels/{cad}/analyze graceful fallback: inline await до 25s
(fast path), затем 202 + job_id + eta_seconds для polling, либо
400/404/503 в зависимости от статуса (с Retry-After 60s на 503).
- GET /parcels/{cad}/fetch-status новый endpoint для polling.
Frontend:
- useSiteAnalysis расширен: fetchingState + cancel(). POST analyze + при 202
polling каждые 2с (max 60 итераций = 2 мин cap). status=ready → re-trigger
analyze; not_in_nspd/failed/invalid_format → typed errors.
- apiFetchWithStatus<T> + HTTPError в lib/api.ts — status-aware variant
для 200 vs 202.
- FetchingState.tsx: animated spinner, progress bar (linear до etaSeconds,
asymptote после), elapsed counter, cancel button. Светло-голубая
scheme отличается от обычного pending skeleton.
- site-finder/page.tsx: FetchingState когда fetchingState активен; обычный
pending skeleton — только при первичном analyze без 202.
Edge cases (per #93 acceptance):
- cad валидный в НСПД, fetch <25s → inline 200 OK
- cad валидный, fetch >25s → 202 + frontend polls → ready → analyze
- cad валидный, не в НСПД → 404 с понятным сообщением + формат hint
- cad invalid format → 400 + формат hint
- NSPD rate-limited / failed → 503 + Retry-After 60s
- Параллельные запросы на тот же cad → один job, оба клиента poll'ят (дедуп
через find_active_on_demand_job).
Closes #93.
* fix(site-finder): address PR #95 auto-review minor feedback
Backend (cadastre_fetch.py):
1. (race condition) Advisory lock `pg_try_advisory_xact_lock(hashtext(cad_num))`
обёрнут вокруг шагов "check active job → enqueue" в find_or_enqueue_fetch.
Lock transaction-scoped, released at COMMIT. Параллельные запросы на тот
же cad: первый получает lock и enqueue; второй lock=false → re-check
active job (уже виден после первого COMMIT) → возвращает тот же job_id.
Docstring обновлён, упоминание SELECT FOR UPDATE удалено.
Backend (parcels.py):
3. (threadpool exhaustion) _INLINE_FETCH_WAIT_S снижен 25 → 15s с подробным
комментарием: tradeoff про default Starlette anyio threadpool (40 slots)
и concurrent burst saturation. 15s баланс: НСПД avg 5-15s для quarter,
~70% fast path; остальные 30% получают 202 + polling без блока.
Data (87_on_demand_indexes.sql):
2. (missing index) New migration:
- `nspd_geo_targets_cad_num_idx ON nspd_geo_targets(cad_num)` — для
find_active/recent_completed_job (existing UNIQUE composite не покрывает
WHERE cad_num=:c).
- `nspd_geo_jobs_source_status_idx ON nspd_geo_jobs(source_kind, status)`
composite для filter auto_on_demand + queued/running.
Idempotent (CREATE INDEX IF NOT EXISTS), не блокер при текущем размере,
критично при росте on-demand traffic.
Frontend (useSiteAnalysis.ts):
4. (UI flicker) setFetchingState(null) перемещён ПОСЛЕ `await second
apiFetch`. Иначе между clear и resolve есть момент когда mutation
isPending=true + fetchingState=null → пустой экран ~1 RTT.
NOT addressed (rebuttal):
5. (Tailwind convention) — проверил: в проекте нет ни globals.css, ни
@tailwind directives. ВСЕ существующие site-finder components используют
inline styles (ConfidenceBadge, GeotechRiskBlock, ScoreBreakdownPanel etc).
Tailwind в deps но не wired up. Keep inline styles для consistency.
6. (animate-spin) — требует Tailwind globals (см. #5). `<style jsx>`
keyframes — built-in Next.js, работает.
Per auto-review on 2252236.
---------
Co-authored-by: lekss361 <claudestars@proton.me>
36 lines
1.6 KiB
PL/PgSQL
36 lines
1.6 KiB
PL/PgSQL
-- #93 (PR #95): indexes for on-demand cadastre fetch lookups.
|
||
--
|
||
-- Context:
|
||
-- `find_active_on_demand_job` и `find_recent_completed_job` в
|
||
-- `app/services/site_finder/cadastre_fetch.py` фильтруют
|
||
-- `nspd_geo_targets` по cad_num + `nspd_geo_jobs` по source_kind+status.
|
||
-- Existing UNIQUE(job_id, cad_num, thematic_id) на targets не помогает
|
||
-- при WHERE cad_num=:c — нужен secondary index.
|
||
--
|
||
-- Не блокер при текущем размере таблицы (~10K targets), но критично при
|
||
-- ускорении on-demand traffic (per #93 trigger любым посетителем
|
||
-- /site-finder).
|
||
--
|
||
-- Apply: idempotent, CREATE INDEX IF NOT EXISTS — safe to run multiple
|
||
-- times. CONCURRENTLY не используется чтобы коммит был атомарным; на
|
||
-- prod размере index build занимает <1s.
|
||
|
||
BEGIN;
|
||
|
||
-- Targets — поиск активных on-demand job'ов для конкретного cad
|
||
CREATE INDEX IF NOT EXISTS nspd_geo_targets_cad_num_idx
|
||
ON nspd_geo_targets(cad_num);
|
||
|
||
-- Jobs — фильтр source_kind='auto_on_demand' + status IN (queued,running)
|
||
CREATE INDEX IF NOT EXISTS nspd_geo_jobs_source_status_idx
|
||
ON nspd_geo_jobs(source_kind, status);
|
||
|
||
COMMIT;
|
||
|
||
COMMENT ON INDEX nspd_geo_targets_cad_num_idx IS
|
||
'on-demand cadastre fetch lookups (#93). Used by find_active_on_demand_job '
|
||
'и find_recent_completed_job в cadastre_fetch service.';
|
||
|
||
COMMENT ON INDEX nspd_geo_jobs_source_status_idx IS
|
||
'Filter source_kind=auto_on_demand + status (#93). Composite чтобы '
|
||
'покрыть оба predicate сразу.';
|