From 434341e98f9801e06007a421d4ff903c80457fe8 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 16 May 2026 12:50:10 +0300 Subject: [PATCH] fix(layouts): address review-bot minor items (#113 PR D) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (parcels.py + layout_tz_pdf.py): - 🟡 /best-layouts/pdf 404 handling: catch ValueError → 404 (consistent с JSON endpoint handling геометрия не найдена) - 🟡 XSS defense-in-depth: html.escape() для r.room_bucket, r.area_bin, m.room_bucket в HTML row generators Frontend (BestLayoutsBlock.tsx): - 🟡 data_window dates → ru-RU locale (toLocaleDateString) - 🟢 target_total_flats clamped к [1, 10000] перед submit (paste-bypass на client защищён, backend Pydantic le=10000) Tests: 5+27 backend pass, frontend tsc/lint/build clean. --- backend/app/api/v1/parcels.py | 2 ++ backend/app/services/exporters/layout_tz_pdf.py | 6 +++--- frontend/src/components/site-finder/BestLayoutsBlock.tsx | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index de982a43..075983ab 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -2161,6 +2161,8 @@ async def get_parcel_best_layouts_pdf( ) except HTTPException: raise + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc except Exception as exc: logger.error("best_layouts PDF endpoint failed for %s: %s", cad_num, exc) raise HTTPException(status_code=500, detail="Internal server error") from exc diff --git a/backend/app/services/exporters/layout_tz_pdf.py b/backend/app/services/exporters/layout_tz_pdf.py index 2e169630..96178212 100644 --- a/backend/app/services/exporters/layout_tz_pdf.py +++ b/backend/app/services/exporters/layout_tz_pdf.py @@ -51,8 +51,8 @@ def render_layout_tz_pdf( top_rows = "".join( "" f"{r.rank}" - f"{r.room_bucket}" - f"{r.area_bin}" + f"{_html.escape(r.room_bucket)}" + f"{_html.escape(r.area_bin)}" f"{r.velocity_per_month:.1f}" f"{r.avg_area_m2:.1f}" f"{_price_cell(r.avg_price_per_m2_rub)}" @@ -64,7 +64,7 @@ def render_layout_tz_pdf( # Recommendation mix table rows mix_rows = "".join( "" - f"{m.room_bucket}" + f"{_html.escape(m.room_bucket)}" f"{m.pct}%" f"{m.abs_units if m.abs_units is not None else '—'}" f"{f'{m.avg_target_area_m2:.1f}' if m.avg_target_area_m2 is not None else '—'}" diff --git a/frontend/src/components/site-finder/BestLayoutsBlock.tsx b/frontend/src/components/site-finder/BestLayoutsBlock.tsx index 79f0ba4c..63224696 100644 --- a/frontend/src/components/site-finder/BestLayoutsBlock.tsx +++ b/frontend/src/components/site-finder/BestLayoutsBlock.tsx @@ -411,7 +411,8 @@ function RecommendationCard({
Основано на {rec.based_on_obj_count} ЖК ·{" "} {rec.based_on_total_deals.toLocaleString("ru-RU")} сделках · период{" "} - {rec.data_window_start} — {rec.data_window_end} + {new Date(rec.data_window_start).toLocaleDateString("ru-RU")} —{" "} + {new Date(rec.data_window_end).toLocaleDateString("ru-RU")}
@@ -445,7 +446,10 @@ export function BestLayoutsBlock({ cadNum, selectedCompetitorObjIds }: Props) { ? selectedCompetitorObjIds : null, min_velocity_per_month: minVelocity, - target_total_flats: !Number.isNaN(parsed) && parsed > 0 ? parsed : null, + target_total_flats: + !Number.isNaN(parsed) && parsed > 0 + ? Math.min(Math.max(parsed, 1), 10000) + : null, }; }