Commit Graph
186 Commits
Author SHA1 Message Date
Andrey e13542fc45 fix(ci): harden release and deployment workflow 2026-07-14 12:19:38 +03:00
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 a4fcceb838 🚑 fix(calls): TURN-over-TLS на 443 для звонков через VPN клиента
При включённом VPN у клиента прямое P2P не устанавливается и звонок обязан
идти через TURN-relay, но coturn отдавал только turn: на UDP/TCP 3478 — а VPN
режет UDP и нестандартные порты. Добавлен turns:443 (TLS маскируется под HTTPS
и проходит строгие сети). coturn слушает выделенный публичный IP (3478 + TLS
443) с монтированием LE-сертификата; env-пример дополнен HUB_TURN_LISTENING_IP,
HUB_TURN_TLS_PORT и требованиями к облачному firewall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 23:53:00 +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 Fable 5 e358a0b327 Rename code/hub.edevs.tech to code/custocrm, update docs
- Move pack.bat to repository root
- Ignore CustoCRM.zip produced by pack.bat
- Minor doc edits in ADRs, specs, config and plan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:27:39 +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 58925e3036 fix(calls): инициалы точно по центру круга
Прошлый padding-bottom двигал буквы вверх — направление было неверным. По
факту flex ставит baseline в центр круга, а заглавные без нижних выносных
сидят на baseline и уходят вверх на ~0.37em. Сдвиг вниз padding-top:0.75em
(замерено попиксельно по факту рендера: big +0.25px, small -0.25px).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 06:22:01 +03:00
AndreyandClaude Opus 4.8 8cf8f72799 fix(calls): звук аудиозвонка, полноэкранный режим и крестик
- поток собеседника монтируется всё время активного звонка, а не только при
  включённой камере — иначе в аудиозвонке (камера выключена) не было звука
- :fullscreen раскладывает карточку колонкой (media flex:1) — нижняя панель
  кнопок больше не уходит за край экрана
- кнопка полного экрана стала тумблером; крестик в полноэкранном режиме
  сначала выходит из fullscreen, а не завершает звонок

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:33:40 +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
AndreyandClaude Opus 4.8 ddde6e07d7 fix(calls): центрирование инициалов и full-screen звонок на мобильном
- .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>
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 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