feat(layouts): PDF ТЗ endpoint + BestLayoutsBlock UI (#113 PR D) #198

Closed
lekss361 wants to merge 4 commits from feat/113-pdf-tz-and-ui into main
Owner

Summary

Final PR (D из 4) для Issue #113 Layout Analysis. Builds on:

  • PR #193 (A) schemas + helpers
  • PR #194 (B) mv_layout_velocity
  • PR #196 (C) POST /best-layouts endpoint (qa-smoke PASS: 5 layouts, pct=100, abs_units=300)

Backend (~330 LOC)

  • POST /api/v1/parcels/{cad_num}/best-layouts/pdf — WeasyPrint render PDF "ТЗ на проектирование (data-driven)" с unit-mix recommendation, top layouts table, data quality section
  • backend/app/services/exporters/layout_tz_pdf.py (NEW) — render_layout_tz_pdf() с XSS-escape (html.escape) для user-data (cad_num/parcel_address/rationale_text/time_window)
  • 5 unit tests (skip on Windows если GTK/Pango отсутствует — OSError|ImportError guard)

Frontend (~800 LOC)

  • BestLayoutsBlock.tsx (NEW) — главный компонент:
    • Slider/radio/input controls (radius/time_window/min_velocity/target_total_flats)
    • DataQualityCard (badge confidence high/medium/low + coverage %)
    • TopLayoutsTable (rank, room_bucket, area_bin, velocity, avg_area, avg_price, sold_pct)
    • RecommendationCard (rationale + stacked bar unit-mix + mix table + weighted price)
    • Кнопка «Скачать ТЗ (PDF)» → Blob download + revokeObjectURL
  • useBestLayouts.ts (NEW) — TanStack useMutation hook
  • types/best-layouts.ts (NEW) — manual TS types (codegen не запускался без backend live)
  • MarketTab.tsx (+4 LOC) — integration после competitors block

Pre-push review fixes (iteration)

  • 🟠 XSS escape для PDF HTML template
  • 🟠 sold_pct_of_supply — убрать double ×100 (backend уже 0..100 percent)
  • alert() → in-component pdfError state
  • isNaN()Number.isNaN()
  • 7 повторяющихся inline styles → Tailwind utilities (~40 LOC reduction)
  • BytesIO unused import — удалён
  • Test guard OSError(OSError, ImportError)

Phase 2.1 complete

Завершает Issue #113 при условии успешного деплоя + qa. Phase 2.2 (layout_type евро/классика + balcony_count) — отдельный issue после B2B Объектив (#52).

Test plan

  • backend: pytest tests/test_layout_tz_pdf.py -v — 5 tests (skip на Windows, run в Docker CI)
  • backend: pytest tests/api/v1/test_parcel_best_layouts.py tests/test_layout_signature.py -q — 53/53 pass (no regression)
  • backend: ruff + format + mypy clean
  • frontend: tsc --noEmit clean, npm run lint clean, npm run build success (17/17 pages)
  • code-reviewer pre-push: HIGH→APPROVE после fixup
  • qa-tester post-deploy:
    • curl POST /best-layouts/pdf на test cad → 200 + valid PDF (magic bytes %PDF)
    • UI smoke через chrome-devtools: открыть /site-finder/{cad} → MarketTab → BestLayoutsBlock виден → analyze работает → PDF скачивается

Closes #113 (full close — все 4 sub-PR'а).

Related: #197 (test-infra integration test gate — follow-up для site_finder services).

## Summary **Final PR (D из 4)** для Issue #113 Layout Analysis. Builds on: - PR #193 (A) schemas + helpers - PR #194 (B) mv_layout_velocity - PR #196 (C) `POST /best-layouts` endpoint (qa-smoke PASS: 5 layouts, pct=100, abs_units=300) ### Backend (~330 LOC) - **`POST /api/v1/parcels/{cad_num}/best-layouts/pdf`** — WeasyPrint render PDF "ТЗ на проектирование (data-driven)" с unit-mix recommendation, top layouts table, data quality section - **`backend/app/services/exporters/layout_tz_pdf.py`** (NEW) — `render_layout_tz_pdf()` с XSS-escape (`html.escape`) для user-data (`cad_num`/`parcel_address`/`rationale_text`/`time_window`) - **5 unit tests** (skip on Windows если GTK/Pango отсутствует — `OSError|ImportError` guard) ### Frontend (~800 LOC) - **`BestLayoutsBlock.tsx`** (NEW) — главный компонент: - Slider/radio/input controls (radius/time_window/min_velocity/target_total_flats) - DataQualityCard (badge confidence high/medium/low + coverage %) - TopLayoutsTable (rank, room_bucket, area_bin, velocity, avg_area, avg_price, sold_pct) - RecommendationCard (rationale + stacked bar unit-mix + mix table + weighted price) - Кнопка «Скачать ТЗ (PDF)» → Blob download + `revokeObjectURL` - **`useBestLayouts.ts`** (NEW) — TanStack `useMutation` hook - **`types/best-layouts.ts`** (NEW) — manual TS types (codegen не запускался без backend live) - **`MarketTab.tsx`** (+4 LOC) — integration после competitors block ### Pre-push review fixes (iteration) - 🟠 XSS escape для PDF HTML template - 🟠 `sold_pct_of_supply` — убрать double `×100` (backend уже 0..100 percent) - `alert()` → in-component `pdfError` state - `isNaN()` → `Number.isNaN()` - 7 повторяющихся inline styles → Tailwind utilities (~40 LOC reduction) - `BytesIO` unused import — удалён - Test guard `OSError` → `(OSError, ImportError)` ### Phase 2.1 complete Завершает Issue #113 при условии успешного деплоя + qa. Phase 2.2 (`layout_type` евро/классика + `balcony_count`) — отдельный issue после B2B Объектив (#52). ## Test plan - [x] backend: `pytest tests/test_layout_tz_pdf.py -v` — 5 tests (skip на Windows, run в Docker CI) - [x] backend: `pytest tests/api/v1/test_parcel_best_layouts.py tests/test_layout_signature.py -q` — 53/53 pass (no regression) - [x] backend: ruff + format + mypy clean - [x] frontend: `tsc --noEmit` clean, `npm run lint` clean, `npm run build` success (17/17 pages) - [x] code-reviewer pre-push: ✅ HIGH→APPROVE после fixup - [ ] **qa-tester post-deploy**: - `curl POST /best-layouts/pdf` на test cad → 200 + valid PDF (magic bytes `%PDF`) - UI smoke через chrome-devtools: открыть `/site-finder/{cad}` → MarketTab → BestLayoutsBlock виден → analyze работает → PDF скачивается Closes #113 (full close — все 4 sub-PR'а). Related: #197 (test-infra integration test gate — follow-up для site_finder services).
lekss361 added 4 commits 2026-05-16 09:37:20 +00:00
POST /api/v1/parcels/{cad_num}/best-layouts — top layout patterns
(room_bucket × area_bin) у конкурентов в радиусе с ranking by velocity
из mv_layout_velocity + unit-mix recommendation для архитектурного ТЗ.

Service (backend/app/services/site_finder/best_layouts.py, 563 LOC):
- centroid lookup с fallback (cad_parcels → cad_quarters)
- competitors-in-radius через ST_DWithin
- aggregate по mv_layout_velocity per room_bucket
- supply batch query (один SQL вместо N) для sold_pct_of_supply
- Hamilton largest-remainder normalizer (sum_pct=100 invariant)

Endpoint (backend/app/api/v1/parcels.py, +24 LOC) после /competitors.

Tests (backend/tests/api/v1/test_parcel_best_layouts.py, 10 mock tests):
404, empty competitors, MV empty, exclude/filter, min_velocity, time_window
scaling, obj_class_filter, target_total_flats, 3-bucket ranking.

Phase 2.1 (без layout_type / balcony_count — отсутствуют в БД,
ждут B2B Объектив #52). Reuses PR A schemas/helpers, PR B MV.

NOTE: competitors.py:143 status='sold' carryover bug НЕ fixed —
отдельный follow-up issue. PR C использует MV (через
objective_corpus_room_month) что работает корректно.
domrf_kn_objects has no geom_3857 column (only latitude/longitude NUMERIC).
Switch _COMPETITORS_IN_RADIUS_SQL and _SUPPLY_BATCH_SQL to use
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography — consistent
with competitors.py pattern. Rename centroid params x_3857/y_3857 →
center_lon/center_lat. Remove dead _round_to_5 function.

Fixes review-bot 🔴 BLOCK on fdc6456 PR #196.

Tests: 10/10 pass (mock keys updated к center_lon/center_lat), 43/43 regression OK.
3 carryover 🟡 items из re-review verdict на a88917f:

1. Coverage % denominator now post-filter (was mixing pre-filter count
   с post-filter intersection → artificially low confidence when
   user passed exclude/filter list)
2. MAX(snapshot_date) pre-computed via db.scalar() + bind :latest_snap
   instead of subquery in WHERE (avoid re-execution per spatial scan)
3. LAYOUT_CONFIDENCE_HIGH_PCT / LAYOUT_CONFIDENCE_MEDIUM_PCT as
   module-level constants instead of hardcoded literals

Integration test gap (#4 в re-review) — отдельный follow-up issue,
не в scope этого PR.

Tests: 10/10 best-layouts pass, 43/43 regression OK.
Backend (~330 LOC):
- POST /api/v1/parcels/{cad_num}/best-layouts/pdf via WeasyPrint
- services/exporters/layout_tz_pdf.py — HTML template с XSS-escape (html.escape)
  для cad_num/parcel_address/rationale_text/time_window
- 5 unit tests (skip on Windows если WeasyPrint deps missing)

Frontend (~800 LOC):
- BestLayoutsBlock.tsx — DataQualityCard + TopLayoutsTable + RecommendationCard
  + PDF download button (Blob → revokeObjectURL)
- useBestLayouts.ts — TanStack useMutation hook
- types/best-layouts.ts — manual TS types
- MarketTab.tsx +4 LOC integration после competitors

Pre-push fixes per review:
- XSS escape user-data в PDF HTML
- sold_pct_of_supply — убрать double ×100 (backend уже 0..100)
- alert() → pdfError state
- isNaN → Number.isNaN
- 7 inline-styles → Tailwind utilities

Phase 2.1 complete. Phase 2.2 (layout_type/balcony_count via B2B Объектив
#52) — follow-up issue.
lekss361 closed this pull request 2026-05-16 09:39:39 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#198
No description provided.