Ruff в CI читает pyproject.toml из корня и сортирует импорты; локальный прогон
из каталога backend этого правила не видел.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка «веб и TURN на разных адресах» ушла вместе с самим правилом, проверка запуска стека теперь ждёт coturn среди обычных сервисов. doctor спрашивает про relay только на поднятом стеке: его запускают и до первого деплоя. Из исключения по bind-mount убран coturn — сертификатов он больше не монтирует.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
По журналу с боевого: на голосовое MAX присылает message_created, в котором `message` отсутствует целиком — разбирать нечего, и реплика клиента пропадала. Теперь на такое событие транспорт забирает последние сообщения тех чатов, где что-то происходило рядом с ним, и прогоняет их через обычный разбор. Повторы безвредны: входящие отсеиваются по идентификатору сообщения.
Проверено: два новых теста транспорта (с телом — лишних запросов нет, без тела — сообщение доезжает), тесты разбора и backoff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Звонок через симметричный NAT или VPN без relay не соединяется — значит relay должен стоять из коробки, а не быть отдельным профилем с выделенным IP и сертификатом. Coturn поднимается вместе со стеком на том же адресе: порт 3478 не спорит с 80 и 443, TURN-over-TLS выключен, потому что он и требовал второго адреса.
Адреса relay и STUN считаются от адреса установки и появляются в настройках сами; вписанное владельцем по-прежнему побеждает. Deploy и doctor больше не требуют второго публичного адреса, README обоих языков переписан: вместо «опционально, выделенный IP» — какие порты открыть.
Проверено: тесты TURN (включая четыре новых на автонастройку), тесты API звонков и адреса установки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Подсказка про «Общую очередь» и «На мне» использовала заметку ленты переписки, а у той отрицательный отступ сверху — текст затягивало на строку вкладок, и он ложился поверх названий. У подсказки списка теперь своя строка под вкладками с отступами и разделителем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Число на пункте «Чат» — это очередь к оператору. Считалось оно по всей организации и вместе с архивными диалогами: сотрудник видел в бейдже то, чего нет в его списках, и до нуля счётчик не опускался. Теперь граница та же, что у счётчиков вкладок списка: видимость сотрудника и без архивных.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кнопка «Завершить» у оператора обязана заканчивать звонок из любой живой фазы. Отмена умела только REQUESTED/RINGING, и после того как клиент принял вызов, а соединение не установилось (обычное дело за NAT), оператор получал 409 и звонок висел. Фазу выбирает finish_call — тот же код, что и на стороне клиента; в интерфейсе кнопка больше не молчит при ошибке, а заканчивает звонок вторым путём.
Фото контакта из Telegram и MAX скачивается и хранится у нас, а отдаётся своим адресом: страница рабочего места живёт под CSP «img-src 'self'», и ссылка на CDN мессенджера до экрана не доезжала — оператор видел инициалы. Источник запоминается, поэтому фото качается один раз; у Telegram оно спрашивается отдельным запросом, которого в апдейте нет.
Голосовое из MAX с незнакомой формой вложения больше не пропадает: раньше такое сообщение уходило в никуда, теперь оператор видит его заглушкой, а в журнал попадает сам payload. В журнал же пишется причина, по которой диалог сразу уходит в очередь: у канала нет активного AI-агента.
Проверено: 83 теста звонков, 6 новых тестов фото контакта, тесты ingest, вложений и голосовых, typecheck рабочего места.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
«Удалить диалог» удаляет: переписку, вложения, звонки и уведомления о нём. Право только у владельца и администратора, писать в удалённый диалог некуда, лента виджета у клиента обнуляется по признаку reset в поллинге.
База знаний: категории слева сворачиваются и разворачиваются (свёрнутое помнит браузер), в подвале появился выбор «На странице» 20/50/100 — общий для всех списков компонент.
Окно «Знания» карточки агента разложено по категориям и порталам со счётчиками и «Выбрать все»; набор материалов грузится целиком, а не первой страницей.
Пилюля онбординга перетаскивается и помнит место. Голосовое в веб-чате нарисовано плеером оператора без расшифровки, рисунок волны вынесен в общий пакет. Убрана горизонтальная полоса прокрутки в меню меток.
Проверено: typecheck internal-ui и web-chat, backend-тесты диалогов, виджета и i18n.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Создавать недостающие уровни categoryPath в транзакции документа и повторно использовать существующие категории. Разрешить импорт новых путей в предпросмотре, обновить переводы и baseline.
Исправить конфликт ширины кнопки создания метки со стилями меню. Выделить метки, заметку и план импорта в отдельные компоненты. Проверено: 13 backend-тестов, 7 frontend-тестов, сборка и браузерный предпросмотр файла из 66 документов.
Логотип организации в сайдбаре без обводки, крупные загрузчики системы и
портала, загрузчик портала сразу по центру, подпись «Работает на Chatballs»
в футере порталов.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Логотип организации в сайдбаре рисовался с рамкой и подложкой — снято,
знак стоит на прозрачном фоне.
Загрузчик портала до прихода манифеста и темы появлялся в левом верхнем
углу и только потом прыгал в центр: его стиль лежал в CSS портала, а этот
чанк грузится лениво, и первый кадр шёл без стилей. Стиль перенесён в
общий state.css, слой фиксирован на весь экран и центрирован независимо
от отступов документа. Загрузчик системы на экране входа в сессию и
загрузчик портала увеличены.
В футере всех шаблонов портала — подпись «Работает на Chatballs» со
ссылкой на chatballs.ru.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Тарифные лимиты сняты (ADR-CHATBALLS-0042), дневного бюджета в коде нет,
но два текста продолжали о них говорить.
Уведомление AI_STOPPED шлётся только при ошибке провайдера — подпись
«или лимитом» вводила в заблуждение. Подсказка первого шага онбординга
обещала учёт лимитов, которого не существует: расходы считает провайдер
по своему тарифу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ход обновления виден в интерфейсе: шаги установки, время, итог с кнопкой
перезагрузки. Перезагрузка страницы во время установки больше не теряет ход.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Установка перезапускает сам бэкенд, который о ней и рассказывает, — и до сих
пор человек об этом не узнавал: признак «идёт установка» жил в useRef и
исчезал при перезагрузке страницы, прогресса не было вовсе (одна строка
текста), а готовый результат карточка «Обновления» не показывала совсем.
Запустив обновление, человек оставался перед экраном без единого признака
того, что что-то происходит.
Теперь ход установки — отдельный блок: шаги (запуск, файл релиза, образы,
перезапуск) с отметками пройденного, текущий шаг с пульсом, номер шага и
время с начала. Молчание бэкенда — это и есть перезапуск, так он и подписан.
Метка установки живёт в sessionStorage, поэтому перезагрузка страницы
(в том числе во время лежащих сервисов) ход не теряет, а время считается от
момента запроса, а не от перезагрузки. Когда версия встала, карточка и полоса
показывают итог с кнопкой «Обновить страницу» — и молчат, если страница уже
открыта в новой версии. Прогресс появляется по нажатию кнопки, а не по ответу
сервера. Полоса наверху молчит, пока карточка «Обновления» на экране.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Очередь к оператору с честным временем ожидания, эскалация забытых диалогов,
уведомления в браузер, присутствие операторов и визард «Начало работы».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# VERSION
Очередь помнит, с какого момента ждёт клиент (waiting_since), и «дольше всех
ждущий» больше не считается по последнему сообщению: клиент, напомнивший о
себе, уезжал в конец очереди. Постановка в очередь сведена в одно место
(conversations.queue) вместо шести копий одного правила.
Уведомление видно тем же, кому виден диалог: раньше оно фильтровалось только
правом, и оператор чужой группы получал оклик с именем клиента и куском
переписки по диалогу, который не может открыть. Получатели считаются одним
запросом вместо запроса на каждого сотрудника.
Уведомления доезжают событием по сокету, а не следующим опросом: сокет поднят
на уровень оболочки и работает на любом экране. Появились уведомления самого
браузера — без service worker и Web Push, чтобы не тащить на self-hosted
зависимость от чужого push-сервиса.
Настройка «о чём звать» стала одна на все транспорты (NotificationPreference),
набор событий приведён к макету: «новый диалог» и «клиент запросил оператора»
разделены, добавлены «назначили на меня» и «долго ждёт человека».
Забытый диалог больше не тонет в тишине: свип напоминает группе, потом всем,
потом руководству; сроки — настройка организации в новом разделе «Когда звать
на помощь». Назначение стало осмысленным актом — назначенного зовут лично, у
него есть срок, и не взял — диалог возвращается всем.
Присутствие берётся из открытого сокета и подсказывает двум местам: очереди —
что напоминать некому, и выбору ответственного — кто сейчас за рабочим местом.
Макет: design/baseline/Очередь и уведомления.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Суффикс приходил из переменной окружения с умолчанием localhost, а задать её
в коробке негде: у продукта нет .env, и production-манифест её не передаёт.
На живой установке диалог создания портала предлагал help.localhost — адрес,
который не резолвится и не получает сертификат. Источник теперь тот же, что у
IPv4 для A-записи: адрес из мастера первого запуска. Переменная осталась
переопределением для контуров с собственной конфигурацией.
Пока установку открывают по IP, домена нет вовсе: hosted-адрес не собирается,
диалог говорит об этом и не даёт создать портал вместо выдачи мёртвой ссылки.
Порталы, успевшие осесть на .localhost, миграция переводит на домен установки —
такие ссылки всё равно никуда не вели.
Заодно поле адреса в диалоге: без color и background оно красилось системным
белым и светилось в тёмной теме. Скин по базлайну — 36px, текст --n-1, суффикс
на --n-9.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ошибка прожила три релиза именно потому, что её нечем было поймать: сценарий
выглядел правильно, а Docker молча передавал имя команды аргументом в цикл
ожидания. Тест разбирает оба сценария updater'а и требует --entrypoint у
каждого запуска $SELF_IMAGE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ошибка прожила три релиза именно потому, что её нечем было поймать: сценарий
выглядел правильно, а Docker молча передавал имя команды аргументом в цикл
ожидания. Тест разбирает оба сценария updater'а и требует --entrypoint у
каждого запуска $SELF_IMAGE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Входная точка образа updater'а — сценарий ожидания запросов, поэтому
`docker run ... chatballs-updater-apply.sh` не запускал установку, а
передавал имя сценария аргументом в тот же цикл. Помощник поднимался без
compose-меток, уходил в вечный sleep и никогда не дописывал статус:
интерфейс крутил установку без конца, а новые запросы отбрасывались как
«уже идёт». Та же ошибка была во втором вызове — копировании compose.yaml
на хост.
Обе команды теперь подменяют входную точку. Плюс страховка в цикле: если
помощник исчез, не дописав результат, установка помечается неудачной.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Входная точка образа updater'а — сценарий ожидания запросов, поэтому
`docker run ... chatballs-updater-apply.sh` не запускал установку, а
передавал имя сценария аргументом в тот же цикл. Помощник поднимался без
compose-меток, уходил в вечный sleep и никогда не дописывал статус:
интерфейс крутил установку без конца, а новые запросы отбрасывались как
«уже идёт». Та же ошибка была во втором вызове — копировании compose.yaml
на хост.
Обе команды теперь подменяют входную точку. Плюс страховка в цикле: если
помощник исчез, не дописав результат, установка помечается неудачной.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Учёт стоимости вызовов остался без потребителя, когда сняли лимиты, и сам
по себе не работал: цену присылает только OpenRouter, для остальных
провайдеров была прайс-таблица из двух моделей и ноль для всех прочих.
Ни одна цифра расхода в интерфейсе не показывалась. Удалены ai/pricing.py,
ai/limits.py, поля LlmInvocation.cost_micros и currency, статус BLOCKED
вместе со строками журнала, ChatResult.cost_micros и чтение usage.cost,
настройки CHATBALLS_AI_PRICING и CHATBALLS_AI_GLOBAL_DAILY_COST_LIMIT_MICROS,
поля aiCostMicros/aiCostPrevMicros в сводке и стоимость в демо-данных.
Обработка входящих ловит теперь только сбой провайдера — лимиту неоткуда
сработать.
Валюта организации удалена целиком: поле принимало единственное значение
RUB, не читалось нигде и ни одна сумма в продукте в ней не считалась.
Убраны колонка с миграцией, валидация «только рубль», payload настроек и
платформенной выдачи, обязательное поле currency в API провижининга,
типы и транспорт на фронте.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кнопка «Показать список» жила только в шапке переписки. При пустом списке
или просто без выбранного диалога переписки нет — свёрнутый список
оказывался не вернуть ничем. Теперь кнопка есть и в пустой области, на том
же месте, что и в шапке. Сценарий закрыт e2e-тестом.
Цвет продукта по умолчанию — зелёный #0f9b8e: он же первый в палитре
акцентов профиля. Прежний синий остался вторым пресетом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
После удаления валюты в ряду полей осталась пустая половина. Теперь ряд
занимают часовой пояс и язык интерфейса: язык был сегментом во всю ширину
карточки, стал таким же селектом, как соседнее поле. Подпись под ним
убрана вместе с ключом словаря.
Форма создания организации и «Настройки → Организация» — одна форма по
смыслу, поэтому у них теперь одна ширина колонки и одни отступы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Дневной бюджет агента (limits.dailyCostUsd) снят вместе с полем. Проверка
работала, а учёт — нет: расход брался из ответа провайдера, а его шлёт
только OpenRouter; для Custom и локальной модели оставалась прайс-таблица
из двух моделей, и на любой другой стоимость записывалась нулём. То есть
на всех провайдерах, кроме OpenRouter, лимит не срабатывал никогда и давал
ложное чувство защиты. Общий лимит установки из переменной окружения
CHATBALLS_AI_GLOBAL_DAILY_COST_LIMIT_MICROS остаётся.
Выбор валюты убран из настроек организации и из формы её создания. Сервер
принимал только RUB, то есть в списке был один вариант, а само поле не
читается нигде: ни одна сумма в продукте не считается в валюте
организации. Колонка в базе остаётся, интерфейс её больше не спрашивает —
сервер проставляет значение сам.
Второй шаг онбординга больше не велит задавать дневной бюджет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Онбординг из восьми шагов: приветствие, визард с точным путём и превью
результата на каждом шаге, тур «Показать где» с подсветкой реального
элемента и финальный экран. Точки возврата — ссылка внизу субменю
«Настроек» и пилюля в углу рабочей области. Вёрстка по макету
design/baseline/Онбординг.
Признак «закрыл» и «прошёл» живёт на членстве человека в организации, а
не в localStorage: требование — показать визард всем, кто его ещё не
закрывал, включая тех, кто работает в установке давно. У каждого он свой,
поэтому один администратор не прячет визард команде. Прогресс шагов
считается по факту настройки, ручное «Далее» его не подменяет.
Заодно все селекты приложения переведены на общее меню: SelectMenu в
shared/ui-controls, поверх него FilterDropdown (фильтры списков) и
SelectField (поля форм). Нативных <select> не осталось, вместе с ними
ушли пять копий скина селекта. Изменения переплетены с онбордингом через
общие файлы (словари, e2e-сценарии), поэтому одним коммитом.
Устаревший design/design-system удалён: источник истины по интерфейсу —
макеты design/baseline/<фича>/*.dc.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обновление видно на установке в пределах четверти часа после публикации; кнопка «Проверить» по-прежнему делает запрос сразу. Интервал переопределяется CHATBALLS_UPDATE_CHECK_INTERVAL_SECONDS.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Кнопка «Добавить организацию» внизу переключателя у логотипа (A1) ведёт на
страницу /organizations/new: логотип, название, часовой пояс, валюта, язык.
Создавший становится владельцем и сразу переключается в новую организацию.
Право — у администратора установки и у владельца или администратора любой
организации; сервер проверяет то же (POST /api/v1/organizations/).
После входа учётная запись с несколькими организациями выбирает, с какой
начать: экран в рамке входа, строки «логотип · название · роль». Прямая
ссылка на организацию экран минует.
tenancy/0035: роль app вставляет организацию только в контексте заранее
выделенного id (как мастер первого запуска) вместо политики «только первая»;
security-barrier каталог invitation_directory — ссылка /join открывается без
контекста, и под ролью app приглашение раньше не находилось вовсе. Тем же
путём язык организации в профиле: членства читаются через каталог входа.
Тесты: создание организации по API, RLS под реальной ролью app (вставка
только в своём контексте, поиск приглашения по токену), e2e переключателя,
страницы создания и экрана выбора. UpdateState исключён из проверки покрытия
демо-набором — одна строка на установку.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Карточка агента рисовала свои выпадающие селекты: «Группа» и «Провайдер»
жили на собственной вёрстке и своём CSS, а рядом, в модалке создания
агента, стоял общий SelectField. Два стандарта одного элемента успели
разойтись в высоте, подложке, размере текста, подписи и фокусе.
Общий SelectField научился тому, из-за чего карточка писала свой: метке
перед значением (точка группы), невалидному состоянию и режиму без прав
правки, где значение показывается текстом. Форм у него две — обычный
селект и бокс, который держит рамку, когда внутрь нужно поставить точку
или текст вместо контрола. Карточка перешла на него, локальный скин
удалён, кольцо фокуса кадра G2 сохранено.
Геометрия базлайна v2 (поле 36px на подложке n-10, текст 13.5px, подпись
12.5/600) стала дефолтом shared/form-controls.css. До этого дефолтом
были 38px на surface-card, а базлайн добирали поверх профиль, настройки
и модалка создания агента — вместе с карточкой четыре копии одного поля.
Копии сняты, мобильные 44px тоже общие. Заодно шеврон встал по центру
поля: его отступ считался от прежних 38px.
Справочник дизайн-системы обновлён следом, чтобы не описывать геометрию,
которой в приложении больше нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Установка раз в шесть часов и по кнопке проверяет страницу релизов
GitHub. Администратор установки видит баннер о новой версии и ставит
её одной кнопкой; карточка «Обновления» — в «Настройки → Платформа».
Установку выполняет отдельный сервис updater с Docker-сокетом:
backend-app общается с ним только файлами в томе chatballs-updates
(heartbeat, request.json, status.json). Updater принимает лишь релизы
своего репозитория с образами по digest и применяет compose.yaml в
одноразовом контейнере-помощнике, после чего кладёт файл в каталог
установки. Запрос установки попадает в аудит.
Релизный workflow собирает образ updater и пришпиливает его в
compose.yaml; версия бэкенда зашивается в образ (CHATBALLS_VERSION).
Миграции updates.0001 и tenancy.0034 (гранты на таблицу состояния).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SVG-логотип организации; воркер больше не засоряет журнал сбоями опроса
подключений и не опрашивает демо-подключения.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
К PNG, JPEG и WebP добавлен SVG. Файл проверяется при загрузке и
отклоняется целиком, если содержит скрипты, обработчики событий,
foreignObject, внешние ссылки в href или url() внутри стилей, DOCTYPE и
сущности — переписывать чужую графику молча хуже, чем попросить другой
файл. При отдаче логотип получает Content-Security-Policy с sandbox и
nosniff как второй рубеж: адрес можно открыть и напрямую.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Подключение с ненастоящим токеном или недоступным сервером писало
предупреждение на каждом цикле опроса, каждые три секунды, на каждое
подключение. Теперь сбой транспорта — исключение PollFailed, а слой пауз
(transports.backoff) пропускает подключение с удвоением паузы от шести
секунд до четверти часа и пишет в журнал только смену состояния: первый
сбой, выход на максимальную паузу и восстановление. Курсор при сбое не
двигается. Демо-подключения из демо-набора помечаются demoSeed и не
опрашиваются вовсе: их токены ненастоящие.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Исправления после установки на чистый хост: «Ошибка загрузки» сразу после
мастера первого запуска (кэш адреса установки в соседних процессах) и
пустое состояние «Базы знаний», прижатое к левому краю.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Коммит 776bc895 при переносе библиотеки на серверную пагинацию удалил
правило контейнера .knowledge-empty, оставив только вложенный блок шириной
480px — он прижимался к левому краю рабочей области. Правило возвращено
как было: flex, по центру, отступ 40px (кадр KB1).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Кэш адреса установки живёт в каждом процессе gunicorn по 10 секунд. Мастер
первого запуска и смена адреса в «Настройках» сбрасывали его только в том
процессе, где выполнялись: соседний отвечал «Invalid host» на первый же
запрос после мастера, и приложение показывало «Ошибка загрузки» до
обновления страницы. Промах по хосту теперь перечитывает строку настроек,
но не чаще раза в секунду на процесс, чтобы поток запросов с чужим Host не
превращался в поток запросов к базе.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GitHub снимает Node.js 20 с раннеров и уже принудительно запускает старые
версии actions на Node.js 24, о чём предупреждает в каждом прогоне. Все семь
actions подняты до мажорных версий, чей action.yml объявляет node24:
checkout v7, setup-python v7, upload-artifact v7, build-push-action v7,
login-action v4, setup-buildx-action v4, action-gh-release v3. Входы и выходы
шагов не менялись.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Несколько организаций на одной установке: переключатель организаций,
приглашения существующих учётных записей и владельца из провижининга,
администратор установки, минимальные привилегии процессов и три тома
секретов. Миграции tenancy 0031–0033, identity 0036–0037; настройки
установки переехали на /api/v1/instance/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Переключатель организаций в сайдбаре (дизайн-базлайн v2, A1) показывает все
организации человека, текущая отмечена, выбор другой пересобирает Shell по
ключу организации. Вход без организации в адресе открывает последнюю
открытую или первую по списку вместо экрана «нет доступа»; предпочтение
переехало в localStorage.
Разделы «Платформа» и «Хранилище файлов» видны только администратору
установки, relay для звонков у остальных менеджеров только на чтение;
настройки установки запрашиваются с /api/v1/instance/.
Ссылка-приглашение /join: вошедший принимает приглашение и попадает в новую
организацию, гость без учётной записи задаёт имя и пароль полями мастера
первого запуска. Ожидающие приглашения показаны в списке сотрудников
строками «Приглашён» с меню «отправить ещё раз» и «отозвать» — теми же
элементами, что строка сотрудника.
e2e: проекту internal-ui задана русская локаль браузера, моки сессии отдают
язык установки — сценарии перестали зависеть от языка машины; добавлены
сценарии переключателя, строки приглашения и регистрации гостя.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Том chatballs-secrets целиком монтировался во все сервисы: публичный
backend-app читал пароли платформенной роли и роли миграций, которая обходит
RLS. Теперь томов три — общий, платформенной роли и роли миграций вместе с
паролем владельца кластера — и каждый процесс монтирует только свои.
Генератор переносит файлы существующих установок: копия, побайтная сверка,
только потом удаление из общего тома; повторный запуск ничего не трогает,
пароли не меняются. doctor на работающем стеке проверяет, что backend-app
паролей не видит. Воркер получает алиас platform флагом
CHATBALLS_DB_PLATFORM_ALIAS. README перечисляет три тома в бэкапе.
nginx (dev-шлюз и production-фронтенд) резолвил имя сервиса один раз при
старте: после пересоздания контейнеров старый адрес backend-app достался
backend-platform, и туда уходил WebSocket приложения. Upstream задан
переменной с resolver на DNS Docker. ADR-CHATBALLS-0048 §3, §5.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>