Header/footer сидели В ПОТОКЕ каждой секции (~110pt/стр.) → cover переполнялся
(advice-таблица уезжала на почти-пустую стр. 2), footer listings-секции сиротел
на полностью пустую стр. 4 → отчёт 6 страниц вместо 4.
Fix: header+footer вынесены в running @page margin-boxes (position:running() +
@top-center/@bottom-center) — рендерятся в полях каждой страницы, не занимая
поток. Chrome строится ОДИН раз в generate_trade_in_pdf (else-ветка, не
insufficient_data) и оборачивается в .running-header/.running-footer; из 4
билдеров inline-вызовы header/footer убраны + очищены осиротевшие переменные.
Top-margin 20→25mm под running-header. Дисклеймер offer-страницы перенесён из
footer page_note в inline <p> (сохранён). Убрана дубль-строка advice на cover
(«до 15% при самопродаже» — детально расписана на стр. 04).
Верифицировано рендером реального WeasyPrint 69 в прод-образе: ровно 4 чистые
страницы, бренд МЕРА в шапке/футере всех страниц, «Год постройки —», дисклеймер
на месте. ruff clean; test_pdf_security 32/32; test_trade_in_pdf_dual_price 25/25.
Отчёт печатал измерения, которых не делал:
1. Выдуманный «срок продажи 4–118 дней». _days_on_market_range при
отсутствии реальных days_on_market (~48% оценок) возвращал хардкод
(4, 118), а обложка и страница объявлений рисовали его как
измеренный срок экспозиции. Теперь функция возвращает None, и оба
call-site'а не рисуют срок (show_days=False) — как уже делает
страница сделок.
2. № отчёта хардкодил «EKБ» для любой оценки — объект в Серове /
Нижнем Тагиле получал екатеринбургский код на обложке, в шапках и
футере. Префикс теперь выводится из target_address (ЕКБ / НТ / СЕР /
…); неизвестный город → нейтральный «МЕРА», а не ложный «EKБ».
3. Статичный «сделки на 10–18% ниже» противоречил рассчитанному в том
же PDF «−N%». Совет на обложке и баннер на странице сделок теперь
ссылаются на реальный _discount_pct (тот же, что chip «−N%»), с
нейтральной формулировкой когда дисконт неизвестен. Удалён ложный
хвост «(Екатеринбург, 2026)».
Тесты: +11 кейсов (suppression (4,118), city-aware № отчёта,
computed-discount вместо «10–18%»). Все 23 проходят, ruff чисто.
«Этот отчёт онлайн» и QR-код в PDF вели на
gendsgn.ru/trade-in/trade-in?id=… — двойной /trade-in/: PUBLIC_URL
на проде уже = https://gendsgn.ru/trade-in, а код добавлял /trade-in
ещё раз → ссылка/QR давали 404.
Плюс «Балкон / лоджия» печатался как «1»/«0» (сырое булево) вместо
«Есть»/«Нет».