1514991ceb
style(sales): Pint — порядок импортов в SalesClientsController Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
75 lines
5.7 KiB
Markdown
75 lines
5.7 KiB
Markdown
# Поиск клиентов был мёртв с 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` → 200 `application/javascript` `no-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
|
||
с точностью до дня.
|