Commit Graph
167 Commits
Author SHA1 Message Date
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
Andrey 79a9c55a61 docs, fiat(webRTC): add feature to docs, implement 1-3 PLAN-HUB-0001 2026-07-11 20:17:34 +03:00
AndreyandClaude Fable 5 6ec2266c51 fix(ui): зазор между карточками отделов + проверка Web-виджета
- командный центр: карточки отделов разделены отступом
- «Проверить» доступна для Web-виджета: backend валидирует привязку к
  каналу и что виджет канала обслуживает именно это подключение;
  во frontend флаг checkable отделён от testable (секрета у WEB нет)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:05:36 +03:00
AndreyandClaude Fable 5 8910ffc76c feat(hub): реальный командный центр, прочтение диалогов, один мессенджер уведомлений
- командный центр: endpoint command-overview (OWNER) — оба отдела с живыми
  метриками диалогов, коммерция в карточке продаж, реальные «Требует
  внимания»/интеграции/расходы AI; фронт переведён с мок-данных, статичные
  статусы в топбаре и «Отделах» заменены на реальные
- ConversationRead: открытие диалога двигает персональную отметку прочтения,
  бейдж непрочитанных в списке гаснет без ответа оператора
- уведомления: привязка мессенджера эксклюзивна — новая заменяет прежнюю

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 04:44:57 +03:00
AndreyandClaude Fable 5 c6b5f9ccc8 fix(notifications): привязка MAX по deep-link, поллинг клиентских ботов, выбор типов
- 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>
2026-07-11 03:36:56 +03:00
AndreyandClaude Fable 5 b6d281f485 feat(notifications): уведомления сотрудникам в мессенджер через сервисного бота
- 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>
2026-07-11 03:01:01 +03:00
AndreyandClaude Fable 5 2e542a4a49 feat(ui): раздел Контакты, статусы Лид/Клиент, фиксы бейджа и запроса контакта
- Клиенты → Контакты: сайдбар, крошки, роуты, тексты detail-страницы
- таблица: колонка Статус (Лид/Клиент — из оплаченных заказов), реальные
  телефон и @логин вместо заглушек, фильтры Лиды/Клиенты, поиск по
  имени/телефону/логину, реальные счётчики вместо фейковых 248
- сайдбар компании: группа «Инструменты» с неактивной «Доской»
- fix: бейдж «Диалоги» считает только очередь (PAUSED) — взятые оператором
  диалоги без ответа больше не подсвечиваются
- fix: состояние «Контакт запрошен» сбрасывается при смене диалога

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:56:24 +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
AndreyandClaude Fable 5 a086675283 refactor(ai): упрощение агента и знаний (ADR-HUB-0023)
Знания: плоская сущность 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>
2026-07-10 17:08:50 +03:00
Andrey 179e4a6dec Merge branch 'fix/seed-primary-install-only' into main 2026-07-10 15:14:42 +03:00
Andrey b9f0f13c81 fix(products): seed только при первичной установке (skip если OWNER есть)
Деплой падал на 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.
2026-07-10 15:14:34 +03:00
Andrey fdabc3588a Merge branch 'fix/ai-limit-units' into main 2026-07-10 14:35:22 +03:00
Andrey 3b88c980d7 fix(ai): дневной лимит стоимости в долларах + обработка LimitExceeded в ingest
Лимиты агента раньше хранились вразнобой (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.
2026-07-10 14:29:20 +03:00
Andrey 1dc7e79612 chore(ai): упразднение тестового чата (frontend + docs)
Удалён фронтенд-only статический прототип тестового чата (features/ai/test-chat/)
и все точки его подключения: роут aiTestChat, таб поднавигации, кнопки в заголовках
AI-агента/release и таблице агентов, CSS-классы. Мёртвая ветка логики статусов релиза
(параметр tested) убрана, тексты про тест-чат заменены на нейтральные.

Baseline-прототип Тестовый чат.dc.html удалён; упразднение AI-04 зафиксировано
в SPEC-HUB-0001, SPEC-HUB-0004, SPEC-HUB-0005.
2026-07-10 13:46:43 +03:00
Andrey ee764a00e7 Merge branch 'feat/socks-proxy' into main 2026-07-10 04:19:22 +03:00
Andrey 4b112ae3f8 feat(integrations): SOCKS5-прокси + единый opener (DRY)
Раньше ProxyHandler-логика дублировалась в openrouter/checks/transports и
поддерживала только http/https. Telegram/SOCKS-сервисы требуют SOCKS5 — теперь
единый build_opener поддерживает http/https/socks5/socks5h во всех интеграциях.

- integrations/proxy.py (новый): build_opener(proxy_url) — http/https через
  stdlib ProxyHandler; socks5/socks5h через кастомный _SocksProxyHandler (PySocks,
  изолированный от остального процесса: переопределённый .connect, не глобальный
  monkeypatch socket). socks5h → DNS через прокси (rdns=True). Без PySocks при
  SOCKS-схеме — ValueError с явным сообщением.
- openrouter.py / checks.py / transports/base.py: убрано дублирование ProxyHandler,
  все используют build_opener (chat, embeddings, check_*, MAX/Telegram transport).
- requirements.txt: PySocks>=1.7.
- IntegrationForm.tsx: placeholder упоминает socks5://.
- tests.py: build_opener socks-схема строит handler; http → ProxyHandler; без
  PySocks → ValueError; check_*/OpenRouter прокидывают proxy_url (вкл socks5).
2026-07-10 04:19:03 +03:00
Andrey 654ea53de4 Merge branch 'feat/integration-proxy' into main 2026-07-10 03:55:28 +03:00
Andrey bf34db377d feat(integrations): поле «Прокси» в форме интеграции (frontend)
- model.ts: config.proxyUrl в типе Integration.
- IntegrationForm.tsx: стейт proxyUrl, поле «Прокси» (placeholder
  http://user:pass@host:port) для всех провайдеров кроме WEB (виджет клиентский),
  proxyUrl в payload create/update.
2026-07-10 03:54:52 +03:00
Andrey 4f12d050ab feat(integrations): per-integration HTTP-прокси для всех провайдеров
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-виджет
клиентский — прокси не нужен.
2026-07-10 03:53:56 +03:00
Andrey c21e9f7666 Merge branch 'fix/yaml-import-report-counts' into main 2026-07-10 02:36:56 +03:00
Andrey ff090100cd fix(ai): корректные счётчики created/updated/unchanged в отчёте импорта
_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 зелёные.
2026-07-10 02:36:41 +03:00
Andrey a59300de70 Merge branch 'feat/yaml-content-import' into main 2026-07-10 02:16:26 +03:00
Andrey cb799bcaf9 feat(ai): UI импорта YAML + шаблон формата (frontend + docs)
Владелец грузит YAML на вкладке Знания/Инструкции → документы создаются/
обновляются и сразу публикуются. Парсинг на фронте (js-yaml) → JSON-пост через
существующий api() (без multipart).

- js-yaml + @types/js-yaml (новая зависимость).
- docApi.ts: importDocs + тип ImportResult.
- parseDocYaml.ts: парсер YAML со структурной валидацией (читаемые ошибки).
- DocImportForm.tsx: UI file→preview→импорт→отчёт (created/updated/unchanged/
  failed). По паттерну DocCreateForm (inline-форма).
- Knowledge/InstructionsTab: кнопка «Импорт YAML».
- styles.css: блоки import-preview/report.
- parseDocYaml.test.ts: валидный YAML, global, невалидный синтаксис, пустые поля,
  невалидный inclusionMode.
- SPEC-HUB-0012: формат-контракт + 2 примера (знания/инструкции) + промпт-
  набросок для ИИ-генерации.
2026-07-10 02:15:53 +03:00
Andrey 8c3835ddf9 feat(ai): импорт знаний и инструкций из YAML (backend)
Mass-import документов из YAML: upsert по (org, product, code) + немедленная
публикация. Поле product/scope задаётся на верхнем уровне файла (файл=продукт).

- ai/document_import.py: import_documents — обобщённый upsert-сервис по образцу
  существующего content_importer (update_or_create, latest_published-check,
  publish_version, after_publish=reindex для знаний). ImportReport(created,
  updated, unchanged, failed).
- doc_views.py: DocumentImportView — POST {product?, documents:[{...}]}.
  Валидация category против enum (невалидные → failed, остальные импортируются).
  Concrete KnowledgeImportView/PromptImportView через миксин-паттерн.
- urls.py: роуты knowledge/import/, prompts/import/.
- tests.py: DocumentImportTests — создание+публикация, реимпорт без дубль-версий,
  изменённый контент → новая версия, невалидная категория → failed, несуществующий
  продукт → failed.
2026-07-10 02:15:31 +03:00
Andrey 3e5129e164 Merge branch 'feat/agent-and-channel-edit' into main 2026-07-10 01:17:03 +03:00
Andrey 587aa7f426 feat(ai): редактирование параметров агента и канала + ослабление gate публикации
Владелец не мог изменить ни параметры агента (имя/модель/инструменты/лимиты),
ни название канала — 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 проходит → публикация → правки знаний/инструкций
вступают в силу.
2026-07-10 01:16:28 +03:00
Andrey fa560136d4 feat(channels): endpoint переименования канала
У канала не было 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.
2026-07-10 01:16:12 +03:00
Andrey 82b6865b43 Merge branch 'fix/integrations-delete-and-release-publish' into main 2026-07-09 21:57:35 +03:00
Andrey 92dc202169 chore(deploy): прокинуть VITE_PUBLIC_HUB_URL в сборку frontend
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).
2026-07-09 21:43:54 +03:00
Andrey 20457e6137 Merge branch 'fix/integrations-delete-and-release-publish' into 'main'
feat(integrations): embed-сниппет для Web-виджета

See merge request edevs/hub.edevs.tech!4
2026-07-09 17:42:54 +00:00
Andrey aab9faf84a feat(integrations): embed-сниппет для Web-виджета
После создания 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).
2026-07-09 20:30:42 +03:00
Andrey d7400748e1 Merge branch 'fix/integrations-delete-and-release-publish' into 'main'
fix(integrations): delete confirmation and 204 handling

See merge request edevs/hub.edevs.tech!3
2026-07-09 16:50:10 +00:00
Andrey a312248d49 fix(ai-release): не блокировать публикацию пустым retrieval index
Опубликовать версию агента было нельзя: жёлтый баннер «Черновик версии неполный»,
кнопка заблокирована всегда — даже при заполненном составе (модель, 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 проверяет только
статус) — изменения чисто фронтендные.
2026-07-09 19:36:56 +03:00
Andrey 63e2d9e581 fix(integrations): delete confirmation and 204 handling
Удаление интеграции из 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 для «задействованных» интеграций.
Отдельная задача.
2026-07-09 19:36:36 +03:00
Andrey fd4abf7314 docs(webchat): TODO CSP frame-ancestors for support widget embed
SPEC-HUB-0010 §7.3: для production настроить CSP frame-ancestors для /chat/
(раздаётся vite/nginx, не Django — настраивается в infra/deploy), разрешив домены
продуктов Edevs. Только origin недостаточен — support-виджет дополнительно
проверяется signed Product Support Token. Домены — у владельца.
2026-07-09 17:01:51 +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
Andrey 4ea4593131 feat(webchat): loader support-mode (data-mode, data-support-token)
SPEC-HUB-0010 §7.1: loader принимает data-mode="support" + data-support-token.
В support-режиме panelUrl дополняется &mode=support&token=<token>. Sales-режим
(default) без изменений. Токен передаётся в URL (MVP; короткоживущий, HTTPS).
2026-07-09 16:49:01 +03:00
Andrey 616343d986 feat(support): widget-credential and sessions messages endpoints
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.
2026-07-09 16:47:51 +03:00
Andrey df6389ac75 feat(support): support dialogs inbox
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).
2026-07-09 16:23:20 +03:00
Andrey 8957fe15d4 feat(support): operator_cards renderer and support context panel
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 по структуре, контент по контракту.
2026-07-09 16:20:40 +03:00
Andrey 1a05ae244a refactor(conversations): extract shared ConversationWorkspace
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).
2026-07-09 16:17:15 +03:00
Andrey fbf4919bee fix(conversations): prefetch support_identity_snapshot
conversation_payload для support-диалогов дёргает support_identity_snapshot FK
на каждую строку списка inbox → N+1. Добавлен select_related в
conversations_for_organization. Для sales (snapshot=null) безвредно.
2026-07-09 16:06:47 +03:00
Andrey 94e2d1a19c feat(departments): add AI agent count to department cards
В состав отдела (card meta) вернул AI-агентов, как в исходной sales-карточке:
«N сотрудник(ов) · M операторов · K AI-агент(ов)».

- Backend: _department_payload отдаёт agentCount — реальное число AIAgent на
  каналах отдела (channel.department, ADR-HUB-0019).
- Frontend: тип Department.agentCount; обе карточки (sales + support).
2026-07-09 15:05:37 +03:00
Andrey 9602379a9b fix(ui): unify departments grid and use real sales metrics
Раньше 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).
- Ответственный отдела — первый активный оператор этого отдела (или «—»).
2026-07-09 14:25:47 +03:00
Andrey 58a7e88b22 feat(ui): support department landing pages and workspace navigation
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).
2026-07-09 03:41:00 +03:00
Andrey caa51f4e29 test(support): cover token verify, session start and denials
SPEC §13.15 / §16.14 acceptance criteria.

- test_helpers.make_support_token: HS256-токен для тестов (зеркало token.py).
- SupportSessionTests: happy path (snapshot+conversation+audit SUCCESS), invalid
  signature, expired, schema error, missing subject, disabled contract, wrong
  channel (CHANNEL_NOT_SUPPORT), product mismatch, missing token, continue session
  (reuses open conversation), raw token/jti не попадает в audit.
- SupportContractApiTests: OWNER регистрирует контракт (version из code), OPERATOR
  forbidden (403), code должен соответствовать продукту.
- Багфиксы по ходу тестов: code=CharField (точки в foxray.support.v1), jsonpath
  return current, audit-deny вне write-транзакции (откат не стирает audit),
  DRAFT/DISABLED → CONTRACT_DISABLED, extraction вынесен в extract.py (NO GOD FILES).
- 109 passed (95 baseline + 14 support).
2026-07-09 01:20:04 +03:00
Andrey 95919c83f5 feat(support): department-scoped operator access and inbox filter
SPEC-HUB-0010 §8.1/§10: изоляция inbox по отделу (sales operator не видит
support, и наоборот; OWNER видит всё).

- identity/permissions.py: can_access_support_workspace, is_sales_operator,
  is_support_operator, _operator_department_code. TODO §8.1 (multi-department
  operator) отмечен в коде — одиночный FK department покрывает §10, но не §8.1.
- conversations ConversationListView: фильтр по channel.department.code для
  OPERATOR (свой отдел); ?department=sales|support для явного выбора (OWNER).
2026-07-09 00:58:00 +03:00
Andrey 092553941f feat(support): verify product support token and start session
SPEC-HUB-0011 §12 (алгоритм start) + §14 (error model).

- token.py: HS256 JWT verify через stdlib (hmac/hashlib/base64), envelope claims
  (iss/aud/contract/iat/exp/jti/data). RS256/EdDSA — TODO.
- jsonpath.py: ограниченный JSONPath-subset ($.field, $.a.b, $.arr[0].field).
- session.py: verify_and_resolve (channel policy → token → contract → schema →
  identity/operator/ai/search extraction) + start_support_session (snapshot upsert,
  create-or-continue conversation, audit success/denied). Raw token не сохраняется
  и не логируется — только хэш jti.
- errors.py: машинные коды (TOKEN_*, CONTRACT_*, PAYLOAD_SCHEMA_INVALID, ...) +
  SupportSessionError; публичное сообщение безопасное.
- services.py: register_contract / set_contract_status (зеркало products).
- selectors.py / serializers.py / views.py / urls.py: CRUD контрактов (IsOwner),
  POST /api/v1/support/sessions/ (AllowAny, продукт→Hub), snapshot-by-subject.
- Публичный error envelope {error, message} для start (как OrderIngestView).
2026-07-09 00:55:03 +03:00
Andrey e2c9295295 feat(support): seed support department, channels and identity contract
SPEC-HUB-0010 §4.1/§4.2, SPEC-HUB-0011 §5.2.

- bootstrap_edevs_owner: создаёт отдел support (BootstrapResult.support_department).
- seed_hub_initial_data: support-каналы foxray-support/firepage-support с политикой
  requires_auth=True, anonymous/sales/checkout=False; CHANNEL_SPECS вынесены в
  _seed_specs.py (NO GOD FILES: команда 226 строк).
- support/seed_support.py: контракт foxray.support.v1 (ACTIVE, пример из SPEC §5.2),
  привязка к каналу через allowed_channels; генерация support_token_secret для
  продуктов (EncryptedCharField, не печатается/не логируется).
- Обновлены существующие тесты: departments теперь {sales, support}.
2026-07-09 00:45:14 +03:00