Commit Graph
107 Commits
Author SHA1 Message Date
Andrey ae98760824 ✨ feat(access): implement stage 3 scoped authorization 2026-07-13 09:22:43 +03:00
Andrey 9a701a354b fix(design): remove old, add new baseline 2026-07-13 07:27:18 +03:00
AndreyandClaude Opus 4.8 389e1e0710 ✨ feat(identity): административная иерархия OWNER/ADMIN и передача владения (ADR-HUB-0027 этап 2)
Единая target-aware policy can_manage_employee(actor, target, action) и can_create_role
(governance.py) по матрице SPEC-HUB-0016 §8: OWNER управляет ADMIN и EMPLOYEE, ADMIN —
только EMPLOYEE, смена роли и передача владения — только OWNER.

Роль ADMIN активирована: is_manager/IsManager, обычные capability уровня организации
открыты OWNER и ADMIN (каналы, AI, интеграции, продукты, заказы, корректировки продаж,
контракты поддержки, командный центр, звонки, уведомления). Governance-действия над
привилегированными целями остаются за OWNER.

Employee endpoints переведены на governance: create ADMIN/EMPLOYEE по policy, update
делит проверки profile/role/placement, block/unblock/reset/terminate — через
can_manage_employee с аудитом отказа. Новый атомарный OwnershipTransferView сохраняет
инвариант ровно одного владельца (понижение прежнего перед промоутом, row-lock).
Аудит по SPEC-HUB-0016 §16; payload отдаёт per-actor permissions.

Frontend: ADMIN получает доступ уровня владельца (access.ts + тесты), действия над
сотрудником скрываются по backend permissions. UI передачи владения не проектируется
до утверждения design gate (SPEC-HUB-0016 §14).

Проверено полным backend-suite в Docker (293 tests, OK), typecheck и vitest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:50:42 +03:00
AndreyandClaude Opus 4.8 4e26502984 🐛 fix(conversations): command overview uses primary_department (ADR-HUB-0027)
Пропущенный при переименовании department->primary_department ORM-фильтр в
command_center_overview падал с FieldError. Проверено полным backend-suite в Docker
(276 tests, OK), паритет с CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:51:12 +03:00
AndreyandClaude Opus 4.8 cb3932e3f6 ✨ feat(identity): employee roles, position and org placement (ADR-HUB-0027 этап 1)
Системные роли OWNER/ADMIN/EMPLOYEE (удалён OPERATOR), обязательная должность
position_title и основной отдел primary_department. Инварианты: владелец на уровне
компании, ровно один OWNER на организацию. Двухфазная миграция OPERATOR->EMPLOYEE
без расширения прав; операционный доступ сохранён compatibility-адаптером.
Обновлены DTO/API/формы, список и карточка сотрудника, тесты модели и инвариантов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:25:10 +03:00
AndreyandClaude Fable 5 192497ead0 💄 rebrand(ui): CustoCRM logo and product name instead of Edevs Hub
- New logo: favicon.svg and LogoIcon (replaces PulseIcon) in sidebar and auth screens
- Source logo asset saved to resources/icon.svg
- Replace "Edevs Hub" with "CustoCRM" across internal-ui, web-chat, backend strings (email sender/subject, TOTP issuer, notification bot texts, OpenAPI title), env examples and README
- Add .claude/launch.json for internal-ui dev server (bind 127.0.0.1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:49:02 +03:00
AndreyandClaude Opus 4.8 19ad83ff0a ✨ feat(sales): legacy orders migration command (ADR-HUB-0025 §11)
Phase 3 миграции legacy orders → домен продаж. Команда import_legacy_orders,
по умолчанию DRY-RUN (только отчёт по классам, без записи).

- classify_legacy_order: external / manual / pending_ambiguous; PENDING не
  считается продажей и требует ручного решения (§11.2)
- import_legacy_order: подтверждённый Order (PAID/REFUNDED/CANCELLED) →
  Sale + SaleEvent(sale.legacy_imported), sales_source=null, идемпотентно по
  metadata.legacy_order_id; OrderItem → line_items_snapshot; fulfillment_status
  и legacy id — только в metadata; contact/conversation сохраняются
- provision_product_sales_source: создать SalesSource(PRODUCT_API) и скопировать
  действующий Product.ingest_token_hash (§11 шаг 2-3) — без генерации/ротации
- флаги --apply / --provision-sources / --product; команда НИКОГДА не удаляет
  legacy, не отключает legacy write API и не ротирует token (гейты §11 11-12)
- 7 тестов (dry-run, import, идемпотентность, refund→0, PENDING-skip, provision)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 09:51:35 +03:00
AndreyandClaude Opus 4.8 1b93b1ee80 ✨ feat(sales): external sales tracking domain + UI (ADR-HUB-0025)
Заменяет учёт заказов Hub на учёт подтверждённых внешних продаж по
ADR-HUB-0025 / SPEC-HUB-0014. Legacy orders оставлен как есть до миграции.

Backend (новый app hub_platform.sales):
- модели Sale (проекция) / SaleEvent (append-only журнал) / SalesSource /
  AttributionToken / ExternalCustomerIdentity
- единый application service: событие сохраняется raw, проекция применяется
  идемпотентно и синхронно в транзакции (async-воркера в проекте нет)
- Product Sales API POST /api/v1/product-sales/events (Bearer на источник)
- ручная фиксация + correct/cancel/partial-refund/refund через /api/v1/sales/
- attribution token issue, аналитика gross/refund/net
- команда issue_sales_source_credential; 19 тестов

Frontend (internal-ui):
- единый реестр продаж (registry) вместо вкладок Заказы/Платежи/Исполнение
- карточка продажи (sale-detail) с append-only таймлайном событий и
  действиями OWNER; вкладка клиента Заказы -> Продажи
- роуты salesOrders/salesOrderDetail перенаправлены на новые страницы

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 09:43:32 +03:00
Andrey 2b9946776a ✨ feat(calls): add audio alerts and widget indicators 2026-07-12 08:10:05 +03:00
AndreyandClaude Opus 4.8 9052fa5c4b fix(inbox): диалоги по времени последнего сообщения + вкладка «Контакт»
- инбокс сортируется по времени последнего сообщения (annotate
  Max(messages.created_at)), а не по служебной активности (claim/takeover)
- правый сайдбар: вкладка «Клиент» → «Контакт» (sales и support)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 06:22:02 +03:00
AndreyandClaude Opus 4.8 8a14d39c26 fix(worker): короткий long-poll мессенджеров — outbox не простаивает
Единый воркер делает inbound getUpdates/updates и outbox-диспатч в одном
потоке. Hold-time long-poll был 20с: при подключённых MAX+Telegram цикл
блокировался до ~40с, из-за чего приглашения звонков, уведомления и ответы
AI уходили с задержкой ~30с и дольше. Hold-time вынесен в
HUB_MESSENGER_POLL_TIMEOUT_SECONDS (default 2с) и применяется в обоих
транспортах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:33:37 +03:00
AndreyandClaude Opus 4.8 c55a3199fe feat(calls): call-метрики без медиаконтента — проход D этапа E09
- 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>
2026-07-12 04:37:54 +03:00
AndreyandClaude Opus 4.8 e8183777df feat(calls): Coturn TURN fallback — проход C этапа E09
- turn.py: краткоживущие TURN REST-credentials (coturn use-auth-secret),
  username "<expiry>:hub", credential base64(HMAC-SHA1(secret, username))
- ice_servers_payload: direct-first STUN + TURN fallback, генерация на
  каждый запрос токена; без секрета TURN-URL не отдаются
- settings + env: HUB_CALL_TURN_URLS/SECRET/TTL_SECONDS
- compose.production: сервис coturn (host net, realm/secret/relay-range/
  external-ip, hardening, healthcheck turnutils_stunclient)
- tests: формат credentials, HMAC, порядок direct-first, отказ без секрета

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:44:39 +03:00
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 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 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 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 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 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 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 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 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 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 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 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 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
Andrey bf77a56c93 feat(support): link conversation to support identity snapshot
ADR-HUB-0022: sales Contact и Support Identity разделены. Conversation теперь
содержит ровно один источник identity (XOR): nullable sales Contact ИЛИ nullable
FK support_identity_snapshot.

- Conversation.contact → nullable (существующие sales-диалоги валидны).
- Conversation.support_identity_snapshot — nullable FK (PROTECT).
- CheckConstraint conversation_exactly_one_identity.
- conversation_payload: contact опционален, добавлен supportIdentitySnapshot
  (краткая карточка); история выбирается по тому же источнику identity.
- stats.py: safe fallback имени клиента (snapshot.display_name) для support-диалогов.
- clients.py остаётся sales-специфичным (строится от Contact — корректно).
2026-07-09 00:36:09 +03:00
Andrey e12e0bde39 feat(support): add channel policy flags and product support secret
SPEC-HUB-0010 §4.2 / SPEC-HUB-0011 §4.3.

- Channel: флаги политики (requires_authenticated_product_identity,
  allow_anonymous_sessions, allow_self_reported_contact, allow_sales_attribution,
  allow_checkout_actions). Значения по умолчанию сохраняют поведение sales-каналов.
- Product.support_token_secret: EncryptedCharField (Fernet at rest) — HS256-секрет
  проверки Product Support Token. В API не отдаётся (product_payload не включает).
- channel_payload отдаёт флаги политики (camelCase) для UI.
2026-07-09 00:32:16 +03:00
Andrey 7af0796d31 feat(support): add ProductSupportContract and SupportIdentitySnapshot models
Новый bounded-context app hub_platform.support (ADR-HUB-0022, SPEC-HUB-0011).

- ProductSupportContract: версионированный контракт идентификации клиента
  поддержки (code <product>.support.v<major>, статусы DRAFT/ACTIVE/DEPRECATED/
  DISABLED, schema/mapping/operator_ui/ai_context/search/sensitive JSON).
- SupportIdentitySnapshot: проверенный снимок identity (subject_key обязателен,
  raw token не хранится — только хэш jti; исторический снимок диалога).
- Регистрация app в INSTALLED_APPS и префикс /api/v1/support/ (endpoints далее).
- clean() проверяет формат code, версию и принадлежность продукта организации.
2026-07-09 00:29:00 +03:00
Andrey b61e0a8b6d feat(hub): add production deploy pipeline and initial seed 2026-07-05 00:00:52 +03:00
AndreyandClaude Opus 4.8 c99bfdd828 feat(orders): product-backend order ingest webhook (ADR-HUB-0018)
Orders originate in the product backends (FoxRay/FirePage); Hub records
them. Added an authenticated ingest webhook instead of manual order entry:

- POST /api/v1/orders/ingest/ (AllowAny + product-token auth via
  X-Product-Token; token stored only as SHA-256). Idempotent by
  (organization, source=product.code, externalId). Resolves the contact
  from conversationId when given, else creates one from contactName;
  resolves items by offerCode within the product; PAID stamps paid_at +
  fulfillment PENDING. Audited (orders.ingested / ingest_duplicate).
- Order gains source/external_id with a partial-unique constraint;
  Product gains ingest_token_hash. Migrations included.
- issue_product_ingest_token <code> management command generates + prints
  the token once (user-run; rotates on re-run).

Tests: 10 orders tests (incl. ingest auth + idempotency); full suite 95/95.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:58:22 +03:00
AndreyandClaude Opus 4.8 5b831e6cae feat(orders): commerce orders domain + real sales metrics
New orders app (ADR-HUB-0018: catalog & sale recorded in Hub, execution
in product backends):
- Order + OrderItem models with payment/fulfillment status; items snapshot
  offer + price version so the record is stable. Migration included.
- Services (create_order, mark_paid, cancel_order, set_fulfillment),
  selectors, serializers, REST API (list/create/detail + status actions)
  under IsAuthenticated/IsOwner, org-scoped. Tests: 6 green.
- seed_orders management command for local demo (idempotent).

Wired real numbers into the spots that were "—":
- Sales overview: Продажи / Выручка / Конверсия + per-product sales/revenue.
- Clients list: orders count and purchases total per contact.
- Client detail: Заказы tab + summary (orders, purchases).

Full backend suite: 91/91.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:30:04 +03:00
AndreyandClaude Opus 4.8 a91ab516a8 feat(sales): real client detail page
The client card was a single hardcoded mock. It now loads a real contact
by id and renders real data:

- Backend: conversations/clients.py client_detail() returns the contact's
  channels/products, dialog list, channel identities, lifecycle-derived
  activity, and real audit events (AuditEvent for the contact's
  conversations). Served at /api/v1/conversations/clients/<id>/.
- Routing: clients list passes the contact id; App/Shell thread
  selectedClientId through to the detail page, which fetches by id.
- UI: header/overview/dialogs/identities/audit show real data; orders and
  consent have no domain in Hub, so they render honest empty states (no
  fabricated orders, revenue, or consent log). Removed the fake
  "Согласие получено" header badge.

Backend suite 85/85; typecheck green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:36:21 +03:00
AndreyandClaude Opus 4.8 ca453269e1 feat(sales): real data in the Clients list
The Clients section rendered a hardcoded mock array. Now it lists real
contacts with their conversations:

- Backend: conversations/clients.py aggregates each Contact into channels
  used (MAX/TG/WEB), products (via channel.product), open-dialog count,
  last activity and current mode (ai/operator/wait/closed). Served at
  /api/v1/conversations/clients/ (IsAuthenticated, org-scoped).
- UI: useClientsData fetches; useSalesClients now takes the data as input
  (filters/sort unchanged); the page shows loading/empty states.
  Avatar initials/colour and relative time are derived client-side.

Orders/revenue have no domain in Hub, so those columns stay 0/"—" — no
fabricated numbers. The client detail page is still mock (separate task).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:01:48 +03:00
AndreyandClaude Opus 4.8 d56eb61e6f test: realign AI suite to channel-anchored model; fix stale TOTP tests
hub_platform.ai tests still used the pre-refactor product-anchored API
(AIAgent.product, invoke_chat(product=...), ProductAIRelease, product on
doc/release create), so 4 failed + 18 errored. Rewritten against the
current channel anchoring (ADR-HUB-0019):
- helper seeds channels + agents (bootstrap no longer does — seed_channels
  owns that in prod); agents/releases/invocations/retrieval/test-chat are
  all channel-scoped now.
- knowledge/prompt create uses scope (GLOBAL default; PRODUCT for the
  release/retrieval fixtures); foreign-product rejection uses scope=PRODUCT.

Also fixed two stale identity tests that asserted the old TOTP-on default:
bootstrap now sets totp_required=False by design, and the setup→confirm
flow opts the profile into TOTP first.

Full backend suite: 85/85 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:42:48 +03:00
AndreyandClaude Opus 4.8 b85cbd6761 feat(ai): record real per-dialog cost from OpenRouter usage
AI cost was always 0 because the pricing table had no entry for the
channel model. Now:
- OpenRouter chat requests ask for usage accounting (usage.include=true)
  and capture the provider-reported usage.cost into ChatResult.cost_micros.
- invocation recording uses the provider cost when present, falling back
  to the local price table otherwise.
- Added Anthropic Sonnet to the fallback table.

Verified live: a real channel turn recorded cost_micros=11688 (~$0.012)
and the sales overview "Стоимость AI" now reflects it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:09:47 +03:00