- CallMetric: тип соединения DIRECT/RELAY/UNKNOWN + категория ICE-кандидата
(host/srflx/prflx/relay) и RTT; храним только КАТЕГОРИЮ, без адресов/SDP/ICE
- record_call_metric: derive connection_type, whitelist кандидатов, clamp RTT,
идемпотентный upsert по (call_session, side)
- signaling/consumer: тип participant.metrics — сохраняем, не ретранслируем
и не логируем; call_payload отдаёт metrics для internal-ui/аналитики E15
- callRtc.ts: на connected снимает getStats выбранной candidate-pair и шлёт
только категорию кандидата + RTT (подтверждает direct vs TURN relay)
- tests: derive/relay/unknown, санитизация мусора и RTT, upsert, payload
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .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>
- командный центр: карточки отделов разделены отступом
- «Проверить» доступна для Web-виджета: backend валидирует привязку к
каналу и что виджет канала обслуживает именно это подключение;
во frontend флаг checkable отделён от testable (секрета у WEB нет)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- командный центр: endpoint command-overview (OWNER) — оба отдела с живыми
метриками диалогов, коммерция в карточке продаж, реальные «Требует
внимания»/интеграции/расходы AI; фронт переведён с мок-данных, статичные
статусы в топбаре и «Отделах» заменены на реальные
- ConversationRead: открытие диалога двигает персональную отметку прочтения,
бейдж непрочитанных в списке гаснет без ответа оператора
- уведомления: привязка мессенджера эксклюзивна — новая заменяет прежнюю
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- fix: JSON-exclude по config.purpose отбрасывал в SQL все интеграции без
ключа — клиентские TG/MAX боты переставали поллиться; фильтр в Python
- fix: MAX передаёт payload деплинка апдейтом bot_started — нормализуем
в «/start <payload>» (как Telegram); «Начать» у клиентских MAX-ботов
теперь тоже открывает диалог
- профиль: одна кнопка «Привязать бота» (код выдаётся заранее, клик =
переход по диплинку) + чекбоксы типов уведомлений (PATCH pushTypes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MessengerBinding/MessengerBindingCode: привязка сотрудника к боту по
одноразовому коду (deep-link ?start= для TG и MAX, TTL 10 мин)
- сервисный бот = messenger-интеграция с config.purpose=notifications,
без канала продаж; worker поллит его отдельно (только коды привязки)
- доставка через outbox: notify() -> notification_created -> handler
разворачивает аудиторию и шлёт привязанным сотрудникам (best-effort)
- API messenger-bindings (список/код/отвязка); профиль: карточка
«Уведомления в мессенджер»; форма интеграций: флажок бота уведомлений
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Клиенты → Контакты: сайдбар, крошки, роуты, тексты detail-страницы
- таблица: колонка Статус (Лид/Клиент — из оплаченных заказов), реальные
телефон и @логин вместо заглушек, фильтры Лиды/Клиенты, поиск по
имени/телефону/логину, реальные счётчики вместо фейковых 248
- сайдбар компании: группа «Инструменты» с неактивной «Доской»
- fix: бейдж «Диалоги» считает только очередь (PAUSED) — взятые оператором
диалоги без ответа больше не подсвечиваются
- fix: состояние «Контакт запрошен» сбрасывается при смене диалога
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
Знания: плоская сущность Knowledge (заголовок, описание, MD) + файловые
вложения с оригинальными именами (retrieval из md/txt/pdf/docx, публичная
ссылка для клиента). Агент: без релизов, инструкции из трёх частей
(персонализация/тон/инструкции) + выбор знаний из библиотеки; лимит
диалогов упразднён (остался dailyCostUsd). channel.system_prompt/model
удалены — AI-поведение только на агенте. Продукт — техническая запись
без summary/sales_description. Data-миграция переносит опубликованные
документы и активные релизы в новую модель. Internal-UI: раздел Знания,
новая карточка агента, упрощённые продукты, release-страницы удалены.
ADR-0005/0007/0017 → superseded; SPEC-HUB-0012 переписан.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Деплой падал на seed_catalog → Price.objects.update_or_create(... version=1)
реактивировал архивную версию цены, нарушая констрейнт uniq_active_price_offer.
Причина: цену оффера меняли через UI (создана новая версия), а seed при каждом
деплое пытался вернуть version=1 с is_active=True. На проде seed выполнялся
13 раз — команда跑了 на развёрнутой установке.
seed_hub_initial_data теперь no-op, если уже есть OWNER (маркер первичного
bootstrap). На новой БД сеет как раньше; на развёрнутой — skip, не трогая
данные, изменённые через UI/API (каталог, цены, AI-контент).
Тест: test_seed_skipped_when_owner_exists покрывает guard.
Лимиты агента раньше хранились вразнобой (dailyCostMicros/dailyBudgetRub/
dailyDialogs/maxMessagesPerDialog), из них бэкенд читал только dailyCostMicros.
Из-за путаницы валют на агенте FoxRay стоял лимит dailyCostMicros:100 (= \$0.0001),
который блокировал ответы ИИ — 2 сообщения зависли без ответа.
- limits.py: ключ dailyCostUsd (целые центы USD), сравнение с расходом в micros.
У агента FoxRay ключа нет → лимит больше не блокирует ответы.
- services.py: _normalize_limits отбрасывает устаревшие ключи при сохранении.
- ingest.py: LimitExceeded теперь ловится как ProviderError — диалог передаётся
оператору с fallback вместо зависания (раньше исключение пробрасывалось до poller).
- AgentEditForm: одно поле «Бюджет в день, \$» вместо четырёх разнородных.
- release/model.ts: удалён мёртвый limitLabel; расходы уже везде в долларах.
- tests: обновлены под dailyCostUsd, добавлен тест на LimitExceeded в ingest.
Удалён фронтенд-only статический прототип тестового чата (features/ai/test-chat/)
и все точки его подключения: роут aiTestChat, таб поднавигации, кнопки в заголовках
AI-агента/release и таблице агентов, CSS-классы. Мёртвая ветка логики статусов релиза
(параметр tested) убрана, тексты про тест-чат заменены на нейтральные.
Baseline-прототип Тестовый чат.dc.html удалён; упразднение AI-04 зафиксировано
в SPEC-HUB-0001, SPEC-HUB-0004, SPEC-HUB-0005.
- model.ts: config.proxyUrl в типе Integration.
- IntegrationForm.tsx: стейт proxyUrl, поле «Прокси» (placeholder
http://user:pass@host:port) для всех провайдеров кроме WEB (виджет клиентский),
proxyUrl в payload create/update.
OpenRouter (LLM+embeddings), MAX, Telegram заблокированы из РФ (403/недоступно).
Мессенджер-транспорт уже поддерживал config["proxy_url"], но OpenRouter и кнопка
«Проверить» прокси не использовали. Теперь единый механизм для всех интеграций.
- openrouter.py: __init__ принимает proxy_url; _post через opener с ProxyHandler
(по образцу transports/base.py). chat и embed идут через прокси.
- factory.py: proxy_url из config интеграции передаётся в OpenRouterProvider.
- checks.py: _get принимает proxy_url (ProxyHandler); check_openrouter/max/telegram
пробрасывают его — кнопка «Проверить» гоняет реальный запрос через прокси.
- services.py: _normalized_config сохраняет proxy_url (для всех провайдеров);
test_integration передаёт proxy_url в check.
- serializers.py: отдаёт proxyUrl.
- integrations/tests.py: persist/clear proxy_url, serializer proxyUrl, проверка
ProxyHandler в check_openrouter и OpenRouterProvider (mock build_opener).
Формат proxy_url: http://user:pass@host:port (auth в URL, stdlib-конвенция).
Только HTTP/HTTPS — SOCKS5 потребует PySocks (отдельная задача). WEB-виджет
клиентский — прокси не нужен.
_import_one возвращал неверные счётчики: новый документ давал created=1 и
updated=1 (лишний updated), а существующий без изменений не учитывался в
unchanged. Из-за этого падали test_creates_new_documents_and_publishes и
test_reimport_same_content_is_unchanged.
Разделены три состояния: новый → created=1; существующий без изменений →
unchanged=1; существующий с изменённым контентом → updated=1. Создание версии
вынесено в _publish_version.
Проверено: 43 теста hub_platform.ai зелёные.
Владелец не мог изменить ни параметры агента (имя/модель/инструменты/лимиты),
ни название канала — UI отсутствовал, хотя backend-эндпоинт агента уже работал.
Из-за пустых allowed_tools/limits релиз нельзя было опубликовать → правки
знаний/инструкций не вступали в силу.
- AgentEditForm.tsx: модалка name/model/инструменты(чекбоксы)/лимиты(числовые
поля) → PATCH /api/v1/ai/agents/<id>/update/ (endpoint уже существует).
- ChannelEditForm.tsx: модалка переименования канала → PATCH /api/v1/channels/<id>/.
- AiAgentOverviewTab.tsx: кнопка «Изменить агента» (была disabled-заглушкой).
- AiAgentDetailHeader.tsx: клик по имени канала открывает форму переименования.
- AiAgentDetailPage.tsx: стейт и подключение обеих форм.
- styles.css: стили .ai-edit-* для инструментов/лимитов.
- release/model.ts: убран чек #5 «Sales behavior» (искал подстроку «sales» в
коде промпта, не в категории — блокировал публикацию). Остаются 4 проверки.
- release/model.test.ts: публикация без sales-промпта; блокировка без tools/limits.
Цепочка: владелец задаёт инструменты/лимиты → новый черновик релиза наследует
их → canPublishRelease проходит → публикация → правки знаний/инструкций
вступают в силу.
У канала не было endpoint редактирования вообще (только GET list + POST
test-chat) — владелец не мог изменить название канала.
- channels/services.py: update_channel (минимум — name; code не трогаем,
смена сломала бы embed-сниппеты data-channel и URL).
- channels/views.py: ChannelDetailView.patch — org-scoped lookup,
валидация пустого имени, audit channels.channel_renamed.
- channels/urls.py: route channels/<id>/.
- channels/tests.py: переименование, пустое имя → 400, чужая орга → 404,
operator → 403.
VITE_PUBLIC_HUB_URL — build-time переменная Vite (инлайнится в бандл при сборке
образа), в runtime контейнера бесполезна. Прокидывается на этапе сборки образа
frontend в CI.
- deploy/docker/frontend.Dockerfile: ARG/ENV VITE_PUBLIC_HUB_URL по образцу
VITE_API_BASE_URL.
- .gitlab-ci.yml (images:build): --build-arg VITE_PUBLIC_HUB_URL с fallback
https://hub.edevs.tech (если CI-переменная не задана — сниппет не пустой).
Значение можно переопределить CI/CD Variable VITE_PUBLIC_HUB_URL в GitLab.
Локальный dev: добавить VITE_PUBLIC_HUB_URL в gitignored .env (рядом с .env.example).
После создания WEB-интеграции «код вставки на сайт» негде было посмотреть —
функционал не был реализован (вариант A, без миграций и backend-изменений).
- model.ts: webWidgetSnippet(channelCode) собирает сниппет
<script src="<VITE_PUBLIC_HUB_URL>/chat-widget.js" data-channel="<code>" async>
из публичного домена Hub и кода канала (лоадер уже работает по data-channel).
- vite-env.d.ts: типизация VITE_PUBLIC_HUB_URL (+ VITE_API_BASE_URL).
- .env.example: VITE_PUBLIC_HUB_URL с примером (SPEC-HUB-0003 §3).
- IntegrationForm.tsx: для WEB + выбранного канала — поле «Код вставки на сайт»
(mono, readonly) + кнопка «Копировать» (clipboard, состояние «Скопировано»).
- styles.css: блок .integration-snippet.
- model.test.ts: сборка сниппета, обрезка trailing slash, fallback на относительный URL.
Расхождение со спекой §3: используем data-channel вместо data-widget-key
(public-widget-key в коде не существует). Полное соответствие §3 — вариант B
(отдельная задача: поле widget_key в модели + валидация ключа/origin).
Опубликовать версию агента было нельзя: жёлтый баннер «Черновик версии неполный»,
кнопка заблокирована всегда — даже при заполненном составе (модель, prompts,
tools, limits).
- release/model.ts: убрана проверка «Retrieval index собран» из buildChecks
(вариант B). retrieval_index_version на бэкенде всегда "" (create_draft/
initial/seed/rollback), поле не заполняется нигде — gate был структурно
невыполним. canPublishRelease теперь работает по оставшимся 5 проверкам.
Инфо об индексе в карточке состава (ReleaseComposition.tsx) сохранено.
- release/model.test.ts: публикация разрешена без retrievalIndexVersion;
блокируется для не-DRAFT и при отсутствии prompts/tools/limits.
Бэкенд состав публикации не валидирует (publish_release проверяет только
статус) — изменения чисто фронтендные.
Удаление интеграции из row-меню не работало: 0 реакции на клик «Удалить».
- IntegrationsPage.tsx: статический Modal.confirm (antd v5 без обёртки <App>)
не отрисовывался — заменён на контролируемую <Modal open> (стейт
deleting/deletingError, кнопки Отмена/Удалить danger-outline, ошибка внутри
модалки). По образцу IntegrationForm/ProductFormModal.
- api/client.ts: для 204/205 возвращать undefined без json(). Раньше пустое
тело ответа DELETE давало SyntaxError, onOk рвался и load() не выполнялся —
список не перечитывался.
- api/client.test.ts: 204/205 → undefined, 200 → парсинг, 404 → detail.
Известный риск (не в этом коммите): бэкенд delete не ловит ProtectedError от
PROTECT-FK (Conversation/Channel) → 500 для «задействованных» интеграций.
Отдельная задача.
SPEC-HUB-0010 §7.3: для production настроить CSP frame-ancestors для /chat/
(раздаётся vite/nginx, не Django — настраивается в infra/deploy), разрешив домены
продуктов Edevs. Только origin недостаточен — support-виджет дополнительно
проверяется signed Product Support Token. Домены — у владельца.
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) не тронут — минимум риска.
SPEC-HUB-0010 §7: виджет поддержки polling/send. Support-сессия не создаёт
WebSession (стартует по токену), а webchat/messages_payload ищет по contact
(support contact=null) — поэтому отдельные support-specific endpoints.
- widget_credential.py: stateless HMAC-signed credential {conversation_id,
snapshot_id, exp}, TTL 1ч. Poll/send авторизуются им, не Product Support Token.
- messages.py: support_messages_since (по conversation, без contact-lookup) +
post_support_message (Message(CONTACT) + AI-путь run_channel_turn/handoff,
без Contact-creation и transports.send_reply — ответ идёт через polling).
- session.start_support_session: issue widget-credential, отдаётся в ответе
sessions/ (widgetCredential).
- views: SupportSessionMessagesView GET/POST (Bearer widget-credential).
- urls: sessions/messages/.
- Тесты: start→credential, send→poll возвращает, invalid/no-credential→401,
empty→400.
SPEC-HUB-0010 §8.2: support inbox переиспользует общий ConversationWorkspace
(department=support). Оператор видит обращения, тред, composer (claim/release/
return-queue/close/send) и правую панель по контракту — без sales-сущностей.
- SupportDialogsPage: обёртка над ConversationWorkspace (department=support,
SupportContextPanel через render-prop, заголовок «Обращения»).
- ShellRouteContent: SupportDialogsPage получает selectedConversationId.
- App.navigate: supportDialogs ставит selectedConversationId (как salesDialogs).
- Shell: isDialogsWorkspace применяет sales-dialogs scroll/page-классы и
sales-workspace-page для support workspace (layout sidebar).
SPEC-HUB-0010 §8.3 + ADR-HUB-0022: правая панель оператора рендерится по
operator_cards из Product Support Identity Contract, не хардкод.
- backend: conversation_payload в detail-режиме отдаёт operatorContextJson +
accountKey в supportIdentitySnapshot (один запрос, без доп. polling).
- OperatorCards: renderer operator_cards[] из snapshot (field-renderer по type:
text/email/phone/url/code/badge/datetime/boolean/number; unknown→text; null→—).
- SupportHistory: история прошлых обращений по subject_key (detail.history уже
группируется по snapshot на backend).
- SupportContextPanel: табы Клиент (OperatorCards) / История (SupportHistory),
зеркало SalesContextPanel по структуре, контент по контракту.
SPEC-HUB-0010 §8.2: общий conversation workspace для sales и support.
- features/conversations (shared): model (ApiConversation с опциональным contact
+ supportIdentitySnapshot, toConversationListItem, fetchConversations(department)),
types (ConversationListItem/DialogMode/ControlMode/ListTab), data (modeDots/
statusFor/channelMeta без mock), DialogList/ConversationThread/Composer,
ConversationWorkspace (оркестратор, render-prop правой панели), ContextSection,
FieldRow.
- SalesDialogsPage → тонкая обёртка над ConversationWorkspace (department=sales,
SalesContextPanel через render-prop). Sales context-компоненты переключены на
shared-типы.
- Удалены дубликаты sales dialogs (model/types/data/SalesDialogList/
SalesConversation/SalesComposer/ContextSection/ContactRow) — заменены shared.
- CSS sales-* не переименовываю (design baseline, layout без изменений §8.4).
- fetchConversations теперь с department-фильтром (изоляция inbox §10).
conversation_payload для support-диалогов дёргает support_identity_snapshot FK
на каждую строку списка inbox → N+1. Добавлен select_related в
conversations_for_organization. Для sales (snapshot=null) безвредно.
В состав отдела (card meta) вернул AI-агентов, как в исходной sales-карточке:
«N сотрудник(ов) · M операторов · K AI-агент(ов)».
- Backend: _department_payload отдаёт agentCount — реальное число AIAgent на
каналах отдела (channel.department, ADR-HUB-0019).
- Frontend: тип Department.agentCount; обе карточки (sales + support).
Раньше support-карточка была во втором .departments-grid под болтающимся
muted-note и без блока метрик — выглядела обрезанной и разрывала layout.
- Одна сетка .departments-grid: sales + support рядом как единый набор.
- muted-note убран (отделов уже два, фраза про «появятся» устарела).
- Sales-карточка: реальные метрики из /api/v1/conversations/stats/ (ops.openDialogs,
period.sales, period.revenueMinor) через новый хук useDepartmentStats вместо
захардкоженных mock-чисел (42 / 18 / ₽146 200).
- Support-карточка: симметрична sales (Ответственный + Состав + Продукты + stats),
метрики «—» (без выдумки; backend support-метрик — этап 3, SPEC §12).
- Ответственный отдела — первый активный оператор этого отдела (или «—»).
SPEC-HUB-0010 §8.1/§8.4 (этап 2 — посадочные страницы без данных).
- Маршруты supportOverview/supportDialogs (/departments/support[/dialogs]).
- SupportSidebar: навигация отдела поддержки (Обзор, Диалоги) — зеркало
SalesSidebar, переиспользует существующие CSS-классы baseline.
- SupportOverviewPage / SupportDialogsPage: посадочные с EmptyState (без
выдуманных данных; backend support-метрики и inbox — этап 3, design gate §8.4).
- DepartmentsPage: карточка отдела поддержки с реальными данными (memberCount,
operatorCount, связанные продукты) — без выдуманных метрик.
- access.ts: department-scoped доступ (§10 изоляция inbox) — sales operator не
видит support и наоборот; support operator лендингит в support dialogs.
- TopBar/Shell/ShellRouteContent/App: wiring support-маршрутов, breadcrumbs,
выбор сайдбара по отделу оператора.
- Тесты: router (support routes) + access (department-scoped matrix).