Commit Graph
470 Commits
Author SHA1 Message Date
AndreyandClaude Opus 5 4e56b10e6a 🔖 release: 1.14.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.14.1
2026-09-20 10:49:00 +03:00
AndreyandClaude Opus 5 bcb66fadbc 🐛 fix(webchat): первое открытие чата — с анимацией и кнопкой «свернуть»
Первый кадр силуэта рисовался раньше, чем создавался слой, на котором он
живёт. Обращение к нему падало, и остаток обработчика не выполнялся: окно
появлялось рывком, а кнопка оставалась со знаком агента вместо шеврона —
свернуть чат было нечем. Со второго раза слой уже существовал, и всё
работало, поэтому дефект был виден только при первом открытии.

Слой создаётся вместе с панелью, до первого кадра.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:49:00 +03:00
AndreyandClaude Opus 5 2ffad1e787 🔖 release: 1.14.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.14.0
2026-09-20 10:36:10 +03:00
AndreyandClaude Opus 5 c69cd1b436 🔥 chore(resources): убрать неиспользуемые файлы
Иконка приложения и два hero-изображения ни на что не ссылались: ни сборка,
ни README, ни документация их не читают. Удалены владельцем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:35:39 +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 83b1119d6b ✨ feat(conversations): ответ AI считается отдельно от приёма входящих
Ход AI выполнялся прямо в приёме сообщения: цикл опроса мессенджеров и
HTTP-запрос виджета ждали провайдера, держа открытой транзакцию организации.
Один ход — это два обращения к модели (эмбеддинг и чат) по тридцать секунд с
двумя повторами, то есть до трёх минут, и всё это время ни одно входящее по
всей установке не забиралось. Владелец видел это как «бот залипает»: сайт и
MAX на одном агенте отвечали с задержками или молчали.

Приём теперь доводит дело до записи сообщения и ставит событие
`conversation.ai_turn_requested`. Ход считает роль событий воркера
(`run_worker --role=events`) короткими транзакциями, между которыми остаются
походы к провайдеру и в мессенджер. Туда же уехала расшифровка голосовых —
последнее обращение наружу из цикла опроса.

Воркер разделён на роли: `poller` опрашивает подключения и ведёт периодические
работы (один экземпляр — курсоры и паузы после сбоя живут в его памяти),
`events` разбирает outbox и масштабируется репликами (`CHATBALLS_EVENT_WORKERS`,
по умолчанию две). Роль `all` осталась для разработки.

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

Попутно убраны мины, которые тот же залип и продлевали:
- ход клиенту ограничен своим таймаутом (CHATBALLS_AI_TURN_TIMEOUT, 20 с)
  и сроком годности (CHATBALLS_AI_TURN_DEADLINE_SECONDS, 120 с) — просроченный
  ход не зовёт модель, а передаёт диалог оператору;
- отказ провайдера по существу запроса (4xx, кроме 429) больше не повторяется
  трижды по таймауту;
- предохранитель провайдера считает сбои по ключу «организация + интеграция»,
  а не один на процесс: отозванный ключ одной организации гасил AI у всех;
- потолок паузы после сбоя опроса — минута вместо четверти часа: он был
  компромиссом ради журнала однопоточного воркера.

Виджет узнаёт, что ответ считается, по признаку `thinking` в ленте.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 10:35:00 +03:00
AndreyandClaude Opus 5 c8b8246b35 🔖 release: 1.13.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.13.0
2026-09-19 11:32:54 +03:00
AndreyandClaude Opus 5 b95a4edf6e 🐛 fix(settings): адрес установки помнит порт, ссылки ведут на него
Мастер первого запуска и «Настройки» отбрасывали порт из адреса. На
установке, опубликованной как ip:8081, ссылки на файлы, звонки, приглашения
и сброс пароля уходили без порта и вели в пустоту.

Порт хранится отдельно от хоста (InstanceSettings.public_port): по хосту
проверяются входящие Host, строятся домены порталов и адреса TURN, и порт им
чужой. В ссылки он добавляется, когда не совпадает с портом схемы. В поле
адреса его вписывают через двоеточие; смена одного порта прежний адрес не
сдвигает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:32:46 +03:00
AndreyandClaude Opus 5 19448eb05a ✨ feat(ai): размер контекста диалога задаётся в карточке агента
Модель получала последние 20 сообщений диалога — число было зашито в
ingest.py, и поменять его можно было только правкой кода, которую стирало
первое же обновление. Длинному разговору 20 мало, локальной модели с
маленьким окном — бывает много.

Теперь это поле «Контекст диалога, сообщений» в блоке «Модель» карточки
агента: от 1 до 200, по умолчанию 20. Проверочный чат карточки видит то же
окно, что и живой диалог.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:32:46 +03:00
AndreyandClaude Opus 5 a321109a8f 🐛 fix(gateway): установка на нестандартном порту принимает формы
Шлюз передавал дальше Host без порта ({host} в Caddy, $host в nginx). Браузер
при этом шлёт Origin с портом, и CSRF-проверка Django отвергала любой POST на
установке, опубликованной как ip:8081: «Origin checking failed».

Теперь Host уходит с портом. X-Forwarded-Proto и X-Forwarded-For Caddy
принимает от прокси из частных сетей — установку часто ставят за прокси
панели, который снимает TLS и ходит к шлюзу по http; без этого CSRF падал бы
снова, как только перед установкой появлялся https.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 11:32:25 +03:00
Andrey e1cda4dd8b 🔖 release: 1.12.0 v1.12.0 2026-09-17 12:29:02 +03:00
AndreyandClaude Opus 5 fed079e02d ✨ feat(notifications): открытый диалог гасит свои уведомления
Прочтением считалось только нажатие на строку в шторке. Сотрудник мог уже
отвечать клиенту, а оклик «клиент ждёт» по этому же диалогу продолжал висеть
непрочитанным и держать счётчик в шапке. Счётчик переставал что-либо значить:
он звал туда, где человек уже был.

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

Отметка прочтения теперь шлёт сотруднику событие: счётчик живёт в шапке
каждой открытой вкладки, и без события они разъезжались до следующего опроса.

Проверено: тесты уведомлений (гасится только свой диалог, гасятся все его
уведомления, старый маршрут узнаётся, нечисловой идентификатор отклоняется),
рассылка событий, каталоги переводов, ruff, проверка типов рабочего места.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:55:01 +03:00
AndreyandClaude Opus 5 89c44db2bc 🐛 fix(chat): точка режима держится на аватаре в любой строке диалога
Строка списка — flex, и её ячейки по умолчанию растягиваются на всю высоту.
Обёртка аватара растягивалась вместе с ними, а точка режима привязана к её
низу — в строке с метками или таймером ожидания обёртка вырастала с 38 до 62
пикселей, и точка уезжала под аватар на четверть сантиметра. Чем больше меток
у диалога, тем дальше.

Обёртка прижата к началу поперечной оси и снова ростом с аватар, поэтому
точка стоит в его углу независимо от того, что показано в строке ниже.

Проверено на короткой строке и на строке с меткой группы и таймером: высота
обёртки 38 в обеих, точка на 1 пиксель за краем аватара.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:43:00 +03:00
AndreyandClaude Opus 5 ca23cd285f 🐛 fix(vk): марка ВКонтакте вместо чужого значка в ленте и карточке
Канал ВКонтакте появился, а марки у него не было: в ленте диалогов и в
карточке контакта рисовался общий пузырь сообщения, а в строке логина —
значок отправки, который для Telegram выглядел уместно только по
совпадению. Клиент из ВКонтакте выглядел пришедшим непонятно откуда.

Теперь `ChannelGlyph` знает ВКонтакте и отдаёт тот же официальный
монохромный глиф, что стоит на плитке подключения, а строка логина берёт
марку своего канала вместо общего значка: Telegram, MAX и ВКонтакте —
каждый свой.

Проверено: глифы на 13-18px в тёмной теме рядом с Telegram и MAX, лента
диалогов, строка логина и плитки каналов в карточке контакта; tsc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:38:40 +03:00
Andrey d34aea8a8a 🔖 release: 1.11.0 v1.11.0 2026-09-17 11:02:20 +03:00
AndreyandClaude Opus 5 eb98b9b781 ✨ feat(settings): у каждого подключения ссылка на статью справки
Токен бота, ключ доступа сообщества, пароль ящика — всё это выдаёт не наша
система, и человек уходит искать инструкцию в поиске, хотя она написана и
лежит в Центре помощи. Теперь под выбором типа стоит ссылка на статью
именно про этот тип: Telegram, MAX, ВКонтакте, почтовый ящик, веб-виджет.

Адрес Центра помощи один на продукт — это документация Chatballs, а не
портал организации, поэтому он лежит константой в shared/help и от
установки не зависит. Статья у типа подключения необязательна: у
LLM-провайдеров ссылки пока нет, и форма просто её не рисует.

Ссылка оформлена общим классом `.link` в роли второстепенной и стоит
подписью к полю «Тип» — своего стиля у неё нет.

Проверено: tsc, тесты каталога переводов и модели подключений, вид формы
на изолированном макете со штатными стилями.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:02:20 +03:00
AndreyandClaude Opus 5 31985fc35c ✨ feat(vk): канал ВКонтакте — сообщество как точка входа
Сообщения сообщества ВКонтакте принимаются через Bots Long Poll: тот же
способ, что у Telegram и MAX, поэтому подключению не нужен ни публичный
адрес, ни доступ извне — установка за NAT работает наравне с остальными.

Позиция потока живёт в poll_marker, адрес сервера и ключ — в памяти
процесса: их выдают на несколько часов, и колонка под них означала бы
запись в базу на каждом цикле опроса. Ответы failed 1-3 восстанавливаются
в том же цикле, иначе подключение висело бы с протухшим ключом до
перезапуска воркера.

Идентификатор сообщества владелец не вводит: его называет сам ключ
доступа, и проверка подключения кладёт его в конфигурацию — как имя бота
у Telegram и MAX. Проверка заодно смотрит настройки сообщества: без Long
Poll и события о входящем сообщении приём невозможен, и об этом честнее
сказать сразу, а не молчать зелёным статусом. Настройки чужого сообщества
при этом не меняются. Нехватка прав у ключа объясняется словами: ВКонтакте
отвечает на неё английским «no access», из которого не видно, что включить.

Имя, логин и фото отправителя ВКонтакте в апдейте не присылает — их
забирает один users.get на пачку сообщений, а не на каждое: на оживлённом
сообществе запрос на реплику упёрся бы в частоту обращений.

Телефона и кнопки «поделиться контактом» у ВКонтакте нет, поэтому просьба
уходит обычным сообщением, как и почтой. Голосовые принимаются, но не
отправляются: провайдер ждёт ogg/opus, а композер пишет webm.

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

Проверено на живом сообществе: проверка подключения, опрос Long Poll,
приём текста, голосового и фото с подстановкой имени, логина и аватара.
Тесты транспорта и проверки подключения, каталоги переводов, ruff из
корня, tsc и vitest по internal-ui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 11:02:20 +03:00
Andrey Ermolaev 1ea0faf1c6 Update README.md 2026-09-17 09:38:32 +03:00
AndreyandClaude Opus 5 a41b23a5e2 docs: обложка и ссылки в README, design/ вне гита
Дизайн-хендоффы (design/) — рабочие материалы, а не часть продукта:
сняты с отслеживания и добавлены в .gitignore.

В обоих README перед оглавлением — обложка рабочего места (1024px,
сжата из исходных 3840px) и строка ссылок: телеграм-канал, сайт,
центр помощи.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:03:27 +03:00
Andrey 089dd6ddc2 🔖 release: 1.10.0 v1.10.0 2026-09-15 08:45:54 +03:00
AndreyandClaude Opus 5 ae46508d22 ✨ feat(ai): у агента своя модель для текста и своя для расшифровки
Ключ провайдера один на организацию, а агентов на нём несколько, и модель им нужна разная. Раньше модель принадлежала интеграции и копировалась на агента при каждом сохранении — выбрать её агенту было нельзя. Теперь на карточке агента две пары «провайдер + модель»: чем отвечать и чем расшифровывать голосовые. Пустое поле модели означает «как в интеграции», и подсказка в нём показывает её значение, поэтому агент, которому модель не выбирали, по-прежнему следует за настройкой ключа.

Миграция освобождает поле у тех агентов, чья модель совпадает с моделью их интеграции: такие агенты продолжают следовать за ней, а не фиксируют старое значение.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 08:40:04 +03:00
Andrey bd3b9309bf 🔖 release: 1.9.1 v1.9.1 2026-09-15 08:10:11 +03:00
AndreyandClaude Opus 5 e35133b159 🐛 fix(integrations): продукт представляется своим именем, отказ объясняется словами
Исходящие запросы уходили с умолчанием urllib — «Python-urllib/3.12». Защита перед чужим API такой запрос банит раньше, чем его увидит сам провайдер: на боевом стенде Groq через рабочий прокси отвечал 403 «error code: 1010», а с обычным User-Agent тот же запрос и тот же прокси проходят. Теперь opener представляется как «Chatballs/<версия>», свой User-Agent конкретного запроса он не перебивает.

Проверка подключения больше не показывает голый код. Причину, если провайдер её назвал, видно сразу («Invalid API Key»), HTML-страница защиты сжимается в строку, молчаливый 401/403 отправляет смотреть ключ и доступность провайдера из сети. Ответ целиком уходит в журнал.

Проверено на боевом стенде: без User-Agent — 403 «error code: 1010», с любым User-Agent — ответ самого Groq. Пять новых тестов, тесты интеграций, транспортов, провайдеров и i18n.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 08:07:33 +03:00
Andrey e9b64b6afc 🔖 release: 1.9.0 v1.9.0 2026-09-15 07:33:57 +03:00
AndreyandClaude Opus 5 65f7de6bc3 ✨ feat(ai): расшифровка голосовых отдельным провайдером
Модель, которой агент отвечает, не обязана уметь речь в текст: у Anthropic и Yandex Foundation Models эндпоинта /audio/transcriptions нет вовсе, и голосовые у такого агента расшифровать было нечем. На карточке агента появился выбор «Расшифровка голосовых»: по умолчанию «Как у ответов», иначе любая другая интеграция организации — модель берётся из её поля «Модель расшифровки голосовых».

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 07:33:11 +03:00
Andrey 290b4ccea9 🔖 release: 1.8.1 v1.8.1 2026-09-15 07:14:10 +03:00
AndreyandClaude Opus 5 ca1b892e11 🐛 fix(calls): отбой клиента закрывает окно звонка у оператора
Клиент кладёт трубку, сервер переводит звонок в терминал, а peer connection у оператора узнаёт об этом позже — или не узнаёт вовсе. Проверка терминального статуса в аудиозвонке стояла последней, после состояния соединения, поэтому у оператора оставался экран активного разговора с кнопкой «Завершить», которой нечего было завершать: она молча выходила по той же проверке. Теперь терминальный статус решает первым (как в видеозвонке), а «Завершить» на уже завершённом звонке просто закрывает окно.

Проверено: четыре теста разбора фазы аудиозвонка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 07:13:59 +03:00
AndreyandClaude Opus 5 e5d3e98ce3 🎨 style: порядок импортов по конфигу репозитория
Ruff в CI читает pyproject.toml из корня и сортирует импорты; локальный прогон
из каталога backend этого правила не видел.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 06:56:38 +03:00
Andrey 129e4a40ec 🔖 release: 1.8.0 v1.8.0 2026-09-15 06:51:37 +03:00
AndreyandClaude Opus 5 9b54c544af ✅ test(cli): relay больше не профиль и не требует второго адреса
Проверка «веб и TURN на разных адресах» ушла вместе с самим правилом, проверка запуска стека теперь ждёт coturn среди обычных сервисов. doctor спрашивает про relay только на поднятом стеке: его запускают и до первого деплоя. Из исключения по bind-mount убран coturn — сертификатов он больше не монтирует.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 06:51:16 +03:00
AndreyandClaude Opus 5 e8155db4f9 🐛 fix(max): достаём сообщение, когда MAX присылает событие без тела
По журналу с боевого: на голосовое MAX присылает message_created, в котором `message` отсутствует целиком — разбирать нечего, и реплика клиента пропадала. Теперь на такое событие транспорт забирает последние сообщения тех чатов, где что-то происходило рядом с ним, и прогоняет их через обычный разбор. Повторы безвредны: входящие отсеиваются по идентификатору сообщения.

Проверено: два новых теста транспорта (с телом — лишних запросов нет, без тела — сообщение доезжает), тесты разбора и backoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 06:41:44 +03:00
AndreyandClaude Opus 5 d9bc540bc3 ✨ feat(calls): relay работает сразу после установки, без настройки
Звонок через симметричный 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>
2026-09-15 06:38:47 +03:00
AndreyandClaude Opus 5 1de6239eb4 🐛 fix(chat): подсказка вкладок больше не наезжает на сами вкладки
Подсказка про «Общую очередь» и «На мне» использовала заметку ленты переписки, а у той отрицательный отступ сверху — текст затягивало на строку вкладок, и он ложился поверх названий. У подсказки списка теперь своя строка под вкладками с отступами и разделителем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 06:24:25 +03:00
AndreyandClaude Opus 5 73ed0ae6cd 🐛 fix(chat): бейдж «Чат» считает только видимые живые диалоги
Число на пункте «Чат» — это очередь к оператору. Считалось оно по всей организации и вместе с архивными диалогами: сотрудник видел в бейдже то, чего нет в его списках, и до нуля счётчик не опускался. Теперь граница та же, что у счётчиков вкладок списка: видимость сотрудника и без архивных.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 00:47:16 +03:00
AndreyandClaude Opus 5 8d334de3f3 🐛 fix: завершение звонка, фото контактов и голосовые из MAX
Кнопка «Завершить» у оператора обязана заканчивать звонок из любой живой фазы. Отмена умела только 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>
2026-09-15 00:44:35 +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
Andrey 5dbc062b1e 🔖 release: 1.7.4
Автоматическое создание категорий при импорте знаний и исправление добавления меток диалога.
v1.7.4
2026-09-14 22:37:02 +03:00
Andrey ee8367e205 🐛 fix: создавать категории при импорте и восстановить добавление меток
Создавать недостающие уровни categoryPath в транзакции документа и повторно использовать существующие категории. Разрешить импорт новых путей в предпросмотре, обновить переводы и baseline.

Исправить конфликт ширины кнопки создания метки со стилями меню. Выделить метки, заметку и план импорта в отдельные компоненты. Проверено: 13 backend-тестов, 7 frontend-тестов, сборка и браузерный предпросмотр файла из 66 документов.
2026-09-14 22:36:35 +03:00
Andrey 839a1fe815 🔖 release: 1.7.3 v1.7.3 2026-09-14 17:34:03 +03:00
Andrey c62cb903ab 🐛 fix(gateway): подключать дополнительные сайты из постоянного тома 2026-09-14 17:34:03 +03:00
AndreyandClaude Fable 5.1 0bd76042d2 🔖 release: 1.7.2
Логотип организации в сайдбаре без обводки, крупные загрузчики системы и
портала, загрузчик портала сразу по центру, подпись «Работает на Chatballs»
в футере порталов.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
v1.7.2
2026-09-14 07:32:00 +03:00
AndreyandClaude Fable 5.1 c286ba7903 💄 fix(ui): логотип без обводки, крупные загрузчики, подпись «Работает на Chatballs»
Логотип организации в сайдбаре рисовался с рамкой и подложкой — снято,
знак стоит на прозрачном фоне.

Загрузчик портала до прихода манифеста и темы появлялся в левом верхнем
углу и только потом прыгал в центр: его стиль лежал в CSS портала, а этот
чанк грузится лениво, и первый кадр шёл без стилей. Стиль перенесён в
общий state.css, слой фиксирован на весь экран и центрирован независимо
от отступов документа. Загрузчик системы на экране входа в сессию и
загрузчик портала увеличены.

В футере всех шаблонов портала — подпись «Работает на Chatballs» со
ссылкой на chatballs.ru.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 07:31:50 +03:00
AndreyandClaude Opus 5 234a3e43da 📝 fix(i18n): убрать упоминания лимитов из текстов интерфейса
Тарифные лимиты сняты (ADR-CHATBALLS-0042), дневного бюджета в коде нет,
но два текста продолжали о них говорить.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 06:34:03 +03:00
AndreyandClaude Opus 5 17b0c9387f 🔖 release: 1.7.1
Ход обновления виден в интерфейсе: шаги установки, время, итог с кнопкой
перезагрузки. Перезагрузка страницы во время установки больше не теряет ход.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v1.7.1
2026-09-14 03:12:26 +03:00
AndreyandClaude Opus 5 27091c0234 ✨ feat(updates): ход обновления виден в интерфейсе
Установка перезапускает сам бэкенд, который о ней и рассказывает, — и до сих
пор человек об этом не узнавал: признак «идёт установка» жил в useRef и
исчезал при перезагрузке страницы, прогресса не было вовсе (одна строка
текста), а готовый результат карточка «Обновления» не показывала совсем.
Запустив обновление, человек оставался перед экраном без единого признака
того, что что-то происходит.

Теперь ход установки — отдельный блок: шаги (запуск, файл релиза, образы,
перезапуск) с отметками пройденного, текущий шаг с пульсом, номер шага и
время с начала. Молчание бэкенда — это и есть перезапуск, так он и подписан.
Метка установки живёт в sessionStorage, поэтому перезагрузка страницы
(в том числе во время лежащих сервисов) ход не теряет, а время считается от
момента запроса, а не от перезагрузки. Когда версия встала, карточка и полоса
показывают итог с кнопкой «Обновить страницу» — и молчат, если страница уже
открыта в новой версии. Прогресс появляется по нажатию кнопки, а не по ответу
сервера. Полоса наверху молчит, пока карточка «Обновления» на экране.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 03:12:17 +03:00
AndreyandClaude Opus 5 3c01486aa3 🐛 fix(presence): isinstance через union — на этом падал ruff в CI
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 02:47:09 +03:00
Andrey 15ff9bf2c3 🔖 release: 1.7.0
Очередь к оператору с честным временем ожидания, эскалация забытых диалогов,
уведомления в браузер, присутствие операторов и визард «Начало работы».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	VERSION
v1.7.0
2026-09-14 02:46:07 +03:00
AndreyandClaude Opus 5 5d8362e8f6 ✨ feat(queue): очередь к оператору, эскалация и уведомления по макету
Очередь помнит, с какого момента ждёт клиент (waiting_since), и «дольше всех
ждущий» больше не считается по последнему сообщению: клиент, напомнивший о
себе, уезжал в конец очереди. Постановка в очередь сведена в одно место
(conversations.queue) вместо шести копий одного правила.

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

Уведомления доезжают событием по сокету, а не следующим опросом: сокет поднят
на уровень оболочки и работает на любом экране. Появились уведомления самого
браузера — без service worker и Web Push, чтобы не тащить на self-hosted
зависимость от чужого push-сервиса.

Настройка «о чём звать» стала одна на все транспорты (NotificationPreference),
набор событий приведён к макету: «новый диалог» и «клиент запросил оператора»
разделены, добавлены «назначили на меня» и «долго ждёт человека».

Забытый диалог больше не тонет в тишине: свип напоминает группе, потом всем,
потом руководству; сроки — настройка организации в новом разделе «Когда звать
на помощь». Назначение стало осмысленным актом — назначенного зовут лично, у
него есть срок, и не взял — диалог возвращается всем.

Присутствие берётся из открытого сокета и подсказывает двум местам: очереди —
что напоминать некому, и выбору ответственного — кто сейчас за рабочим местом.

Макет: design/baseline/Очередь и уведомления.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 01:49:52 +03:00
Andrey eeed0ed51a 📝 docs(readme): убрать упоминания бюджета и учёта расхода
Хвост рефакторинга 09fb0177: возможности больше нет, а описания её обещали.
2026-09-14 01:49:33 +03:00
AndreyandClaude Opus 5 42a468ec97 🐛 fix(portals): базовый домен порталов — адрес установки, а не localhost
Суффикс приходил из переменной окружения с умолчанием 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>
2026-09-14 00:11:06 +03:00