40 Commits
Author SHA1 Message Date
Andrey 91fab586fa ✨ feat(deploy): сохранить override и постоянные dev-окружения
Сохранить порядок дополнительных Compose-файлов при обновлении и подключать их помощнику на чтение. Добавить проверки override, постоянных каталогов dev-данных и документацию проекта. Явно задать русский язык в тестах русских подписей.
2026-10-01 18:18:58 +03:00
Andrey 1a71abfe77 ✨ feat(webchat): добавить форму перед началом чата 2026-10-01 17:54:56 +03:00
Andrey 61b0dc2416 ✨ feat(webchat): добавить настройки оформления подключения 2026-10-01 13:11:16 +03:00
Andrey 29e2232fc3 ✨ feat(webchat): передавать поля сайта через лоадер и виджет 2026-10-01 12:42:41 +03:00
Andrey b160335bf6 ✨ feat(webchat): применять оформление кнопки из конфигурации 2026-10-01 00:27:29 +03:00
Andrey 5527a13230 ✨ feat(web-chat): стабильные классы и оформление окна виджета 2026-09-30 18:59:53 +03:00
AndreyandClaude Opus 5.5 8f3b1e43a4 🐛 fix(web-chat): на телефоне окно чата на весь экран
На экранах до 480px лоадер раскрывает панель на весь экран без
скруглений и прячет круглую кнопку, пока панель открыта: иначе она
висит поверх поля ввода. Кнопку «Развернуть окно» панель не показывает —
разворачивать некуда. Ширину экрана iframe не видит, поэтому режим
сообщает лоадер (chatballs-chat-layout), а панель после монтирования
сама его запрашивает.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:27:59 +03:00
Andrey 7e0a12a2c9 ♻️ refactor: убрать устаревшие упоминания Hub и CustoAI 2026-09-24 18:02:27 +03:00
AndreyandClaude Opus 5 1f46ed62bd 💄 feat(webchat): виджет по утверждённому образцу и разворот окна
Виджет переложен по образцу, который дал владелец.

Лента: имя и аватар агента стоят над пузырём, у пузыря не скруглён верхний
угол со стороны автора — «хвостик». Аватар агента — знак робота на круге
цвета акцента; он же стоит в шапке вместо буквы, пока у виджета нет способа
получить логотип компании, и он же на кнопке чата вместо прежней подписи.
Пока ответ считается, в ленте висит «печатает» с бегущими точками.

Поле ввода: без рамки, скрепка слева, справа запись голосового, которая
сменяется отправкой, как только клиент начал печатать. При росте поля кнопки
держатся верхней строки. Подсказки убраны. Подвал — одного тона с лентой,
внизу «Работает на Chatballs» со ссылкой на сайт.

В шапке появилась кнопка разворота: окно становится почти вдвое шире и
немного выше. Размер держит лоадер на сайте-хосте — панель живёт в iframe и
сама себя не растянет.

Появление и исчезновение — эффект джина, как в доке macOS: окно вытягивается,
изгибается горловиной и втягивается в кнопку. Аффинными преобразованиями
такое не делается, поэтому силуэт считается контуром SVG покадрово, тем же
контуром режется контейнер панели, а само окно внутри получает матрицу,
построенную по этому силуэту, — вместе с окном деформируется и переписка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:35:29 +03:00
AndreyandClaude Opus 5 733b44f186 ✨ feat: удаление диалога, дерево знаний и перетаскиваемый онбординг
«Удалить диалог» удаляет: переписку, вложения, звонки и уведомления о нём. Право только у владельца и администратора, писать в удалённый диалог некуда, лента виджета у клиента обнуляется по признаку reset в поллинге.

База знаний: категории слева сворачиваются и разворачиваются (свёрнутое помнит браузер), в подвале появился выбор «На странице» 20/50/100 — общий для всех списков компонент.

Окно «Знания» карточки агента разложено по категориям и порталам со счётчиками и «Выбрать все»; набор материалов грузится целиком, а не первой страницей.

Пилюля онбординга перетаскивается и помнит место. Голосовое в веб-чате нарисовано плеером оператора без расшифровки, рисунок волны вынесен в общий пакет. Убрана горизонтальная полоса прокрутки в меню меток.

Проверено: typecheck internal-ui и web-chat, backend-тесты диалогов, виджета и i18n.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:59:28 +03:00
AndreyandClaude Opus 5 0e6a3db9a6 🌐 feat(i18n): язык ответов агента, английское демо и язык в мастере
Язык ответов агента. Поле AIAgent.answer_language: «как у клиента»
(по умолчанию), «язык организации» или конкретный язык. Системный промпт при
этом не переводится — его читает модель, а не человек; язык задаёт отдельная
директива, потому что русский промпт сам по себе тянет ответ в русский язык.
База знаний остаётся одноязычной: поиск ведёт семантическая ветка, а
эмбеддинги кроссязычные.

Язык уходящего наружу текста. Приглашение на звонок, просьба поделиться
контактом, подписи кнопок мессенджеров, тема письма, приветствие виджета и
ответы бота уведомлений берут язык организации (i18n.customer_language), а не
язык запроса: клиент не должен получать письмо на языке того сотрудника, кто
нажал кнопку, и половина такого текста рождается в воркере, где запроса нет.

Английское демо. Манифесты разложены по demo_seed/data/<язык>/, набор
выбирается языком организации. Английский набор повторяет русский по составу —
тот же каст, те же тринадцать диалогов, — переводился только текст. Свои
документы знаний и каталог тканей; аватары и голосовые общие. Демо-провайдер
стал двуязычным: наборы стоп-слов и ключевых слов на оба языка, язык
определяется по письму клиента.

Выбор языка в мастере первого запуска. Переключатель меняет экран без
перезагрузки: форму уже заполняют, а пароль в хранилище браузера класть
нельзя. Выбранный язык становится языком установки, и на нём же идёт вся
установка — должность владельца, демо-данные, ошибки формы.

Попутно исправлено:

- Демо-стенд нельзя было редактировать. configure_agent_provider принимал
  только OpenRouter и Custom, а демо ставит агентов на встроенный DEMO.
  providerIntegrationId уходит с каждым PATCH карточки, поэтому падало любое
  сохранение агента — даже правка инструкций.
- Список типов уведомлений отдавал ярлык TextChoices — захардкоженную русскую
  строку. Ярлык переехал ключом в реестр TYPE_META.
- Результат «Проверить» у подключений (20 строк) и ошибки политики исходящих
  адресов уходили владельцу по-русски.
- Правило data/ в .gitignore ловило demo_seed/data: русские манифесты лежали в
  индексе с прежних времён, а новые файлы туда молча не попадали бы.

Каталог бэкенда — 519 ключей, наборы ru и en совпадают. Полный прогон
бэкенда: 759 passed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 03:12:18 +03:00
AndreyandClaude Opus 5 6c3dec7339 🌐 feat(i18n): язык интерфейса — русский и английский
Интерфейс был русским в коде: строки лежали прямо в JSX и в ответах API,
даты форматировались прибитым «ru-RU», склонение по числу писалось руками
в каждом файле. Теперь текста в коде нет — он живёт в словарях, а язык
выбирается человеком.

Цепочка одна на обе стороны: профиль сотрудника → организация → установка
→ браузер. Пустое значение на каждом уровне значит «как выше», а не
«русский»: тот, кто язык не трогал, поедет за организацией, когда владелец
её переключит, а выбравший явно — останется на своём.

Владелец задаёт язык организации («Настройки» → «Организация», рядом с
часовым поясом) и язык установки («Платформа») — на нём открываются вход,
сброс пароля и мастер первого запуска. Сотрудник переопределяет его в
профиле.

Русский каталог задаёт набор ключей, английский обязан его повторить:
во фронтенде это ловит tsc — пропущенный перевод становится ошибкой
сборки, — на бэкенде тест каталога, который заодно сверяет имена
параметров в фразах. Формы множественного числа берутся из CLDR через
Intl.PluralRules, даты и размеры — через Intl, а не через свои списки
месяцев.

Отдельно пришлось разобраться с тем, что уже записано в базу. Системные
события диалога писались готовой русской фразой, и перевести историю
задним числом нельзя — теперь пишется код события, а фразу собирает
сервер на языке читателя. То же с уведомлениями: они адресованы
операторам, а не одному человеку, и в смешанной команде готовая фраза
неверна для половины. Тон системной строки в треде и признак анонимного
посетителя больше не угадываются регуляркой по русским словам: под
английским бэкендом это просто перестало бы работать.

Письма уходят на языке получателя, а не отправителя: приглашение
отправляет владелец, а читает другой человек.

gettext не взят намеренно: .po/.mo потребовали бы msgfmt в сборке образа
ради того же результата, что дают обычные словари, одинаковые на обеих
сторонах. Язык запроса при этом активируется штатным механизмом Django,
поэтому сообщения DRF переводятся тоже.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 00:36:52 +03:00
AndreyandClaude Opus 5 3461556446 🐛 fix(calls): окно аудиозвонка держит пропорцию и переживает обрыв
Вёрстка:
- мобильная сцена брала высоту как `height:100%` от родителя, у которого задан
  только `min-height` — процент схлопывался в высоту по контенту, и окно выходило
  412x427 вместо 412x915. Высота берётся от вьюпорта (vh/dvh), как у видеозвонка;
- на низком десктопном окне карточка 9:16 не влезала и кнопки приёма уезжали за
  фолд — ширина зажата по высоте вьюпорта, пропорция остаётся ровно 9:16;
- операторской модалке нужен `height:auto`, иначе мобильная ветка растягивала
  сцену на весь экран внутри antd Modal;
- пустая «пилюля» канала больше не рисуется без метки.

Жизненный цикл:
- токен из хеша сохраняется в sessionStorage: хеш срезается replaceState, а в
  пути лежит callId, поэтому после F5 звонок было не восстановить;
- обрабатывается CONNECTED от собеседника — UI выходит из «Переподключения»,
  когда обрыв был на его стороне;
- у режима reconnecting появился нижний бар: экран был без единой кнопки;
- клиенту не показывается «Позвонить снова», которого он не может сделать; на
  DECLINED/MISSED/EXPIRED это была единственная кнопка, и уйти было некуда;
- «Повторить» на упавшем соединении рестартует RTC, а не перепроверяет микрофон.

Чистка:
- TERMINAL_CALL_STATUSES стал единственным источником вместо трёх копий набора;
- волна в тишине спокойная: амплитуда растёт от idle к speak по громкости;
- сигналинг и track.enabled вынесены из updater'а setState.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 03:35:20 +03:00
AndreyandClaude Opus 5 e088ead06f 📝 docs: идентификаторы проектных документов приведены к CHATBALLS
Документация переработана: действующее отделено от истории, отменённые
контуры (продажи, биллинг, managed AI, отделы, сущность Product) убраны из
действующих документов в архив.

Здесь — только кодовая часть: ссылки на документы в комментариях. Ссылки на
действующие документы переименованы ADR/SPEC/ARCH/BUS-HUB-NNNN →
*-CHATBALLS-NNNN (274 ссылки в 173 файлах). Ссылки на документы, ушедшие в
архив, намеренно сохранили прежний идентификатор: он совпадает с именем
архивного файла.

Логика не менялась — правки только в комментариях, докстрингах и одном
описании теста.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 18:13:33 +03:00
AndreyandClaude Opus 5 6f422ac67c 🔒 fix(webchat): виджет открыт миру — origin по заголовкам, лимиты, срок сессии
Публичные endpoint'ы виджета принимают кого угодно без входа и без CSRF, и
до сих пор у них не было ни одного ограничителя. Три дыры разом.

Origin страницы-хозяина брался из поля `hostOrigin` тела запроса — раньше
заголовков. Поле заявляет клиент, поэтому ограничение по доменам виджета
(`allowed_origins`) обходилось строчкой в curl. Теперь решают заголовки,
которые ставит браузер, а `hostOrigin` читается только там, где они указывают
на нас самих: виджет живёт в iframe на нашем же origin, и узнать хозяина
оттуда больше нечем. Что это заявление, а не доказательство, записано в
докстринге `host_origin` — чтобы поверх не построили настоящий контроль.

Лимитов не было вовсе. Каждое сообщение — ход AI по ключу организации
(ingest зовёт провайдера прямо в запросе), каждый файл — место в хранилище:
один скрипт тратил чужие деньги и диск, а поток порождённых событий вдобавок
вытеснял из воркера поллинг мессенджеров. Контуров два. По адресу клиента —
для того, у кого сессии ещё нет; потолки нарочно высокие, потому что за одним
адресом мобильного оператора сидят тысячи живых посетителей. По токену сессии
— строго: за NAT адрес общий, и счёт по нему либо бьёт по соседям, либо
бесполезен. Токен для лимита достаётся из заголовка, а не из тела: иначе
двадцатимегабайтная загрузка доезжала бы до сервера прежде, чем её отобьют.

Сессия не истекала никогда. Токен лежит в localStorage: на общем компьютере
он открывал бы чужую переписку сколько угодно долго. Теперь срок считается от
последней активности (30 суток), отметка обновляется не чаще раза в час —
иначе опрос раз в 2.5 с писал бы в строку сессии четыре раза в минуту.
Виджет на 401 забывает токен и предлагает начать заново, а не висит молча.

Пять тестов. Для главного отдельно проверено, что он ловит прежнее поведение:
со старым `_origin` подделанный `hostOrigin` давал 201 вместо 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:57:10 +03:00
AndreyandClaude Opus 5 146052916b 🔥 refactor: снос сущности Product и авторизованного in-product чата (ADR-HUB-0045)
`Product` оставался в схеме «скрытой технической осью» (ADR-HUB-0041 §7), но
из интерфейса не исчез: в настройках портала жил раздел «Продукты и поддержка»,
в списке порталов — колонка «Продукты». Решение владельца — убрать сущность
целиком вместе со всем контуром, который на ней держался.

Удалено:
- приложения `products` и `support` (контракты идентификации, снимки личности,
  Product Support Token, публичные endpoints сессии);
- `Channel.product` и `requires_authenticated_product_identity` (инварианты
  P3-P5; остались P1-P2), `LlmInvocation.product`, `SupportPortalProduct`;
- `WebChatWidget.mode` целиком: различать было нечего, у веб-виджета одна
  анонимная точка входа. Ключ `mode` ушёл и из ответов API виджета;
- `mode=support` и `ChatballsChat.init` в лоадере, `SupportApp` в web-chat;
- capability `products.*`, аудит-действия `products.*`, продукты в карточке
  контакта и в статистике (`byProduct`);
- раздел настроек портала и колонка списка — в коде и в дизайн-базлайне v2.

У диалога один источник identity — контакт: XOR заменён на
`conversation_requires_contact`. `PUT /portals/{id}/products/` удалён,
`GET /portals/{id}/support-channels/` → `GET /portals/{id}/widgets/`.

Миграции — по образцу сноса продаж: исторические миграции уцелевших приложений
вычищены от ссылок (чистая установка объектов не создаёт), существующие БД
чинит `tenancy/0029_drop_product_support`. Данные не экспортируются, но диалоги
со снимком личности не удаляются — каждому создаётся контакт с именем из снимка.

Проверено: backend 583 OK, фронт tsc + vitest 77 OK, сборки internal-ui и
web-chat OK, миграция прогнана на локальной БД, публичный портал открывается.

В коммит также вошли накопленные незакоммиченные правки рабочего дерева
(библиотека знаний, настройки, dev-compose и nginx, макеты базлайна).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 00:39:12 +03:00
Andrey 41a4b0dd84 ✨ feat: запуск без .env — всё настраивается в UI, и порталы по дизайну
Первичное требование: поднял докер — прошёл мастер — дальше всё в интерфейсе.
Ни одной переменной окружения задавать не нужно и негде.

Запуск и секреты:
- .env.example удалён, мёртвые переменные вычищены; секреты инстанса
  генерирует одноразовый сервис secrets в именованный том, пароли БД —
  через POSTGRES_*_PASSWORD_FILE;
- Caddy: catch-all :80 и on_demand TLS вместо хостов в конфиге — свежая
  коробка отвечает по IP и по любому домену, до мастера дойти можно;
- адрес установки, SMTP и TURN переехали в настройки (InstanceSettings,
  миграции 0027–0030), внешние ссылки строятся от него;
- deploy/cli больше не читает .env; сборка образов в ghcr через GitHub
  Actions, release.env с digest-пинами;
- куки Secure/__Host- выставляются по факту TLS запроса, а не настройкой.

Порталы (дизайн-базлайн v2, кадры PT1–PT8):
- список, карточка портала, библиотека материалов, редактор статьи и
  настройки — ширины и ритм как в остальных разделах;
- файлы статьи: изображение вставляется в текст своим механизмом, файл
  прикрепляется вложением и выводится на портале списком с иконкой формата;
- ссылки на файлы приводятся к относительным: абсолютный хост резал CSP
  портала и картинка не появлялась;
- «Опубликовать» сверяется с сервером и публикует то, что на экране, а не
  ранее выбранную редакцию;
- колонка «Оценки» в списке статей и блок оценок в редакторе.

Первый запуск: полоса «демо-данные устанавливаются» — установка идёт в
worker, и без неё человек видел пустые разделы без объяснения.

Починен фон: вторичное хранилище стало best-effort — при живых остаточных
S3-ключах повторная загрузка файла падала в 500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:37:48 +03:00
AndreyandClaude Fable 5.1 2bfe69c80a ✨ feat(chat): AI отвечает на голосовые по стенограмме, голосовые и файлы в портале поддержки, настройка «Голосовые и звонки»
AI: входящее голосовое (TG/MAX/виджет/портал) расшифровывается BYOK-провайдером
при входе, AI отвечает текстом по стенограмме; стенограммы участвуют в истории
контекста. Без расшифровки (провайдер не умеет/недоступен, FAILED) — диалог
оператору с уведомлением, как прежде. Демо-провайдер честно отказывает
(ProviderError) вместо текста-заглушки. Кнопка «Расшифровать» использует тот
же helper (transcribe_voice_message).

Портал поддержки: POST /support/sessions/messages/ принимает multipart audio и
file (+подпись), отдача /messages/<id>/audio|attachment/?credential=; в poll —
kind/hasAudio/durationSeconds/attachment; старт сессии возвращает features.
Виджет support-режима: запись голосового, скрепка, плеер и карточки.

Настройки → «Голосовые и звонки»: матрица точек входа × Голосовые /
Аудиозвонки / Видеозвонки (Integration.voice_messages_enabled,
audio_calls_enabled, video_calls_enabled; integrations/0007;
GET/PATCH company/administration/communication/). Флаги отдаются в
conversation.connection и в config/сессии виджетов: композер прячет микрофон,
карточка контакта — кнопки звонка, бэкенд отклоняет запись/звонок (400/403).
Почта звонки не поддерживает («недоступно»).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 02:50:32 +03:00
AndreyandClaude Fable 5.1 5be03dd993 ✨ feat(chat): файлы и фото в диалоге — «Прикрепить» в композере, приём из TG/MAX/почты/виджета
Сообщение kind=file с вложением (имя, тип, размер) в тенантном хранилище.
Приём: Telegram (document/photo через getFile), MAX (attachments file/image),
почта (вложения письма вместо заглушки «не поддерживаются»), web-виджет
(multipart file + подпись). Файл без текста уходит оператору, как голосовое.
Отправка оператором: POST /conversations/<id>/attachments/ (файл + подпись,
перехват диалога как у текста) — Telegram sendDocument/sendPhoto, MAX /uploads,
почта MIME-вложение, Web поллингом. Отдача: /messages/<id>/attachment/ (?inline
для фото/PDF) и /webchat/messages/<id>/attachment/ по токену сессии.
Ограничения: 20 МБ, исполняемые типы отклоняются.

UI: кнопка «Прикрепить» (32px, как в макете), чип файла над полем, вставка из
буфера; в ленте — превью фото или карточка файла со ссылкой «Скачать» (.link);
превью в списке «Фото / Файл · имя». Виджет: скрепка в композере, чип, карточки.
email.py: выправлены двойные CR в концах строк.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 02:04:45 +03:00
AndreyandClaude Fable 5.1 a891947016 🧹 chore!: продукт без привязок к вендору — домены, почта, продукты, скоупы, события виджета
- нет зашитых доменов: help-домен по умолчанию localhost (задаёт установщик через
  CHATBALLS_HELP_BASE_DOMAIN), фронт определяет портал помощи пробой /api/v1/help/,
  ALLOWED_HOSTS без вендорских хостов, CI environment url — из CHATBALLS_APP_DOMAIN
- DEFAULT_FROM_EMAIL по умолчанию no-reply@localhost; JWT-аудитория support-токена
  chatballs.support; загрузчик виджета: window.ChatballsChat, события chatballs-chat-*
- «Контакты»: продукты берутся из данных организации ({code, name}), цвет —
  детерминированно по коду; зашитый список продуктов вендора убран
- npm-скоупы @chatballs/*, тема chatballsTheme/buildTheme
- тестовый bootstrap: организация demo, сотрудник staff.member@example.org,
  продукты site/app; фикстуры на example.com
- убрана мёртвая привязка Shell к e-mail сотрудника вендора

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 00:33:22 +03:00
AndreyandClaude Fable 5.1 1a61780dec 🚚 chore!: переименование инфраструктуры CustoCRM/hub → Chatballs с миграцией
- каталог 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>
2026-09-05 22:21:12 +03:00
AndreyandClaude Fable 5.1 dbd7ae3150 🚚 chore: переименование Chatbolls → Chatballs во всём проекте
Бренд, API-схема, TOTP issuer, письма, тексты бота уведомлений, загрузчик виджета
(window.ChatballsChat, типы postMessage), ключи localStorage, id/параметры
виджета помощи, health-service, README, скрипты, демо (пароль
Chatballs-Demo-2026). Макеты в design/baseline не тронуты — это файлы владельца.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 21:43:33 +03:00
AndreyandClaude Fable 5.1 19106620a9 💄 feat(ui): логотип Chatballs (файл владельца) без подложки, цвет темы; имя сервиса в UI — Chatballs
- LogoIcon: два круга и перемычка с вырезами, fill currentColor; в сайдбаре,
  рейке, экране входа/мастера, портале помощи — без акцентной/тёмной подложки,
  цветом текста темы (--n-1); favicon без фона, цвет по prefers-color-scheme
- видимое имя сервиса — Chatballs (заголовки, тексты, title); внутренние ключи
  (localStorage, postMessage, id виджета) пока прежние — переименование проекта позже

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 21:22:19 +03:00
AndreyandClaude Fable 5 ce8c753357 ✨ feat(chat): голосовые в web-виджете (дизайн-базлайн v2, кадр H)
Виджет записывает голосовое (MediaRecorder, зеркало хука internal-ui) и
шлёт multipart в /webchat/messages/; входящее байтами — voice_content в
InboundMessage, скачивание не нужно. Аудио отдаётся по токену сессии
(/webchat/messages/<id>/audio/), в пузыре — нативный плеер. Оператор
может отправлять голосовые и в WEB-канал (доставка поллингом виджета).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 01:48:57 +03:00
AndreyandClaude Fable 5 446a438776 🔥 feat(platform)!: контакт-центр Chatbolls — снос домена продаж и ребрендинг
Пивот по ADR-HUB-0041/0042: продукт становится open-source контакт-центром,
домен продаж удаляется целиком, прод-данные по решению владельца не сохраняются.

Backend:
- удалены приложения sales и orders (модели, API, ingest, команды, миграции);
- из products удалены Offer/Price/MarketplacePublication и ingest_token_hash
  (products/0011); таблицы, ingress-вьюхи и служебные записи Django снимаются
  миграцией tenancy/0017_drop_commerce;
- вычищены метрики выручки и заказов из conversations (stats/clients/command),
  типы уведомлений PAYMENT_* (notifications/0007), capabilities sales.*
  (identity/0019), коммерческие блокеры каналов, ingress-роуты продаж;
- демо-сид очищен от commerce; исторические миграции tenancy/0002-0004
  вычищены от таблиц orders_*/sales_* для чистой установки, tenancy/0003
  получила явные зависимости sessions/auth/contenttypes.

Frontend (internal-ui):
- удалены разделы «Продукты», «Обзор продаж», «Заказы», реестр и карточка
  продажи, вкладка «Продажи» и статус лид/клиент в контактах, коммерция
  в командном центре; роуты, навигация и доступы приведены в соответствие.

Ребрендинг CustoCRM → Chatbolls (видимый слой): UI, письма, TOTP issuer,
тексты ботов, заголовок API, README; виджет — window.ChatbollsChat
с legacy-алиасом CustoCRMChat. Технические идентификаторы (схема БД,
роли, cookie, CUS_-переменные, домены) — отдельным шагом (ADR-HUB-0042 §5).

Тесты: internal-ui 100/100, web-chat 5/5, backend RLS-сьют 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:36:22 +03:00
Andrey f2187f3e83 ✨ feat(webchat): support multiple widget entry points 2026-08-01 21:44:53 +03:00
AndreyandClaude Opus 5 8c4775f8a4 💄 feat(webchat): pill-лаунчер, открытие на последних сообщениях, окно шире
- 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>
2026-08-01 15:59:07 +03:00
Andrey bb7ed5e8f9 fix(call): fix audio calls 2026-08-01 00:37:24 +03:00
Andrey 7db1a3de88 🚑 fix(calls): show Accept button for incoming audio call + load DS tokens on call page
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.
2026-07-31 22:39:10 +03:00
Andrey c34b181681 🚑 fix(calls): bootstrap call before kind dispatch in web-chat call page
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.
2026-07-31 21:35:16 +03:00
Andrey e78eb9f17d ✨ feat(calls): add audio calls alongside video calls
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
2026-07-31 01:35:29 +03:00
AndreyandClaude Fable 5 192497ead0 💄 rebrand(ui): CustoCRM logo and product name instead of Edevs Hub
- New logo: favicon.svg and LogoIcon (replaces PulseIcon) in sidebar and auth screens
- Source logo asset saved to resources/icon.svg
- Replace "Edevs Hub" with "CustoCRM" across internal-ui, web-chat, backend strings (email sender/subject, TOTP issuer, notification bot texts, OpenAPI title), env examples and README
- Add .claude/launch.json for internal-ui dev server (bind 127.0.0.1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:49:02 +03:00
Andrey 2b9946776a ✨ feat(calls): add audio alerts and widget indicators 2026-07-12 08:10:05 +03:00
AndreyandClaude Opus 4.8 ddde6e07d7 fix(calls): центрирование инициалов и full-screen звонок на мобильном
- .hub-call-avatar: padding-bottom 0.1em компенсирует ассиметрию метрик
  Segoe UI (ink уходил на ~1.5px ниже центра круга при flex-центрировании)
- @media <=480px: клиентская вьюха звонка разворачивается на весь экран
  (height 100dvh, media flex:1, aspect-ratio снят) вместо squat 16:9

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:23:11 +03:00
Andrey bba6f17557 feat(calls): Step B 2026-07-12 01:17:17 +03:00
AndreyandClaude Fable 5 bb9fc5d210 feat(calls): контур приглашений P2P-звонков — проход A этапа E09
Пункты 4, 6-8 (частично), 17 PLAN-HUB-0001/E09 по SPEC-HUB-0013:
- timeline-события звонка в диалоге при переходах lifecycle (без дублей)
- API: отмена сотрудником, активный звонок диалога, клиентские
  state/accept/decline по call access token (throttle call_access)
- доставка приглашения TG/MAX через OutboxEvent с ретраями; token
  ротируется на каждую попытку, сырой token не хранится в БД и payload
- Web Chat: приглашение в payload поллинга, баннер входящего звонка,
  accept открывает /calls/ с access token во фрагменте URL
- customer call view /calls/<token>: проверка token до запроса камеры,
  pre-call preview, терминальные состояния по baseline
- internal-ui: кнопка запроса звонка (атомарный takeover), CallOverlay
- sweep воркера: MISSED/EXPIRED по истечению invite, FAILED по grace
  (HUB_CALL_CONNECT_GRACE_SECONDS)
- nginx: /calls/ → web-chat call.html (local + production)

Backend 218/218; typecheck/build internal-ui и web-chat зелёные.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:44:39 +03:00
AndreyandClaude Fable 5 b3e7d9e60e feat(conversations): username клиента и запрос контакта (TG/MAX/Web)
- username из TG/MAX сохраняется в ConnectionIdentity и виден во вкладке «Клиент»
- кнопка «Запросить контакт»: TG/MAX — кнопка «Поделиться контактом» в чате бота,
  Web — форма телефона с маской в виджете; телефон пишется в Contact.phone
- приём контакта без AI-хода: сообщение kind=contact + подтверждение
  (в TG со снятием reply-клавиатуры)
- POST /conversations/<id>/request-contact/ и POST /webchat/contact/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:53:11 +03:00
Andrey aeb7412cf4 feat(web-chat): support mode widget
SPEC-HUB-0010 §7.2: authenticated in-product support chat.

- api.ts: startSupportSession (verify Product Support Token → conversation +
  widget-credential), pollSupport/sendSupport по widget-credential.
- SupportApp.tsx: support-режим виджета — нет consent/lead form, нет полей
  имя/email/purchase; старт по токену один раз; приветствие «Здравствуйте,
  <displayName>»; poll/send через widget-credential; unavailable при истечении
  токена. Переиспользует Bubble/Typing/Header pattern sales-виджета.
- main.tsx: режим по ?mode (sales default, support по data-mode в loader).
- App.tsx (sales) не тронут — минимум риска.
2026-07-09 16:52:01 +03:00
AndreyandClaude Opus 4.8 d79ebda2b7 feat(webchat): embeddable Web Chat widget as a channel connection
Web Chat is a WEB connection of a processing channel (SPEC-0003/ADR-0012),
reusing the whole contour: a public widget loads from the hub domain via one
async tag (<script src=hub.edevs.tech/chat-widget.js data-channel=...>), opens
an isolated iframe panel. Backend webchat app: public config/session/messages
endpoints (anonymous session, hash-at-rest token); inbound goes through the same
ingest (AI release + retrieval + handoff + notifications); WEB transport send is
a no-op (browser polls). Panel rebuilt 1:1 from the baseline (welcome+consent /
ai / operator / unavailable, quick replies, typing). nginx serves /chat-widget.js
and /chat/ from the hub domain; demo host page at /chat/demo.html. Verified
end-to-end: config→session→message→AI reply→poll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:55:00 +03:00
Andrey aacdd422b3 Initialize Edevs Hub repository 2026-06-17 18:14:16 +03:00