7c8618becc
Живой замер 05.08.2026: любой вошедший клиент, запросив несуществующий или чужой номер, получал в ответ «No query results for model [App\Models\AdCampaign].» Три беды в одной строке. Английский язык — клиенту он ничего не говорит. Названы внутренние имена: и класс, и раскладка папок портала. И сообщение подтверждало, что за номером стоит именно кампания, — а на чужой номер портал обязан отвечать ровно так же, как на несуществующий, ничего не подтверждая. Само имя класса не отмычка, но экономит время тому, кто ищет вход: видно, на чём написан портал, как называются сущности и где их искать. Теперь на любой ненайденный путь, просящий JSON, уходит «Не найдено.» — и на чужой номер ответ байт в байт тот же, что на несуществующий. В журнал прежний текст пишется без изменений: разработчику он нужен. Место общее на весь портал: так отвечал КАЖДЫЙ firstOrFail() во всех разделах. Ловушка: обработчик на ModelNotFoundException не срабатывает никогда — Laravel успевает завернуть её в NotFoundHttpException раньше. Поймано тем, что сторож остался красным с тем же самым английским текстом. Второе: под слотом баннера есть красная подпись — единственное место, где клиент узнаёт, почему картинка не загрузилась. Сторожа на неё не было ни одного. Пропади она — заметить было бы некому: клиент жмёт «загрузить», ничего не появляется, и никакого объяснения. Написаны три: подпись показывает слова сервера, а не общую отговорку; пока отказа нет, подписи нет вовсе; сервер промолчал — портал всё равно объясняет. Ловушка: extractErrorMessage достаёт слова сервера только у настоящей ошибки axios. Подделка без isAxiosError проваливается в общую отговорку, и сторож зеленеет, не проверив того, ради чего написан. Прогоны: 3909 тестов Feature зелёные, vue-tsc чисто. Заодно промт смене 21 дописан утренними итогами: выкат сделан, восемь карточек пройдены, снята ложная тревога про главную ветку, добавлен разбор чужого слияния.