Commit graph

2323 commits

Author SHA1 Message Date
lekss361
a4e8fff5ac feat(site-finder): POST /parcels/{cad}/analyze endpoint + UI
Backend:
- analyze endpoint: cad → geom lookup → POI within 1km → district
  context → competitors within 3km. Tram_stop carries negative weight.

Frontend /site-finder:
- CadInput regex-validated (default 66:41:0204016:10)
- SiteMap (Leaflet via next/dynamic) with parcel polygon + legend
- ScoreCard color-coded (>5 green, 2-5 yellow, <2 red) with tram-warning
- CompetitorTable top-20 with same-district highlight
- useSiteAnalysis hook + TS types
2026-05-11 18:11:58 +03:00
lekss361
6dea34186b feat(site-finder): OSM POI loader + Celery weekly sync
- New poi_loader.py: Overpass QL query for 13 POI categories in EKB bbox,
  UPSERT into osm_poi_ekb with soft 2-year filter tracking (skipped_old counter)
- New poi_sync.py Celery task wrapping sync_poi_to_db()
- celery_app.py: include poi_sync module, add poi-sync-weekly beat entry (Mon 03:00 MSK)
2026-05-11 18:08:34 +03:00
lekss361
688e094844 feat(db): osm_poi_ekb table for site-finder POI analysis 2026-05-11 18:06:28 +03:00
lekss361
ceaf553eb7 fix(analytics): recommend page UI — caveat badge wrap + headline baseline label
Two UX nits:
- Caveats badge had whiteSpace:nowrap on a 250-char data_caveat → overflowed
  visibly outside the section in the screenshot. Drop nowrap, keep small
  font + light grey background. Reads as a compact note.
- Headline KPIs (369 млн ₽ · 19.4 мес · темп 6.0 · ликв 97/100) are
  computed by backend at price_factor=1.0, while live KPI cards below
  recompute with the current slider position. When user moves price to
  +40% the discrepancy is confusing. Append small grey '(базовая цена)'
  marker after headline with a tooltip explaining baseline vs live.
2026-05-11 17:53:35 +03:00
lekss361
0de32cd2ce fix(analytics): dedupe comparables — domrf_kn_objects has 3x snapshots per obj_id
The comparables block on /analytics/recommend was showing the same ЖК
multiple times (ЖК СТАРТ × 3, ЭХО ЛЕСА × 2 etc) because domrf_kn_objects
has ~3 historical snapshots per obj_id. The previous query joined all
rows and LIMIT 5 cherry-picked duplicates by flat_count.

Fix: wrap base in CTE `latest_obj` with `DISTINCT ON (obj_id) ORDER BY
obj_id, snapshot_date DESC` to pick only the latest snapshot per object
before sorting by flat_count.
2026-05-11 17:52:37 +03:00
lekss361
9658baa6b2 fix(deploy): keep last 2 SHA-tagged images per repo, drop older
Previously `docker image prune -af --filter until=72h` only removed
images older than 72h. With multiple deploys/day we accumulate many
fresh (<72h) tagged images — each gendesign-backend / worker / frontend
SHA-tag is 500MB-1GB, filling /var/lib/docker.

New logic: for each ghcr.io/lekss361/gendesign-* repo, list tags sorted
by Created DESC, skip :latest, keep top 2, remove the rest. Still runs
the standard prune after for dangling layers + build cache.
2026-05-11 17:45:20 +03:00
lekss361
03e572330b feat(analytics): recommend page shows cad_quarter, buildings count, district stats 2026-05-11 17:37:57 +03:00
lekss361
2847262953 feat(analytics): extend recommend comparables with cad_quarter, lat/lon, buildings_n 2026-05-11 17:30:09 +03:00
lekss361
b18f44e1ad feat(analytics): add cad_quarter_count to districts endpoint 2026-05-11 17:29:40 +03:00
lekss361
ae86d62e9b fix(worker): worker_ready geo-resume blocked by early return in kn-resume
Root cause of "auto-resume never fired": the kn_scrape_runs resume section
hit `if not rows: return` (and similar `return` in except) before reaching
the nspd_geo_jobs resume section. Whenever there were no zombie kn-runs
(the normal case), the handler bailed out and geo jobs stayed forever
'running' with stale heartbeats — users had to manual cancel/resume after
every deploy.

Fix: don't return early. Initialize `ids = []`, only run UPDATE if rows
exist, drop the inner `return` from exception branch. The for-loop over
ids becomes a no-op when empty, and execution falls through to the geo
section. Same pattern as the breadcrumb above — fail soft, continue.

cleanup_zombies beat task (added in caa467f) stays as belt-and-suspenders
in case worker_ready signal ever misbehaves again.
2026-05-11 17:12:31 +03:00
lekss361
caa467fb7e fix(worker): periodic zombie cleanup via beat instead of worker_ready
worker_ready signal handler was NOT firing in our setup (verified via
DB breadcrumb after 3 deploys — zero rows of stage='worker_ready' in
nspd_geo_log). Root cause of unreliability unknown — possibly Celery
internals, possibly compose recreate timing. Either way, after every
redeploy users had to manually cancel/resume jobs to keep them moving.

Replace signal-based resume with periodic beat task:
- cleanup_zombies runs every minute (* * * * *)
- Finds nspd_geo_jobs in status running/paused with heartbeat >2 min stale
- Sets status='queued' + apply_async with queue=geo
- Idempotent — if no zombies, no-op

worker_ready handler kept (with FK-fix breadcrumb on NULL job_id) for
diagnostic purposes — if signal ever does fire, we'll have evidence.
2026-05-11 17:02:29 +03:00
lekss361
17feaa408e debug(worker): persistent breadcrumb in nspd_geo_log on worker_ready 2026-05-11 16:51:50 +03:00
lekss361
f539e0e414 fix(admin): bump UI stale threshold 60/90s -> 300s
The 'stale' badge in /admin/scrape/* tables was a UI-only label
computed as Date.now() - heartbeat_at > 60s (90s for geo). Heartbeats
naturally lag past 60s during WAF backoff (30s) or NSPD HTTP timeout
(15s) — every page render flashed running jobs as 'stale' even though
they were processing.

300s threshold matches the real auto-resume threshold in the worker
(jobs are zombies only after >5 min of silence). Less false-positive
churn in the UI.
2026-05-11 16:49:37 +03:00
lekss361
fdd5b4e0e2 fix(worker): dispose SQLA engine in worker_process_init + log resume scan
Two related fixes:

1. worker_process_init handler disposes the SQLAlchemy engine in each
   prefork child. Without this, child processes inherit open psycopg
   sockets from the parent. First use in a child raises
   ProgrammingError: can't change 'autocommit' now: connection in
   transaction status INTRANS. This was killing 1 of every 5 parallel
   geo jobs on cold start (job 13 in latest bulk run).

2. Add logger.info at start/end of worker_ready resume handler so we
   can see in worker logs whether it actually fired and how many jobs
   it resumed.
2026-05-11 16:40:21 +03:00
lekss361
5c92c2a0e9 fix(worker): auto-resume after redeploy + bump concurrency 5->8
Auto-resume bug: kn and nspd_geo resume-on-worker_ready required
heartbeat >5min / >10min stale. After redeploy worker boots in
~1-2 min, so jobs killed seconds before deploy had fresh heartbeat
and were NEVER auto-resumed — required manual cancel + resume.
Fix: drop time threshold entirely. On worker_ready ANY 'running'
or 'paused' job is by definition a zombie (no worker exists yet),
safe to resume all of them.

Concurrency bump: 5 -> 8 prefork slots. Headroom for 5 geo jobs +
1 kn sweep + 2 objective tasks running simultaneously. Each slot
~150MB RSS -> ~1.2GB total, well within 6GB VPS RAM budget.
2026-05-11 16:23:16 +03:00
lekss361
43672a0068 fix(api): bulk geo includes all doc_types by default (only_ddu opt-in) 2026-05-11 16:12:58 +03:00
lekss361
1b65f55802 feat(analytics): buildings block + dynamic developer comparison
- ObjectBuilding type + useObjectBuildings hook
- Objects detail page: render Корпуса ЖК table (cad_num, floors, area,
  purpose, name, address) when buildings_count > 0
- Developers page: drop hardcoded compareIds, use top-3 from
  useTopDevelopers sorted by jk_count DESC (fallback to legacy static
  array when API has no data yet)
2026-05-11 16:03:16 +03:00
lekss361
8724a4731b feat(admin): unify job settings + bulk geo trigger under /admin/scrape/all
Move JobSettings UI from /admin/jobs/settings into JobSettingsPanel component,
render it alongside new BulkGeoPanel on /admin/scrape/all. BulkGeoPanel has a
parallelism input (1-10, default 5) and POSTs /admin/scrape/geo/bulk. Remove
the standalone /admin/jobs/ route and the nav tab pointing to it.
2026-05-11 16:02:52 +03:00
lekss361
fe2a881cec feat(api,analytics): bulk geo backfill + complex_buildings query
- POST /api/v1/admin/scrape/geo/bulk — splits pending Sverdlovsk cad-nums
  into N chunks (parallelism 1..10, default 5), creates N jobs and enqueues
  each in queue=geo. source_kind='rosreestr_pending_chunk' for tracking.
- analytics_queries.complex_buildings(db, obj_id) — returns list of buildings
  from cad_buildings (cad_num, floors, area, purpose, name, address, geom).
- object_detail: LEFT JOIN v_complex_buildings, adds buildings_count.
- top_developers: adds complexes_count via correlated subquery.
- GET /api/v1/analytics/object/{obj_id}/buildings → list[ComplexBuilding].
2026-05-11 15:56:17 +03:00
lekss361
b18170d3c0 perf(worker): bump concurrency 2->5 for parallel nspd_geo jobs 2026-05-11 15:56:06 +03:00
lekss361
7e1540f84b feat(admin): /admin/jobs/settings page — inline-edit Celery job config
Add GET+PUT table for all job_types (scrape_kn, nspd_geo, objective_etl,
objective_sync). Per-row drafts, collapsible extra_config JSON editor,
toast notifications. Tab added to admin layout. Hand-rolled types until
codegen picks up new endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:47:39 +03:00
lekss361
2fee7739e8 feat(jobs): centralized job_settings API + DB-driven beat schedule
- JobSetting ORM model (JSONB extra_config) + Pydantic schemas
- GET/PUT /api/v1/admin/jobs/settings + /{job_type} (X-Admin-Token auth)
- celery_app.py builds beat_schedule from job_settings DB (env fallback
  retained for safety on first deploy / DB unreachable)
- nspd_geo task reads rate_ms from job_settings when per-job row has
  no override
- enqueue/resume geo jobs route to queue_name from job_settings
- Worker container: --queues=celery,scrape_kn,geo (one container,
  three named queues — kn sweep no longer blocks nspd_geo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:38:13 +03:00
lekss361
cbe1253461 feat(db): backfill complexes nulls + add job_settings (migrations 80, 81)
80_complexes_backfill_nulls.sql:
- cad_quarter: +722 rows via ST_Contains spatial join on cad_quarters_geom
- developer_id: +247 rows via fuzzy name match on domrf_developers
- obj_class: +2 rows via objective_lots.class mode
- cad_building_num TEXT dropped (100% NULL; relation lives in
  cad_buildings.complex_id)
- v_complex_full rebuilt: cad_building_nums TEXT[] replaces dead column
- v_complex_buildings created: complex_id → cad_building_nums[], buildings_count

81_job_settings.sql:
- New table job_settings (job_type PK, enabled, queue_name, cron_schedule,
  rate_ms, max_retries, max_concurrency, extra_config JSONB)
- Seeded 4 rows: scrape_kn (queue=scrape_kn, '15 4 * * mon'), nspd_geo
  (queue=geo, rate_ms=600), objective_etl, objective_sync (cron from prod
  objective_sync_config)
- objective_sync_config marked DEPRECATED via COMMENT — kept for backward
  compat until backend refactor

Both already applied on prod via postgres MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:30:44 +03:00
lekss361
de21fac0a8 perf(ci): skip unchanged docker builds via paths-filter
Add `changes` job (dorny/paths-filter@v3) to detect which parts of the
codebase changed. Build jobs now run only when their source tree is
touched; deploy runs as long as no build failed (skipped builds are OK).

Split IMAGE_TAG (latest) from SENTRY_RELEASE_VAL (git sha) so Sentry
release tracking still points to the exact commit while compose pulls
the :latest tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:10:23 +03:00
lekss361
a7d4223dfa perf(ci): parallelize docker builds + add npm cache mount
Split single build-and-deploy job into 4 jobs: build-backend,
build-worker, build-frontend run in parallel (all 3 independent),
deploy job has needs:[...] on all three. Removes ~2 min of sequential
build time on warm cache runs.

Also: scope GHA cache keys per image (backend-lean / worker-chromium /
frontend) so builds don't invalidate each other's layer cache. Replace
sleep 8 health-check with a 30-iteration polling loop (fails fast on
bad deploy, doesn't over-wait on fast ones). Add --mount=type=cache to
frontend npm ci for node_modules cache across builds.
2026-05-11 15:01:10 +03:00
lekss361
1a0bf10017 fix(nspd-geo): json.dumps for quarter raw_props (Python True → JSON true)
_save_quarter использовал str(dict).replace("'", '"') как hack для GeoJSON
→ JSON. Это ломалось на Python booleans (True/False/None), которые в JSON
должны быть true/false/null. После dumps=False фикса rosreestr2coord
возвращает Python dict с булями в properties (is_actual=True) → INSERT
падал с psycopg.errors.InvalidTextRepresentation. _save_building уже
использовал json.dumps корректно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:59:46 +03:00
lekss361
ce5e29f92e fix(nspd-geo): rosreestr2coord v5 compat — delay arg, tmp cache, dumps default
Three incremental bugs surfaced while wiring rosreestr2coord v5 into our
worker. All in backend/app/services/scrapers/nspd_lite.py.

1. `Area.__init__()` removed `delay` kwarg in v5 (was throttling). Drop
   it from our call. Rate limit now happens in worker via
   time.sleep(rate_ms/1000) after each fetch.

2. `Area(..., use_cache=True)` default writes media cache to ./tmp/
   relative to CWD. In our docker image CWD=/app (owned by root), worker
   runs as non-root → PermissionError on every target. Fix:
   use_cache=False, media_path="/tmp/rosreestr2coord".

3. `Area.to_geojson_poly(dumps=True)` default returns JSON-serialized
   STRING, not dict (changed in v5). Worker's _persist_target expects
   dict with .get("properties") → AttributeError. Fix: dumps=False.

Vault entries:
- fixes/Bug_Rosreestr2coord_Delay_Arg_v5_Fixed.md
- fixes/Bug_Nspd_Geo_Tmp_Permission_Fixed.md
- fixes/Bug_Nspd_Geo_Str_Object_No_Get_Fixed.md
2026-05-11 14:43:44 +03:00
lekss361
5b741578e6 feat(claude): add 4 workers + tech-analyst subagents
Worker agents (write code):
- backend-engineer (blue, sonnet)
- frontend-engineer (green, sonnet)
- devops-engineer (orange, sonnet)
- database-expert (purple, sonnet)

Analyst/planner (read-only, no code):
- tech-analyst (yellow, sonnet) — decomposes vague/cross-domain
  requests into structured plans for workers. Has read-only tools
  (Read/Glob/Grep, MCP obsidian/postgres read APIs, WebSearch) but
  NO Write/Edit/execute_sql/destructive Bash.

CLAUDE.md updated:
- Workers (4) vs Analyst (1) classification
- Routing rules: trivial → main, single-domain → worker direct,
  vague/cross-domain → tech-analyst first
- Cross-domain orchestration pattern with explicit flow

Each agent file:
- Pre-loaded vault MOC pointers for the domain
- Tech stack + conventions specific to the layer
- 5-8 critical pitfalls from real bugs this sprint
- Explicit deny-list of cross-scope operations
2026-05-11 14:36:34 +03:00
lekss361
b19ca1ed41 fix(nspd-geo): rosreestr2coord cache permission in non-root worker
rosreestr2coord.Area() by default creates ./tmp/ media cache relative
to CWD. In our worker container CWD=/app (owned by root), runs as
non-root user `app` → PermissionError: '/app/tmp' on every target.

Fix: pass explicit kwargs to Area():
  - use_cache=False (we don't need cache — each cad_num is unique)
  - media_path='/tmp/rosreestr2coord' (world-writable in container)

Vault: fixes/Bug_Nspd_Geo_Tmp_Permission_Fixed.md
2026-05-11 14:22:11 +03:00
lekss361
3e6cddde33 docs(claude): obsidian as the only knowledge store
- TL;DR header pin: all memory in Obsidian vault, JSONL deprecated,
  no knowledge .md outside vault.
- Workflow: vault-first search step (mcp__obsidian__simple_search)
  before any non-trivial task.
- New post-task step: update vault with bug fix / ADR / runbook /
  session entry.
- Don't-do-these grouped (knowledge / code-git / schema-migrations).
- MEMORY.md gets matching rule (persists across sessions).
2026-05-11 14:19:48 +03:00
lekss361
3919a40c49 refactor(nspd-geo): unify on rosreestr2coord v5, drop legacy column
Объединяю несколько связанных изменений вокруг NSPD geo bulk-fetcher:

Adapt to rosreestr2coord v5 API:
- nspd_lite.fetch_via_rosreestr2coord: drop `delay` kwarg from Area()
  (removed upstream in v5); keep it in our function signature for
  backward-compat, comment why.
- nspd_geo worker: add explicit time.sleep(rate_ms/1000) after lib-branch
  fetch — in v4 the library throttled internally via delay, in v5
  rate limiting is the caller's job. Без этого получали Area.__init__()
  unexpected kwarg `delay` на каждом target.

Drop use_rosreestr2coord switch:
- Removed urllib-vs-lib choice everywhere. We always use community
  rosreestr2coord library — авторы регулярно обновляют WAF-tricks,
  наш urllib-fetcher (fetch_geoportal) уже неактуален.
- admin_scrape.py: Pydantic schema, INSERT, SELECT, API response
  cleaned of `use_rosreestr2coord`.
- nspd_geo.enqueue_geo_job: param dropped, INSERT shrunk.
- worker process loop: dropped `if use_lib:` branch + import of
  fetch_geoportal.
- frontend/geo/page.tsx: removed checkbox + GeoJob.use_rosreestr2coord
  field + POST body field.

DB column drop:
- data/sql/78_drop_use_rosreestr2coord.sql (NEW):
  DROP COLUMN nspd_geo_jobs.use_rosreestr2coord + CREATE OR REPLACE
  VIEW v_scrape_runs_unified (которая depended on the column).
- data/sql/77_nspd_geo_jobs.sql: cleaned historical DDL for fresh setups.
- Migration applied to prod (in-conversation via postgres MCP).

Frontend polish:
- Thematic ID changed from free-form number input to labeled select
  (1=parcel / 2=quarter / 4=admin / 5=building / 7=zone / 15=complex).
- Auto-sync thematic_id from Job kind on change (override possible).
- ScrapeLogsPanel: extended union type with "nspd_geo" + fixed
  /admin/scrape/geo to pass scraperType="nspd_geo" (was "nspd",
  filtering empty legacy nspd_scrape_log table; real logs live in
  nspd_geo_log via v_scrape_log_unified).

Verified: ruff ✓, tsc --noEmit ✓, migration ran (BEGIN..COMMIT clean).

Deploy order safe: prod column уже удалена → новый backend код, который
не INSERT'ит use_rosreestr2coord, совпадёт со схемой после deploy.
2026-05-11 14:13:33 +03:00
lekss361
ccd60aea7d upd readme 2026-05-11 13:43:15 +03:00
lekss361
ead0de99ed fix(worker): rosreestr2coord in lockfile + admin logs panel for NSPD/objective
Two fixes for the NSPD geo bulk-fetcher:

1. rosreestr2coord was listed in pyproject.toml but missing from uv.lock,
   so Docker's `uv sync --frozen` didn't install it. NSPD jobs failed every
   target with `No module named 'rosreestr2coord'`. Ran `uv lock` to add it
   (v5.3.3) plus requests + charset-normalizer transitive deps.

2. /admin/scrape/geo and /admin/scrape/objective lacked the log panel that
   /admin/scrape (DomRF) had. Extracted ScrapeLogsPanel component over the
   unified /api/v1/admin/scrape/all/logs endpoint with scraper_type filter
   and wired into both pages.

Tests: ruff ✓, tsc --noEmit ✓
2026-05-11 12:47:19 +03:00
lekss361
769b16df47 fix(worker): port objective_etl from psycopg2 → psycopg v3
Worker крашился на старте с `ModuleNotFoundError: No module named 'psycopg2'`
при импорте app/workers/tasks/objective_etl, потому что app/services/objective_etl
писал на psycopg2 API, а в pyproject.toml есть только psycopg[binary]>=3.2.0.

Изменения:
- psycopg2 → psycopg (v3)
- psycopg2.extras.execute_values → _bulk_upsert helper, эмулирующий тот же
  behavior через cur.executemany (в psycopg v3 это pipeline-mode, overhead
  на сетевые round-trips минимален)
- psycopg.connect совместим с тем же connection string

Тесты: AST parse ✓, ruff ✓, import ✓
2026-05-11 12:07:57 +03:00
lekss361
b14700e583 fix deploy 2026-05-11 10:01:36 +03:00
lekss361
d50626b32a fix 2026-05-11 09:57:40 +03:00
lekss361
b0f5c38894 feat(deploy): split obsidian stack from main + path-filtered GHA
Obsidian Self-hosted LiveSync (CouchDB) выведен в отдельный docker-compose
stack и GHA workflow. Любой стек теперь редеплоится независимо.

* docker-compose.obsidian.yml: только couchdb в shared external network
* docker-compose.prod.yml: убрана couchdb-секция; caddy подключён к
  shared network для маршрута obsidian.gendsgn.ru → couchdb:5984
* .github/workflows/deploy-obsidian.yml: новый workflow с path-filters
  (триггерится только при изменениях obsidian-related файлов)
* .github/workflows/deploy.yml: + path-filters (триггерится только при
  main-related файлах) + создание shared network в bootstrap
* docs/obsidian-livesync.md: обновлены инструкции под split-архитектуру

После push:
1. ssh gendesign 'docker network create gendesign_shared'  (один раз)
2. Прописать COUCHDB_USER/PASSWORD в backend/.env.runtime
3. Добавить DNS A-record obsidian.gendsgn.ru → IP VPS
4. git push → GHA задеплоит обе части независимо

Преимущества:
- Изменения main не разрывают LiveSync клиентов
- Изменения obsidian не рестартуют main
- Падение obsidian-стека не влияет на основное приложение
2026-05-11 09:51:31 +03:00
lekss361
79e2e94e09 refactor(nspd): remove Playwright-based scraper, rosreestr2coord = default
УДАЛЕНО (~1700 строк):
* backend/app/services/scrapers/nspd_kn.py (Playwright + WAF-bypass)
* backend/app/workers/tasks/scrape_nspd.py
* frontend/src/app/admin/scrape/nspd/page.tsx
* 5 NSPD admin endpoints (POST /nspd, /nspd/release-lock,
  GET /nspd/runs, /logs, /coverage)
* NSPD beat schedule + worker_ready resume hook + nav tab + link
* _nspd_default_regions() helper

СОХРАНЕНО (история):
* nspd_scrape_runs / nspd_scrape_log таблицы (3 row + log) для аудита
* UNION ALL в v_scrape_runs_unified / v_scrape_log_unified —
  старые runs видны под фильтром scraper_type='nspd'
* settings scrape_nspd_* помечены DEPRECATED

CHANGED:
* nspd_geo_jobs.use_rosreestr2coord DEFAULT FALSE → TRUE
* UI checkbox "rosreestr2coord lib" по умолчанию = checked
* /admin/scrape/all + /admin/scrape/geo — единственный путь к NSPD-данным

NSPD-доступ теперь только через rosreestr2coord (community lib) —
upstream поддерживает обновления headers/WAF-tricks. Локальный
nspd_lite.py (urllib) остаётся как fallback (use_rosreestr2coord=FALSE).
2026-05-11 09:21:12 +03:00
lekss361
54dbc77348 feat(geo): NSPD bulk-fetcher без Playwright + resume-friendly UI
ОТКРЫТИЕ: stdlib urllib проходит WAF nspd.gov.ru (TLS-fingerprint stdlib
отличается от mainstream HTTP-clients). Это позволяет полностью убрать
Playwright + Chromium для NSPD-задач.

* nspd_lite.py: urllib + ssl._create_unverified_context(). 4 публичных
  fetcher'а (geoportal/quarter/parcel/building) + fetch_via_rosreestr2coord
  fallback на community-lib

* schema 77: nspd_geo_jobs (журнал) + nspd_geo_targets (cad-номера со
  статусом pending/done/failed). Resume-state в БД.

* tasks/nspd_geo.py: Celery task process_nspd_geo_job(job_id). Heartbeat
  каждые 5 items, WAF backoff 30s × 2^N (max 8 → paused). UPSERT в
  cad_quarters_geom / cad_buildings → идемпотентно.

* worker_ready hook: stale jobs (>10min без heartbeat) автоматически
  re-enqueue после redeploy. Никаких потерь прогресса.

* 4 admin endpoint + UI /admin/scrape/geo с формой запуска (manual_list /
  rosreestr_pending для авто-наполнения из ДДУ-кварталов), progress-bars
  и cancel/resume кнопками per job.

* settings: use_nspd_lite=True, nspd_lite_rate_ms=600.
* dep: rosreestr2coord>=4.0.0 (community lib для fallback).

TODO следующих шагов:
- smoke-test на проде через SSH (validation RU-IP не банит urllib)
- feature toggle USE_NSPD_LITE в scrape_nspd.py для переключения с старого
  Playwright-пути
- docker lean image (-300 MB после убирания Chromium)
- расширение SCRAPE_NSPD_DEFAULT_REGIONS на 66,74,72,59 (Челябинск/Тюмень/Пермь)
2026-05-11 08:53:28 +03:00
lekss361
304a9a623b Merge branch 'main' of https://github.com/lekss361/-gendesign 2026-05-10 21:11:31 +03:00
lekss361
52bcf9d30c feat(db): canonical complexes + unified views + naming consistency
* Task 2+3: complexes.obj_class и district_name из Objective
  (350 ЖК с классом, 1525 с district; objective приоритетнее)

* Task 1 (fuzzy match orphan Objective ↔ domrf_kn):
  - pg_trgm extension
  - auto-merge 14 high-confidence (similarity ≥ 0.85)
  - v_complex_match_review для остальных 82 кандидатов
  - complexes 1752 → 1740 · cross-source 114 → 124

* Task 4 (naming unification без RENAME COLUMN):
  - GENERATED ALWAYS AS (...) STORED для синонимов
  - region_id в 15 таблиц + 9 partitions
  - developer_id в 7, developer_name в 5
  - COMMENT 'DEPRECATED' на region_cd/region_code/dev_id/dev_name

* Task 5 (unified scrape dashboard):
  - GET /api/v1/admin/scrape/all/runs + /all/logs (поверх v_scrape_runs_unified)
  - new page /admin/scrape/all с filter табами + 4 stat-плитки
  - +tab «📊 Все скраперы» в admin layout

Файлы: data/sql/75_unification_naming.sql · backend/app/api/v1/admin_scrape.py
       · frontend/src/app/admin/scrape/all/page.tsx · admin/layout.tsx
2026-05-10 21:11:20 +03:00
d8d76be86d Memory: +rel Project_GenDesign→SiteFinder_v0_2_May08 (deploys), SiteFinder_v0_2_May08→MVP_Scope_AreaSelect_Apr25 (contradicts... (2026-05-10 22:44:45 +05) 2026-05-10 22:44:45 +05:00
32a9666f68 Memory: +ent SiteFinder_v0_2_May08 (2026-05-10 22:43:45 +05) 2026-05-10 22:43:45 +05:00
97b19a0b85 Import Site Finder app from analysis/ vibe-coding session
Adds site-finder/ subfolder with:
  - server.py — FastAPI scoring service v2 (35 endpoints, ~85KB)
  - 01_load_sites.py … 12_more_pois.py — data ingest pipeline
  - db_init.py — SQLite schema bootstrap
  - static/ — Leaflet UI (index.html ~3500 lines + sw.js)
  - cache/ — small persistent caches (admin districts, jk polygons,
    geocode warm cache, parcel polygons drop-zone with README)
  - reports/ — sample generated parcel report (HTML+JSON)

Excluded via .gitignore (regeneratable, too big for git):
  - analysis.db (336MB SQLite — rebuild via 01_*..12_*.py)
  - cache/objective_raw/ (1.2GB Объектив raw dumps)
  - cache/overpass_raw.json, cache/osm_buildings_all.geojson
    (regen from Overpass API)

Production deploy: /opt/gendesign/site-finder/ on gendsgn.ru
(container gendesign-site-finder-1, served at /sf/).
2026-05-10 22:42:25 +05:00
lekss361
20625f78f3 fix ui 2026-05-10 20:16:26 +03:00
lekss361
7c05d0a0d8 feat(objective): full sync pipeline + dynamic admin config
Objective API (api.objctv.ru) интегрирован как новый source of truth для
per-flat данных по новостройкам УрФО. Заменяет промежуточный Anton-SQLite
(legacy bootstrap-ETL остался как fallback в свёрнутом блоке UI).

Schema (data/sql/68_v2 — applied на проде, 6 таблиц):
- objective_lots          (UPSERT по lot_id; 303 677 rows)
- objective_corpus_room_month (long-формат месяц×корпус×room_bucket; 19 738 rows)
- objective_lots_history  (append-only weekly snapshots для elasticity)
- objective_complex_mapping (Objective ComplexName ↔ domrf_kn_objects.obj_id)
- objective_raw_reports   (jsonb страховка на смену схемы API)
- objective_scrape_runs   (журнал прогонов)
+ data/sql/72_objective_sync_config (single-row динамический конфиг)

Backend:
- services/scrapers/objective.py: ObjectiveClient — Bearer-токен (Redis +
  in-memory fallback), retry на 401/429/5xx, Retry-After header support
- services/objective_etl.py: ETL SQLite Антона → PG (legacy)
- services/objective_sync_config.py: read/update single-row config
- workers/tasks/scrape_objective.py:
  * sync_objective_group: 2 рабочих отчёта (corp_sum, lots_pf), inline-парсинг
  * sync_all_groups: wrapper, перебирает группы из БД-конфига с
    inter-group паузой; PATCH-merge explicit args > DB config
- workers/tasks/objective_etl.py: Celery task для legacy bootstrap
- workers/celery_app.py: beat читает cron из БД при старте (fallback на env)
- api/v1/admin_scrape.py: 5 новых endpoints для /objective/*

Frontend (frontend/src/app/admin/scrape/objective/page.tsx):
- PRIMARY blue блок «🌐 Наш sync» с input для override групп
- Collapsible «⚙️ Настройки» с формой (cron + 8 параметров) → PUT в БД
- Coverage-панель с PG counts + строка про SQLite Антона как legacy
- Collapsible «🛠 Bootstrap ETL» — legacy-инструмент

Beat schedule: вторник 06:00 МСК, ~10-15 мин на 4 группы (Свердл.обл +
Челябинск + Тюмень + Пермь = ~700K квартир УрФО). Расписание и параметры
меняются через админку без редеплоя (cron требует restart beat).

Эмпирические находки об API (probe 2026-05-10):
- 13 из 21 проверенных group_name доступны на тарифе (включая «Свердловская
  область», «Челябинск», «Тюмень», «Пермь», но НЕ «Свердловская обл» —
  формат имени строгий)
- ComplexName требует БЕЗ префикса «ЖК» и БЕЗ кавычек
- Поле «Банк» для всех 303k = NULL (тариф не отдаёт), но «Тип обременения»
  работает (36% строк = ипотека) → ipoteka_share возможен, банковская
  атрибуция — нет

Docker:
- bind-mount /opt/gendesign/site-finder:/data/anton-sqlite:ro в worker

GitHub backlog: добавлены #22-25 (recommend_mix v3 — 4 сабтаска по
улучшению алгоритма на основе фидбэка про POI / границы районов /
конкурентов / окно данных / success-driven mix).

Knowledge graph (memory/memory-gendesign.jsonl): обновлены entities
Objective_Integration_May07_2026, Schema_Objective_v2_May07,
Objective_API_Findings_May07, Module_Objective_Client + новый
Session_End_May07_2026.

TODO для прода: прописать OBJECTIVE_API_KEY=<key> в backend/.env +
docker compose restart worker beat.
2026-05-10 19:54:15 +03:00
lekss361
12732949b9 ignore 2026-05-07 21:04:38 +03:00
lekss361
aaa5d44e77 feat(objective): integrate api.objctv.ru — auth, schema, weekly sync
backend/app/services/scrapers/objective.py:
  ObjectiveClient — GetToken (Bearer cached в Redis 25 min TTL) +
  GetReport v2 с UseDdu/UseDkp. 4 высокоуровневых метода:
  report_corpuses_summary, report_lots_summary, report_corpuses_per_flat,
  report_lots_per_flat. Retry 401/429/5xx, rate-limit 500ms, brotli.
backend/app/workers/tasks/scrape_objective.py:
  Celery task sync_objective_group — еженедельно тянет 4 канон. отчёта по
  группе, raw payload в objective_raw_reports.
celery_app.py: +include + beat «0 5 * * mon».
data/sql/68_schema_objective.sql: 6 таблиц — runs, raw_reports (jsonb),
oks (готовые ТЭП + escrow/debt), lots, lots_history (per-flat per-day),
complex_mapping (Objective ↔ domrf_kn_objects, is_reviewed workflow).
data/sql/69_objective_smoke.py: stand-alone GetToken + 4 отчёта →
data/raw/objective_smoke/<ts>/. Используется один раз чтобы понять
реальную схему payload перед написанием parser-слоя.
config: OBJECTIVE_API_KEY, OBJECTIVE_DEFAULT_GROUP=Екатеринбург,
OBJECTIVE_SYNC_CRON='0 5 * * mon'.
2026-05-07 21:04:00 +03:00
lekss361
b44c7f157e fix(analytics): bucket by area/deal_count — rosreestr aggregates packed deals
Critical bug в quartirography() и _BUCKET_SQL: rosreestr с 2025Q1 публикует
пакетные ДДУ одной строкой (area=SUM, deal_count=N). Bucket по сырой area
загонял 5×40м² в bucket «80+ м²», давая 70% портфеля 80+ вместо реальных 4%.
Изменения:
- area → (area / deal_count) при bucket'инге
- COUNT(*) → SUM(deal_count) для подсчёта реальных квартир
- удалён area<=200 фильтр (отрезал 95% свежих агрегированных строк,
  realestate_type_code='002001003000' уже фильтрует не-квартиры)
Эффект:
- Dashboard chart «парадокс портфеля»: 80+ был 70% → стал 3.1%
- recommend_mix shares: 1-к 58%, 2-к 29%, 3-к 5%, Студии 4%, 80+ 4%
- total_deals для Свердл за 24 мес: 7553 → 40084 (агрегация теперь учтена)
2026-05-03 18:31:38 +03:00
lekss361
f80a2c1d05 fix(scrape): SQLAlchemy text() парсил ':00:' regex как named-param
В nspd_kn.py:get_pending_cads и admin_scrape.py:nspd_coverage regex-фильтры
quarter_cad_number !~ '^00:00:' и !~ ':0000000$' падали с psycopg.errors.
DataError — двоеточие в text() трактуется как ':param'. Заменил на NOT LIKE
с bindparams (быстрее regex и без issue с парсингом).

Эффект на проде:
- /api/v1/admin/scrape/nspd/coverage возвращал 500 → теперь 200
- Кнопка «Запустить sweep» больше не дизейбленa (pending=964 теперь видно)
- Сам scraper тоже бы валился на этом же баге при beat-запуске
2026-04-30 23:16:11 +03:00