Принятые решения

Что решено не делать и почему. Страница существует, чтобы очевидная идея не приходила второй раз: каждая из них уже приходила, была проверена и отклонена — с причиной.

Формат записи: решение — почему — что было бы иначе.

Архитектура и API

Поля vetted_status, vetted_user, vetted_datetime из API не удаляются. Парк станций не обновится целиком никогда, а часть клиентов читает именно их (инвариант 7, см. Контекст для ИИ-агента). Они помечены deprecated в схеме; дата снятия появится, когда телеметрия покажет, что их больше никто не запрашивает. Иначе: станция, которая не просила ничего менять, получает ответ без знакомых полей.

DEFAULT_PAGINATION_CLASS в DRF не вводится. Ответ /api/satellites/ — голый массив, и клиент станции разбирает его как массив; общий пагинатор сменил бы тело на {count, results} для всех эндпоинтов разом. Вместо него — тест, который требует у каждого ViewSet либо pagination_class, либо явный лимит с причиной. Иначе: одна строка в настройках ломает контракт v1.

/health/ не заводится. Есть /ready/ (база и применённые миграции), его читает healthcheck контейнера. Полной проверки зависимостей никто не потребляет: мониторинга, который бы её опрашивал, у проекта нет. Иначе: эндпоинт, который никто не читает, и который надо поддерживать.

Ретраи Celery стоят на обёртках в network/celery.py, а не на самих задачах. Beat зовёт обёртку, а не shared_task напрямую, и retry() на вызванной напрямую задаче не срабатывает. Иначе: настройка, которая выглядит рабочей и молчит.

Зависимости

django-shortuuidfield остаётся. Снять его через SeparateDatabaseAndState нельзя: миграции 0001, 0008 и 0017 импортируют shortuuidfield.fields на уровне модуля, а Django загружает весь граф миграций при каждом migrate — пакет обязан остаться установленным, даже если поле в состоянии модели заменить на CharField. Единственная альтернатива — squash, а он запрещён (см. ниже). Иначе: migrate падает на импорте у любого, кто поднимает базу с нуля.

satellite.js остаётся на версии 6. В 7 пакет стал ESM-only, без UMD-сборки, а скрипты портала подключаются классическими тегами без сборщика. Иначе: страницы с расчётом пролётов перестают работать до появления бандлера.

Squash миграций base не делается. Стенды и станции обновляются вразнобой; squash ломает любую базу, которая остановилась внутри схлопнутого отрезка. Правило и шаблон батчей — в Как вносить изменения. Иначе: часть парка не мигрирует и восстанавливается только из дампа.

Фронтенд

Каталоги спутников и передатчиков не переводятся на serverSide: true. Данные и так приходят ajax-фидом, <tbody> в шаблоне пустой, и приёмка «страница каталога меньше 200 КБ» выполнена. Остаток выигрыша — 80–120 КБ gzip — стоил бы сортировки на сервере, счётчиков, переписывания восьми тестов и переноса группировки передатчиков. Иначе: неделя работы ради страницы, которая и так грузится.

Сборка статики включается только двумя переменными сразу: COMPRESS_ENABLED=True и COMPRESS_OFFLINE=True. Одна первая ломается на следующем же деплое: collectstatic --clear стирает CACHE/, а кэш компрессора в Redis всё ещё называет стёртые файлы. Иначе: страницы без стилей и 404 на бандлах после выкатки.

Имена кнопок — из Bootstrap, а не из палитры. btn-primary, btn-info, btn-secondary, btn-success, btn-danger и их outline-пары плюс btn-sm/btn-lg; btn-orange, btn-blue, btn-small и прочие цветовые имена запрещены тестом. Роль действия определяет вариант: удаление сохранённых данных — всегда btn-danger. Иначе: одно и то же действие красное на одной странице и синее на другой.

Сегменты полосы успешности станции не доводятся до 24px (WCAG 2.5.8). Ширина сегмента — это и есть данные: 8,7 % неуспешных нельзя нарисовать шириной в 24px, не соврав. Здесь работает исключение «Essential». Иначе: полоса показывает не то соотношение, которое описывает.

Две навигации DataTables с одинаковой подписью (landmark-unique) остаются. Пагинация рисуется дважды, сверху и снизу таблицы, потому что так задана строка dom; крючка для подписи nav библиотека не даёт. Иначе: постобработка разметки после каждой перерисовки таблицы.

Безопасность

unsafe-eval снят из script-src полностью. Он пришёл с mapbox-gl 0.40 в 2017 году; в 2.15 ни eval, ни new Function не вызываются — проверено по всему static/lib. Аварийный клапан на случай неожиданностей — CSP_STYLE_SRC="'self','unsafe-inline'" в .env сервера; на страницах с плеером он не действует, там nonce. Иначе: политика разрешает выполнение строк на всех страницах ради библиотеки, которой это не нужно.

/api/jobs/ не ограничен троттлингом. Ключ авторизованного клиента — владелец, один на все его станции, а интервал опроса у старого клиента настраивается: общий лимит на владельца отрезал бы станции, которые ничего не нарушают. Лимиты стоят только на запись — приём SIDS и статус станции. Иначе: станция, которая не обновлялась, начинает получать 429 на штатной частоте.