style(sales): Pint — порядок импортов в SalesClientsController Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
5.7 KiB
Поиск клиентов был мёртв с 15.07 — CSP резал встроенный скрипт
Дата: 18.07.2026 · Где: моя/sales-finder (служба /opt/sales-finder, lk.liderra.ru/finder/)
Симптом (со слов владельца)
«Вкладки по отраслям пропали», «выбираю — не работает». На скриншоте: выбран раздел («Автотовары», потом «Медицина»), второй список не появляется, кнопка «Собрать список» серая.
Корень
Вся логика страницы жила во встроенном <script> внутри HTML (плюс атрибут
onsubmit="return confirm(...)" у крестика удаления). 15.07.2026 в 10:46 UTC службу завели
за nginx lk.liderra.ru (location ^~ /finder/, SF_BASE_PATH=/finder). Там на уровне
server висит
script-src 'self' https://smartcaptcha.cloud.yandex.ru https://yastatic.net https://*.yandex.ru https://*.yandex.net
— без 'unsafe-inline', без nonce/hash. Внутри location своих add_header нет, значит
CSP наследуется целиком. Браузер молча не исполняет ни встроенный скрипт, ни on*=-атрибуты.
style-src при этом содержит 'unsafe-inline' — поэтому страница выглядела целой, но была мёртвой.
Подтверждение датой: из 87 прогонов все done, последний создан 14.07.2026 10:24 UTC —
за день до правки nginx. После неё — ни одного нового сбора.
Второй, тихий дефект того же корня: подтверждение удаления не срабатывало → крестик ✕ удалял список молча с первого клика.
Про «вкладки по отраслям»: их не было никогда. До 14.07 разделы 2ГИС показывались группами
<optgroup> в одном списке (коммит 87db3bc5), 14.07 коммит c864fbd0 заменил их каскадом
«раздел → ниша → глубже». Это отдельный вопрос UX, к поломке отношения не имеет.
Что сделано
- JS вынесен в
web/static/app.js, отдаётся маршрутомGET /app.js(application/javascript,Cache-Control: no-cache). Файл со своего домена CSP пропускает по'self'— защиту портала не ослабляли. - Данные (дерево рубрик, карта «уже собрано», адрес просмотра) едут в data-атрибутах
#cascade(data-catalog/data-done/data-viewurl) — атрибуты CSP не касается. Выбран этот способ, а не<script type="application/json">, чтобы не зависеть от трактовки «неисполняемых» script-блоков браузерами. onsubmit-подтверждение заменено делегированным обработчиком вapp.js.
Проверка
- Тесты: 252 зелёных (
pytest), из них 13 новых/переписанных — сначала RED, потом GREEN. Ключевые: нет<script>иon*=в HTML, данные читаются из атрибутов,/app.jsотдаётся как JS сno-cache. - Живой браузер под боевым CSP: локальный двойник с точь-в-точь прод-заголовком
(Playwright): выбор «Медицина» → появился второй список, «Частные стоматологии» → третий,
rubric=chastnye-stomatologii, кнопкаdisabled=false, 85 зелёных городов, встроенных скриптов 0, в консоли только 404 favicon. - На бою после выката: страница 200, встроенных
<script>0,on*=0,<script src="/finder/app.js">,/finder/app.js→ 200application/javascriptno-cache.
Выкат
Прод не под git → перед копированием сверил sha256: боевые ui.py/app.py были байт-в-байт
равны HEAD (чужого на проде не было). Бэкап /tmp/sf-backup-csp-20260718-113602.tgz +
копии /tmp/ui.py, /tmp/app.py. Скопированы 3 файла, chown www-data,
systemctl restart sales-finder (11:37:08 UTC, стартовала чисто).
Откат: распаковать бэкап в /opt/sales-finder/web и перезапустить службу.
Грабли на будущее
- 🪤 Любая страница за
lk.liderra.ruне имеет права на встроенный JS иon*=-атрибуты. Отдельная служба за прокси наследует CSP портала целиком. - 🪤 Проверка «служба жива + логов ошибок нет» ничего не доказывает для фронта: поломка случается в браузере, до сервера запрос не доходит. Смотреть глазами/браузером.
- 🔑 Дата последнего успешного действия в БД — хороший датчик: разрыв совпал с правкой nginx с точностью до дня.