Корень жалоб «дизайн не премиум»/«бары самодельные» — не в WeasyPrint, а в
execution gap: CSS объявлял font-family DejaVu Serif/Sans, но ни один
@font-face не embedded — документ всегда рендерился на системном
fallback-шрифте. Price-range bar был буквально <div> с фиксированной
64%/14% заливкой, никак не связанной с реальными числами диапазона.
Подтверждено 4 параллельными прототипами (ReportLab/Typst/Playwright/
улучшенный WeasyPrint) на одном и том же образце: WeasyPrint с честным
шрифтом и настоящим графиком визуально не уступает более дорогим
альтернативам (Chromium, Typst) при нулевом новом риске в Docker.
Изменения:
- PT Sans (400/700) + PT Serif (700, только для заголовков секций) —
base64 @font-face из локальных .ttf (app/services/exporters/fonts/,
OFL-лицензия ParaType), embedded один раз при импорте модуля.
- Обязательный WeasyPrint FontConfiguration() — без него @font-face
молча игнорируется и рендер откатывается на DejaVu (реальный баг,
найденный и исправленный в процессе).
- _price_range_bar (декоративный div) → _price_range_chart_svg —
matplotlib (Agg backend, объектный Figure() API, не pyplot — потокобезопасно
для конкурентных PDF-запросов в FastAPI threadpool) bullet-chart на
реальном диапазоне значений, embedded как SVG data URI.
- url_fetcher теперь передаётся в HTML() и CSS() конструкторы, а не в
write_pdf() — в WeasyPrint 69 write_pdf(url_fetcher=...) молча
игнорируется, из-за чего SSRF-guard фактически не применялся к
CSS-ресурсам (@font-face src). Побочная находка при верификации, не
связанная с шрифтами/графиком напрямую, но тот же вызов кода.
- pyproject.toml: httpx[socks]>=0.27.0 — тянет socksio, socks5:// прокси в _probe_current_ip работает прозрачно
- admin.py list_scrape_runs_unified: status Literal["done","running","banned","zombie","failed","cancelled"] вместо str|None — невалидные значения → 422
- avito_houses.py save_house_catalog_enrichment: гард ext_id=0 + нет адреса → skip without DB touch; два id-less novostroyka URL не схлопываются в bogus avito:0 в house_sources
- тесты: test_unified_runs_invalid_status_422 + test_unified_runs_valid_statuses_200; test_save_house_catalog_enrichment_skips_zero_ext_id_no_address + *_with_address_persists
Подключаем tradein-backend к GlitchTip (Sentry-совместимый, уже на
сервере). Необработанные исключения FastAPI улетают в дашборд —
баг estimate-500 (#388/#389) был бы виден сразу с трейсбеком.
- sentry-sdk в зависимостях.
- main.py — sentry_sdk.init(), env-gated: пусто GLITCHTIP_DSN → off.
- config.glitchtip_dsn + GLITCHTIP_DSN в docker-compose.prod.yml.
- traces_sample_rate=0 (только ошибки), send_default_pii=False.
Активация: создать проект в GlitchTip → DSN в .env.runtime.
Фото квартиры прикрепляются к оценке, хранятся в Postgres (bytea) —
без отдельного файлового тома, уезжают вместе с бэкапом БД.
- data/sql/007_estimate_photos.sql — таблица estimate_photos
(FK на trade_in_estimates, ON DELETE CASCADE).
- POST /estimate/{id}/photos — multipart-загрузка (image/*, ≤10 МБ,
≤12 фото на оценку).
- GET /estimate/{id}/photos — список метаданных.
- GET /estimate/{id}/photos/{photo_id} — отдать содержимое.
- python-multipart добавлен в зависимости (FastAPI UploadFile).
Фронтенд (file-drop в форме) — отдельным PR.