fix(layouts): address review-bot minor items (#113 PR D)

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.
This commit is contained in:
lekss361 2026-05-16 12:50:10 +03:00
parent 5878596bc5
commit 434341e98f
3 changed files with 11 additions and 5 deletions

View file

@ -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

View file

@ -51,8 +51,8 @@ def render_layout_tz_pdf(
top_rows = "".join(
"<tr>"
f"<td>{r.rank}</td>"
f"<td>{r.room_bucket}</td>"
f"<td>{r.area_bin}</td>"
f"<td>{_html.escape(r.room_bucket)}</td>"
f"<td>{_html.escape(r.area_bin)}</td>"
f"<td>{r.velocity_per_month:.1f}</td>"
f"<td>{r.avg_area_m2:.1f}</td>"
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(
"<tr>"
f"<td>{m.room_bucket}</td>"
f"<td>{_html.escape(m.room_bucket)}</td>"
f"<td>{m.pct}%</td>"
f"<td>{m.abs_units if m.abs_units is not None else ''}</td>"
f"<td>{f'{m.avg_target_area_m2:.1f}' if m.avg_target_area_m2 is not None else ''}</td>"

View file

@ -411,7 +411,8 @@ function RecommendationCard({
<div style={{ fontSize: 11, color: "#9ca3af" }}>
Основано на {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")}
</div>
</div>
</div>
@ -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,
};
}