Дата: 27.04.2026. ПАРСЕР НАШ.ДОМ.РФ для КЖ (kn/object) и квартир (слоённый portal/table) реализован. План: C:\\Users\\user\\.claude\\plans\\crispy-waddling-swing.md.

REAL ENDPOINTS (проверены через chrome-devtools): GET /сервисы/api/kn/object?offset=0&limit=999999&place={place}&objStatus={s}&sortField=default&sortType=desc · GET /portal-kn/api/sales/portal/table?externalId={objId}. Auth header: Authorization: Basic MTpxd2U= (декодируется в 1:qwe — НЕ jwe как было в предыдущей памяти).

PARAMS КЛЮЧЕВЫЕ: place=66 (Регион Свердл) или place='0-4' (город ЕКБ). objStatus: 0=строящиеся, 1=архив-1 (~2 регион), 2=сданные/архив-2 (самый большой бакет). place=66 все 3 статуса = 442+2+1072 = 1516 ЖК (точно совпало с domrf_kn_objects).

СЕРВЕРНЫЙ ФИЛЬТР developers/regions НЕ РАБОТАЕТ — size/page/regions/developers все игнорируются. Поэтому фильтр по девелоперу реализован ЛОКАЛЬНО: фильтруем по developer.companyGroup из ответа. PRINZIP companyGroup=6208.

СТЕК: Playwright Chromium держит живую страницу «новостройки» (cookies+JA3 фингерпринт реального Chrome 147), fetch() из in-page context. httpx вариант НЕ РАБОТАЕТ — ServicePipe WAF проверяет TLS-fingerprint, различает Python httpx от Chrome даже с идентичными cookies. План был 'gibrid Playwright bootstrap + httpx' — НЕ СРАБОТАЛ, пивотнул на 'pure Playwright in-context'.

BACKEND ФАЙЛЫ: backend/app/services/scrapers/{stealth.py, domrf_kn.py, __init__.py}. backend/app/workers/celery_app.py (beat из settings.scrape_kn_cron). backend/app/workers/tasks/scrape_kn.py (с jitter 0..SCRAPE_KN_JITTER_SECONDS). backend/app/api/v1/admin_scrape.py (POST /api/v1/admin/scrape/kn + GET /runs, авторизация через X-Admin-Token = SCRAPE_ADMIN_TOKEN env). data/sql/50_schema_kn_extensions.sql + 50_scrape_kn_flats.py CLI.

FRONTEND: frontend/src/app/admin/scrape/page.tsx — кнопка «Запустить сейчас» (region, devs csv, fetch_flats checkbox) + таблица последних 20 прогонов из kn_scrape_runs (обновляется каждые 5 сек). Token хранится в localStorage.

КОНФИГ: env vars (settings) — SCRAPE_KN_CRON='15 4 * * mon' (любой 5-филд cron), SCRAPE_KN_JITTER_SECONDS=1800 (0..30мин random delay), SCRAPE_KN_DEFAULT_REGIONS='66' (csv), SCRAPE_KN_STATE_PATH='data/playwright_state.json', SCRAPE_ADMIN_TOKEN='' (пусто=выкл admin endpoint).

PLAYWRIGHT FINGERPRINT СНЯТ с локальной машины: data/playwright_state.json (7.3 KB, 26 cookies — AUTH_SESSION_ID, KC_AUTH_SESSION_HASH, ServicePipe NSC_*, Yandex tracking). Закоммичен в git для использования на сервере (worker подхватит при starting cold — ServicePipe видит returning user, не запускает JS-challenge). CLI flags --save-state PATH и --load-state PATH для периодического обновления.

DOCKER: docker-compose.yml +сервисы worker и beat (оба из образа backend). backend/Dockerfile +строка 'playwright install --with-deps chromium' — доп. ~80MB system libs.

СХЕМА-МИГРАЦИЯ ПРИМЕНЕНА: PK сняты с domrf_kn_flats.id и domrf_kn_objects.obj_id, заменены на UNIQUE (id|obj_id, snapshot_date). +индексы (region_cd, snapshot_date). +таблица kn_scrape_runs (журнал прогонов). Backfill region_cd=66 на Антоновых flats: 0 рядов затронуто (их obj_id не матчился с kn_objects).

SMOKE-ТЕСТ PRINZIP ПРОШЁЛ: 4 минуты, run_id=2 в kn_scrape_runs status='done'. Получено: place=66 × (status 0,1,2) = 442+2+1072 = 1516 объектов, локальный фильтр по companyGroup=6208 = 28 PRINZIP ЖК (= jk_count в v_developer_full_metrics!) + 826 квартир (с 814 agg_flats_total разница +12). 31 request total, 22 из них = 404 для сданных (квартиры уже нет в продаже — ок).

TYPE COERCION ИСПРАВЛЕН ДЛЯ BOOLEAN (0/1→bool), DATE ('YYYY-MM-DD'/'DD-MM-YYYY HH:MM:SS'/'IV кв. 2028' → последний день квартала), TEXT (problem_flag из int).

РАСПИСАНИЕ + ПЛАВАЮЩИЙ СТАРТ + КНОПКА выполнены: cron из SCRAPE_KN_CRON env, внутри таска random delay 0..SCRAPE_KN_JITTER_SECONDS берёт время, ручной триггер передаёт skip_jitter=True для немедленного старта.

СЛЕДУЮЩИЙ ШАГ: 1) Развёрнуть worker+beat в docker-compose, проверить celery beat schedule в живюю. 2) Добавить link на /admin/scrape с главной (пока только по прямому URL). 3) Full РФ-sweep (89 регионов) с multi-region cron после стабилизации Свердл. 4) Изучить /сервисы/api/object/{id}/sale_graph + sales_agg для time-series по продажам (видели в нетворке объектной страницы).

EXTRAS-ЭНДПОЙНТЫ ДОБАВЛЕНЫ (per-КЖ, после 27.04.2026 вечер): /сервисы/api/object/{id}/sale_graph?type=apartments|parking (time-series продаж), /sales_agg (текущие агрегаты 3 типа), /infrastructure (POI), /construction/progress/photo (мета+binary).

EXTRAS-ТАБЛИЦЫ: domrf_kn_sale_graph (obj_id, report_month, type, realised, contracted, area_sq, price_avg, snapshot_date), domrf_kn_sales_agg (obj_id, type ∈{apartments,nonliv,parking}, name, total, realised, perc), domrf_kn_infrastructure (нормализованная POI с lat/lon/dist_m/category), domrf_kn_photos (метаданные + local_path к binary). Schema: data/sql/51_schema_kn_extras.sql.

FAILURE LOG: kn_scrape_failures (run_id, obj_id, endpoint, full_url, http_status, error, body_preview, occurred_at). API GET /api/v1/admin/scrape/failures?run_id=N. В admin UI раскрывающийся блок с кнопкой «копировать URL» для ручной проверки в Chrome.

CLI РАСШИРЕН 50_scrape_kn_flats.py: флаги --no-extras / --download-photos / --no-flats / --probe URL. Probe поднимает Playwright-сессию и дёргает один URL — полезно для дебага конкретного фейла.

DOCKER FIX (буг #25): uv.lock не был реген после добавления playwright+tenacity — uv sync --frozen игнорил новые депы. Надо всегда делать 'cd backend && uv lock' после изменения pyproject.toml. Также Dockerfile реструктурирован: Chromium-libs вынесены явно в apt-get install (libnss3, libnspr4, libdbus-1-3, libatk1.0-0, libatk-bridge2.0-0, libcups2, libdrm2, libxkbcommon0, libxcomposite1, libxdamage1, libxfixes3, libxrandr2, libgbm1, libasound2t64, libatspi2.0-0), playwright install chromium без --with-deps, перед rm -rf /var/lib/apt/lists/*. ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright для shared chromium-cache.

DOCKER ОПТИМИЗИРОВАН (#16, 27.04 поздний вечер): backend/Dockerfile расщеплён на 2 таргета. target=runner (lean, ~1.6 ГБ) для backend/beat. target=runner-with-chromium (~2.9 ГБ) для worker. docker-compose.{yml,prod.yml} и .github/workflows/deploy.yml пушат 2 GHCR-образа: gendesign-backend + gendesign-worker. Экономия ~1.3 ГБ на backend image.

PER-OBJECT DRILL-IN (#15) ГОТОВ: 5 backend GET endpoints (/object/{id}, /sale_graph, /sales_agg, /infrastructure, /photos) + /prinzip/objects с sparkline-data. UI: новая страница /analytics/objects/[id] (KPI + sale-chart + POI-таблица с фильтром по категории + фото-галерея с lightbox), плюс PrinzipObjectsTable на /analytics/prinzip с sparkline в каждой строке и кнопкой Drill-in.

GitHub issues #13, #14, #15, #16 закрыты 27.04.2026 поздний вечер. README обновлён с разделами 'Запуск 442-sweep' + PRINZIP CRM в карте таблиц + ETL artefacts +52_schema/52_import.


Дата: 27.04.2026 поздний вечер. ПАЙПЛАЙН ДЛЯ ИМПОРТА PRINZIP CRM SQLite (analytics.prinzipevent.ru, 49 МБ, 102K заявок, 9K сделок) реализован, ждёт файла. GitHub #14 закрыт с instructions.

SCHEMA: data/sql/52_schema_prinzip_crm.sql — prinzip_leads (lead_id PK, source/channel, phone_hash SHA256[:16] + phone_last4, email_hash MD5, obj_interest soft FK к domrf_kn_objects, status, converted, deal_id) + prinzip_deals (deal_id PK, lead_id, closed_at, obj_id soft FK, deal_price, payment_type, mortgage_*) + 2 MV (funnel_monthly по месяцам×source, funnel_by_object по КЖ). Применено в прод.

IMPORT SCRIPT: data/sql/52_import_prinzip_crm.py. UPSERT по ID. PII-hashing при импорте (152-ФЗ). Использовать --inspect-only для discovery реальных колонок SQLite и подправить LEAD_FIELD_MAP/DEAL_FIELD_MAP. REFRESH MV автоматически в конце.

BACKEND: 3 endpoints в backend/app/api/v1/analytics.py — /prinzip/funnel/monthly?months=, /prinzip/funnel/by-source?months=, /prinzip/funnel/by-object. Используют MV быстрых выборок.

FRONTEND: 2 новых ECharts-компонента на /analytics/prinzip — PrinzipFunnelSankey (leads→engaged→converted by source, type=sankey) + PrinzipFunnelMonthly (бары leads/engaged/converted + линия conv%). Оба обрабатывают пустой dataset изящно (текст 'Нет данных CRM').

СЛЕДУЮЩИЙ ШАГ: user скачивает SQLite в data/raw/prinzip_crm.sqlite (в .gitignore), запускает импорт. Ожидаемые после импорта: SUM(deal_price)/1e9 ≈ 39.9 (млрд ₽), conversion 2025 ≈ 5.4%, пик 12.81% в 2023. UI будет живым автоматически после импорта.


Sitemap-страница с карточками-ссылками на все разделы. 3 секции: Аналитика (4 карточки), Админка (2), Generative (2). Footer со ссылками на /api/v1/docs, /health, GitHub.


Свердловский рынок: 4 KPI-карточки + market-pulse line chart + парадокс «строят vs покупают» + pipeline + treemap районов ЕКБ + яндекс listings pie. Публичный доступ.


PRINZIP drill-down: 4 KPI карточки + velocity vs benchmarks (toggle) + квартирография pie + gap-bar к рынку + районы ЕКБ + таблица 28 ЖК с sparkline + insight-карточки. Публичный (CRM-funnel вынесён в /admin/leads).


Топ-15 девелоперов: sortable leaderboard + velocity scatter + сравнение sold% по месяцам. drill-in через ?id=


Drill-in одного ЖК: 6 KPI + sale-chart (apartments+parking+price line) + POI-таблица с фильтром по категории/радиусу + фото-галерея с lightbox. Пример: /analytics/objects/60160 (Парк Победы).


Файл: frontend/src/app/admin/scrape/page.tsx + admin/layout.tsx

Token-protected. Ручной запуск скрапера (регион, devs, extras, photos) + живая очередь Celery (active/reserved/scheduled) с кнопками '🛑 Прервать' (SIGTERM) и '⊘ Удалить из очереди' (revoke) + журнал прогонов + журнал ошибок с copy-URL.


Файл: frontend/src/app/admin/leads/page.tsx + admin/layout.tsx

Token-protected. CRM-воронка PRINZIP: Sankey leads→engaged→converted by source + monthly bars + 5 KPI карточек + 4 фильтра (поиск/статус/source/converted) + таблица лидов с пагинацией. PII (phone/email) хешированы, в UI только ***1234.


GET /api/v1/analytics/prinzip/objects (с sparkline-data)

Источник: backend/app/api/v1/analytics.py + analytics_queries.py. Публичные.


GET /api/v1/admin/scrape/queue (Celery inspect: active/reserved/scheduled/workers)

POST /api/v1/admin/scrape/revoke {task_id, terminate}

Auth: X-Admin-Token. Файл: backend/app/api/v1/admin_scrape.py.


GET /api/v1/admin/leads/ (фильтры: status/source/converted/obj_id/search/date_from/date_to + lim/off)

GET /api/v1/admin/leads/sources (distinct list)

Auth: X-Admin-Token. Файл: backend/app/api/v1/admin_leads.py.


BrowserSession — Playwright Chromium + cookie jar + storage_state load/save. get_json() в браузерном контексте (TLS-fingerprint = real Chrome 147). download_binary() для фото. Auth: Basic MTpxd2U= (1:qwe). Jitter 600-1500мс + concurrency 3.


Endpoints для /сервисы/api/kn/object + portal/table + 4 extras (sale_graph/sales_agg/infrastructure/photos). place=66 для Свердл, objStatus 0/1/2 = 442+2+1072. Локальный фильтр по developer.companyGroup. _classify_and_log() — 404 для sale_graph/sales_agg не пишет в failures (ожидаемые для сданных).


Beat schedule из SCRAPE_KN_CRON env (default '15 4 * * mon'). Random jitter 0-1800с внутри таска. SCRAPE_KN_DEFAULT_REGIONS csv. Ручные триггеры из admin endpoint передают skip_jitter=True.


backend/Dockerfile target=runner (lean, 1.6GB) для backend+beat. target=runner-with-chromium (2.9GB) для worker.

docker-compose.yml выбирает target per service.

.github/workflows/deploy.yml пушит 2 GHCR-образа: gendesign-backend + gendesign-worker.


deploy.yml после pull вызывает: docker image prune -af --filter 'until=72h' + docker builder prune -af.

Разовая очистка 27.04: 21→13 GB (-8 GB).

Будущие деплои сами чистят image старше 72ч + build cache.


PRINZIP CRM импорт (data/sql/52_import_prinzip_crm.py): phone → SHA256[:16] + last 4 digits, email → MD5. raw никогда не попадают в БД. В UI только ***1234. Поддержка 152-ФЗ при будущих запросах удаления.


X-Admin-Token заголовок, значение из SCRAPE_ADMIN_TOKEN env. Пустая строка → admin endpoints отвечают 503 'admin disabled'. Неверный → 401 'invalid admin token'.

Активный токен в prod хранится в backend/.env (gitignored). В UI — localStorage после ввода на /admin/scrape, работает для обеих admin-вкладок.


data/playwright_state.json (в git, 7.3 KB).

26 cookies вкл. AUTH_SESSION_ID, KC_AUTH_SESSION_HASH, ServicePipe NSC_*, Yandex tracking.

Обновляется через 'python data/sql/50_scrape_kn_flats.py --regions 66 --bootstrap-only --save-state data/playwright_state.json'.

Worker подхватывает в cold-start — ServicePipe видит returning user без JS-challenge.


#13 Расширить kn-scraper на 442 строящихся Свердл ЖК

Closed 27.04.2026. Инструкции в README «Запуск 442-sweep». Готов к UI/CLI запуску — кода нового не нужно.


Closed 27.04.2026. Pipeline реализован — schema (52_schema_prinzip_crm.sql) + import script (52_import_prinzip_crm.py) + 6 endpoints + funnel UI в /admin/leads. Ждёт файла от user.


#15 API endpoints /api/v1/analytics/object/{id}/...

Closed 27.04.2026. 5 endpoints + /analytics/objects/[id] page + PrinzipObjectsTable с sparkline.


Closed 27.04.2026. lean backend (1.6GB) + worker-with-chromium (2.9GB).


Body: {district_name, area_total_m2?, target_class?, months_window=12}

Response: {scope, buckets[5], summary, comparables[≤5]}

Pydantic: app/schemas/recommend.py · response_model=RecommendMixOutput


backend/app/services/analytics_queries.py :: recommend_mix(db, *, district_name, area_total_m2, target_class, months_window, region_code=66)

Возвращает dict (scope/buckets/summary/comparables). Через response_model FastAPI приводит к Pydantic.

Lookup района через ekb_districts ILIKE; исключает 'не определён'.

Класс multiplier через AVG price_from FROM yandex_realty_zk FILTER (WHERE obj_class=:cls).

Comparable ЖК: domrf_kn_objects WHERE addr ILIKE '%район%' AND obj_class match → LEFT JOIN domrf_kn_sales_agg для sold%.


RecommendMixInput (district_name str, area_total_m2 float|None ge=100 le=500000, target_class Literal[Comfort/Comfort+/Business/Elite]|None, months_window int 3-36)

RecommendBucket (bucket, share_pct, deal_count, area_avg_m2, area_median_m2, price_p25/median/p75, units_planned, revenue_planned_rub)

RecommendComparable (obj_id, comm_name, dev_name, obj_class, flat_count, sold_perc)

RecommendMixOutput (scope dict, buckets, summary dict, comparables)


backend/app/services/analytics_queries.py :: _bucket_distribution(db, region_code, months_window) + _BUCKET_SQL

Вынесено отдельно чтобы переиспользовать при auto-extend до 24 мес.

Возвращает 5 бакетов: deals, area_avg, area_median, price p25/median/p75 (city-wide).


Публичная (как другие /analytics/*).

Layout: 360px sticky form слева + основной поток справа (KPIs, charts, sliders, table, comparables, caveats).

Все вычисления выручки/weighted-avg клиентские по effectiveShares (после слайдеров).


Селект района из useDistricts() с фильтром 'не определён'.

Площадь (опц. 100..500000), класс (опц. Comfort/Comfort+/Business/Elite), окно 12/18/24 мес.

submit triggers useRecommendMix() mutation; кнопка disabled пока пустой район.


5 слайдеров с auto-rescale: при изменении одного, остальные пропорционально пересчитываются к (100-X)%.

Если у других нет массы — распределяет поровну. Round 1 decimal, drift compensation на изменённом слайдере.

Кнопка ↻ Сбросить к рекомендации (disabled когда не было правок). Бейдж '+X.X' рядом со значением показывает delta от API.


Три ECharts-обёртки в frontend/src/components/analytics/:

RecommendBucketsChart.tsx — horizontal bar share% с двумя сериями (серый 'Рекомендация' / синий 'Ваше распределение'), barGap=-100%.

RecommendRevenueChart.tsx — stacked bar (1 категория) с цветовыми сегментами по бакетам, label 'млн ₽' если segment ≥5%.

RecommendBucketsTable.tsx — sortable таблица с p25/median/p75, units_planned, revenue. Алгоритм allocation клиентский.

Chart-серии reactive: пересчитываются при изменении effectiveShares/areaTotal.


Карточки 3-5 ЖК с comm_name/dev_name/obj_class/flat_count/sold% (цветной бейдж: green ≥50, amber 30-50, red <30).

Каждая карточка — Link на /analytics/objects/{obj_id}.


useMutation (НЕ useQuery — это POST с body, нет кэширования).

POST /api/v1/analytics/recommend/mix · application/json

TypeScript типы в frontend/src/types/analytics.ts (RecommendMixInput/Output/Bucket/Comparable).


Если в любом из 5 бакетов <30 сделок при months_window<24, recommend_mix автоматически расширяет окно до 24 мес для ВСЕХ бакетов (одинаковое окно для согласованности).

Добавляет warning 'Окно расширено до 24 мес' в summary.warnings.

scope.effective_window_months отражает реальное использованное окно (12/18/24).


Class multiplier = AVG(price_from) FILTER (WHERE obj_class=:cls) / AVG(price_from) overall — из yandex_realty_zk.

Если данных по классу нет → 1.0 + warning. target_class=None → 1.0 без warning.

Применяется к price_p25/median/p75 каждого бакета в ответе.


В _BUCKET_SQL добавлены фильтры выбросов:

realestate_type_code='002001003000' (квартиры; 001=земля, 002=нежилые)

area BETWEEN 10 AND 200 (отсечь коммерческие площади 1974 м² в bucket 80+)

Без них bucket 80+ показывал 719 'сделок' с area_avg=1974 — это были склады/коммерция.


Все 8 районов в ekb_districts имеют median_price_per_m2 = NULL (импорт Antona не наполнил).

Текущее поведение recommend_mix: district_factor = 1.0 + warning 'коэффициент района = 1.0'.

Чтобы оживить district_factor — нужен ETL job, наполняющий ekb_districts.median/mean из rosreestr_deals (через парсинг addr → district mapping или geometry).


rosreestr_deals имеет только quarter_cad_number, без района. ekb_districts не имеет PostGIS-полигонов.

Поэтому в MVP recommend_mix bucket-распределение город-wide (регион 66). Район влияет ТОЛЬКО на ценовой коэффициент.

v2: добавить PostGIS-полигоны → spatial-join (точка→район) или (cad_quarter→район) → per-district demand.


size=thumb: WebP 320×240 q=70 из data/raw/domrf_photos/<obj>/<file>_thumb.webp.

Cache-Control: public, max-age=604800, immutable.

size=full: оригинал PNG/JPG из локалки или 302 redirect на DOM.РФ.


backend/app/services/photos/thumbs.py :: make_thumbnail(src, size=(320,240), quality=70)

Pillow + ImageOps.fit (cover) + WebP encode.

thumb_path_for(src) → src.with_name(stem + '_thumb.webp').

Idempotent: skip if dst.exists() and not overwrite. Логирует ошибки и продолжает.

Зависимость: pillow>=10.4.0 в backend/pyproject.toml.


При запросе thumb на фото без локальной копии — endpoint синхронно качает оригинал с DOM.РФ через httpx + Basic auth (1:qwe).

Сохраняет оригинал в data/raw/domrf_photos/<obj>/<file>.{png,jpg}, генерит thumb рядом, UPDATE-ит local_path/thumb_path/downloaded_at, отдаёт thumb.

Подтверждено: urllib + Basic auth работает (200 OK 3.3 МБ PNG) — Playwright-fingerprint не требуется для image CDN, в отличие от JSON API.

Last resort: 302 redirect на upstream если download failed.


data/sql/53_schema_kn_thumbs.sql — применена в прод 27.04.2026 поздний вечер.

ALTER TABLE domrf_kn_photos ADD COLUMN IF NOT EXISTS thumb_path TEXT;

CREATE INDEX idx_kn_photos_obj_visible ON domrf_kn_photos (obj_id) WHERE COALESCE(hidden, FALSE) = FALSE;

object_photos analytics-query возвращает thumb_url + full_url (всегда, ссылаются на наш backend).


GET /api/v1/admin/scrape/queue: 'active' читается из kn_scrape_runs WHERE status='running' (instant).

inspect.reserved() + inspect.ping() запускаются параллельно в ThreadPoolExecutor с дедлайном 0.8s.

queue_depth — Redis LLEN 'celery'. scheduled — пустой (мы не используем ETA).

Worst-case latency ~600ms vs ~8s раньше (4×inspect timeout=2s).


В начале /api/v1/admin/scrape/queue: UPDATE kn_scrape_runs SET status='zombie', finished_at=NOW(), error='auto-marked: no heartbeat 60+ min' WHERE status='running' AND started_at < NOW() - INTERVAL '60 minutes'.

Срабатывает при каждом обращении UI к /queue (poll каждые 3 сек).

Дополнительно: worker_ready hook режет на 5 минут (с heartbeat) — см. Worker_Ready_Hook.


backend/app/workers/tasks/scrape_kn.py :: _region_lock(region_code, devs) context manager.

SET NX EX 7200s по ключу 'scrape:kn:lock:<region>:<devs_csv|*>'.

Если beat double-fire или два worker-а — второй сразу выходит {skipped:True, reason:'lock_held'} без INSERT.

TTL 2ч прикроет случай умершего worker (lock сам очистится).

Используется в scrape_kn_region и resume_kn_run одинаково.


Дата: 27.04.2026 ночь. Решает Bug_Worker_SIGKILL_Lost_Run.

Phase A в run_region_sweep: после fetch_objects+filter — upsert_objects + _insert_raw + UPDATE kn_scrape_runs SET objects_snapshot=:objs::jsonb, total_obj_count, heartbeat_at=NOW(). Один commit.

Phase B/C (per-object): каждые 10 объектов — _checkpoint(db, run_id, i+1) → UPDATE progress_obj_index, heartbeat_at + commit.

При resume_from_run_id: пропускаем Phase A, читаем objects_snapshot+progress_obj_index+params из старого run, INSERT нового run с resumed_from_run_id, продолжаем с start_index.

UPSERT-семантика всех таблиц защищает от дублей при reprocessing 10-объектового хвоста между checkpoint-ами.


data/sql/55_schema_scrape_resume.sql — применена в прод 27.04.2026 ночь.

ALTER TABLE kn_scrape_runs ADD: heartbeat_at TIMESTAMPTZ, progress_obj_index INT, total_obj_count INT, objects_snapshot JSONB, params JSONB, resumed_from_run_id BIGINT REFERENCES kn_scrape_runs(run_id).

INDEX idx_kn_scrape_runs_running_heartbeat ON (heartbeat_at) WHERE status IN ('running','resuming').

Размер objects_snapshot для Свердл sweep ~1-2 МБ JSONB на ряд (1516 объектов).


backend/app/workers/tasks/scrape_kn.py :: resume_kn_run(run_id) Celery task.

Читает region_codes/developer_ids из исходного run, берёт Redis-lock, вызывает run_region_sweep(resume_from_run_id=N).

Не требует jitter — это срочный resume.


backend/app/workers/celery_app.py :: @worker_ready.connect _resume_zombie_runs(sender=None, **_kwargs).

Триггерится после каждого старта Celery worker (включая redeploy).

SELECT runs WHERE status='running' AND objects_snapshot IS NOT NULL AND COALESCE(heartbeat_at, started_at) < NOW() - INTERVAL '5 minutes' LIMIT 20.

UPDATE их в 'zombie' одним апдейтом (audit trail), затем enqueue resume_kn_run.apply_async([rid]) для каждого.

Защищён try/except — провал scan не валит worker.


backend/app/services/scrapers/domrf_kn.py :: log_progress(db, run_id, message, *, level='info', stage=None, obj_id=None).

INSERT INTO kn_scrape_log (run_id, level, stage, obj_id, message). message[:500] чтобы не раздуть.

Best-effort: SQL ошибки swallowed → не валят sweep.

Расставлен в run_region_sweep на: start, fetch_objects (per status), filter_devs, phase_a, fetch_flats (каждые 25), extras (каждые 10), resume, done, failed.


data/sql/54_schema_scrape_log.sql — применена в прод 27.04.2026 ночь.

kn_scrape_log (log_id BIGSERIAL PK, run_id BIGINT REFS kn_scrape_runs ON DELETE CASCADE, ts TIMESTAMPTZ DEFAULT NOW(), level TEXT CHECK in ('info','warn','error'), stage TEXT, obj_id INT, message TEXT NOT NULL).

Индексы: (run_id, ts DESC), (ts DESC). ON DELETE CASCADE — лог чистится с run.


GET /api/v1/admin/scrape/logs?run_id=&since_id=&limit=200 (max 1000).

Auth: X-Admin-Token. since_id — для инкрементального polling (передавай max log_id виденного).


Секция в frontend/src/app/admin/scrape/page.tsx :: 'Прогресс задач'.

Тёмная консоль (background #0f172a, моноширинный) с цветовой раскладкой INFO/WARN/ERROR.

Формат строки: 'HH:MM:SS LEVEL [run-N · stage · obj=X] message'.

useQuery(refetchInterval: 3000) с фильтром по run_id (input).


Симптом: при redeploy/SIGKILL/OOM worker умирал без вызова except/finally → row в kn_scrape_runs оставался status='running' навечно, вся работа sweep терялась.

Closed 27.04.2026 ночь. Resolved by Resume_Checkpoint_Mechanism + Worker_Ready_Hook + Schema_Resume_Columns.

User-reported: 'после авто деплоя умираю и остаются не завершенные зомби'.


Симптом: 27.04 у user'а run_id=4 и run_id=5 стартовали в одну секунду (16:38:01) — beat double-fire или два beat-контейнера.

UI показывал два 'в работе' дубликата одного и того же региона.

Closed 27.04.2026 ночь. Resolved by Redis_Singleton_Lock в scrape_kn_region (SET NX EX). Второй выходит с skipped:True/lock_held без INSERT.


Симптом: GET /admin/scrape/queue ждал до 8s (4 вызова inspect.* с timeout=2s последовательно).

User-reported: 'очередь так же долго получается'.

Closed 27.04.2026 ночь. Resolved by Queue_Optimised_DB (active из БД, inspect в ThreadPoolExecutor с дедлайном 0.8s).


Симптом: bucket 80+ в recommend_mix выдавал 719 'сделок' с area_avg=1974 м² и medianna 155K ₽/м² — это нежилые помещения / склады.

rosreestr_deals.realestate_type_code: 002001003000=квартиры, 002001001000=земля, 002001002000=нежилые.

Closed 27.04.2026 ночь. Resolved by Quartirography_Filter_Fix в _BUCKET_SQL.


Дата: 27.04.2026 поздний вечер. Кеширование thumb-копий фото DOM.РФ на backend.

Архитектура: scraper скачивает оригинал PNG/JPG → Pillow генерит 320×240 WebP q=70 рядом → path в thumb_path колонке.

Endpoint /api/v1/photos/{obj}/{file}?size=thumb|full отдаёт из локалки (FileResponse, Cache-Control 7d).

Lazy fetch при первом запросе thumb на фото без локальной копии (httpx + Basic auth).

Frontend перешёл с Next/Image на прямой <img src={thumb_url}> — backend выдаёт уже оптимизированный WebP, никаких cold-hit оптимизатора в dev.

lightbox использует full_url — выдаёт оригинал или redirect на DOM.РФ.


Дата: 27.04.2026 ночь. Реализован Уровень 1 rule-based рекомендатора квартирографии. База для ML v2.

Алгоритм: city-wide ДДУ-сделки Свердл (rosreestr_deals) бакетизируются по площади (5 бакетов) × дистрикт multiplier (median_price ratio) × класс multiplier (yandex_realty_zk).

Allocation: если задан area_total_m2 — units_planned = round(allocated/area_avg), revenue = units × area_avg × price_median.

User-confirmed дизайн-решения: input=district_name, class через yandex_realty_zk, output=4 секции, slider auto-rescale, fallback 24мес при <30 сделок.

Реальные цифры для Свердл 12 мес (после фильтров): студии 72 сделок/119K₽, 1к 27/105K, 2к 12/105K, 3к 10/154K, 80+ 73/164K.


Дата: переход 27→28.04.2026 ночь. Продолжение Session_End_Apr27_2026.

СДЕЛАНО ЗА СЕССИЮ (ссылки на entity):

1. Photo_Storage_Pipeline (Pillow тхумбы + endpoint /api/v1/photos + lazy fetch + frontend переход с Next/Image).

2. Queue_Optimised_DB + Zombie_Autocleanup + Redis_Singleton_Lock (исправили 8s→0.6s латенси и beat double-fire).

3. Module_Scrape_Log + Schema_Scrape_Log_Table + Endpoint_Scrape_Logs + UI_Scrape_Progress_Panel (живой лог прогресса в админке).

4. Resume_Checkpoint_Mechanism + Schema_Resume_Columns + Task_Resume_Kn_Run + Worker_Ready_Hook (resume после redeploy с checkpoint каждые 10 объектов).

5. Recommend_Level1_Apr27 + Endpoint_Recommend_Mix + Module_Recommend_Query + Schema_Recommend_IO + Module_Bucket_Distribution + Route_Analytics_Recommend + 5 UI-компонентов + useRecommendMix.

BUGS CLOSED: Bug_Worker_SIGKILL_Lost_Run_Fixed, Bug_Beat_Double_Fire_Fixed, Bug_Inspect_Slow_Queue_Fixed, Bug_RealEstate_Outliers_Fixed.

OPEN TODOs (новые): Limit_District_Median_Null — нужен ETL для наполнения ekb_districts.median_price; Limit_District_Geometry — PostGIS-полигоны районов для per-district demand.

ПРОД SCHEMA: 4 новые миграции применены: 53_kn_thumbs.sql, 54_scrape_log.sql, 55_scrape_resume.sql, плюс изменения в domrf_kn_photos (thumb_path) и kn_scrape_runs (heartbeat_at, progress_obj_index, total_obj_count, objects_snapshot, params, resumed_from_run_id).

ПРИОРИТЕТЫ НА СЛЕДУЮЩУЮ СЕССИЮ: 1) ETL ekb_districts.median из rosreestr_deals (оживит district_factor в recommend_mix). 2) PostGIS-полигоны РАЙОНОВ ЕКБ для spatial-join. 3) PRINZIP CRM импорт (ожидает SQLite). 4) Тест resume в проде (запустить sweep × restart worker × проверить '♫ Resume from index N').


Системный баг: SQLAlchemy 2.0 + psycopg3 ИГНОРИРУЕТ привязку в конструкции :name::type. Парсер named-bind видит :name, но ::type не распознаётся как PostgreSQL cast → остаётся литералом в скомпилированном SQL.

Симптом: psycopg.errors.SyntaxError: syntax error at or near ':' на INSERT/UPDATE.

Решение (canonical): всегда использовать CAST(:name AS type) вместо :name::type.

Найден в 5 местах (28.04.2026): исправлены. analytics_queries.py:1222 (recommend_mix comparables :cls::text) · domrf_kn.py:1024 (resume INSERT :rc/devs/params/objs::*) · domrf_kn.py:1060 (new run INSERT :params::jsonb) · domrf_kn.py:1144 (Phase A snapshot UPDATE :objs::jsonb) · admin_leads.py:70 (lead filter :date_to::date).

Эффект бага: 100% блокировка run_region_sweep на INSERT, worker принимал task'и но никогда не создавал run_id.


Использовать CAST(:name AS type) в любых SQL-выражениях SQLAlchemy text() вместо :name::type.

Обратный паттерн ARRAY[:rc]::int[] РАБОТАЕТ — :: приклеено к закрывающей скобке, а не к bind-name.

Правило проверки перед PR: grep -nE ':[a-z_]+::[a-z]' backend/app — должен выводить пустоту.

Это попадало в 4 разных PR'а (recommend_mix, run_region_sweep refactor, admin_leads filter) — выработать lint-правило с ruff или pre-commit на будущее.


Дата: 28.04.2026. Расширение Recommend_Level1_Apr27 — live-калькулятор 'цена ↔ темп ↔ срок ↔ ликвидность'.

По запросу Антона для CEO: 'Изменил цену → изменилась скорость продаж. Изменил срок → система показала какая цена/ассортимент нужны. Изменил ассортимент → изменилась ликвидность, средний чек, темп.'

Архитектура: API выдаёт BASELINE (velocity_baseline + elasticity коэффициент). Все слайдеры и target_months рассчитываются КЛИЕНТСКИ — без round-trip.

Новые компоненты: Component_Recommend_Velocity_Panel + Component_Recommend_Liquidity_Chart. Расширены: BucketsTable (+колонки Темп/Срок), RecommendForm (+target_months input).

Headline для CEO: 'Цена × выручка · срок · чек · темп · ликвидность' одной тёмной плашкой.


backend/app/services/analytics_queries.py :: _velocity_baseline(db, *, region_code, district_name, target_class)

SQL: median+avg из domrf_kn_sale_graph WHERE type='apartments' за 24 мес для ЖК в (район+класс) через obj_pool с addr ILIKE район.

Результат: {realised_per_month_avg, realised_per_month_median, objects_count, observations}.

Если sale_graph пуст — fallback на rosreestr deals/month (city-wide). Сейчас sale_graph покрывает 2 ЖК, поэтому в 99% случаев идёт fallback.


backend/app/services/analytics_queries.py :: _elasticity_coef(db, *, region_code, district_name, target_class)

SQL: regr_slope(LN(realised), LN(price_avg)) + regr_r2 на sale_graph за 36 мес для (район+класс).

Выдаёт {elasticity, r2, n, source: 'regression'|'fallback'}. fallback = -1.5 при n<30 или r2<0.1 или slope>=0.

Используется во фронте для расчёта velocity = base × price_factor^elasticity.


backend/app/services/analytics_queries.py :: внутри recommend_mix (пункт 5c).

Inverse mode: если target_months задан, система решает required_price_factor = (total_units / target_months / base_total_velocity)^(1/elasticity).

Если < 0.7 → warning 'скидка >30% нереалистична, рассмотри сдвиг ассортимента'.

В UI: 'Применить →' кнопка копирует значение в price_factor слайдер (обрезая в [0.85, 1.15]).


Клиентский расчёт в RecommendVelocityPanel.tsx + backend дублирует в recommend_mix пункте 5d.

Формула: liquidity = Σ (min(1, 24/months_to_sellout) × units_planned) / total_units × 100. Диапазон 0..100.

Цветовые пороги: ≥0a7a3a (зелёный) >=70, #9a6700 (жёлтый) 40–⋅0, #b3261e (красный) <40.


4 больших KPI: Средний чек / Срок реализации / Темп продаж / Ликвидность (светофор).

Slider price_factor 0.85..1.15 step=0.01 — live recompute всех KPI по формуле base × pf^elasticity.

Inverse-mode плашка когда target_months задан: показывает required_price_factor + кнопка 'Применить'.

Нижняя подпись 'Эластичность -1.5 (регрессия sale_graph: R²=X, n=N или fallback)' — для CEO-легитимности.


ECharts line+area: кумулятивные % продаж по месяцам 0..36, пунктир-markline на 24 мес.

При изменении priceFactor или mix-слайдеров кривая live пересчитывается.

Empty-state: 'Укажи площадь — посчитаем сколько и когда продастся'.


Тёмная плашка сразу под H1 на /analytics/recommend.

Формат: '💼 «Район · Класс · X м²»: выручка млн ₽ · за ~Y мес · ср. чек Z М ₽ · темп W кв/мес · ликвидность V/100'.

Генеритсь backend'ом в recommend_mix.summary.headline — для screenshots/экспорта. Фронт обёртывает в div с emoji + bold район/класс/площадь.

Известная проблема: int(months) округляет 0.66 → 0 мес. TODO — использовать f'{x:.1f}'.


Симптом: при движении mix-слайдеров KPI Срок/Темп практически не меняются. Из скрина 28.04: 'Темп 159.0 кв/мес' статичен, 'Срок 0.8 мес' округляется в 0 мес в headline.

Причина: все 5 бакетов используют ОДИН market_velocity_per_month. Групповая формула baseVelocity = Σ(per_bucket_v × effective_share/share) ≡ city_total независимо от mix.

Установлен и отложен (Tier α work).

Решение: per-bucket velocity = bucket_market_share_in_total × city_v / N_competitors. Из rosreestr по бакетам отдельно (студии 710/24=30, 1к 1306/24=54, ит.д.) и поделить на N_active_ЖК в районе/классе.


Симптом: 'Срок реализации 0.8 мес' / 'Темп 159 кв/мес' / 'Ликвидность 100/100' — фантастичные числа.

Причина: при fallback (sale_graph пуст) market_velocity_per_month = total_deals / months_window. Для Свердл 3815 сделок / 24 = 159 кв/мес — темп РЫНКА, не проекта. 107 юнитов ÷ 159 = 0.7 мес — 'все сделки рынка идут у нас'.

Решение (Tier α): velocity_per_project = market_velocity / N_active_competing_ЖК (из domrf_kn_objects с site_status='Строящиеся' в (район+класс)). Для Свердл ~442 active → 0.36 кв/мес на ЖК → срок 100-300 мес.

Плюс prominent warning-badge в UI 'Темп грубый, нужен sale_graph для точных данных'.


Симптом: POST /api/v1/analytics/recommend/mix возвращал 500 с 'syntax error at or near :' на строке :cls::text IS NULL.

Причина: SQLAlchemy не парсит :name::type (см. Bug_SQLAlchemy_DoubleColon_Cast).

Closed 28.04.2026. Resolved by Pattern_CAST_AS_Type в analytics_queries.py:1222 (CAST(:cls AS TEXT) IS NULL).


Симптом (28.04.2026 23:29): в /admin/scrape 'Кнопка не работает' — worker принимал task'и (task_received в kn_scrape_log), но НИКОГДА не создавал run_id в kn_scrape_runs.

Диагностика: docker compose logs worker показал трейсбек 'syntax error at or near : LINE 8: :params::jsonb' на INSERT INTO kn_scrape_runs.

Причина: в 4 местах (resume INSERT, new run INSERT, snapshot UPDATE) были :name::jsonb / :name::int[] / :name::text[] — SQLAlchemy bug.

Closed 29.04.2026. Resolved by Pattern_CAST_AS_Type в domrf_kn.py:1024,1060,1144 и бонусом admin_leads.py:70 (:date_to::date).

Smoke: выполнил INSERT руками через SSH-тоннель — run_id=8 создан (и удалён вручную), SQL парсится ОК.


Симптом: при повторном нажатии кнопки task_id возвращаетсь, но run_id не появляется. Никаких логов.

Причина: Redis-lock от зомби-воркера с TTL=2ч блокирует новые task'и · воркер своим retry получает lock_held и выходит без записи в БД.

Closed 28.04.2026 by: TTL_Lock_Reduction (7200→1800), Module_Lock_Diagnostic_Logs (task_received/skipped в kn_scrape_log), Endpoint_Release_Lock + Param_Force_Trigger + UI '🔓 Снять Redis-lock' кнопка.


Body: {region_code, developers}. Auth: X-Admin-Token.

Вручную удаляет Redis ключ 'scrape:kn:lock:<region>:<devs>'. Нужен для emergency-режима (зомби-воркер оставил lock).

Возвращает {released: bool, region_code, developers}.


Новый параметр в TriggerKnRequest: force: bool = False.

При force=True эндпоинт /admin/scrape/kn вызывает force_release_lock() ПЕРЕД apply_async — снимает зомби-lock.

Фронтенд: чекбокс '⚡ Force (снять lock)' в форме запуска (красный при включении).

Ответ содержит lock_was_released: bool — видно был ли lock.


backend/app/workers/tasks/scrape_kn.py :: _log_task_received(region, devs, task_id) и _log_task_skipped(region, devs, task_id, reason).

Пишут в kn_scrape_log с run_id=NULL (run_id не существует до Phase A INSERT). FK accept NULL.

task_received вызывается в первой строке scrape_kn_region и resume_kn_run.

Даёт UI понимание 'worker подхватил и что сделал' даже когда run_region_sweep не запускается.


_LOCK_TTL_SECONDS в backend/app/workers/tasks/scrape_kn.py: 7200 (2ч) → 1800 (30мин).

Обоснование: full Свердл sweep идёт ~25-30 мин — 30 мин это потолок. Зомби-lock истекает за полчаса, не блокирует новые task'и в рабочий день.

Heartbeat-based autoclean в /queue endpoint по всё равно подхватит зомби-runs за 10 мин по heartbeat_at.


Добавлено _checkpoint(db, run_id, 0) в run_region_sweep ПОСЛЕ каждого фетча objStatus (Phase A).

Phase A для 1516 ЖК идёт ~3 мин. Без heartbeat-tick новый 10-мин autoclean threshold мог бы ложно пометить легитимный sweep как zombie.

progress_obj_index ставится 0 (снапшот ещё не сохранён), но heartbeat_at = NOW() обновляется в каждой итерации фетча.


Изменён фильтр zombie autoclean в /api/v1/admin/scrape/queue.

Было: status='running' AND started_at < NOW() - 60 minutes (резал валидные долгие sweep'ы).

Стало: status='running' AND COALESCE(heartbeat_at, started_at) < NOW() - 10 minutes (heartbeat обновляется каждые ~30 сек, 10 мин без него = точно мёртв).

COALESCE подхватывает legacy-runs без heartbeat по старому критерию.


Проверка 28.04.2026: GHA cache (cache-from/cache-to type=gha,scope=*) УЖЕ в .github/workflows/deploy.yml для всех 3 билдов (backend/worker/frontend). Не нужно добавлять.

Сделаны дополнительные исправления для фактического хита кэша:

1. .gitignore +*.tsbuildinfo (frontend/tsconfig.tsbuildinfo был tracked и менялся в каждом коммите → invalidate frontend context).

2. git rm --cached frontend/tsconfig.tsbuildinfo.

3. frontend/.dockerignore расширен: +.git +*.tsbuildinfo +.next/cache +tests +*.md +coverage.

4. backend/.dockerignore расширен: +.git +data/raw +data/raw_endpoints +*.log +*.md +.coverage.

5. backend/Dockerfile +RUN --mount=type=cache,target=/root/.cache/uv на uv sync. +RUN --mount=type=cache,target=/var/cache/apt на apt install (во всех 3 стейджах).

6. frontend/Dockerfile +RUN --mount=type=cache,target=/root/.npm на npm ci.

Ожидаемый эффект: 8 мин → 2-3 мин на типовых Python-правках.


Отложенная работа (Tier α, введён в плане 28.04.2026): переделка velocity-модели recommend_mix.

A. _active_competitors_count(db, district, class) — SELECT COUNT FROM domrf_kn_objects WHERE site_status='Строящиеся' AND addr ILIKE district AND obj_class=cls.

B. Per-bucket velocity = bucket_market_share × city_total_velocity / N_competitors. Отдельный темп для каждого bucket'а.

C. headline format: f'{months:.1f} мес' вместо int().

D. Warning-badge в RecommendVelocityPanel когда velocity_source==fallback.

E. Headline compact: 3 главных метрики вместо 5.

Делается ПОСЛЕ стабилизации скрапера (Bug_Phase_A_INSERT_500_Fixed).


Дата: 28.04.2026 вечер → ночь. Продолжение Session_Resume_Recommend_Apr28_2026.

1. Recommend_Velocity_Calculator_Apr28 (live калькулятор цена↔темп↔срок↔ликвидность) + Module_Velocity_Baseline + Module_Elasticity_Coef + Module_Required_Price_Factor + Module_Liquidity_Score_24mo + Component_Recommend_Velocity_Panel + Component_Recommend_Liquidity_Chart + Feature_Headline_CEO.

2. Bug_Recommend_500_CAST_Fixed (CAST(:cls AS TEXT) вместо :cls::text).

3. Deploy_Cache_State_Apr28 (.gitignore tsbuildinfo, .dockerignore tighten, uv/npm/apt cache mounts).

4. Module_Lock_Diagnostic_Logs (task_received/skipped в kn_scrape_log) + Endpoint_Release_Lock + Param_Force_Trigger + TTL_Lock_Reduction (7200→1800) + Autoclean_Heartbeat_Threshold (60→10 мин) + Phase_A_Heartbeat_Tick.

5. Bug_Phase_A_INSERT_500_Fixed (4 места :name::type → CAST). Главный corner-case сессии — объясняет почему скрапер не работал 26+ часов.

OPEN BUGS ПОСЛЕ СЕССИИ: Bug_Velocity_Mix_Static_OPEN, Bug_Velocity_Unrealistic_OPEN. Оба решает Tier_Alpha_Velocity_Overhaul_TODO.

ПРИОРИТЕТЫ НА СЛЕДУЮЩУЮ: 1) Развернуть Bug_Phase_A_INSERT_500_Fixed в прод, проверить что sweep реально проходит 1516 ЖК + 28K flats. 2) Tier_Alpha (Bug_Velocity_Mix_Static + Bug_Velocity_Unrealistic). 3) ЭТИЛЬАРНЫе тиры 1-3 предикта (mortgage rate impact, region trend multiplier, pipeline pressure, hedonic features, developer reputation).


Дата: 29.04.2026. Главная победа сессии — ЛОКАЛЬНОЕ решение Limit_District_Geometry: железная привязка ЖК ЕКБ к 8 районам через PostGIS-полигоны OSM.

Архитектура: Overpass API (admin_level=9 в bbox ЕКБ) → shapely сборка OSM way-members в MultiPolygon → ekb_districts_geom с GIST-индексом → ST_Contains UPDATE domrf_kn_objects.district_name.

Результат backfill: Академический 586/208 строящихся, Чкаловский 510/158, Верх-Исетский 288/80, Ленинский 284/98, Орджоникидзевский 266/58, Кировский 236/68, Октябрьский 204/50, Железнодорожный 204/39. NULL=454 ЖК вне ЕКБ (Первоуральск, Н.Тагил).

Эффект в recommend_mix: velocity_source переключился с 'rosreestr_fallback' на 'sale_graph' (Академический: 104 ЖК × 1872 точки), competitors_count 208 вместо fallback singleton, comparable ЖК реальные (ТЭН СТАРТ, Эхо леса, KORTROS Eleven).


data/sql/56_fetch_ekb_districts.py — CLI загрузчик Overpass API → PostGIS.

Overpass query: relation['boundary'='administrative']['admin_level'='9']['name'~'район'] в bbox ЕКБ (56.71..57.00 шир. / 60.42..60.95 долг.). Возвращает 8 relations.

shapely-ассемблинг: way-members рваные в OSM, polygonize() сшивает их в closed rings, .buffer(0) исправляет self-intersections.

Кэш в data/raw/ekb_districts.geojson (коммитится в git — повторно Overpass не дыбаем).

Flags: --force-refetch (игнор кэш), --dry-run (без БД).

OSM names → ekb_districts.district_name хардкод-мап из 8 районов ('Академический район' → 'Академический').


data/sql/56_schema_ekb_districts_geom.sql — применена в прод 29.04.2026.

CREATE TABLE ekb_districts_geom (district_name TEXT PK FK→ekb_districts, geom geometry(MultiPolygon, 4326), osm_id BIGINT, fetched_at TIMESTAMPTZ).

GIST index idx_ekb_districts_geom_geom для spatial-join performance.

8 районов залиты из OSM (osm_id 5803327..14040275). Площади ST_Area в диапазоне 5-50 км².


data/sql/57_kn_objects_district.sql — применена в прод 29.04.2026.

ALTER TABLE domrf_kn_objects ADD COLUMN district_name TEXT.

INDEX idx_kn_objects_district WHERE district_name IS NOT NULL.

Initial backfill UPDATE через ST_Contains(d.geom, ST_MakePoint(o.lon, o.lat)) для region_cd=66.

3032 ЖК Свердл: 2578 привязаны к 8 районам, 454 NULL (вне ЕКБ).


data/raw/ekb_districts.geojson — коммитится в git.

FeatureCollection с 8 MultiPolygon-фичами. Properties: name (OSM), osm_id, admin_level=9.

Источник: Overpass API в 29.04.2026 в 7:57 утра. При обновлении OSM-бордеров перефетчить через --force-refetch.


backend/app/services/scrapers/domrf_kn.py :: _backfill_district_names(db, snapshot_date, region_code).

Вызывается в run_region_sweep ПОСЛЕ Phase A commit (когда свежие upsert_objects уже в БД).

UPDATE через ST_Contains только для region_cd=66 (другие регионы полигонов не имеют сейчас).

Best-effort: try/except, не валит sweep при сбое привязки.


Для России в OSM городские районы = admin_level=9. Город как целое = 8.

Прямой query: relation['boundary'='administrative']['admin_level'='9'](bbox) работает лучше чем area-based поиск (last раз area['name'='Екатеринбург'] → 0 элементов).

User-Agent header обязателен — Overpass банит default urllib UA.


Симптом: несмотря на успешный ночной sweep (1516 объектов + 192K квартир + 7938 sale_graph rows), recommend_mix пишет 'недостаточно данных'.

Причина: фильтр addr ILIKE '%Академический%' даёт 0 объектов — все domrf_kn_objects.addr = 'Город Екатеринбург', без названия района.

Closed 29.04.2026 by Geo_District_Mapping_Apr29: в 4 местах в analytics_queries.py заменили 'addr ILIKE :dn' на 'district_name = :dn' (_velocity_baseline, _elasticity_coef, _active_competitors_count, comparable query).


В recommend_mix: target_class_for_geo — если в районе нет ни одного ЖК с НЕ-NULL obj_class — фильтр по классу ОПУСКАЕТСЯ на уровне velocity/elasticity/comparable запросов.

class_multiplier (из yandex_realty_zk) на ценах продолжает работать.

Причина: kn-API возвращает obj_class=NULL для ВСЕХ 3032 ЖК Свердл (отдельный баг скрейпера). Без degrade фильтр obj_class='Comfort' режет всё в НОЛЬ.

Warning пользователю: 'obj_class не заполнен — фильтр по классу игнорируется'.


Симптом: domrf_kn_objects.obj_class=NULL для ВСЕХ 3032 ЖК Свердл. SELECT obj_class, COUNT(*) GROUP BY obj_class даёт только (NULL, 3032).

Причина: kn-API возвращает фильтр по objClass но сам objClass в ответе пустой, или _norm_object не читает правильный ключ.

Временный обход: Module_Graceful_Obj_Class_Degrade игнорирует target_class в geo-запросах.

Реальный фикс: проверить реальные ключи kn-API response через chrome-devtools пробив, поправить _norm_object.


Симптом: domrf_kn_flats.price_per_m2 заполнен только для 288 из 192K квартир (0.15%). Практически все квартиры без цены.

Эффект на backfill ekb_districts.median_price_per_m2: сработало только для 2 районов (Орджоникидзевский, Октябрьский). 6 районов остаются median=NULL.

Причина: kn-API не возвращает price для большинства квартир в sales/portal/table респонсе.

Реальный фикс: разобраться почему price=NULL (пробив chrome-devtools), возможно другой ключ в JSON. Или: брать price из sales_agg вместо per-flat.


Сырой SQL: UPDATE ekb_districts SET median = (PERCENTILE_CONT(0.5) FROM kn_flats JOIN kn_objects ON district_name) WHERE n>=30.

Результат 29.04.2026: Орджоникидзевский=205051 (n=233), Октябрьский=191786 (n=55). Для расчёта district_factor оживило 2 из 8 районов.

При вызове recommend_mix: Орджоникидзевский district_factor=1.03 (выше city median), Октябрьский=0.97 (ниже).

Остальные 6 районов — median NULL, district_factor=1.0 + warning. Блокир Bug_Kn_Flats_Price_Per_M2_Sparse_OPEN.


frontend/src/app/analytics/recommend/page.tsx — реорганизация порядка секций.

Было: Velocity → Liquidity Chart → KPI strip (city-wide) → Buckets → Revenue → Sliders → Table → Comparable.

Стало: Headline → Velocity (с price slider) → SLIDERS MIX → Liquidity → District-KPI → Buckets → Revenue → Table → Comparable.

Причина: user 'хочу видеть как меняются параметры при изменении mix' — все контроли (цена+mix) выше результатов.


Новый KPI-strip в page.tsx вместо старых city-wide дублей ('Район / Сделок / Weighted / Выручка' — все одинаковые между районами).

4 новых KPI: 'Конкуренты в районе' (с hint о scope), 'Sale_graph покрытие' (X ЖК / Y точек / fallback), 'District factor', 'Class multiplier'.

Все 4 меняются при переключении района. Старые city-wide вынесены в headline-плашку и velocity-panel.


Убраны totalRevenue + weightedAvgPrice из page.tsx (были в useMemo но нигде не использовались после рефактора).

Single source of truth: backend.recommend_mix считает эти значения, фронт отображает из summary.headline и RecommendVelocityPanel.


min=0.01 (НЕ 0) чтобы избежать pf^elasticity = ∞ (при elasticity=-1.5 и pf=0).

Pydantic schema: RecommendMixInput.price_factor Field(ge=0.01, le=3.0).

Inverse-mode 'Применить required_pf' тоже клампит на [0.01, 3.0].

Практически для CEO-демо: -50% в цене → velocity × 0.5^-1.5 = ×2.83 (в почти 3 раза быстрее), +100% → ×0.35.


Дата закрытия: 29.04.2026.

Изначально: rosreestr_deals имеет только quarter_cad_number, без района. ekb_districts не имела PostGIS-полигонов.

Решение: Geo_District_Mapping_Apr29 (Overpass API → PostGIS → spatial-join через lat/lon ЖК). Обходим отсутствие cad_quarter→район маппинга через kn_objects как мост (lat/lon → district).

Per-district demand работает в recommend_mix: 4 запроса используют district_name = :dn.


Частично решен Limit_District_Median_Null (29.04.2026): 2 из 8 районов имеют median (Орджоникидзевский 205051, Октябрьский 191786) из kn_flats.

Остальные 6 = NULL: kn-API возвращает price для всего 0.15% квартир (Bug_Kn_Flats_Price_Per_M2_Sparse_OPEN).

Блокирующий fix: разобраться почему kn-API не отдаёт price (возможно другой ключ в JSON, или взять из sales_agg.price_avg).

Альтернативный путь (отложен): rosreestr_deals → cad_quarter → район через NSPD кадастровые полигоны.


Дата: 29.04.2026. Продолжение Session_Apr28_Calc_Scraper_Apr28_2026.

ГЛАВНОЕ: Geo_District_Mapping_Apr29 — PostGIS-полигоны 8 районов ЕКБ из OSM Overpass, backfill domrf_kn_objects.district_name, recommend_mix переведён на district_name = :dn (вместо addr ILIKE).

1. Module_Fetch_Ekb_Districts (Overpass loader) + File_Ekb_Districts_Geojson_Cache + Schema_Ekb_Districts_Geom + Schema_Kn_Objects_District_Column + Module_Backfill_District_Names_Scraper.

2. Pattern_OSM_Overpass_Admin_Level (admin_level=9 для районов РФ).

3. Bug_Recommend_Addr_ILIKE_Returns_Zero_Fixed (4 места).

4. Module_Graceful_Obj_Class_Degrade (kn-API obj_class=NULL не валит фильтры).

5. Backfill_District_Median_From_Kn_Flats (2/8 районов).

6. Component_Recommend_Page_Reorder_Apr29 + Component_Recommend_District_KPI_Strip + Cleanup_Unused_Vars_Recommend.

7. Pricing_Slider_Range_Expanded_Apr29 (±15% → −99..+200%).

CLOSED: Limit_District_Geometry_Closed, Bug_Recommend_Addr_ILIKE_Returns_Zero_Fixed, Limit_District_Median_Partial.

ПРОД SCHEMA: 2 новые миграции применены: 56_schema_ekb_districts_geom.sql + 57_kn_objects_district.sql. ekb_districts.median обновлён для 2 районов.

ПРИОРИТЕТЫ: 1) Разобраться с obj_class/price из kn-API (оба ОПЕН бага). 2) NSPD GeoJSON cad-кварталов для rosreestr-маппинга. 3) Этильбарные тиры 1-3 предикта.


Дата: 29.04.2026. Расширение recommend_mix тремя district-специфичными факторами + макро-контекст для CEO.

Факторы: sat_factor (зрелость рынка), trend_factor (ускорение/выхлаждение 6мо), poi_factor (развитость района).

Результат (Comfort, 50K м²): Орджоникидзевский 31 мес vs Академический 58 мес — разнос 1.9x при одинаковых вводных параметрах.

Macro context: ставка ИЖК 7.84% (Свердл, Янв 2026) в footer headline для CEO-легитимности.


backend/app/services/analytics_queries.py :: _district_market_saturation(db, *, district_name).

Median sold% активных строящихся ЖК в районе из domrf_kn_sales_agg.perc.

Применение: sat_factor = 1 + (sold_median - 50) / 100 * 0.3 — ±15% при крайних (>50% зрелый → +bonus, <20% свежий → -penalty).

Требует ≥5 ЖК в районе с perc != NULL.

Данные (29.04.2026): Ленинский 48% (зрелый), Академический 38%, Кировский 29.5% (свежий). Разнос ~20пп.


backend/app/services/analytics_queries.py :: _district_velocity_trend(db, *, district_name).

Ratio recent_6mo / prior_6mo realised из domrf_kn_sale_graph (apartments). H1 2025 vs H2 2025+.

Применение: trend_factor = clamp(ratio, 0.7, 2.0) — блокировка экстремумов (Академический raw 6.66 в начале 2025).

Данные: все 8 районов НАРАСТАЮТ (ratio 2.07..6.66) — H1 2025 был холодным. Когда наберём 24+ мес истории — числа станут дифференцированными.

UI: в KPI strip 🚀 (>1.5 рынок ускоряется) / → (стабильный) / ❄ (<0.8 остывает).


backend/app/services/analytics_queries.py :: _district_poi_score + _city_avg_poi_score.

Weighted POI count по РАЙОНУ: SUM(weight) per ЖК в радиусе 1000м, потом AVG по ЖК района.

Веса категорий: Метро 2.0, Транспорт 1.5, Медицина 1.3, Образование 1.2, Спорт 1.0, Продукты 0.8, Развлечения 0.7, Новостройки 0.0 (сами ЖК, не amenity).

Применение: poi_factor = 1 + (district / city_avg - 1) * 0.05 — ±5% влияние на ЦЕНЫ (не на velocity).

Сейчас ratios 0.99..1.02 — все районы ~city avg. Даст больше эффекта когда POI покрытие выравняется.


SQL: title ILIKE 'Средневзвешенная ставка по ипотечным жилищным%' AND '%в рублях, %' AND value BETWEEN 1 AND 30.

Выдаёт 7.84% (Свердл, Янв 2026) — СО ЛЬГОТАМИ (семейная, IT, ДВ-ипотека входят в средневзвешенную).

Рыночная ставка без льгот ~20.5% (РБК, Янв 2026) — в БД НЕТ такой серии. UI footer явно пишет 'со льготами; рыночная ~20%'.


Симптом: scope.mortgage_rate_pct выдавал 5.57% вместо реальных 7.84%. User заметил: 'слишком маленькая'.

Причина: SQL filter title ILIKE '%ипотечн%жилищн%' слишком широкий — матчил 'ДОЛЮ ипотечных жилищных кредитов на ИЖС' (5.57%, это НЕ ставка). ORDER BY period DESC LIMIT 1 случайно брал эту строку.

Closed 29.04.2026 в Module_Current_Mortgage_Rate: title ILIKE 'Средневзвешенная ставка...' + '%в рублях, %' + value BETWEEN 1 AND 30. Теперь выдаёт 7.84%.


Дата: 29.04.2026. Проверено вебом (РБК, cbr.ru) по запросу user 'погугли про ставку'.

Рыночная ставка (без льгот) ПЕРВИЧКА Янв 2026: 20.49%. ВТОРИЧКА: 20.5%.

Средневзвешенная (с учётом льготных программ) Янв 2026: 7.6% по РФ, 7.84% по Свердл.

Ключевая ставка ЦБ ~21% (Ноябрь 2025+).

70% покупателей Comfort/Comfort+ подпадают под льготы (семейная основная — 6%); Business/Elite в большинстве берут рыночную ~20%.

БД cbr_mortgage_series имеет ТОЛЬКО средневзвешенную (с льготами), рыночной раздельно нет. UI footer явно уточняет это.


frontend/src/app/analytics/recommend/page.tsx — обновлён district-KPI strip.

4 KPI: Конкуренты / Sale_graph покрытие / Sold% медиана / Тренд 6 мес / Class × District (combined).

Sold% хинт: 'зрелый рынок' / 'свежий, мало распродано' / 'умеренная зрелость' с числом N ЖК.

Тренд: 🚀 (>1.5) / → (стабильный) / ❄ (<0.8) + ratio.

Class × District: перемноженный mult из 3 (class * district * poi) + breakdown в hint.


frontend/src/app/analytics/recommend/page.tsx — footer в headline-плашке под основной строкой.

Показывает: 📊 Средневзв. ИЖК 7.84% (Янв 2026, со льготами; рыночная ~20%) · POI 130/82 (выше среднего).

Отделён в визуале border-top с opacity 0.1, fontSize 12 · color #94a3b8.


frontend/src/components/analytics/RecommendVelocityPanel.tsx — расширен methodology note.

Теперь показывает все 3 macro factors: 'sat ×0.96 (sold% 38) · trend ×2.00 (raw ×6.66 clamp 0.7..2.0) · POI ×1.02 (на цену)'.

Полная прозрачность для CEO: откуда каждая цифра.


Дата: 29.04.2026. trend_factor во всех 8 районах хлопается в max=2.0.

Причина: H1 2025 был холодным (мало сделок в свежевыпущенных ЖК) и sale_graph покрытие было тонким. recent_6mo / prior_6mo выходит всюду >2.0.

Решение отложено: когда наберём 24+ мес истории sale_graph (в мае/июне 2026), перевести trend window на H2 2025 vs H1 2026 — будут реальные ratios.

Пока эффект trend_factor одинаковый для всех районов (×2.0) — не дифференцирует их.


Дата: 29.04.2026. poi_factor во всех районах в диапазоне 0.99..1.02.

Причина: POI покрытие равномерное по всем районам (1284 ЖК с POI из 1516 покрыты ровно), весовая формула даёт похожие числа.

Решение отложено: повысить sensitivity 0.05 → 0.15-0.20 или изменить формулу на percentile-rank вместо ratio. Или ввести разные weights для Comfort/Business (премиум покупатели больше ценят amenity).

POI категории: Новостройки 65K (игнор), Продукты 55K, Спорт 53K, Образование 48K, Медицина 5K, Развлечения 1.8K.


Дата: 29.04.2026 днём. Продолжение Session_GeoMapping_Apr29_2026.

1. Tier2_District_Macro_Factors_Apr29 — sat_factor + trend_factor + poi_factor + mortgage_rate.

2. 5 helpers: Module_District_Saturation_Factor + Module_District_Velocity_Trend + Module_District_Poi_Score + _city_avg_poi_score + Module_Current_Mortgage_Rate.

3. Component_Tier2_KPI_Strip_Update + Component_Headline_Macro_Footer + Component_Velocity_Panel_Methodology_Tier2.

4. Bug_Mortgage_Rate_5_57_Wrong_Match_Fixed + Macro_Context_Mortgage_Rate_Apr29 (верифицировано веб-поиском).

Smoke (Comfort, 50K м²): 5 районов, сроки 31..58 мес (разнос 1.9x). Орджоникидзевский 31, Кировский 34, Ленинский 39, Чкаловский 51, Академический 58. Чек 2.7-2.8 М ₽. Ликвидность 40/100.

OPEN BUGS: Limit_Trend_Always_Capped_OPEN (trend_factor везде max 2.0 пока sale_graph история тонкая), Limit_POI_Factor_Tiny_OPEN (±1-2% эффект).

OPEN BUG скрейпера (с прошлых сессий): Bug_Kn_API_Obj_Class_Always_Null_OPEN (kn-API отдаёт obj_class=NULL), Bug_Kn_Flats_Price_Per_M2_Sparse_OPEN (kn-API отдаёт price для 0.15% квартир).


Симптом: «Верх-Исетский · 5 000 м²»: за ~3.7 мес, темп 33.6 кв/мес, ликвидность 100/100 — фантастика для одного ЖК.

Корень: bucket_market_velocities в analytics_queries.py (lines 1685–1693) использовал rosreestr_deals/window per bucket по ВСЕМУ region_code=66. Window auto-extend до 24mo → ~159 deals/mo на регион → после /sqrt(80 competitors) × sat × trend = 33.8 кв/мес total. Точно совпадает с UI-цифрой.

footer показывал «Базовый темп 4.2 кв/мес (sale_graph: 40 ЖК / 720 точек)», но это число НЕ использовалось в bucket-расчёте — лишь декоративно.

1) market_vel_pm = sale_graph median per ЖК района (либо rosreestr-fallback / N_competitors → per-ЖК proxy).

2) bucket_market_velocities = market_vel_pm × bucket.share / 100 (аллоцируем per-ЖК baseline на размерные сегменты).

3) Удалена нормировка /sqrt(competitors) — market_vel_pm уже per-ЖК.

4) Removed import math (больше не нужен).

Результат для 5K м² по 8 районам ЕКБ: 7.5..18 мес (вместо 3.7). Между районами реальный разброс 2.4x: Октябрьский 7.5 → Академический 18.

Closed by: 3 правки в backend/app/services/analytics_queries.py:1627–1739.


Симптом: footer показывал 5.57% (старая неправильная), хотя _current_mortgage_rate был исправлен в Bug_Mortgage_Rate_5_57_Wrong_Match_Fixed.

Не код-баг: DB напрямую отдаёт 7.84% (Январь 2026, 'Средневзвешенная ставка по ипотечным жилищным... в рублях, %', value BETWEEN 1 AND 30 — корректная строка).

Backend на проде не передеплоен с фиксом → продолжает кешировать старый ответ.

Action: nginx reload / pm2 restart backend → подхватит новый код, footer покажет 7.84%.

Документировано в плане crispy-swinging-gadget.md секция Approach — B.


NSPD geoportal v2 API: nspd.gov.ru — публичная аналитика недвижимости РФ.

ВАЖНО: WAF блокирует не-РФ IP (HTTP 403 + 'Rule: ...'). Для скрейпа нужен российский egress.

chrome-devtools MCP не помогает обойти — sandboxed Chrome у Anthropic тоже блокируется (тот же IP).

ПРОВЕРЕННО РАБОТАЮЩЕЕ: same-origin fetch со страницы NSPD у пользователя — WAF пропускает, certs OK.

1. GET /api/geoportal/v2/search/geoportal?thematicSearchId={id}&query={text} — основной search.

thematicSearchId: 1=Объекты недвижимости, 2=Кадастровое деление, 4=Адм-тер деление, 5=Зоны и территории, 7=Терр зоны, 15=Комплексы.

query=cad-номер квартала: id=2 → 1 polygon квартала; id=1 → до 20 features (земля+здания+помещения+машино-места+сооружения) внутри квартала.

2. GET /api/aeggis/v4/{layer_id}/wms?REQUEST=GetFeatureInfo&BBOX=...&I=256&J=256&FEATURE_COUNT=10&INFO_FORMAT=application/json — векторные данные слоя.

FEATURE_COUNT > 10 → HTTP 500. Только 10 features per call.

3. POST /api/geoportal/v1/download/objects/csv — кнопка 'Скачать список'. CSV метаданных, не GeoJSON.

4. GET /api/geoportal/v1/layers-theme — список тематик (PKK, ARN, etc.).

5. GET /api/geoportal/v1/layers-theme-tree?themeId={id} — слои в тематике.

- 1 (PKK): 63 слоя — Кадастровые округа/районы/кварталы (36071), Здания (36049), Объекты незавершенного строительства (36329), Кадастровая стоимость (37236).


Дата: 29.04.2026. Цель — связать rosreestr_deals.quarter_cad_number ↔ domrf_kn_objects (lat/lon) через PostGIS spatial-join.

Schema: cad_quarters_geom (cad_number, geom MultiPolygon 4326) + cad_buildings (cad_num, geom geometry, cost_value, year_built, purpose, area, floors, addr).

ALTER domrf_kn_objects ADD cad_quarter, cad_building_num.

Pipeline (через debug-Chrome пользователя):

1. fetch /api/geoportal/v2/search/geoportal?thematicSearchId=2&query={cad} → polygon квартала

2. fetch /api/geoportal/v2/search/geoportal?thematicSearchId=1&query={cad} → 20 зданий + помещения + земля

3. Filter buildings by categoryName='Здания' AND cad_num LIKE '66:41:%' (исключить out-of-EKB).

4. Convert geometry to WKT (EPSG:3857), batch-INSERT в Postgres через mcp__postgres__execute_sql с ST_Transform→4326.

5. После полного scrape: backfill domrf_kn_objects.cad_quarter через ST_Contains.

Estimate: 670 × 2 fetch × 700ms = 15-20 мин в browser + 134 × 5s INSERT = 12 мин = ~30 мин.

ВАЖНО: search id=1 включает здания из других округов (66:15, 66:36, 66:51 — Невьянский/В.Пышма/Красноуральск) с похожими суффиксами. Фильтр 'cad LIKE 66:41:%' обязателен.

Полные файлы артефактов: data/sql/58_schema_cad_quarters.sql, 58_fetch_cad_quarters_nspd.py (ineffective без РФ-IP), 59_backfill_obj_cad_quarter.sql, 60_v_zk_rosreestr_velocity.sql + data/raw/cad_quarter_list.txt.


Дата: 30.04.2026. Завершён полный скрейп NSPD кадастровых данных через debug-Chrome (обход WAF через same-origin XHR + JWT gosuslugi.ru auth).

ИТОГ: 709 cad-кварталов / 10590 зданий в БД. Покрытие свежих ДДУ-сделок Свердл (12mo): 91% (713/784).

Scope: ЕКБ (674 квартала из rosreestr_deals) + 43 пригорода с активными ДДУ за 12mo (Сысерть, Арамиль, В.Пышма, Сухой Лог, Богданович, Реж, Артёмовский).

Schema: cad_quarters_geom (cad_number PK, geom MultiPolygon 4326, raw_props jsonb) + cad_buildings (cad_num PK, geom Geometry, purpose, building_name, address, area, floors, year_built, year_commisioning, cost_value, registration_date, status, ownership_type, cultural_heritage, underground_floors, build_record_*, common_data_status, obj_type, raw_props jsonb).

Pipeline: chrome-devtools MCP browser → fetch NSPD endpoints (thematicSearchId=2 для квартала + =1 для зданий внутри) → save via 61_import_nspd_batch.py через SSH-tunnel localhost:15432.

Время: ~3-4 часа активной работы (с 60-90s drain интервалами + import batches).

Closed limitations: Limit_District_Geometry_Closed, Limit_District_Median_Partial (теперь rosreestr_deals → cad_quarter → ЖК работает).

Files: data/sql/58_schema_cad_quarters.sql, 61_import_nspd_batch.py (v2 with raw_props), data/raw/nspd_drain_*.jsonl + nspd_refetch_*.jsonl (50+ batch files).


Дата: 30.04.2026. Полная карта NSPD API endpoints (из debug-Chrome обхода).

АУТХ: JWT через gosuslugi.ru SSO. JWT exp ~30 мин. Cookie: authAccessToken+authRefreshToken. Без auth — HTTP 403 на API (но статика проходит).

WAF: блокирует не-РФ IP на /api/* endpoints. chrome-devtools MCP sandboxed Chrome тоже блокируется (egress = Anthropic IP). РАБОЧИЙ ОБХОД: same-origin XHR со страницы nspd.gov.ru/map в браузере пользователя.

thematicSearchId: 1=Объекты недвижимости (земля+здания+помещения+маш-места+сооружения), 2=Кадастровое деление (квартал/район/округ), 4=Адм-тер деление, 5=Зоны, 7=Терр. зоны, 15=Комплексы.

query=cad-номер: id=2 → 1 polygon квартала; id=1 → до 90 features (по 20 на категорию) внутри квартала.

2. GET /api/aeggis/v4/{layer_id}/wms?REQUEST=GetFeatureInfo&BBOX=...&I/J=&FEATURE_COUNT=10 — векторные данные слоя по bbox+pixel. FEATURE_COUNT > 10 → HTTP 500.

3. POST /api/geoportal/v1/download/objects/csv — CSV-экспорт выбранных объектов (но не GeoJSON).

4. GET /api/geoportal/v1/layers-theme — список тематик.

5. GET /api/geoportal/v1/layers-theme-tree?themeId={id} — слои в тематике.

Тематики: 1 (PKK, 63 слоя — Кадастровые квартал=36071, Здания=36049, Объекты незавершенного строительства=36329, Кадастровая стоимость=37236), 665 (ARN=Аналитика рынка недвижимости, 12 слоёв — объекты по площади 0-25/25-40/40-60/60-80/80-100/100+ м² WMS+MVT).

Geometry SRID: EPSG:3857 (web mercator). На стороне БД ST_Transform(3857→4326).

ВАЖНО: search id=1 включает здания из других кадастровых округов с похожими суффиксами. Фильтр 'cad_num LIKE "66:41:%"' обязателен.


Дата: 30.04.2026. cad_buildings (10590 строк) — итоговая схема после v2 extension.

Колонки структурированные:

- cad_num TEXT PK — кадастровый номер здания (66:41:0108043:59)

- quarter_cad_num TEXT — родительский квартал (3 первых сегмента)

- geom Geometry(Geometry, 4326) — Point/Polygon/MultiPolygon

- purpose TEXT — Многоквартирный дом / Жилое / Жилой дом / Нежилое / Гараж

- building_name TEXT, readable_address TEXT

- area NUMERIC, floors TEXT (например '10-18')

- year_built INTEGER, year_commisioning INTEGER

- cost_value NUMERIC — кадастровая стоимость ₽

- status TEXT — 'Учтенный' (1679) / 'Ранее учтенный' (8148)

- ownership_type TEXT, cultural_heritage TEXT, underground_floors TEXT

- build_record_area NUMERIC, build_record_type TEXT

- common_data_status TEXT, obj_type TEXT

- raw_props jsonb — ПОЛНЫЙ options-объект NSPD (для будущих запросов)

Распределение purpose: Нежилое 4040 / Многоквартирный дом 3245 / Жилое 1567 / Гараж 559 / Жилой дом 601.

Cross-validation: NSPD кадастровая стоимость per м² ≈ 30-40K ₽/м², рыночная (sale_graph) 130-244K — разница в 3-5×. Premium 50-90% типично, аномалии (-34% Белая Башня) сигнализируют outlier.


Дата: 30.04.2026. Spatial-join: domrf_kn_objects.cad_quarter заполнен через ST_Contains(cad_quarters_geom.geom, lat/lon).

Coverage: 2262/2578 ЖК (88%) ЕКБ привязаны к cad_quarter, 237 уникальных кварталов используются.

По районам: Академический 96%, Октябрьский 93%, Железнодорожный 93%, Ленинский 88%, Чкаловский 87%, Кировский 87%, Орджоникидзевский 80%, Верх-Исет 69%.

ЖК↔здание матч через ST_DWithin(50м): 349 ЖК сматчены с конкретным МКД из cad_buildings.


Дата создания плана: 30.04.2026. План реализации Sprint 1 (нед 1-2 мая).

Tier 1 — критичный путь:

A. NSPD-scraper индустриализация (~2 дня): Playwright + gosuslugi SSO auth → backend/app/services/scrapers/nspd_kn.py + Celery task scrape_nspd_region + nspd_scrape_runs table + crontab(day=20, month='2,5,8,11'). 80% переиспользование domrf_kn.py паттернов.

B. Recommend_mix Tier 3 (~3 дня): per-bucket elasticity вместо скаляра. SQL window-functions по area_bucket + frontend per-bucket curve.

C. NSPD кадастр vs sale_graph cross-check UI (~1 день): premium % колонка + warning badge для аномалий >50%.

D. Issue #4 SENTRY_RELEASE (~30 мин): SSH→.env.runtime + sentry_sdk init.

Итого Sprint 1: ~7 дней работы.


Sprint 2 (нед 3-6): Stage 1 Generative MVP — продуктовое ядро, делает gendesign продаваемым.

Issue #5 Stage 1a (~5 дней): app/services/generative/geometry.py (Shapely парсер GeoJSON, отступы, placement grid), Pydantic ConceptInput/Output, frontend Leaflet drag-drop + 3 таба-варианта (заглушки).

Issue #12 Stage 1b (~5 дней): Greedy алгоритм 3 стратегии (max_area/max_insolation/balanced) с STRtree коллизиями. Цель: 3 варианта ≤15сек на 1 га. Frontend визуализация корпусов + ТЭП метрики.

Issue #6 Stage 1c (~6 дней): services/generative/teap.py + financial.py (revenue/cost/IRR), экспортеры PDF/Excel/DXF, API POST /concepts/{id}/export. Завершение Stage 1.

Итого: ~16 дней. После этого Stage 1 = продаваемый артефакт для встреч с инвесторами.


Sprint 3 (нед 7-10): Stage 2 Site Finder MVP — конец продуктового vision.

Issue #7 Stage 2a (~5 дней): site_finder/parser.py (ПКК Rosreestr, GeoAlchemy2 Parcel модель, ≥1000 участков ЕКБ) + workers/sales_tracker.py (шахматки топ-7 застройщиков: Атомстрой, Брусника, ТЭН, Стройтек, Прогресс, УГМК, КОРТРОС).

Issue #8 Stage 2b (~5 дней): scorer.py (3-4 критерия: distance_to_center, POI_density, district_price + опц. velocity_score), POST /parcels/search ≤500мс, frontend Mapbox color-graded markers + sidebar filters.

Issue #9 Stage 2c (~3 дня): кнопка 'Обсчитать концепцию' на карточке участка → pipe в /concepts. + Реестр ПО подача (60-180 дней). + финмодель 5-летняя для инвест-deck.

Pre-existing data: NSPD cad_quarters (709 кварталов) переиспользуем как один из источников для site_finder. ПКК даст vri+address+другие слои.


#3 Selectel/Yandex S3 backup setup — ❌ ИСКЛЮЧЕНО пользователем (не делаем).

#4 SENTRY_RELEASE deploy.yml tracking — Tier 4 (~30 мин).

#5 Stage 1a Shapely service + API contract — Sprint 2.

#6 Stage 1c TEAP + finmodel + экспорты PDF/Excel/DXF — Sprint 2.

#7 Stage 2a ПКК parser + sales_tracker — Sprint 3.

#8 Stage 2b Scoring + API + UI — Sprint 3.

#9 Stage 2c Integration + реестр ПО + финмодель — Sprint 3.

#11 Антон onboarding к knowledge-graph (Claude Desktop + MCP) — Tier 4 (~30 мин документации).

#12 Stage 1b Greedy алгоритм 3 стратегии STRtree — Sprint 2.


Дата: 30.04.2026. Открытые ограничения после NSPD-pipeline.

1. Limit_Trend_Always_Capped_OPEN — все 8 районов ЕКБ имеют raw trend ratio 5+ (capped к 2.0). Решится через 6-12 мес sale_graph истории. Не действуем сейчас.

2. Limit_POI_Factor_Tiny_OPEN — POI ratio 0.99-1.02 везде (равномерное распределение POI). Можно увеличить вес с 5% до 10% или добавить distance-weighting к метро.

3. Bug_Kn_API_Obj_Class_Always_Null_OPEN — kn-API НЕ отдаёт obj_class для всех 3032 ЖК Свердл. Решение: fallback на yandex_realty_zk.obj_class через name-match (~1 день).

4. Bug_Kn_Flats_Price_Per_M2_Sparse_OPEN — kn-API даёт price для 0.15% квартир. ekb_districts.median для 6 районов NULL. Workaround есть в recommend_mix (sale_graph baseline). Дополнительно: можно взять median из cad_buildings.cost_value (но это кадастр, не рыночная).

5. ✅ CLOSED: Limit_District_Geometry_Closed (через OSM ekb_districts), Limit_District_Median_Partial (через cad_quarter mapping).


Дата: 30.04.2026. Пользователь явно исключил из roadmap.

❌ Issue #3 Selectel/Yandex S3 backup — пропускаем.

❌ Любое перемещение infra в Yandex Cloud / Selectel — пропускаем.

❌ pg_dump → cloud — пропускаем.

Причина не указана. Если потребуется ДР-стратегия — спросить отдельно.


Дата: 30.04.2026. Длинная сессия (~5+ часов): NSPD scrape end-to-end + planning.

1. NSPD_Scrape_Complete_Apr30 — 709 кварталов / 10590 зданий, 91% покрытие свежих ДДУ.

2. Cad_Buildings_Schema_v2 — расширили схему raw_props jsonb + 8 новых колонок.

3. Cad_Quarter_Backfill_Apr30 — spatial-join 2262/2578 ЖК (88%).

4. Cross-check: NSPD кадастр vs ДОМ.РФ рынок — premium 50-90% типично.

5. Sprint1_Plan_May2026 + Sprint2_Stage1_Generative + Sprint3_Stage2_SiteFinder — план на 10-12 недель до Phase 0 финиша.

OPEN на следующую сессию: Sprint 1 task A (NSPD scraper индустриализация Playwright+Celery).

Артефакты: ~50 JSONL drain-файлов в data/raw/, обновлённый 61_import_nspd_batch.py, план в C:\Users\user\.claude\plans\next-iteration-roadmap.md.

ИНСАЙТ: rosreestr_deals.realestate_type='002001003000' даёт 91% покрытие за 12mo. Sale_graph ДОМ.РФ остаётся золотым стандартом для velocity (rosreestr недополнен — для топ-ЖК показывает 0.1 кв/мес против реальных 16-45 из sale_graph).

Pipeline через debug-Chrome работает но мануальный — индустриализация в Sprint 1.


Дата: 07.05.2026. Интеграция с api.objctv.ru (Объектив) — внешний B2B аналитический API с per-flat данными по новостройкам РФ.

Цель: получить data feed богаче чем ДОМ.РФ kn-API: per-flat статус/цена/банк/дата регистрации + per (месяц × корпус × room-bucket) агрегаты для recommend_mix.

Архитектура: settings.objective_api_key → ObjectiveClient (httpx + Bearer-токен в Redis с in-memory fallback) → 4 канонические отчёты → objective_raw_reports (jsonb) → 70_parse_objective_raw.py → objective_corpus_room_month + objective_lots + objective_lots_history.

Celery-task tasks.scrape_objective.sync_objective_group: еженедельно пн 05:00 МСК (settings.objective_sync_cron='0 5 * * mon'). Тянет 2 рабочих отчёта (Сводные/Корпуса + Поквартирные/Лоты), сохраняет raw, сразу же парсит через late-import парсера 70.

Smoke-test 07.05 подтвердил: Парк Культуры (1 ЖК × 7 дней) → corp_sum 8 строк (13.5 KB) + lots_pf 563 квартир (818.8 KB). Парсер dry-run обработал все строки без warning.

ОТЛИЧИЕ ОТ ДОМ.РФ kn-API: точные per-flat поля — Статус, Цена за м2, Дата договора, Дата регистрации, Тип сделки (ДДУ/ДКП), Банк (ипотека!), Тип обременения. Это ×100 точнее domrf_kn_flats и даёт банковские предпочтения покупателей по проектам.

Открывает: velocity-anomaly detection (issue #17), elasticity per-flat-history (Tier 4 точнее regression × лет), competitor-360 (issue #19), mortgage-rate sensitivity (issue #20).

ОГРАНИЧЕНИЯ ТАРИФА: 2 из 4 канонических отчётов возвращают HTTP 500 'An unexpected error occurred' — Сводные/Лоты и Поквартирные/Корпуса. Не блокер: оба нужных получаем.

СТАТУС: код готов и протестирован локально (dry-run). Не применено: schema 68 на проде (mcp postgres был на svc_mosgas — нужно вручную через SSH-tunnel localhost:15432). OBJECTIVE_API_KEY в backend/.env прода не прописан.

Файлы: backend/app/services/scrapers/objective.py (ObjectiveClient) · backend/app/workers/tasks/scrape_objective.py (Celery task с inline-парсингом) · backend/app/workers/celery_app.py (beat schedule) · data/sql/68_schema_objective.sql v2 · data/sql/69_objective_smoke.py · data/sql/69b_objective_minimal.py · data/sql/70_parse_objective_raw.py.


backend/app/services/scrapers/objective.py — HTTP-клиент к api.objctv.ru.

Auth: GET /Users/User/GetToken?apiKey=<KEY> → {token: '...'} (~30 мин TTL). Bearer на /v2/Report/GetReport.

Кеширование токена: Redis (ключ 'objective:bearer_token', TTL settings.objective_token_ttl_seconds=25*60) + in-memory fallback _TOKEN_MEMORY_CACHE (когда Redis недоступен — локальные скрипты, сетевой сбой). Лог про Redis печатается ОДИН раз на инстанс (флаг _redis_warned), не спамит.

Rate-limiting: _DEFAULT_RATE_MS=500мс между запросами. Ретраи: 3 попытки на 401/429/502/503/504. Backoff на 429: 30, 60, 120, 240с (потолок 300с). Слушает Retry-After header если присутствует.

401 → один раз форс-рефреш токена; если повтор — ObjectiveAuthError. 429/5xx → exponential backoff. Брotli accept по дефолту в Accept-Encoding (их рекомендация).

Публичные методы: report_corpuses_summary, report_lots_summary, report_corpuses_per_flat, report_lots_per_flat. Универсальный get_report() с явными именованными параметрами (report_section, report_type, report_name, group_name, complex_name, start_date, end_date, use_ddu, use_dkp, page='Отчеты', v2=True).

Date format Объектива: YYYY.MM.DD (точка-разделитель!) — _fmt_date конвертит из date/str.

fetch_with_raw_log() — one-shot helper для smoke-сценариев и backfill: вызывает <fn_name>(**kwargs), сохраняет raw JSON в файл если save_dir.


data/sql/70_parse_objective_raw.py — парсер raw → normalized слой Объектива.

Режимы: --file-corp-sum/--file-lots-pf (локальные JSON, smoke без БД, --dry-run) | --raw-id N (один объект из objective_raw_reports) | --latest-run (все unparsed raw из последнего objective_scrape_runs.run_id).

Эвристика detect_kind() по полям первого row payload: 'corp_sum' если ['Месяц','Корпус','Объем предложения, шт.'] · 'lots_pf' если ['Id','Номер лота','Площадь, м2(ПД)']. None → skip с warning.

Helpers: _parse_ru_month('апрель-2026' → date(2026,4,1)) · _parse_iso_date · _to_int/_to_num · _normalize_rooms('студия'→0, '1'→1, …'5+'→5) · _parse_pct('17%'→17) · _parse_bool_yes('да'/'нет' → True/False).

parse_corp_sum: маппит ~30 полей → INSERT в objective_corpus_room_month с ON CONFLICT (report_month, group_name, project_name, corpus_name, room_bucket) DO UPDATE (right move — отчёт за месяц уточняется неделя-к-неделе пока месяц не закрыт).

parse_lots_pf: маппит per-flat → INSERT в objective_lots с ON CONFLICT (objective_lot_id) DO UPDATE + INSERT в objective_lots_history с ON CONFLICT (objective_lot_id, snapshot_date) DO NOTHING (один снимок per день — повторный sync в тот же день не создаёт дубль).

Late-import используется в Celery-task: importlib.util.spec_from_file_location → exec_module. Это позволяет inline-парсинг сразу после save_raw без копирования кода в backend.

Smoke 07.05 на локальных файлах: corp_sum 8 rows + lots_pf 563 lots / 563 history rows — все отработали без warning'ов про обязательные поля.


data/sql/68_schema_objective.sql v2 (07.05.2026) — ВЫРОВНЕНА по реальным полям payload-ов вместо изначальных гипотез.

Таблицы (6): objective_scrape_runs (журнал) · objective_raw_reports (jsonb payload) · objective_corpus_room_month (long-формат, заменяет старую objective_oks) · objective_lots (UPSERT по objective_lot_id) · objective_lots_history (append-only weekly snapshots) · objective_complex_mapping (Objective.ComplexName ↔ domrf_kn_objects.obj_id с is_reviewed).

objective_corpus_room_month: UNIQUE (report_month, group_name, project_name, corpus_name, room_bucket). Колонки: capacity по ПД (lots_pd_count/area), сделки ДДУ/ДКП/всего (по 7 метрик каждое), offer (count/area/sum_mln_rub/avg_price_thousand_rub_per_m2). Ключ инсайт: одна строка = (месяц × корпус × room_bucket) — long-формат, не per-corpus как было задумано в v1.

objective_lots: UNIQUE (objective_lot_id). Поля per-flat: 3 источника комнат (rooms_dev_site/rooms_pd/rooms_objective + нормализованный rooms_int), 2 площади (dev_site/pd), полный pricing (calculated_total/per_m2/offer_total/delta_rub/delta_pct + методы и даты), полный deal (contract/registration_date/contract_type/buyer_type/registration_number), encumbrance/mortgage (encumbrance_type/bank_name/encumbrance_start_date/egrn_actual_date).

objective_lots_history: UNIQUE (objective_lot_id, snapshot_date) — сами строим weekly timeline через INSERT при каждом sync (Объектив не отдаёт per-day price/status timeline в одном вызове, поэтому накапливаем сами). Для elasticity (точность × лет) и velocity-anomaly detection.

objective_complex_mapping: связь Objective.ComplexName/objective_project_id ↔ domrf_kn_objects.obj_id. is_reviewed=TRUE — человек подтвердил соответствие.

Размеры (оценка для Свердл.обл): corpus_room_month ~5K rows/мес (~250 ЖК × 5 корпусов × 5 mix × 1 мес), lots ~50K rows (~250 ЖК × 200 квартир), lots_history +50K rows/нед.

Применять: psql через SSH-tunnel localhost:15432 (mcp postgres был воткнут не в ту БД 27.04 → ручной apply). Идемпотентно через IF NOT EXISTS.


Дата: 07.05.2026. Эмпирические находки об api.objctv.ru через локальные пробы (data/sql/69b_objective_minimal.py).

ENDPOINTS: GetToken на /Users/User/GetToken?apiKey=<KEY> → JSON {token: '...'}. GetReport на /v2/Report/GetReport?Page=Отчеты&ReportSection=Объединенные данные&ReportType=...&ReportName=...&GroupName=...&[ComplexName]&[StartDate]&[EndDate]&[UseDdu]&[UseDkp]. Auth: Authorization: Bearer <token>. Date format: YYYY.MM.DD (точка-разделитель).

ФОРМАТ ИМЕНИ ЖК: ComplexName требует БЕЗ префикса 'ЖК' и БЕЗ кавычек. Пример: 'Парк Культуры' → 200 OK, 'ЖК "Парк Культуры"' → HTTP 500. Проверено 07.05 на 2 запросах подряд.

PAYLOAD ОБЁРНУТ В {result: [...]}, не голый список — все ответы такие.

ДОСТУПНЫЕ НА ТАРИФЕ ОТЧЁТЫ (2 из 4): ✅ Сводные/Корпуса (per месяц × корпус × room_bucket) · ✅ Поквартирные/Лоты (per-flat snapshot с Id-stable). HTTP 500 'An unexpected error occurred' на: Сводные/Лоты, Поквартирные/Корпуса. Гипотезы: тариф / косяк бэка / неправильные параметры (попробовать без ComplexName, попробовать с/без StartDate).

RATE-LIMIT: жёсткий — 500мс между запросами недостаточно, нужен ~3000мс + пауза 15с между отчётами (default в скрипте 69b после фиксов). Иначе HTTP 429 на 2-м запросе. Backoff на 429 респектит Retry-After header.

ОТКЛИК: первый запрос отчёта может занимать 18-52 секунды (отчёт реально считается на бэке Объектива). Возможно у них лимит 1 concurrent request — с маленьким inter-delay прилетает 429.

REDIS LOCAL: на локальной машине Redis недоступен (редис только в docker compose) → in-memory fallback токена в ObjectiveClient критичен.

ПОЛЯ ИЗ corp_sum (Парк Культуры): Месяц (текст 'апрель-2026'), Проект, Девелопер ('PRINZIP'), Район ('Автовокзал'), Класс ('Комфорт'), Корпус ('1.1'), 'Количество комнат (Данные Объектива)' (студия|1|2|3), 'Лотов по ПД, шт.', 'Площадь по ПД, м2.', 7 метрик ДДУ × 7 метрик ДКП × 7 метрик 'всего' (count, count_priced, vol, vol_priced, sum_priced_млн_Р, avg_price_тыс_Р/м2, avg_area_м2), Объем предложения шт/м2, Суммарная стоимость лотов в продаже млн.Р, Средняя цена м2 лота в продаже тыс.Р/м2. NB: цены в ТЫС.Р/м2, не Р/м2; стоимости в МЛН.Р.

ПОЛЯ ИЗ lots_pf (per-flat, 563 квартир в Парк Культуры): Id (stable Objective lot_id, 1965382), 'Id проекта' (3802), Проект/Девелопер/Город/Район/Корпус/Адрес/Класс/Секция/Этаж/Номер лота/'Вид помещения' (квартира|нежилое|паркинг|кладовая), Готовность ('17%'), 'Стадия строительства' (строящийся), 'Отделка по корпусу' (под чистовую), Статус (резерв|свободна|продан|бронь), Продано (да/нет), 3 источника комнат, 2 площади (Сайт девелопера/ПД), полный pricing block (Расчетный бюджет, Цена за м2, Цена предложения, Дельта Р/%, Способ определения цены, Дата установки/актуальности цены), полный deal block (Дата договора, Дата регистрации, Тип сделки ДДУ|ДКП, Тип покупателя, Номер регистрации), encumbrance (Тип обременения, Банк, Дата начала обременения, Дата актуальности данных из ЕГРН).

ИНСАЙТ ДЛЯ РЕКОММЕНДА: Сводные/Корпуса УЖЕ даёт нам монетизированную monthly history per (corpus, room_bucket) — не надо строить из sale_graph + median area. Это подменяет половину recommend_mix без всяких регрессий.

ИНСАЙТ ДЛЯ ИПОТЕКИ: lots_pf.Банк = stable банковская атрибуция per flat. За 6 мес weekly sync получим ground-truth статистику ипотечных предпочтений по проектам/девелоперам/районам Свердл.обл — нет в открытых источниках.


Дата: 07.05.2026. Сессия после возвращения юзера ('я вернулся давай вспомним на чем остановились'). Continued работу над Objective integration после context compaction.

СДЕЛАНО ЗА СЕССИЮ:

1. Объяснил юзеру где остановились: scaffold Objective integration был готов (settings, ObjectiveClient, schema 68 v1, Celery task, smoke-script), но не применён и не протестирован.

2. Создал data/sql/69b_objective_minimal.py — минимальный probe (4 отчёта × 1 ЖК × 7 дней) с CLI-флагами (--complex, --no-complex, --group, --days, --only).

3. Юзер запустил 1-й раз: получили формат имени ЖК ('Парк Культуры' без префикса/кавычек) + 429 на 2-м отчёте (Redis недоступен локально → новый GetToken на каждый запрос → rate-limit).

4. Фикс in-memory fallback токена в ObjectiveClient + дефолт --complex без префикса + datetime.utcnow→timezone.aware. Запуск 2 — снова 429 (rate-limit после первого отчёта).

5. Доб --inter-delay 15s между отчётами + --rate-ms 3000ms + Retry-After header + backoff 30/60/120 в клиенте. Запуск 3 — получили 2 рабочих отчёта (corp_sum 8 строк, lots_pf 563 квартиры). 2 других вернули HTTP 500.

6. Прочитал оба payload-а — обнаружил расхождения с schema 68 v1: corp_sum в long-формате (не per-corpus), lots_pf имеет stable Id + богатые поля (банк! 3 источника комнат! полный deal/encumbrance block).

7. ПЕРЕПИСАЛ 68_schema_objective.sql v2 под реальную структуру: objective_oks → objective_corpus_room_month, расширенная objective_lots с UNIQUE objective_lot_id, append-only objective_lots_history с UNIQUE (lot_id, snapshot_date).

8. Написал data/sql/70_parse_objective_raw.py — полный парсер с эвристикой detect_kind, helpers _parse_ru_month/_normalize_rooms/etc., 3 режима (--file/--raw-id/--latest-run). Smoke на локальных файлах: 8 corp + 563 lots без warning.

9. Обновил Celery-task scrape_objective: убрал 2 нерабочих отчёта (HTTP 500 на тарифе), добавил late-import парсера 70 для inline-нормализации сразу после save_raw, обновил _heartbeat/_finish_run новыми счётчиками (rows_corpus_room/rows_lots/rows_history).

OPEN на следующую сессию: применить schema 68 v2 на проде (вручную через SSH-tunnel localhost:15432, mcp postgres до сих пор воткнут не в gendesign) · прописать OBJECTIVE_API_KEY в backend/.env прода · реальный прогон Celery-task (sync_objective_group) на проде с группой 'Екатеринбург' (без ComplexName-фильтра, по всему городу) · обновить knowledge graph (этот entity).

ПРИОРИТЕТЫ ПОСЛЕ APPLY: 1) Прогон без ComplexName на ЕКБ — сколько rows? Объём lots_pf может быть огромным (~50K квартир × ~2KB). 2) Исследовать почему Сводные/Лоты + Поквартирные/Корпуса возвращают 500 (попробовать варианты params без ComplexName / с/без StartDate). 3) Спросить саппорт Объектива.

ИНСТРУМЕНТАРИЙ ПРОВЕРКИ: cd backend; OBJECTIVE_API_KEY=<key> uv run python -X utf8 ../data/sql/69b_objective_minimal.py [--no-complex --days 3 | --only corp_sum]. Сохраняет в data/raw/objective_test/<timestamp>/.

ФИНАЛЬНЫЕ файлы изменены 07.05: backend/app/services/scrapers/objective.py · backend/app/workers/tasks/scrape_objective.py · data/sql/68_schema_objective.sql · data/sql/69b_objective_minimal.py (новый) · data/sql/70_parse_objective_raw.py (новый).


Дата импорта в репо: 8 мая 2026 (вайб-код вёлся с начала мая, коммит 97b19a0 на main)

Название: GenDesign Parcel Scoring v2 (из server.py title), локальный коднейм Site Finder. Путь в репо: site-finder/

Бэкенд: однофайловый FastAPI server.py (~85 KB), 35 эндпоинтов. Основные: GET /api/sites, /api/site/{id}, /api/districts, /api/district-polygons, /api/macro, /api/audiences, /api/district-velocity-trend/{district}, POST /api/analyze. Ключевая оптимизация: в ОП Екб используется кэш 9479 OSM POI вместо Overpass-запроса (~30с -> <100мс)

Данные: SQLite analysis.db = 340 МБ (не в git, регенерируется пайплайном). Объёмы: 380 ЖК, 316 тыс site-POI пар, 303 тыс квартир с Объектива. Схема в db_init.py

Пайплайн сборки данных: 12 скриптов 01_load_sites.py -> 02_fetch_pois.py -> 03_score.py -> 04_report.py -> 05_objective_pull.py -> 06_match_economics.py -> 07_objective_full_pull.py -> 08_enrich_economics.py -> 09_macro_and_trend.py -> 10_score_v2.py -> 11_refetch_pois_extended.py -> 12_more_pois.py

Фронт: статика в static/. index.html около 3500 строк (Leaflet UI: карта + 11 табов + 4 модалки), sw.js — Service Worker для кэша тайлов карты. Не Next.js — ванильный HTML/JS

Кэши в репо: cache/ekb_admin_districts.geojson (148K, адм-районы Екб), cache/jk_polygons.geojson (592K, полигоны ЖК), cache/parcel_polygons/ (drop-zone для ручно выгруженных с nspd.gov.ru полигонов, README внутри), cache/geocode_cache.json (тёплый кэш геокодинга). НЕ в git: cache/objective_raw (1.2 GB), cache/osm_buildings_all.geojson, cache/overpass_raw.json

Production: сайт https://gendsgn.ru/sf/ (лендинг), https://gendsgn.ru/sf/app.html (приложение). Сервер: /opt/gendesign/site-finder/, Docker-контейнер gendesign-site-finder-1

Production memory path: /opt/gendesign/memory/memory-gendesign.jsonl (отличается от локального ~/.aim/ на macOS и C:\mcp\.aim\ на Windows). Для полной синхронизации нужен такой же watcher на проде (сейчас реализован только для anton@macOS)

Контраст с MVP_Scope_AreaSelect_Apr25: этот билд — фактический Site Finder в проде (скоринг участков по POI/экономике/макро), но решение от 25 апреля откладывало Site Finder в Пост-MVP. Нужно сверить: (а) Site Finder всё-таки остаётся в MVP в виде этого прода, (б) он эксплораторский прототип параллельно основному MVP, (в) решение от 25 апр отменено фактически