- каталог code/custocrm → code/chatballs; CLI custocrm → chatballs; пакеты
hub_platform → chatballs, hub_backend → chatballs_backend (app labels прежние)
- переменные CUS_* и CUSTOCRM_* → CHATBALLS_*; образы chatballs-*; compose-проект
и база chatballs (были edevs_hub); роли Postgres chatballs_* (были custocrm_*);
схема RLS chatballs и GUC chatballs.organization_id; cookie chatballs_*
- CI: APP_DIR code/chatballs, DEPLOY_ROOT /opt/chatballs
- deploy/migrate/rename-to-chatballs.sh — миграция существующей установки без
потери данных: остановка старого проекта, .env (с резервной копией),
переименование суперпользователя initdb через временную роль, остальных ролей,
базы, схемы и функции RLS; проверено на локальном стеке
- dev-стек хранит Postgres в bind-mount data/postgres, как prod (именованный том
compose.dev был устаревшим снимком и вводил в заблуждение)
- снятие демо удаляет объекты, созданные поверх демо-данных тестировавшим
(звонки по демо-диалогу), вместо падения на PROTECT
- реальные домены *.custocrm.ru и идентификаторы документов не тронуты
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- LogoIcon: два круга и перемычка с вырезами, fill currentColor; в сайдбаре,
рейке, экране входа/мастера, портале помощи — без акцентной/тёмной подложки,
цветом текста темы (--n-1); favicon без фона, цвет по prefers-color-scheme
- видимое имя сервиса — Chatballs (заголовки, тексты, title); внутренние ключи
(localStorage, postMessage, id виджета) пока прежние — переименование проекта позже
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- обои «Chatbolls — Lucide Pattern» (макет владельца): филлотаксис по золотому углу
с тремя вихрями, те же 25 иконок, размеры, прозрачности и цвета двух тем
(серо-зелёная #dedfd9/#5d635d, графитовая #25292e/#f1f2f3); на экранах входа и
за лентой чата; лента принимает фон обоев (--surface-feed)
- верхней панели нет ни у одной роли; заголовок «Настройки» — в самой странице;
«Знания» и «Аудит» — ссылки из «Настроек», навигация менеджера — ровно 6 пунктов
- фото сотрудников: Avatar с img, загрузка/удаление в «Настройки → Профиль»,
в сайдбаре, подписях сообщений, поле «Ответственный», поповере охвата
- карточка контакта (решение 5): описание, компания, город; карандаш у имени —
inline-правка имени/описания/телефона/компании/города
- цвет группы из настроек (свотчи в модалке группы) — точки в дереве, поповере,
строках списка и меню переноса
- композер: кнопка «Эмодзи» (antd Dropdown с сеткой), как в макете
- история контакта (кадр F): тема — первая реплика клиента, «вела Анна Ким» / «AI»
- список: превью без системных событий; сообщения с одинаковым временем — по id
- демо: длительности голосовых, история Марии в архиве (в списке ровно 7 строк),
Дмитрий ждёт 6 мин; БД: SET NULL на FK аудита/outbox к пользователю и права на
каскад удаления — снятие демо runtime-ролью
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- демо «Ателье Норд» повторяет кадры: сотрудники Елена Кузнецова и Анна Ким (фото),
группы Операторы/Поддержка/VIP-клиенты с цветами, агенты Консультант · Поддержка
сайта · Личный кабинет, семь видимых диалогов A–H с точными сообщениями, история
Марии (кадр F), метки строчными; покрытие остальных функций — в архиве и спаме
- голосовые из Mozilla Common Voice 26.0 (ru, CC0): 4 клипа OGG/Opus, расшифровки
совпадают с записью; README с источниками
- HumanUser.avatar (+ хранилище users/ вне тенантного ограждения), профиль:
GET/POST/DELETE /auth/profile/avatar/, коллегам — /employees/<id>/avatar/;
avatarUrl в сессии, сотрудниках, ответственном, авторе сообщения, счётчиках
- Contact.description/company/city, правка из диалога POST <id>/contact/
- EmployeeGroup.color (HEX) в API групп, счётчиках, справочнике, диалоге
- гость виджета называется «Гость · xxxxxx» (кадр M1); гость может представиться в сиде
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- поповер 250px: «Все диалоги · Группы · Агенты · Ответственный», подзаголовки 11/600,
пункты 32 r7, отмеченный пункт primary, счётчики 11.5/600
- цветные точки групп и тонированные бейджи агентов (как в дереве сотрудника) —
и в дереве сайдбара
- охват «Ответственный»: фильтр списка `assigned=<userId>`, счётчики
`assignees` в /conversations/counters/, `code` агента в счётчиках
- меню приложения: специфичность правил поповера, чтобы antd не перебивал
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- снят глобальный min-width 1024 (медиазапросы ≤768 не срабатывали); ограничение
осталось только у страниц вне чата
- M1: мобильная шапка ☰ · «Все диалоги ▾» + «Организация · N диалогов» · аватар 32;
☰ открывает дерево «Диалоги» выезжающим сайдбаром (состояние поднято в Shell);
строки 12/16, аватар 42, шрифты по макету
- M2: шапка ленты «назад · аватар 36 · имя/статус · контакт · ⋯», компактный
композер (поле + кнопка-иконка 38 r10, плейсхолдер «Сообщение…»)
- S2: затемнение ленты при открытой панели, крестик в шапке панели, кнопка
«Контакт» подсвечена только при открытой панели
- S1: пустой список с иконкой в круге 44; без выбранного диалога — только
«Выберите диалог», без панели и композера
- бренд сайдбара: логотип + переключатель организации (153×32, шеврон 14);
дерево показывает «Группы»/«Агенты» и без элементов
- G: меню полей блока «Диалог» — 283px, заголовок «Перенести в группу», галочка
у выбранного, подпись «Диалог без группы видят все сотрудники»; все меню
приложения по макету: 220px, пункты 33 r7, разделитель n-9, тень 0 6 20
- textarea/select наследуют шрифт (композер был моноширинным)
- backend: GET /conversations/directory/ — все группы и активные коллеги для
оператора (сотруднику менеджерские списки закрыты, а переносить/назначать он
может); чат грузит справочник для всех ролей
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Покадровое сравнение кадров A1/A2 и A–H макета с приложением по вычисленным
стилям (размеры, цвета, границы, радиусы, шрифты, композиция) в обеих темах.
Сайдбар: 220px на surface2, шапка «лого 28 + имя», пункты 36/10px без
акцентной полосы, бейдж очереди — жёлтая плашка, профиль с онлайн-точкой и
«роль · e-mail», меню ⋯ (уведомления переехали сюда — верхней панели в чате у
менеджера по макету нет).
Список: 323px, переключатель охвата со счётчиком на подложке n9, сортировка и
«Скрыть список», подсказка «/» в поиске, вкладки-подчёркивание, строка
диалога: канал + агент → имя · время («вчера») → превью с «↩» и непрочитанными →
таймер ожидания · группа · метки (h20, r5); активная строка — n9 без полосы.
Лента: шапка с точкой режима и строкой «статус · канал · агент · группа»,
одно действие «Взять диалог» (решение 2) и «Вернуть AI», звонки — в карточке
контакта; у входящих нет аватара и обводки (msg-in), исходящие — тинт акцента
22% / AI 16% с подписью «AI · Консультант» и «Имя · вы», разделители дней,
системные события-чипы с тоном. Первое сообщение сотрудника атомарно
перехватывает диалог (backend: ConversationMessageView → claim).
Композер: поле сверху, инструменты снизу (голосовое, «Шаблоны»), «Отправить ⏎»,
строка-предупреждение о режиме; кадры D/E — карточка с замком.
Контекст-панель 320px: карточка контакта (аватар 64, плашка канала, имя 20,
поля с копированием, «Позвонить»/«Видеозвонок»), блок «Диалог» (селекты
36px с точками/индикатором, Агент и Режим read-only, метки-чипы с
«+ Добавить», «Начат»), «Заметка» — жёлтая карточка с карандашом, «История» —
карточки с плашкой канала.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Новый LLM-провайдер IntegrationProvider.DEMO (без секрета, проверка всегда
OK): отвечает детерминированно по знаниям агента из системных сообщений
runtime — выбирает предложения с наибольшим пересечением слов с вопросом,
режет служебные заголовки фрагментов и markdown-таблицы; когда знаний нет или
клиент просит человека, завершает ответ токеном передачи оператору по протоколу
runtime. Эмбеддинги детерминированные, расшифровка голосовых — явное сообщение
о необходимости BYOK-провайдера. В UI помечен как демо с ограничением качества.
Демо-набор «Ателье Норд» использует его как провайдера агентов: после
установки AI отвечает сразу, без ключей OpenRouter.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Open source: «развернуть за минуту». Никаких параметров в .env и CLI —
при пустом инстансе браузер показывает мастер (/api/v1/setup/): название
организации, имя, e-mail, пароль владельца и флаг «Установить демо-данные».
Запись идёт на соединении platform (роль app не создаёт организации) через
tenancy/routing.use_database; после первого владельца мастер закрыт (409).
bootstrap_owner и захардкоженные Edevs/Котова из установки удалены
(bootstrap_edevs_owner остаётся тестовым helper'ом).
Демо ставится в организацию установщика; реестр DemoRecord (post_save во
время сида) даёт точное удаление в обратном порядке с разрывом PROTECT-циклов
только между демо-объектами. Установка/удаление — outbox → worker; статус и
витринные учётки (админ + сотрудники разных групп, пароль Chatbolls-Demo-2026)
— карточка «Демо-данные» в «Настройках» (/company/demo/).
Сид переписан под дизайн-базлайн v2 («Ателье Норд»): 6 сотрудников (группы,
блокировка, TOTP, приглашения), 4 агента (активные/черновик/выключенный),
подключения TG/MAX/почта/два веб-виджета (анонимный и авторизованный) с
ошибкой у одного, знания с иерархией категорий и вложениями (md/txt/pdf),
30 дней истории LLM, 13 диалогов во всех состояниях с метками, приоритетами,
заметками, ответственными, историей контакта, спамом и архивом, веб-гость
через настоящую сессию виджета, шаблоны «/», портал поддержки со статьями
(опубликованные/черновик/архив, вторая ревизия, оценки), звонки с метриками и
приглашением, уведомления всех типов, привязки уведомителя. Аватары
контактов — портреты владельца, публичный /api/v1/demo-media/. Голосовые —
слоты под клипы владельца (media/voice/README.md).
Тест покрытия: каждая модель hub_platform получает демо-запись; удаление
возвращает счётчики к исходным. compose-профиль demo-seed и seed-demo.ps1
удалены; seed_demo --organization --apply/--remove для разработки; start.sh
для Linux/macOS; README — раздел «Быстрый старт».
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Пространство agents отсутствовало в tenantNamespaces, запросы карточек агентов
уходили без /organizations/<id>/. Мёртвые channels и access-profiles убраны.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
По спеке навигация менеджера — шесть пунктов, командный центр удаляется.
Удалены features/command, SupportOverviewPage, роуты command/supportOverview
и бэкенд-сводка conversations/command-overview/ (conversations/command.py).
Корень «/» и дефолтный маршрут менеджера — «Чат»; устаревшие
/command и /departments/support ведут в «Чат» и на «Доску».
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Карточка агента (/agents/) — единственный HTTP-слой поверх Channel+AIAgent
(ADR-HUB-0041 §4). Удалены дублирующие роуты channels/ (list/detail/
connections/counters) и ai/agents/ (list/detail/update/activate/deactivate)
вместе с views/serializers/api_inputs и мёртвым сервисным кодом каналов
(create_channel, delete_channel, filter_channels). Тест-чат перенесён в
AgentCardTestChatView (/agents/<id>/test-chat/). Bulk-роут
ai/agents/<aiAgentId>/knowledge/select-category/ сохранён — он адресует
AIAgent, как и knowledge/bulk/agent/.
Фронт: bulk-attach знаний и статей портала берёт агентов из /agents/
(id для bulk — aiAgentId карточки), AgentLinkOption без channelName.
Тесты старых роутов удалены; уникальное покрытие портировано на карточки
(активация без провайдера, семантика knowledgeIds, атомарный откат PATCH,
payload статей портала).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Виджет записывает голосовое (MediaRecorder, зеркало хука internal-ui) и
шлёт multipart в /webchat/messages/; входящее байтами — voice_content в
InboundMessage, скачивание не нужно. Аудио отдаётся по токену сессии
(/webchat/messages/<id>/audio/), в пузыре — нативный плеер. Оператор
может отправлять голосовые и в WEB-канал (доставка поллингом виджета).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Вертикальные секции вместо отдельных разделов: «Организация» и
«Интеграции» упразднены (старые URL ведут в настройки), IntegrationsPage
разобран на IntegrationsSection по роду, добавлено управление группами
(создание одним полем, переименование, удаление). «Ещё» в сайдбаре:
Обзор, Обзор поддержки, Знания, Использование AI, Аудит. Сотруднику —
только профильная часть (тема, акцент, безопасность).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Поиск в списке диалогов: имена контактов — подстрокой, тексты сообщений —
через websearch-запрос с russian-конфигом и выражительным GIN-индексом
conv_message_text_fts; join+distinct заменён на Exists-подзапрос.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Компонент Hint с реестром закрытий в localStorage; первая подсказка —
про видимость диалогов, под вкладками списка, только менеджерам
(сотрудникам подсказки не показываются по §6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
На ≤768px сайдбар в чате скрыт: экран списка с шапкой бренда (логотип,
организация, меню профиля), лента открывается выбором диалога, назад —
стрелкой в шапке. Контекст — по кнопке в шапке (drawer S2 на всю ширину).
Пустой список — текст кадра S1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Сайдбар на узком экране сжимается в рейку (логотип, «Чат», развернуть,
аватар), «развернуть» раскрывает полный сайдбар поверх контента.
Контекст-панель диалога становится drawer'ом с кнопкой в шапке ленты;
закрывается при смене диалога.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Транспорт MAX: /uploads?type=audio -> multipart-загрузка -> отправка
attachment c токеном, с повторами на attachment.not.ready. Диспетчер
send_voice/supports_voice_send переведён на карту провайдеров; запись
в композере доступна для каналов TG и MAX.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sales- и support-диалоги объединены в один экран «Чат» (маршрут /chat,
старые адреса — алиасы). Контекст-панель сама выбирает представление по
identity диалога. У сотрудника нет верхней панели и разделов — сайдбар
это дерево «Все диалоги · Группы · Агенты», управляющее охватом списка;
у менеджера — плоские шесть пунктов макета и группа «Ещё» для служебных
разделов. Роуты уведомлений и карточек обзора на бэкенде — chat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- приём голосовых из Telegram (getFile → скачивание, с прокси) и MAX (url
вложения); файл хранится в tenant-storage, сбой скачивания оставляет
текстовую заглушку; AI голос не разбирает — диалог уходит оператору;
- отдача аудио /conversations/messages/{id}/audio/ с проверкой видимости
диалога; в payload сообщения kind=voice, длительность и расшифровка;
- расшифровка по кнопке через BYOK-провайдера организации (OpenAI-совместимый
/audio/transcriptions, CUS_AI_TRANSCRIPTION_MODEL, по умолчанию whisper-1);
идемпотентна, статусы NONE/READY/FAILED;
- отправка голосового оператором в Telegram (sendVoice, ручной multipart);
- фронт: плеер с волной и длительностью в ленте, превью «Голосовое сообщение
· 0:14» в списке, запись в композере (MediaRecorder: таймер, отмена,
отправка) для Telegram-диалогов, три состояния расшифровки.
Точечные тесты зелёные; фронт tsc 0, vitest 67 OK, build OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- поле ui_theme (светлая/тёмная/системная) и ui_accent (HEX) на пользователе,
эндпоинт /auth/profile/appearance/, значения в session-payload;
- применение через data-theme на <html> и переменную --primary; производные
оттенки (hover/bg/border/text) считаются color-mix'ом и работают в обеих
темах с любым акцентом; antd переключается на darkAlgorithm;
- полный dark-набор токенов из дизайн-базлайна v2 в base.css (нейтрали,
поверхности, статусные полутона, тени) + токены surface-*/border-strong;
- массовая токенизация: ~1050 захардкоженных цветов в 45+ CSS-файлах и все
TSX-словари статусов переведены на var(--…); осознанно остались белый текст
на насыщенных фонах, QR-код, фирменные цвета каналов, аватары, iframe писем;
- карточка «Внешний вид» в настройках: три темы, четыре пресета акцента из
макета и произвольный HEX.
tsc 0, vitest 67 OK, build OK; backend-прогон зелёный (2 падения — артефакт
параллельного редактирования, воспроизведены и закрыты следующим прогоном).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Вторая волна нового дизайна:
- переключатель охвата в заголовке списка диалогов (Все диалоги · Группы с
точками и счётчиками · Без группы · Агенты) — серверная фильтрация,
счётчики из /conversations/counters/;
- шаблоны ответов в композере: кнопка и ввод «/» открывают список с фильтром
по названию, Enter вставляет первый, Esc закрывает;
- блок «Запуск» в сайдбаре менеджера: три шага с автоотметкой по факту
(/company/launch-checklist/), прогресс, primary-действие текущего шага,
«Скрыть» — per-организация в localStorage; исчезает после прохождения.
tsc 0, vitest 67 OK, build OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Первая волна нового дизайна (design/baseline/Chatbolls Baseline v2, решения
владельца 2026-09-04):
- Conversation: приоритет (HIGH/MEDIUM/LOW/NONE), метки (словарь организации,
создание на лету), внутренняя заметка, архив («удалить» = архив, видят и
восстанавливают только администраторы); RLS/парные триггеры (tenancy/0021);
- шаблоны ответов: общие на организацию, CRUD у администраторов;
- серверные фильтры списка: агент, мои, ждущие, поиск, архив; спам скрыт из
вкладок по умолчанию; счётчики дерева фильтров /conversations/counters/;
- фронт: вкладки Все · Мои · Ждут оператора, бейджи приоритета и меток в строке
диалога, блок «Диалог» в контекст-панели (ответственный, группа, приоритет,
метки, заметка) в обоих чатах, «Вернуть в очередь» и «Удалить диалог» в меню.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Тарифы, подписки, entitlements, quotas и managed AI удалены целиком:
- приложение subscriptions снесено (12 таблиц дропаются в tenancy/0020);
- CustoAI и credential_mode удалены (ai/0016): AI только через BYOK-провайдера
организации; активация агента требует настроенного провайдера;
- вычищены все quota/entitlement-гейты: диалоги, порталы, интеграции, продукты,
звонки (p2p-слоты), storage (остаётся технический учёт без лимитов);
- provisioning без плана/подписки; приглашение владельца без активации тарифа;
- эндпоинт и раздел «Тариф и оплата» удалены (фронт + бэк), HasEntitlement снят;
- уведомление LIMIT_REACHED удалено (notifications/0009);
- заодно: чек-лист запуска /company/launch-checklist/ (SPEC-HUB-0031 §5)
и фикс сохранения provider_integration при обновлении агента.
Backend 550 тестов OK (докер, миграции с нуля), frontend tsc + vitest 67 OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- новый агрегированный API /api/v1/agents/: мастер одного шага (имя+группа,
код из имени), объединённый PATCH канала и AI-конфигурации, удаление с
блокерами, активация AI, привязка/отвязка подключений, тест-чат;
- data-миграция ai/0015: DRAFT-агент каждому каналу без агента;
- UI: разделы «Каналы» и «AI-агенты» слиты в «Агенты» (список + карточка-центр
администрирования); legacy-URL редиректят; форма интеграции выбирает агента;
- чистка: features/channels и features/ai/{list,create,detail} удалены,
битые @import styles.css (этапы 1-2) починены — прод-сборка снова собирается;
- дизайн-базлайн v2 владельца добавлен в design/baseline/Chatbolls Baseline v2/.
Backend 582 теста OK (докер, миграции с нуля), frontend tsc + vitest 69 OK, build OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Отделы и capability/scope-модель полностью удалены:
- снесены Department, AccessProfile, EmployeeAccessAssignment и scope-авторизация;
- новые EmployeeGroup/EmployeeGroupMember: настраиваемые группы, CRUD у OWNER/ADMIN;
- ролевая policy: OWNER == ADMIN (кроме ownership.transfer), EMPLOYEE ограничен чатом;
- группа у канала и диалога (наследуется при ingest, переносится вручную),
видимость EMPLOYEE: свои группы + без группы + назначенные ему диалоги;
- эндпоинты /company/groups/, /conversations/{id}/group/ и /assignee/;
- звонки проверяют групповую видимость диалога (закрыта дыра в calls);
- RLS/гранты/триггеры групп (tenancy/0018), вьюха порталов без отделов (tenancy/0019);
- знания: библиотека общая для организации, visibility/departments удалены;
- фронт: employees/channels/knowledge переведены на группы, features/departments удалён.
Backend 564 теста OK (докер, миграции с нуля), frontend tsc + vitest 91 OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Разрешённые домены виджета нельзя было задать из интерфейса: поля не было, а
показанное для WEB поле «Base URL» в конфиг Web-виджета не уходило. Любое новое
подключение сохранялось с пустым allowed_origins, а пустой список в проде
запрещает вообще все origin'ы (origin_allowed) — виджет на сайте молча отвечал
«Чат временно недоступен», при зелёном статусе подключения в CRM.
- поле «Разрешённые домены» в форме подключения: разбор через запятую/перенос,
дедупликация и валидация трёх форм, которые понимает origin_allowed
(example.com, *.example.com, https://example.com:8443); сохранение
заблокировано, пока не введён хотя бы один валидный домен
- мёртвое поле «Base URL» для WEB убрано
- «Проверить» больше не даёт зелёный статус при пустом списке доменов, а пишет
причину в строку подключения
- проверка Web-виджета запускается сразу после сохранения: раньше любая правка
роняла подключение в UNCHECKED, а виджет в DRAFT, и чат на сайте умирал до
ручного нажатия «Проверить» (раздача требует PUBLISHED + OK)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caddy спрашивает разрешение на on-demand сертификат Help Center по plain
HTTP из внутренней сети, X-Forwarded-Proto не шлёт и по редиректам не
ходит. SECURE_SSL_REDIRECT отвечал 301, Caddy считал домен
неавторизованным и сертификат портала не выпускался: браузер получал
ERR_SSL_PROTOCOL_ERROR на foxray.help.custocrm.ru.
Исключаем /api/v1/gateway/ из SSL-редиректа на platform-сёрфейсе.
Проверено на стенде: ask отдаёт 204 для опубликованного портала и 404
для постороннего домена, сертификат выпущен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Статья портала становится самостоятельным источником знаний агента: агент
ссылается на неё, а не на копию (ADR-HUB-0039, SPEC-HUB-0029).
- KnowledgeFragment получает второй источник portal_article, инвариант
«ровно один источник» закреплён CHECK-constraint; retrieval и каталог
системного промпта работают по обоим источникам одним запросом
- фрагменты статьи строятся при публикации ревизии и снимаются при
архивации: агент отвечает только опубликованным текстом
- новые эндпоинты bulk/agent для знаний и статей: attach/detach одному
агенту, недоступные по отделу возвращаются в skippedIds, а не роняют
операцию
- вкладка знаний в карточке агента стала нередактируемой, состав меняется
только из библиотек; мастер создания агента оставляет канал и credential
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- launcher: круглая кнопка → пилюля с иконкой Tabler brand-hipchat и словом
«Чат» (inline SVG — на сайте продукта шрифта Tabler нет), hover/focus-visible,
анимации bump/shake переделаны под новую форму
- окно 384x600 → 440x720
- фикс: панель открывалась на начале истории. Лоадер держит iframe скрытым
(display:none), у ленты scrollHeight === 0 и автоскролл не срабатывал —
виджет теперь слушает edevs-chat-opened и доскроллит после показа, лоадер
повторяет сигнал на load iframe
- лента ближе к мессенджеру: время у сообщений, растущее под текст поле ввода,
крупнее типографика и шире пузыри
- support-режим переиспользует шапку/пузыри/composer из ChatView вместо своих копий
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On the client call page an incoming invite never showed Принять — only
Отменить. resolveAudioCallViewMode returned "ringing" for REQUESTED/RINGING
before start, but ringing renders a single Отменить button. An invite is an
incoming call for the client (the staff initiates it), so it must be incoming
which renders the Принять/Отклонить pair. AudioCallStage now also passes
onDecline, which the incoming PairBar needs.
The call page also missed the design tokens that audio-call.css consumes
(--font-sans, --radius-4xl/pill/lg, --success/--error): web-chat never defined
them, so the audio scene fell back to inherited system font and a 28px radius.
Added the tokens to web-chat styles.css :root straight from the baseline
(typography/effects tokens); no external webfont per ADR-HUB-0013.
CallApp returned <AudioCallStage> before the bootstrap effect ran: on first
render call was null, so call?.kind !== 'VIDEO' was always true and the audio
scene rendered with empty call/accessToken/iceServers — RTC never started and
the screen stayed idle. The invite banner and the call page also checked kind
inversely (kind !== 'AUDIO' vs kind !== 'VIDEO'), diverging whenever kind was
undefined, so the widget labelled an underdetermined call as video.
- api.ts: single isVideoCall(call) helper (default AUDIO); used by both the
invite banner and the call page so they agree on the kind.
- CallApp: composition-only dispatcher — bootstrap (invite/access token,
polling) runs unconditionally, all hooks called, branching only in JSX by
isVideoCall. No early return from the middle of the hook body.
- VideoCallStage: video path factored out of CallApp (symmetric to
AudioCallStage), reusing the loaded call/access/iceServers.
- useConnectionTimer: shared connection timer; removes the duplicate from
AudioCallStage.
init-runtime-roles.sh и reassign-schema-ownership.sql монтировались относительным путём ./deploy/postgres/, который резолвится относительно --project-directory (INSTANCE_DIR). Ассеты лежат только в релизе, в instance их нет — Docker создавал пустые директории, psql -f падал с «Is a directory», деплой валился на нормализации владения схемой.
Приведено к единому эталонному паттерну ${CUSTOCRM_RELEASE_DIR} (как Caddyfile). compose.dev.yaml ассеты postgres не монтирует — дефекта там нет.
Add a standalone backend endpoint POST /api/v1/support/portals/<id>/articles/import/ that upserts articles by (portal, locale, slug): creates a new article or appends a revision, resolving category by categoryPath. No DB migrations, reuses existing create_article/add_revision services.
Frontend: new portal article import modal + parseArticleYaml validator + article template (knowledge-template.yml / portal-articles-template.yml). Add a ".link has-icon" "Скачать шаблон" link to both the AI knowledge import modal and the new portal import modal, using the existing shared links.css standard and the existing download icon.
- центрирование иконки в квадрате .product-icon вынесено в shared/table-controls.css (единое правило для таблиц продуктов и интеграций)
- из таблицы продуктов убраны пустые колонки SALES-AGENT и FULFILLMENT (нет привязки к данным / атрибут оффера, а не продукта)
- колонка КАНАЛЫ наполнена реальными данными через единый источник: product_payload.channels (id, code, name, isActive, agentId, connections) + prefetch channels__ai_agent, channels__connections
- ProductChannelsTab переведён на product.channels, дублирующий channelsApi.ts (запрос /api/v1/channels/ + клиентская фильтрация) удалён
- висячие стили .agent-state/.fulfillment-state/.channel-tags убраны, min-width таблицы убавлен под 6 колонок
Two related conversation-header improvements.
Owner claim (business rule)
- backend: claim_locked_conversation lets EmployeeRole.OWNER take a dialog
already assigned to another operator (other roles keep the existing guard)
- frontend: route SessionUser.role through ShellRouteContent → sales/support
dialogs pages → ConversationWorkspace → ConversationThread; the owner sees a
single «Перехватить диалог» button on any open dialog not assigned to them,
including ones held by another operator
Audio/video call buttons (design-system IconButton)
- conversation header uses the approved IconButton component with outline icons;
new is-audio/is-video hover modifiers (by the is-danger pattern, success and
primary tokens from the design-system base.css): green hover for audio, blue
for video
- audio-call modal: drop stray white padding so the dark 9:16 scene fills the
modal content (no white side bars)
Production deploy failed at migrate with «must be owner of table
identity_organization»: the table (and others created/imported outside the
migration role) were not owned by custocrm_schema, which migration_user must
belong to to run AddField/AlterField. This is a pre-existing condition that
surfaced on the administration migration and recurred across pipelines
(#922, #937, #938) — not caused by the calls feature.
Fix makes deploy self-healing: before running migrate, reassign ownership of
all public-schema objects (tables, sequences, functions) to custocrm_schema
under the postgres superuser. Idempotent and safe on every deploy.
- deploy/postgres/reassign-schema-ownership.sql: reassign public-schema
ownership to custocrm_schema (verified against PG16)
- compose.yaml: mount the SQL into the postgres container
- deploy/cli/lib/deploy.sh: run the normalization step once postgres is
healthy, before the one-shot migrate
Introduce a call kind (AUDIO default, VIDEO) so dialogs support both audio
and video calls. Video path is unchanged; audio is a separate full-screen
component built 1:1 on the «Аудиозвонок.dc.html» baseline.
Backend
- CallSession.kind (CallKind AUDIO|VIDEO, default AUDIO) + migration 0005
- create_call_request(kind), CallCreateView validates kind from request body
- serializers expose kind in call_payload, public_invite_payload,
public_call_state_payload (drop stale hardcoded capabilities)
- timeline system message reflects kind («аудио-/видеозвонок»)
- tests updated for the kind field and new message wording
@edevs/ui
- useCallRtcSession gains videoEnabled (default true; video unchanged);
audio requests audio-only getUserMedia, no camera fallback
- AudioCallView, AudioCallWaveform (live AnalyserNode oscilloscope),
AudioCallIcons (mic/speaker/phone/status), audioCallStates, audio-call.css
- exported via index.ts
internal-ui
- two call buttons in the conversation header (audio + video)
- CallOverlay becomes a kind dispatcher: VideoCallOverlay (unchanged) /
AudioCallOverlay; kind threaded through model/hook/workspace
web-chat
- CallInfo.kind; invite banner shows audio vs video title and icon
- CallApp dispatches to AudioCallStage for audio; audio resolve/build in model
Infra
- compose.yaml: widen Coturn relay UDP range to 49160-49999 (~420 relay
calls) with explanatory comment