Commit Graph
16 Commits
Author SHA1 Message Date
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 Fable 5.1 5396dda8ad ✨ feat(updates): обновление установки из интерфейса
Установка раз в шесть часов и по кнопке проверяет страницу релизов
GitHub. Администратор установки видит баннер о новой версии и ставит
её одной кнопкой; карточка «Обновления» — в «Настройки → Платформа».

Установку выполняет отдельный сервис updater с Docker-сокетом:
backend-app общается с ним только файлами в томе chatballs-updates
(heartbeat, request.json, status.json). Updater принимает лишь релизы
своего репозитория с образами по digest и применяет compose.yaml в
одноразовом контейнере-помощнике, после чего кладёт файл в каталог
установки. Запрос установки попадает в аудит.

Релизный workflow собирает образ updater и пришпиливает его в
compose.yaml; версия бэкенда зашивается в образ (CHATBALLS_VERSION).
Миграции updates.0001 и tenancy.0034 (гранты на таблицу состояния).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 00:43:11 +03:00
AndreyandClaude Opus 5 8fe66385d6 ✨ feat(release): установка на чистый хост одной командой
Раньше compose монтировал с хоста Caddyfile, init-скрипты базы и генератор
секретов. Из-за этого установка требовала рядом распакованный репозиторий, а
файл, забытый при сборке релиза, Docker молча подменял пустым каталогом — и
стек падал на первом старте у человека. Плюс production-образ backend вообще
не собирался: COPY content ссылался на каталог, которого в репозитории нет,
так что релиза не существовало ни на GitHub, ни на GitLab.

Теперь весь дистрибутив — один compose.yaml со страницы релиза:

  curl -fsSL .../compose.yaml -o compose.yaml
  docker compose up -d --wait

- Caddyfile переехал в свой образ шлюза (caddy validate — в сборке),
  init-скрипты базы — в свой образ postgres, генератор секретов — в
  backend-образ. Bind-mount'ов в production-манифесте не осталось.
- Состояние установки — именованные тома вместо каталогов рабочего каталога.
  Заодно чинит загрузку файлов на Linux: том наследует владельца из образа
  (hub), тогда как bind-mount доставался контейнеру как root:root.
- scripts/pin-release-compose.py закрепляет ссылки на образы по digest и
  падает, если хоть один ключ остался подстановкой.
- GitHub-workflow собирает четыре образа и прикладывает к релизу compose.yaml
  (основной путь) и release.env (для `chatballs deploy`).
- tests/cli/test_release_compose.py держит свойство: манифест без bind-mount'ов
  и полностью закрепляем по digest.
- Из окружения шлюза убраны CHATBALLS_APP_DOMAIN и CHATBALLS_ACME_EMAIL —
  Caddyfile их не читает.

Dev-контур не меняется по смыслу: compose.dev.yaml по-прежнему собирает всё
из исходников и держит состояние в ./data.

AUDIT-TODO.md — временный список остального из аудита; удаляется целиком,
когда закрыт последний пункт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 04:55:17 +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 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 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 0ca9eb865f ✨ feat(setup)!: мастер первого запуска и демо-набор «Ателье Норд» с точным удалением
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>
2026-09-05 16:55:31 +03:00
Andrey 4c622c7542 ✨ feat(seed): replace standalone importer with Django seed_demo command and Russian IIoT dataset
Старый сид dev/local-seed/ (standalone-скрипт под CUS_ENV) заменён полноценной
Django-командой seed_demo с редактируемыми JSON-манифестами в demo_seed/data/.
Полный выдуманный датасет организации «Северная Верфь» (IIoT, РФ-наполнение):
продукты Вектор/Репер, каналы, AI-агенты, база знаний со вложениями, диалоги,
заказы, продажи, поддержка, портал Help-центра, звонки, уведомления.
Идемпотентен (get_or_create/update_or_create), dry-run по умолчанию (--apply),
--force для облака. Удалён dev/local-seed/, скрипты seed-local/seed-support-portal,
compose-профиль переключен на demo-seed. Тесты: создание, идемпотентность, dry-run.
2026-07-30 19:59:46 +03:00
Andrey f8fb9162a6 ✨ feat(administration): add delivery-aware administration 2026-07-30 00:11:37 +03:00
Andrey 3f07d76173 ✨ feat(portals): complete custom domains and unified navigation 2026-07-29 16:49:50 +03:00
Andrey 2b246e193b ✨ feat(support): add reusable multi-portal help centers 2026-07-28 21:39:25 +03:00
Andrey 7e9667ee88 ✨ feat(ui): align channel management with design baseline 2026-07-23 11:08:16 +03:00
AndreyandClaude Fable 5 541d016b35 🔧 fix(dev): drop redundant migrate from backend-app dev command
Runtime-роль app (CUS_DB_ROLE=app) не имеет прав на django_migrations
(RLS-модель), поэтому migrate в dev-команде backend-app всегда падал с
permission denied и уводил контейнер в crash-loop на свежем томе.
Миграции выполняет one-shot init (CUS_DB_ROLE=migration), от которого
backend-app уже зависит через service_completed_successfully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 06:22:00 +03:00
Andrey dd849fa2d4 ✨ feat(platform): isolate runtime surfaces 2026-07-14 22:23:15 +03:00
Andrey e13542fc45 fix(ci): harden release and deployment workflow 2026-07-14 12:19:38 +03:00