Commit graph

2296 commits

Author SHA1 Message Date
lekss361
ab0647e4d5 feat(site-finder): distance to EKB center + success quartirography recommendation 2026-05-11 22:35:21 +03:00
lekss361
c31da62e8d feat(analytics): recommend_mix v3.1-v3.4 - noise + 2D competitors + 24m cap + success-driven 2026-05-11 22:19:41 +03:00
lekss361
369b5a4706 feat(db): v_bucket_success_score view (success-driven mix #25) 2026-05-11 22:04:22 +03:00
lekss361
8be539ddc6 feat(site-finder): ПЗЗ territorial zones from Rosreestr PKK6 + zoning in analyze 2026-05-11 21:51:51 +03:00
lekss361
5b03d6d799 feat(site-finder): isochrones UI + networks VKH + OSM substations 2026-05-11 21:48:21 +03:00
lekss361
923f250926 feat(site-finder): /parcels/{cad}/isochrones via OpenRouteService 2026-05-11 21:44:22 +03:00
lekss361
6ce634a9b9 feat(site-finder): utilities (power/pipeline) + fix Внешние факторы layout 2026-05-11 21:38:20 +03:00
lekss361
2855a01ca7 feat(site-finder): v3.5 - seasonal weather + hydrology + geotech risk 2026-05-11 21:32:07 +03:00
lekss361
7bb51aa838 feat(site-finder): v3.4 - weather enrichment (temp/precip/UV) + geology stub 2026-05-11 21:17:57 +03:00
lekss361
7900dc5238 fix(site-finder): WKT LINESTRING needs commas + rosreestr_deals real column names 2026-05-11 20:58:49 +03:00
lekss361
232c81eae9 feat(site-finder): v3.3 - score label + market_trend + multi-thematic bulk 2026-05-11 20:51:59 +03:00
lekss361
1e9d32ee3c feat(site-finder): v3.2 — noise + air quality + wind analytics 2026-05-11 20:42:37 +03:00
lekss361
a2e8a49e6d feat(db): osm_noise_sources_ekb for site-finder noise scoring 2026-05-11 20:35:27 +03:00
lekss361
d08d06d8a8 fix(geo): parcels (thematic_id=1) come from rosreestr2coord in WGS84 — drop ST_Transform 2026-05-11 20:09:58 +03:00
lekss361
81cd7499f6 feat(site-finder): cad_parcels_geom migration + POI markers on map + bulk-job UX 2026-05-11 19:54:56 +03:00
lekss361
f419900968 feat(site-finder): v3.1 — cad_parcels_geom, analyze fallback, POI lat/lon, OSM expand
- nspd_geo: add _save_parcel() for thematic_id=1 → cad_parcels_geom (UPSERT,
  ST_Transform from Web Mercator); _persist_target now handles 1/2/5
- parcels.py: analyze endpoint geom lookup extended with cad_parcels_geom as
  3rd fallback source (after cad_quarters_geom, cad_buildings); both SELECT
  and WKT subqueries updated
- parcels.py: POI score_breakdown items now include lat/lon for map markers
- poi_loader: OSM_CATEGORIES expanded — college+university→school,
  hypermarket→shop_supermarket; coverage +3 tag pairs
2026-05-11 19:52:19 +03:00
lekss361
ea3d58fa7d feat(routing): /sf -> /site-finder permanent redirect (short URL alias) 2026-05-11 19:49:26 +03:00
lekss361
e6c062cc5b feat(home): site-finder no longer WIP + admin/scrape/all + buildings in object card desc 2026-05-11 19:36:43 +03:00
lekss361
a7d7dc9667 fix(site-finder): dedupe competitors — same domrf_kn_objects 3x snapshots bug 2026-05-11 19:04:29 +03:00
lekss361
3aeda297ee fix(poi-sync): split Overpass query per category (504 Gateway Timeout) 2026-05-11 18:54:28 +03:00
lekss361
93f13795bb fix(poi-sync): add User-Agent header — Overpass returns 406 for python-httpx default UA 2026-05-11 18:46:47 +03:00
lekss361
9742219ef6 debug(poi-sync): DB breadcrumbs to nspd_geo_log for silent-fail diagnosis 2026-05-11 18:37:43 +03:00
lekss361
f81bd710c0 feat(api): POST /admin/scrape/poi-sync — manual OSM POI sync trigger 2026-05-11 18:12:37 +03:00
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