ТЗ, api-map, техническая документация, инструкция пользователя и контракт операторских настроек: настройки ИИ-провайдера перенесены в консоль оператора, пользовательский POST /api/ai/check удалён.
138 KiB
Дейл (Deal) — Техническая документация
Версия: 2.0 (этапы 0–12: единая карточка, оператор-консоль и аналитика, i18n, метрики/устойчивость) Дата: 2026-09-10 Содержание: полный стек, структура, конфигурация, развёртывание, эксплуатация.
1. Обзор стека
| Слой | Технология |
|---|---|
| Язык | C# (современный, актуальная LTS .NET) |
| Бэкенд-ядро | Модульный монолит core (ASP.NET Core: Web API, gRPC, SSE) |
| База данных | PostgreSQL (одна БД, схема на тенанта) |
| ORM/доступ | EF Core (основной) + Dapper (тяжёлые запросы, где нужно) |
| Миграции | Механизм миграций на все схемы тенантов |
| Очередь/шина | Outbox-паттерн в Postgres; порт IEventBus; Kafka — позже |
| ML-сервис | .NET + ONNX Runtime, пул моделей per-tenant |
| AI-сервис | .NET, фасад LLM-провайдеров (OpenAI-совместимые), учёт токенов |
| Telegram | .NET (WTelegramClient/аналог), ферма сессий, анти-бан |
| Файлы | MinIO (S3-совместимое хранилище) |
| Фронтенд | Vue 3 + Vite + Tailwind |
| Межсервисно | gRPC + Protobuf (mTLS — за флагом DEAL_MTLS_*, §10/§13.8) |
| Наблюдаемость | Serilog (JSON: консоль + rolling-файл) → Promtail → Loki → Grafana; метрики OTel → Prometheus; трейсы OTel → Collector → Tempo; ресурсы cAdvisor/node-exporter → Prometheus; единый UI — Grafana |
| Прокси/edge | Caddy (TLS, security-заголовки); Cloudflare/k8s — вне этапа (§10/§11) |
| Контейнеры | Docker / docker compose (VPS); k8s — позже |
| Бэкапы | Ежедневные: pg_dump + MinIO + сессии |
| CI | Сборка, тесты, SAST, сканирование зависимостей и образов |
2. Структура репозитория
src/
core/ # модульный монолит (один sln, один процесс)
Deal.sln
Deal.Api/ # host: /api-контракт, gRPC-сервер, SSE, DI-композиция
Deal.Modules.Cards/ # модель единой карточки + каталог контейнеров (этап 9)
Deal.Modules.Pipeline/
Deal.Modules.Kanban/ # таблицы Cards/Containers, правила, комментарии, архив/корзина + сервисы «Выбранных»
Deal.Modules.Telegram/ # каталог диалогов/каналов и превью сообщений (этап 6)
Deal.Modules.Discovery/
Deal.Modules.Settings/
Deal.Modules.Tenants/
Deal.SharedKernel/
Deal.Infrastructure/
Deal.Contracts/
tests/
ml-service/ # Deal.Ml.sln
ai-service/ # Deal.Ai.sln
telegram-service/ # Deal.Telegram.sln
contracts/ # общие .proto
deploy/ # compose.dev.yml / compose.prod.yml (развёртывание)
frontend/ # Vue 3 + Vite
Принципы:
- один процесс = один sln;
core— единственное место с бизнес-логикой и БД;- сервисы stateless по отношению к данным тенантов (ML получает текст — отдаёт решение);
.proto— общий язык между процессами (вcontracts/, подключается shared-файлами).
3. Модули core
| Модуль | Проект | Владеет |
|---|---|---|
| Карточки (ядро) | Deal.Modules.Cards |
модель единой карточки (ICard/ISource/IContainer/ICardMover), каталог контейнеров по умолчанию, единые префиксы id |
| Пайплайн | Deal.Modules.Pipeline |
очередь, отсев, dedup |
| Канбан (дашборд) | Deal.Modules.Kanban |
таблицы Cards и Containers, правила колонок, комментарии, CardMoves, MlOutbox |
| «Выбранные» | Deal.Modules.Kanban (CardsService.Selected) |
сервисы стадий/напоминаний/файлов/ссылок над теми же строками Cards |
| Discovery | Deal.Modules.Discovery |
задачи поиска, кандидаты, чёрный список |
| Настройки | Deal.Modules.Settings |
настройки тенанта, промпты, валюты |
| Тенанты | Deal.Modules.Tenants |
реестр тенантов, пользователи, инвайты, лимиты (в public) |
| Каналы/Telegram | Deal.Modules.Telegram |
каталог диалогов, превью сообщений, мониторинг источников |
После этапа 9 (единая карточка): модель/контейнеры — в Deal.Modules.Cards; Kanban владеет таблицами
Cards/Containers и сервисами пространства «Выбранные» (CardsService.Selected) над теми же строками
(отдельных модуля Deal.Modules.Projects и таблицы ProjectCards больше нет).
Зависимости между модулями — только через публичные интерфейсы модуля-владельца.
Доменные события — через IEventBus (outbox). Правила:
- внутри модуля таблицы — его собственность;
- чужие таблицы не читаем/не пишем SQL напрямую;
- общие справочники живут в модуле-владельце.
4. Мультитенантность и БД
Схемы
public: тенанты, пользователи, инвайты, ключи приложения, глобальные настройки.tenant_<id>.*: все данные тенанта (карточки, колонки, настройки, обучение и т.д.).
Доступ
tenantId— из сессии/JWT (core) или из gRPC-метаданных (сервисы).- DAL формирует
search_path=tenant_<id>; пул соединений на схему. - Изоляция проверяется: принадлежность объекта тенанту до любого действия (IDOR-защита).
Миграции
- Миграции пишутся один раз (как для одной схемы) и применяются механизмом
«ко всем схемам тенантов»: список схем из
public.tenants, применение по очереди, версия миграции хранится на схему. Детали — в плане реализации этапа 0.
Ключевые таблицы public
tenants(Id, Name, Status, CreatedAt)
users(Id, Login, TenantId, PasswordHash, Status, CreatedAt) -- Login = email пользователя
invites(Code PK, Email, TenantId, Status, ExpiresAt, ActivatedAt, CreatedById, CreatedAt)
global_settings(Key, Value, UpdatedAt)
-- этап 7: оператор/SaaS
token_usage_events(id, tenant_id, at, provider, model, kind, prompt_tokens, completion_tokens, total_tokens, detail_json)
-- этап 12: глобальные настройки сервиса (секреты шифруются)
global_settings(key, value, updated_at)
token_usage_events— история расхода токенов (этап 10, T2; подробнее — §13.10).global_settings— глобальные настройки уровня сервиса; сейчас хранит ключи приложения Telegram (telegramKeys:api_id/api_hash, hash — вenc:) и конфигурацию ИИ-провайдера (aiConfig:providerId/baseUrl/model/apiKey— вenc:), которые задаёт оператор глобально (ручкиGET/PUT /api/operator/settings/telegram-keysи/ai-config, проверка связи —POST /api/operator/settings/ai-config/check); тенант эти настройки не видит и не задаёт. Операторские таблицы этапа 7 (operators,operator_sessions,tenant_limits,audit_log) и их контур описаны в §13.8. С этапа 12 счётчики распределённого rate-limit и попыток входа —public.rate_limit_counters(см. §10).
Ключевые таблицы схемы тенанта (пример)
QueueItems, RejectedItems, DedupEntries,
Cards, Containers, CardMoves, LeadComments, MlOutbox,
DiscTasks, DiscCandidates, DiscBlacklist, DiscLog,
Dialogs, TgMessages, settings
Фактическая схема на конец этапа 1 (2026-09-05)
Реализованный фундамент (см. раздел 13 «Быстрый старт»). Списки выше — целевой вид будущих этапов; ниже — то, что реально создано миграциями этапа 1.
public(системный контекст, миграцияInitialSystem):
tenants(Id uuid PK, Name varchar(200), Status text, CreatedAt timestamptz) -- реестр тенантов
users(Id uuid PK, Login varchar(200) UNIQUE, TenantId uuid → tenants, -- учётные записи
PasswordHash text, Status text default 'active', CreatedAt timestamptz)
sessions(TokenHash varchar(64) PK, UserId uuid → users ON DELETE CASCADE, -- сессии: кука deal_session,
Login varchar(200), ExpiresAt timestamptz, CreatedAt timestamptz) -- срок 30 дней
- Схема тенанта
tenant_<id>(миграцияInitialTenantприменяется на схему):
settings(Key varchar(200) PK, ValueJson text, UpdatedAt timestamptz) -- настройки тенанта
- История миграций:
public.__EFMigrationsHistoryи__TenantMigrationsHistoryв схеме тенанта. - Имена таблиц/колонок — по конвенции EF Core (PascalCase). Схемы тенантов создаются и мигрируются
автоматически (
TenantProvisioningService); при старте API создаётся дефолтный тенант и admin (TenantBootstrapService).
5. Сервисы и контракты
gRPC-контракты (src/contracts/*.proto, общий проект Deal.Proto)
telegram.proto(пакетdeal.telegram.v1) — два сервиса:TelegramService— команды ядра к telegram-service (GetStatus, StartPhone, StartQr, SendCode, SendPassword, Logout, RefreshDialogs, SetMonitor, SetMonitorAll, Backfill, ReadRecent, Search, GetInfo, ReadForEval, Join, Leave);SourceIngressService.PushSource(generic-контракт источников,sources.proto) иIngressService(SyncDialogs, ReportStatus) — исходящий поток telegram-service → core; сервер — gRPC-ингресс core :5082).ml.proto(пакетdeal.ml.v1) —MlService: Predict (text → {take,label,scores,margin,type}), Status, Reset, TrainBatch; всё с metadatatenant-id.ai.proto(пакетdeal.ai.v1) —AiService: Filter, Classify, GenerateKeywords, EvaluateFit; ответ + расход токенов (usage →TokenUsageRecorder, §6).- Полный состав RPC/полей и семантика ошибок — §13.7 и шапки
.proto.
Безопасность сервисов
- Каждый RPC несёт metadata
tenant-id+service-token; интерцепторы всех процессов fail-closed сверяют токен с envDEAL_SERVICE_TOKEN(gRPC-health освобождён). Принадлежность (сессия/модель тенанта) проверяется сервисом по своей модели — полю в теле не доверяем. - Dev — gRPC plaintext + общий service-token (compose.dev); mTLS — за флагом
DEAL_MTLS_*(взаимные сертификаты, цепочка → CA; меняется только транспорт, контракты — нет, Ruling 6 этапа 7). - telegram-service: сессии привязаны к тенанту (файлы AES-256-GCM, ключ
DEAL_TELEGRAM_SESSION_KEY); команда исполняется только на сессии своего tenantId; проверка принадлежности диалога; join под квотами тенанта; исходящие сообщения помечены tenantId на входе.
6. Ключевые сквозные механизмы
Outbox / IEventBus
- Событие и бизнес-эффект пишутся в одной транзакции; фоновый диспетчер доставляет события подписчикам (в процессе) и/или в сервисы (gRPC).
- Реализация сменная (outbox → Kafka) без правки бизнес-логики.
Лимиты токенов (ИИ-бюджет; фактически — этап 7, §13.8)
- ai-service возвращает usage;
TokenUsageRecorderинкрементитpublic.tenant_limits(период месяц/день, ленивый reset), пишет историю вpublic.token_usage_events(этап 10, T2) и инкрементит метрикиdeal.ai.*/deal.ml.*(§7); кроме того ведётся lifetime-KVaiTokenUsage. - Коллекции
public.token_usage_eventsподчищает фоновыйDataRetentionScheduler(§10); накопительные поля прошедших периодовtenant_limitsсбрасываются там же. - Гейт-декораторы
BudgetedAiClassifier/BudgetedAiTools(только приServices:Ai:UseLocal=false): исчерпание/приостановка → Local-фолбэк (приём не блокируется); SSE-тосты на 80/100% бюджета.
Файлы
- MinIO (S3): бакет на продукт (
deal-files); ключ объекта строитCardsService—projects/<card_id>/<file_id>_<unixMs>_<safeName>(tenant_<id>/…-префикса нет). - Тип файла определяется автоматически (MIME + расширение).
- Доступ к файлу — только через core с проверкой tenantId.
Напоминания
- Фоновый планировщик (в core): проверка due-напоминаний отложенных карточек; при срабатывании — уведомление (SSE/тост).
- Если напоминания отключены — запланированные не срабатывают и очищаются.
SSE
- События фронту (4 типа):
new_card(полная карточка),toast({text,icon}),reminder_due({id,title,containerId}),system_status(объектtg.status()).new_leadпереименован вnew_card;pipeline_stats/boards_changed/leads_reclassifiedпрототипа не реализованы. - Поток
GET /api/events— per-tenant (SseBroker), ping-комментарий каждые 15 с. Публикуют только эндпоинты/планировщики Api-слоя (модули — чистые).
7. Наблюдаемость (фактический стек — этапы 7/10, Ruling 7)
- Serilog.AspNetCore во всех 4 процессах (core + telegram/ai/ml): консоль в формате JSON
(
CompactJsonFormatter; в Development — текст) + rolling-файлdata/logs/deal-<процесс>.json(30 дней; envDEAL_LOG_LEVEL/DEAL_LOGS_DIR). Секреты/пароли/ключи не логируются; gRPC-health не логируется. - Access-логи: HTTP —
HttpAccessLogMiddleware(первый в конвейере после ForwardedHeaders — длительность и статус всего пути; с BL-LOG-ACTOR —actorиtenantиз сессии); gRPC-ингресс —RpcCallLoggingInterceptor(health освобождён). - PROD-стек логов: docker-логи контейнеров →
promtail→loki(retention 7 суток) →grafana(127.0.0.1:3001— только оператору по SSH-туннелю). Поднимается профилемobservabilityфайлаdeploy/compose.prod.yml(живой подъём — ⚠ Manual):docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml --profile observability up -d(нуженDEAL_GRAFANA_ADMIN_PASSWORDв.env.prod; порты Grafana/Prometheus — только loopback). Остановка —docker compose -f deploy/compose.prod.yml --profile observability down. - Провижининг Grafana — как код (
deploy/observability/grafana/provisioning, монтируется в контейнер):datasources/datasources.yml— датасорсы Loki (uidloki, default), Prometheus (uidprometheus) и Tempo (uidtempo); у Loki —derivedFieldsTraceID → Tempo (клик по traceId в логе открывает трейс), у Tempo —tracesToLogsV2→ Loki иserviceMap/nodeGraphпо метрикам;dashboards/dashboards.yml— папкаДейлиз/var/lib/grafana/dashboards. Дашборды — файлыdeploy/observability/grafana/dashboards/*.json: правки только в репозитории, UI-изменения не сохраняются (allowUiUpdates: false). - Метки Promtail (
deploy/observability/promtail.yml):service(имя compose-сервиса),container(имя контейнера),stream; пайплайн дополнительно поднимает меткуlevelиз Serilog-поля@l(Information/Warning/Error/Fatal) — только для deal-процессов поservice-селектору, логи прочих контейнеров хоста не парсятся. Запросы Grafana — LogQL, JSON разбирается на лету:{service="core"} | json | StatusCode >= 500. - Дашборды (папка «Дейл», источник — Loki):
Deal-Health— активность логов и строки Error/Fatal по процессам (доступность сервиса);Deal-Auth— успешные/неудачные входы и выходы (контур тенант/оператор по пути + HTTP-код из access-лога core) и активация инвайтов (/api/join);Deal-Errors— HTTP 5xx, необработанные исключения (@x), Error/Fatal, ошибки gRPC и общая лента;Deal-Rps— нагрузка HTTP+gRPC (RPS), top-путей/методов и p50/p95 длительности запроса;Deal-Logs— обзор логов с фильтрами по сервису и уровню, активность по тенантам (AI/ML/Telegram);Deal-Traces— поиск трейсов (Tempo, TraceQL), спаны по сервисам, переход к логам по traceId;Deal-Resources— потребление ресурсов контейнерами (cAdvisor) и хостом (node-exporter): CPU/RAM, свободное место на дисках. Актор в логах: с BL-LOG-ACTOR access-лог включаетactor(login пользователя/оператора) иtenant; полная лента действий с деталями —public.audit_log(append-only) черезGET /api/operator/audit/ экран «Аудит» оператор-консоли.
- Алерты Prometheus (этап 12) — правила
deploy/observability/prometheus-rules.yml(см. подраздел «Метрики»); исчерпание ИИ-бюджета по-прежнему доставляется SSE-тостом тенанту — отдельной бюджетной метрики в Prometheus нет (метки метрик низкокардинальные, без tenantId/бюджета).
Метрики (Prometheus + Grafana — этап 12, пакет A)
- Экспорт из 4 процессов: OpenTelemetry → экспортёр Prometheus, общая настройка —
Deal.Grpc.Hosting(DealMetricsHosting) для сервисов иDeal.Api/Observability/DealMetricsHosting.csдля ядра. Инструментация даёт готовые метрики без ручного кода: входящие запросыhttp.server.request.duration(RPS/латентность/ошибки по route, включая gRPC-вызовы) и исходящие HTTP-клиентыhttp.client.*. - Эндпоинт
/metrics— на отдельном HTTP/1.1 Kestrel-эндпоинте :9464 у всех 4 процессов (gRPC-порты :5101–:5103/:5082 слушают только HTTP/2, обычный GET-scrape по ним невозможен). Порт переопределяется envMETRICS_PORT; наружу не публикуется (scrape — внутри compose-сети). Формат — Prometheus. - Прикладные метрики (meter
Deal,deal.*; метки низкокардинальные — без tenantId/userId/cardId):deal_ai_calls_total/deal_ai_tokens_total{type=prompt|completion}— вызовы и токены платного ИИ;deal_ml_calls_total/deal_ml_tokens_total— вызовы и оценка токенов локального ML;deal_audit_events_total{event,actor}— события аудита по типу/актору;deal_pipeline_queue_depth,deal_ml_outbox_depth— суммарные глубины очередей (пайплайн, MlOutbox) по всем тенантам;deal_sessions_active— активные непросроченные сессии пользователей и операторов;deal_ai_budget_used_ratio{tenant}— доля израсходованного ИИ-бюджета периода (0..1) по тенантам (осознанное исключение из низкокардинального правила: бюджеты пер-тенантные, алерт должен знать тенанта). Gauge-значения собирает фоновыйDealMetricsCollectorядра (каждые 15 с) через существующие сервисы/хранилища (PipelineProcessingService.QueueCountsAsync,IMlLearningStore.CountOutboxAsync,public.sessions/operator_sessions); инкремент счётчиков токенов/аудита — там же, где пишутсяtoken_usage_events(TokenUsageRecorder) иaudit_log(AuditService).
- Scrape/Prometheus: сервис
prometheus(образprom/prometheus:v3.5.0) в профилеobservabilitycompose.prod; конфигdeploy/observability/prometheus.yml— jobdealс таргетамиcore/telegram-service/ai-service/ml-service:9464(target-меткаservice), retention 15 суток (volumedeal_prometheus_data). UI —127.0.0.1:9090(оператору по SSH-туннелю). В dev тот же сервис добавлен вdeploy/compose.dev.yml(профильobservability, UIlocalhost:9090). - Grafana-провижининг:
datasources/datasources.yml— датасорсы Loki (uidloki, default) и Prometheus (uidprometheus,http://prometheus:9090); дашбордDeal-Metrics-Overview(uiddeal-metrics) в папке «Дейл»: RPS/p95/5xx по сервисам, токены и вызовы AI/ML, глубины очередей, активные сессии, события аудита. Правки — файлами вdeploy/observability/grafana/dashboards/*.json. - Правила алертов Prometheus (
deploy/observability/prometheus-rules.yml, подключены черезrule_filesвprometheus.yml): сервис недоступен (up{job="deal"} == 0), рост 5xx (http_response_status_code=~"5.."), лаг очереди pipeline/ML-outbox (deal_pipeline_queue_depth,deal_ml_outbox_depth), пропажа метрик ядра (absent(deal_sessions_active)). Замечание: правила бюджета токенов нет — метрика бюджета в Prometheus отсутствует (см. §6/§10), поэтому алерт не вводится. - Как поднять/проверить:
docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml --profile observability up -d→ Prometheus/targets(все UP) → Grafana → папка «Дейл» →Deal-Metrics-Overview/Deal-Resources/Deal-Traces. Быстрая проверка экспортёра без Grafana:curl http://<процесс>:9464/metricsизнутри сети.
Трейсы (OpenTelemetry Collector + Tempo)
- Экспорт из 5 процессов: OpenTelemetry SDK → OTLP → OpenTelemetry Collector (
otel-collector: 4317) → Tempo (tempo:4317, хранилище трейсов, retention 7 суток). Настройка — общая вDeal.Grpc.Hosting(DealTracingHosting) для telegram/ai/ml/storage иDeal.Api/Observability/ DealTracingHosting.csдля ядра. Инструментируется входящий HTTP/gRPC (AspNetCore), исходящие HTTP-клиенты и gRPC-клиенты (GrpcNetClient) — трейсы сквозные от входа до БД/внешних сервисов. - Включение — опт-ин через env
OTEL_EXPORTER_OTLP_ENDPOINT(адрес коллектора, напр.http://otel-collector:4317); без него трейсинг выключен. В compose env задан пустым (${DEAL_OTEL_ENDPOINT:-}) — чтобы включить, задайтеDEAL_OTEL_ENDPOINTв.env. Имя сервиса в трейсах —OTEL_SERVICE_NAME(дефолт по процессу:core,telegram-service,ai-service,ml-service,storage-service). - Корреляция с логами: Serilog обогащается
TraceId/SpanIdизActivity.Current(TraceContextEnricher) — в Loki-логе естьTraceId, а датасорс LokiderivedFieldsдаёт переход из лога в трейс Tempo (и обратно —tracesToLogsV2). - Сервисы профиля:
otel-collector(otel/opentelemetry-collector-contrib:0.160.0, конфигdeploy/observability/otel-collector.yml) иtempo(grafana/tempo:2.8.1, конфигdeploy/observability/tempo.yml, volumedeal_tempo_data). Наружу порты не публикуются (dev — для отладки).
Ресурсы (cAdvisor + node-exporter)
- cAdvisor (
gcr.io/cadvisor/cadvisor:v0.52.1) — потребление ресурсов контейнерами (CPU/RAM/сеть/диск); node-exporter (prom/node-exporter:v1.9.1) — ресурсы хоста (CPU/RAM/ диски/сеть). Оба scrape'ит Prometheus (jobscadvisor,node-exporterвprometheus.yml). - Дашборд
Deal-Resources(uiddeal-resources): CPU/RAM контейнеров, CPU/RAM хоста, свободное место на дисках. Правила алертов по ресурсам — в отдельном файлеprometheus-resource-rules.yml, отключены по умолчанию (не входят вrule_files); пороги — через envDEAL_ALERT_*при включении.
8. Развёртывание (факт: dev-compose + prod-compose, один VPS)
Два compose-стека: deploy/compose.dev.yml (разработка/демо) и deploy/compose.prod.yml (прод;
единственный наружу — Caddy). Команды/детали — §13.7 (dev-стек этапа 6), §13.8 (этап 7, быстрый
сценарий оператора), §13.9 (бэкапы).
Примечание: наследие LeadRadar (DuckDB + MinIO + Python-ml) и его прежний корневой
docker-compose.ymlвынесены вarchive/leadradar-legacy/и к стеку Дейла не относятся; актуальные стеки — толькоdeploy/compose.dev.ymlиdeploy/compose.prod.yml.
Dev-стек (deploy/compose.dev.yml)
| Контейнер | Порт | Назначение |
|---|---|---|
deal-postgres |
5433 | Postgres 16, БД deal (host-порт; внутри 5432) |
deal-minio |
9000/9001 | MinIO (вложения; в Local-режиме необязателен) |
deal-core |
5080 / 5082 | Deal.Api: HTTP /api + gRPC-ингресс telegram |
deal-telegram-service / deal-ai-service / deal-ml-service |
5101/5102/5103 | автономные сервисы этапа 6 |
docker compose -f deploy/compose.dev.yml up -d --build — весь стек в сквозном gRPC-режиме
(Services__*__UseLocal=false); sh scripts/dev-smoke.sh — одна команда (подъём → health → login →
/api/tg/status → POST /api/cards → trash → флашер MlOutbox → /api/ml/status; trap → down). Host-режим
(core с хоста, Local-заглушки) — §13.1–13.6.
Prod-стек (deploy/compose.prod.yml)
- Одна внутренняя сеть; наружу — только caddy (80/443): TLS (шапка
deploy/caddy/Caddyfile—tls internalдля dev/интранет, для реального домена заменить на Cloudflare-origin/сертификаты), статикаsrc/frontend/dist,reverse_proxy /api → core:5080, security-заголовки (CSP/HSTS — здесь). core(:5080 http + :5082 gRPC-ингресс),telegram/ai/ml/storage-service(mTLS-env, Ruling 6),postgres/minioбез host-портов; healthcheck'и —grpc_health_probe(при mTLS — TLS-проба с PEMdeal-client.crt/.key)/pg_isready.- Профиль
observability:otel-collector/tempo(трейсы),loki/promtail(логи),prometheus/cadvisor/node-exporter(метрики и ресурсы),grafana(UI); см. §7. Секреты — только из.env.prod(шаблонdeploy/.env.prod.example, без дефолтных паролей; отсутствие → fail-fast:?). Rate limiting включён (RateLimit__Enabled: true), CORS — явныйSecurity__AllowedOrigins(DEAL_ALLOWED_ORIGINS), куки Secure,ForwardedHeadersдоверяет Caddy (KnownNetworks). - mTLS внутреннего gRPC — флаг
DEAL_MTLS_ENABLED=1+ сертификатыdeploy/certs/(scripts/mtls-certs.sh); основной HTTP :5080 остаётся http — TLS терминирует Caddy.
Порядок первого запуска (prod)
- Установить docker + docker compose на VPS.
- Скопировать
deploy/.env.prod.example→.env.prod; заполнить секреты: пароли БД/MinIO,DEAL_SERVICE_TOKEN,DEAL_ENCRYPTION_KEY,DEAL_TELEGRAM_SESSION_KEY,DEAL_ALLOWED_ORIGINS(origin фронта); опционально креды оператораDEAL_OPERATOR_LOGIN/PASSWORD,DEAL_DEFAULT_AI_BUDGET,DEAL_MTLS_*. Полный список — шапка.env.prod.example. - Применить системные миграции к БД стека (команда §13.2, строка подключения — прод-БД).
docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build(наблюдаемость — добавить--profile observability). Старт core: провижининг схем всех тенантов, bootstrap оператора (Production без env — warning и пропуск, Ruling 1).- Проверить: оператор
POST /api/operator/auth/login→ создать тенанта → инвайт →POST /api/join(быстрый сценарий — §13.8); health —/api/health,/api/operator/health. - Авто-проверка:
docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml config(rc=0). Живой подъём PROD-стека — ⚠ Manual (нужен docker).
Переменные окружения (prod; без дефолтных значений)
DEAL_PG_PASSWORD=... MINIO_ROOT_USER=... MINIO_ROOT_PASSWORD=...
DEAL_SERVICE_TOKEN=... DEAL_ENCRYPTION_KEY=... (32 байта base64)
DEAL_TELEGRAM_SESSION_KEY=... (32 байта base64, AES-GCM сессий)
DEAL_ALLOWED_ORIGINS=https://deal.example DEAL_OPERATOR_LOGIN=... DEAL_OPERATOR_PASSWORD=...
DEAL_MTLS_ENABLED=0|1 DEAL_MTLS_CERT_PASSWORD=... DEAL_DEFAULT_AI_BUDGET=...
Секреты — только через env/secret-хранилище, не в коде и не в репозитории.
CI/CD
- Единый прогон:
scripts/ci.sh(BL-CI, 2026-09-11) — сборка всех 5 решений (scripts/build.sh), тесты всех сервисов (scripts/test.sh: core/telegram/ai/ml/storage +npm run lint:i18n), скан уязвимых NuGet-зависимостей (dotnet list package --vulnerable --include-transitive), сборка фронта (npm ci && npm run build). Сборка — 0 warnings/0 errors (TreatWarningsAsErrors). - Готовый workflow:
.github/workflows/ci.yml(setup-dotnet 10 + setup-node 20 →sh scripts/ci.sh); первый прогон в удалённом CI — при публикации репозитория. - Нагрузочный прогон:
scripts/loadtest/(bash+curl и k6-вариант; логин admin/admin → контейнеры/карточки; RPS/avg/p95; см. README рядом). - Доставка на VPS: сборка образов →
docker compose ... up -d --build. - Результат локального прогона
scripts/ci.sh(2026-09-11): core 1315, telegram 130, ai 52, ml 38, storage 9 — всё PASS; фронтlint:i18n/buildзелёные. k8s — вне этапа (задел).
9. Бэкапы и восстановление (факт — scripts/backup.sh, Ruling 8; детали §13.9)
- Ежедневный бэкап —
scripts/backup.sh: (1) Postgres —pg_dump -Fcвсех схем (public + tenant_*); (2) MinIO-бакетdeal-files—mc mirror; (3) файловые данные — tar каталогов/томов (attachments, telegram-сессии AES-GCM, ml-модели); (4) retention 14 копий. Планировщик — вне контейнера: cron «0 2 * * *»/systemd-примеры — §13.9. Запуск —bash scripts/backup.sh(из корня). - Восстановление —
scripts/restore.sh(pg → minio → data; pg-шаг пересоздаёт БД целиком, minio/data — overlay): остановить сервисы →bash scripts/restore.sh [TS|pg|minio|data]→ поднять. Порядок и требования — §13.9. - Рекомендация Ruling 8: раз в месяц — тест восстановления на отдельном инстансе/томах.
- Потеря данных при ежедневном бэкапе допустима ≤ 24 ч (SLA тестового этапа).
- Реальный прогон
backup.shи restore-тест — ⚠ Manual (нужен docker-стек; здесь —sh -n, error-path-проверки, offline-проверка retention).
10. Безопасность (эксплуатационная сводка — фактическая, этап 7)
- Rate limiting (Ruling 5): секция
RateLimit(Enabled=false— код-дефолт/dev/тесты,trueв PROD). Политики:auth— 10/мин на IP для/api/auth/loginи/api/operator/auth/login;api— 600/мин на тенанта/IP; интерцептор gRPC-ингресса :5082 — 600/мин/тенанта (health освобождён); ответ 429{detail}. С этапа 12 лимитер store-backed: состояние счётчиков — вpublic.rate_limit_counters(атомарный upsert), т.е. общее для всех инстансов core. Попытки входа —LoginAttemptGuardна том же хранилище (окно ip|login: 5 неудач за 15 мин → 429 «Слишком много попыток входа…»; успех сбрасывает счётчик;Enabled=false— no-op). - Origin-проверка мутаций —
OriginGuardMiddleware: не-GET/HEAD/OPTIONS/apiс заголовком Origin обязаны иметь Origin = «свой» origin (схема + Host) либо изSecurity:AllowedOrigins; несовпадение → 403. CORS — явный allowlist; SameSite=Lax httpOnly-кук — первый рубеж CSRF. - Прокси-заголовки —
UseForwardedHeaders(X-Forwarded-For/X-Forwarded-Proto, один доверенный hop) только за Caddy:ForwardedHeaders:Enabled=true+ KnownProxies/KnownNetworks (пустые списки не допускаются — loopback-фолбэк, fail-fast на невалидных значениях). Без этого за Caddy audit-IP (Ruling 4) и rate-limit-по-IP схлопываются в бакет прокси. - Security-заголовки: core —
SecurityHeadersMiddleware(X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer); CSP/HSTS — на Caddy (фронт-статика; CSP для Vue требует настройки nonce — документируется в шапке Caddyfile). В PROD куки Secure=true. - mTLS — за флагом
DEAL_MTLS_*только для внутреннего gRPC (серверный сертификат + обязательный клиентский, цепочка → CA изDEAL_MTLS_CA_PEM); основной HTTP :5080 остаётся http — TLS терминирует Caddy. Живое рукопожатие — ⚠ Manual. - Приостановка тенанта (Ruling 10(5)): вход — 403 «Учётная запись приостановлена…» (не 401:
неверные учётные данные не раскрывают статус); ИИ-расход заморожен бюджетным гейтом. С этапа 12
активные сессии приостановленного тенанта разлогиниваются сразу:
AuthService.ResolveSessionAsyncпроверяет статус тенанта (включая impersonation) и отказывает в сессии. Impersonation оператором suspended-тенанта разрешена (полностью аудируется; ИИ всё равно заморожен). - Аудит — append-only
public.audit_log: пишет толькоAuditService(без Update/Delete), секреты не попадают; чтение — только оператор (GET /api/operator/audit). С этапа 12 retention 180 дней обеспечивает фоновыйDataRetentionScheduler(раз в сутки; секцияDataRetention), там же — сброс накопительных полейtenant_limitsпрошедших периодов и уборка окон счётчиковrate_limit_counters. - Криптография/код: пароли Argon2id; секреты настроек AES-256-GCM (
enc:, ключDEAL_ENCRYPTION_KEY); сессии Telegram AES-256-GCM (DEAL_TELEGRAM_SESSION_KEY); SQL параметризуется; секреты в логи/аудит не пишутся. - Hardening контейнеров (BL-IMG-HARDEN, 2026-09-11): прикладные образы (core/telegram/ai/ml/storage)
работают non-root (пользователь
deal, UID 10001) сHOME=/tmp; в compose заданыread_only: true,tmpfs: /tmp,security_opt: no-new-privileges,cap_drop: ALLи лимитыmem_limit/cpus(якорьx-service-hardening). Данные — в именованных volume (/app/datacore,/data/sessionstelegram,/data/mlml); логи stateless-сервисов —DEAL_LOGS_DIR=/tmp/logs(tmpfs), у core — volume/app/data/logs. Провереноdocker compose config(dev и prod, включая профиль observability); живой подъём с этими ограничениями — ⚠ Manual. - Вне этапа (не настроено; заделы §11/roadmap): Cloudflare (конфигурация вне кода — шапка Caddyfile), k8s, биллинг, UI админок, саморегистрация.
11. Известные ограничения и TODO
Выполнено на этапе 1 (2026-09-05):
- доступ и сессии:
POST /api/auth/login,POST /api/auth/logout,GET /api/auth/me,POST /api/auth/change-password; httpOnly-кукаdeal_session(30 дней); - мультитенантность и миграции: системный контекст (
public:tenants/users/sessions, миграцияInitialSystem), схемыtenant_<id>с настройками тенанта (settings, миграцияInitialTenantна схему), автоматический провижининг схем и bootstrap дефолтного тенанта + admin при старте API.
Выполнено на этапе 2 (2026-09-06) — модуль Settings (экран «Настройки» обслуживается бэкендом):
- дерево настроек тенанта 1:1 с прототипом:
GET/PATCH /api/settings(дефолты модуля, перекрытые переопределениями в таблицеsettingsтенанта; PATCH мягкий — невалидные поля пропускаются, ответ — полный снимок; секреты наружу только маскамиkeyMasked/apiId; внутренние ключиratesCache/mlDecisions/aiDecisionsне публикуются); - шифрование секретов AI/Telegram: AES-256-GCM, в БД —
enc:+ Base64 (ключ — env/file, см. §13.4a); - проверка подключения ИИ:
POST /api/operator/settings/ai-config/check(операторская; локальный провайдер / HTTP-проверка облачного); - курсы валют:
GET /api/rates,POST /api/rates/refresh(кэшratesCacheв settings;mock/ЦБ); - ML-панель на детерминированной заглушке:
GET /api/ml/status,POST /api/ml/reset|predict(candidates →{items:[]}, apply → 404 — нет telegram-данных до этапа 6); - тестер фильтров:
POST /api/admin/check-message(этап-1 правила из настроек: длина/стоп-фразы/ резюме/тип; ИИ-фильтр тестера на этапе 2 всегда skipped); - 175 unit-тестов PASS; интеграционная приёмка — curl-сценарий на :5080 + psql.
Выполнено на этапе 3 (2026-09-06) — модуль Kanban (дашборд/канбан), см. §13.4c:
- миграция
TenantKanban— таблицы схемы тенантаBoards,Cards,LeadComments,CardMoves,MlOutbox(PascalCase-конвенция; колонки карточки по ТЗ §5:Title/Summary/StackJson/BudgetFrom/BudgetTo/BudgetCur/ConvFrom/ConvTo/ConvCur/ContactsJson/ChannelName/…/ReceivedAt/SourceMsg/SourceDialogId/PrevCol/MatchHitsJson/ArchivedAt); - модуль
Deal.Modules.Kanban:BoardsService/CardsService(доски, переносы, архив/корзина, комментарии, counts, поиск), правила колонокColumnRules(matchHits — «почему карточка в колонке»),StorageTickService+ фоновыйStorageTickScheduler(цикл 30 с, автоархив поarchiveAfterDays), пересчёт конверсийConversionRecomputer(listener на смену курсов/targetCurrency), демо-фабрика, эвристика ИИ-предложенийSuggestHeuristics; - эндпоинты:
/api/boards(+reorder/PATCH/DELETE),/api/columns/state,/api/leads(+counts/ {id}/move/trash/restore/DELETE/clear-col/mark-col-seen/mark-all-seen/comments/reclassify-заглушка),GET /api/search?q=,GET /api/events(SSE),/api/admin/tick+/api/admin/fts/rebuild(заглушка {ok,ready}), демоPOST /api/demo/simulate-lead|age-lead(флагDEAL_DEMO),POST /api/ai/suggest-columns|keywords, boot-заглушкиGET /api/projectsиGET /api/tg/status; - SSE-события:
new_lead(карточка) иtoast(текст+иконка) — публикуют только эндпоинты; потоки per-tenant (SseBroker), ping каждые 15 с; - демо-режим:
DEAL_DEMO=1(Development включает и без env) — simulate из демо-пула 1:1 с прототипом, age-lead состаривает карточку досок и тикает автоархив; без флага — 404 «Демо-режим отключён»; - ML-контракт
IMlClient.PushAsync+ локальная детерминированная реализацияLocalMlClient(MlOutbox/learning; реальный сервис — этап 6); - 410 unit-тестов PASS; сквозная приёмка этапа — curl-сценарий на :5080 + psql (Task 15: PASS=94 FAIL=0: boot-группы, демо-карточки ×14 + SSE new_lead/toast, доски/правила/matchHits, move/trash/restore/комментарий, mark-col-seen, поиск, suggest-columns/keywords, age-lead + автоархив фоновым циклом, admin/tick, пересчёт конверсий 9250 RUB / 100 USD / 92.59 EUR).
Выполнено на этапе 4 (2026-09-06) — модуль Pipeline (вкладка «Обработка»), см. §13.4d:
- миграция
TenantPipeline— таблицы схемы тенантаQueueItems(очередьp_, статус new/filtered),RejectedItems(отсевr_<dialog>_<msgId>, аудит возврата returned/returnReason) иDedupEntries(нормализованный SHA1-хэш, мягкая ссылкаLeadIdна карточку, чистится при жёстком удалении); в той же миграции — FTS-колонкиCards.SearchTsv/RejectedItems.SearchTsv(russian tsvector STORED + GIN); - модуль
Deal.Modules.Pipeline(чистый, без EF/HTTP): ядро разбораMessageTextCleaner/MessageListNormalizer/ContactsQualifier/DedupHasher/SummaryComposer/LocalFieldsParser(1:1 pipeline.py), приёмPipelineIngestService(гвард dialog+msgId), обработка/возврат/очисткиPipelineProcessingService, pumpPipelineWorkerService.PumpOnceAsync1:1 с_pump_unlocked(устарело → правила → дедуп → ML → ИИ → карточка; счётчики wire 1:1),CardComposer+PipelineCardWriter(карточка через публичныйIKanjStore.AddCardAsync+ связь дедупа); - порт
IAiClassifier+ детерминированныйLocalAiClassifier(до реального ai-service этапа 6), ML-слой — существующийIMlClient(локальная модель не готова — все сообщения к ИИ-ветке); - эндпоинты:
GET /api/pipeline/stats|queue|rejected(+qFTS ∪ LIKE),POST /rejected/clear,DELETE /rejected/{id},POST /rejected/{id}/return(400 dup/повтор/нет текста; снятие веса «спама» у ML), демоPOST /api/demo/ingest(флагDEAL_DEMO), реальныеPOST /api/admin/tick(storage+purgedRejected+pipeline+queue+SSE new_lead/тосты) иPOST /api/admin/fts/rebuild; - фоновые циклы:
PipelineWorkerScheduler(pump 2 с, общий гейт с ручным тиком) и автоочистка отсева (3 суток) вStorageTickScheduler(30 с) + SSE-тост «Отсев очищен: N записей (3 дн.)»; - FTS-поиск карточек
GET /api/search?q=(tsvector + LIKE, ts_rank, лимит 12,messages:[]); - 535 unit-тестов PASS; build 0 warnings / 0 errors; сквозная приёмка этапа — curl-сценарии на :5080 + psql (Task 13 — финал: PASS=74 FAIL=0: нули при старте, ingest → карточка с полями §4.1, отсевы правил/dup/нет суммы/устарело на реальных записях, очередь, stats, psql-строки и dedup-связь, поиск отсева FTS (морфология «работой») и LIKE (имя канала), return dup → 400, return → очередь → карточка, повторный return → 400, DELETE, clear, поиск карточек, fts/rebuild, удаление карточки чистит DedupEntries, purge 3 дн. → SSE-тост, logout → 401).
Выполнено на этапе 5 (2026-09-07) — модуль Projects («Выбранные»), см. §13.4e:
Историческое состояние: с этапа 9 модуль
Deal.Modules.Projectsи таблицаProjectCardsупразднены (сервисы «Выбранных» перешли вDeal.Modules.Kanban/CardsService.Selected, данные — вCards); ниже — как было на этапе 5.
- миграция
TenantProjects— таблица схемы тенантаProjectCards(PascalCase; partial UNIQUEIX_ProjectCards_LeadIdпоLeadId— «лид можно взять в работу один раз»); владелец — чистый модульDeal.Modules.Projects(без EF/HTTP; зависимости — Contracts/Settings/Kanban-порты, реверса нет); - стадии
ProjectStages1:1 с PIPELINE_STAGES (planned → reply → work → hold → ready, терминальные finished/rejected), DTO карточки §4.3; история движения — вHistoryJson(созданиеcreated/createdLocal+ каждая смена стадии, Ruling 7), комментарии/ссылки/файлы — JSON-поля карточки; - сервисы:
ProjectsService(список/чтение/ручное создание/take/патч presence-aware (budget:null)/ move+история/clear-rejected/комментарии/ссылки),ProjectFilesService(детект типаFileKindDetectorMIME+расширение → портIFileStorage→ мета в карточку),ProjectReminderService(set/clear/snooze/ фоновая проверка due); портIFileStorage+ адаптерыLocalFileStorage(дефолт:data/attachmentsпод ContentRoot) иMinioFileStorage(секцияStorage:Minio/envDEAL_MINIO_*, compose-сервисdeal-minio:9000/:9001, бакетdeal-filesлениво); - эндпоинты: 16 шт.
/api/projects*— список/создание/take/clear-rejected/GET/PATCH/move/comments/links (add/remove)/files (upload/download/delete)/reminder (set/clear/snooze); boot-заглушкаGET /api/projectsснята (остался/api/tg/status— этап 6);GET /api/projects/remindersиDELETE /api/projects/{id}сознательно не реализованы (Ruling 9); - напоминания: настройка
remindersEnabled(дефолт true; выключено → set 400); фоновыйStorageTickScheduler(30 с) и ручнойPOST /api/admin/tick(reminders:[{id,title,stage}]) помечают due-строки holdReminderFired=trueи публикуют SSEreminder_due {id,title,stage}(публикации — только Api, Ruling 8); move с hold снимает напоминание; snooze = +24 ч; - 620 unit-тестов PASS; build 0 warnings / 0 errors; сквозная приёмка этапа — curl-сценарии на :5080 +
psql (Task 13 — финал: PASS=75 FAIL=0: take-семантика (col=taken/is_new=false, исчезновение из /leads и
/api/search, идемпотентность, partial-UNIQUE дубля), PATCH полей и
budget:null, move по стадиям с историей, комментарии/ссылки, напоминание hold → SSEreminder_dueфоновым циклом БЕЗ ручного tick + psql ReminderFired, файлы upload/download(байты)/delete + объекты на диске, clear-rejected, сортировка UpdatedAt DESC, logout → 401).
Выполнено на этапе 6 (2026-09-07) — сервисы telegram/ai/ml + Discovery + каналы, см. §13.7:
- контракты
src/contracts/*.proto(общий проектDeal.Proto, Grpc.Tools); каждый RPC — metadatatenant-id+service-token, интерцепторы fail-closed (health освобождён); dev-безопасность — общий service-token без mTLS (Ruling 2); mTLS и prod-compose — этап 7; - три автономных процесса в
src/{telegram,ml,ai}-service(свои sln, net10.0): telegram-service (:5101, ферма сессий 1 акк/тенант, AES-256-GCM-файлы/data/sessions, фазы idle/code/password/qr/ready, диалоги/мониторинг/backfill с анти-бан-паузами, канал в coreSERVICES__CORE__INGRESS), ai-service (:5102, LLM-фасад OpenAI-совместимых+Anthropic без БД: Filter/Classify/GenerateKeywords/EvaluateFit, usage-токенов), ml-service (:5103, инкрементальный наивный Байес 1:1mlservice/model.py, SQLite на тенанта/data/ml/<tenant>.sqlite, пул per-tenant); - core: gRPC-ингресс telegram :5082 (
PushMessage→очередь/превью,SyncDialogs,ReportStatus→SSE), модуль Telegram (Dialogs/TgMessages,ITelegramGateway+GrpcTelegramClient за флагом), эндпоинты /api/tg (14 шт., реальный статус вместо boot-заглушки, QR-SVG), GrpcAiClassifier/GrpcAiTools (контекст/промпты/ маппер/usage), GrpcMlClient + MlOutboxFlushScheduler (10 с, TrainBatch 10/≤100), модуль Discovery (DiscTasks/Candidates/Blacklist/Log, план-бюджет, воркер 5 с с каскадом оценки и авто-join под бан-гардом, эндпоинты /api/discovery 13 шт., generate-keywords); - флаги
Services:{Ml,Ai,Telegram}:UseLocal— код-дефолт Local (true), compose.dev.yml задаёт false (полный стек «по-настоящему»); сервисы ходят в core-ингресс черезSERVICES__CORE__INGRESS; - 830 unit-тестов PASS (Deal.Tests.Unit), build 0 warnings / 0 errors всех четырёх sln; приёмки: curl Task 14 (/api/tg) PASS=20 FAIL=0, Task 19 (/api/discovery) PASS=37 FAIL=0, in-proc gRPC-тесты (PushMessage→карточка, флашер, ai-фильтр/классификация/инструменты).
Выполнено на этапе 7 (2026-09-08) — SaaS-контур, Tasks 1–16 (финал), см. §13.8/§13.9:
- оператор/сессии (
public.Operators/OperatorSessions, кукаdeal_operator_session, bootstrap env DEAL_OPERATOR_; dev-only дефолт operator/operator) + ручки/api/operator/*(auth/tenants/invites/ limits/audit/health — API-only); инвайты и активацияPOST /api/join; лимиты ИИ-бюджета (tenant_limits, декораторы-гейт, SSE-тосты 80/100%) с дефолт-бюджетом; append-only аудит-поток; rate limiting (приложение + интерцептор gRPC-ингресса,LoginAttemptGuard); Origin-проверка мутаций и security-заголовки; mTLS за флагом DEAL_MTLS_ (сертификаты scripts/mtls-certs.sh); Serilog JSON во всех 4 процессах (консоль + rolling-файл data/logs, access-логи HTTP/gRPC); compose.prod (caddy, mTLS-env, профиль observability: promtail/loki 7 сут./grafana 127.0.0.1:3001) + .env.prod.example. - Бэкапы (Ruling 8, Task 15) —
scripts/backup.sh(pg_dump -Fc БД deal: docker exec deal-postgres или прямой pg_dump при DEAL_PG_HOST; mc mirror бакета MinIOdeal-files— хостовый mc или разовый контейнер minio/mc; tar файловых данных DEAL_TAR_DIRS: attachments/telegram_sessions/ml — либо docker-volume'ы через DEAL_TAR_VOLUMES; retention 14 дней по дате в имени; лог + trap-очистка) иscripts/restore.sh(dropdb+createdb → pg_restore, обратный mc mirror, распаковка архивов). Команды/порядок/cron-пример «0 2 * * *» — §13.9. Реальный прогон и restore-тест — ⚠ Manual (нужен docker-стек). - Финальный прогон (Task 16): 1123 unit-теста PASS в core (Deal.Tests.Unit), telegram 114/114,
ai 50/50, ml 36/36 PASS; build 0 warnings / 0 errors всех четырёх sln;
docker compose -f deploy/compose.prod.yml configrc=0 (+ профиль observability);sh -ndev-smoke/backup/restore/ mtls-certs rc=0. Живые приёмки (curl-сценарий SaaS, подъём стека, бэкап/restore, mTLS, реальные сервисы) — ⚠ Manual, чек-лист в task-16-report.md.
Выполнено на этапе 9 (2026-09-10) — «единая карточка» (см. docs/architecture/2026-09-09-unified-card.md, docs/architecture/2026-09-10-unified-api-contract.md):
- Модель: карточка — один агрегат во всех дашбордах. Ядро (
ICard: id/title/source) + опциональные модули-роли (IContentCard/IBudgetedCard/IContactCard/IAttributedCard/ICommentableCard/ILinkCard/IFileCard/ITzCard/ITraceableCard/IRemindableCard/ILocatedCard); источник — иерархияISource(ITelegramSource/IRowSource/IApiSource/IAiSource/ICompositeSourceи простые); единый переходICardMover. Вид карточки — композиция модулей, а не класс-наследник (Deal.Modules.Cards). - БД: одна таблица
Cards—ProjectCardsупразднена; единый реестрContainersвместо таблицыBoardsи колонок-строк. Модульные данные — колонки той же строки (StackJson/LinksJson/FilesJson/HistoryJson/TzText/ReminderAt), комментарии —LeadComments;CardMoves,MlOutbox,DedupEntries,QueueItems,RejectedItems— без изменений. ПолнотекстовыеSearchTsv— уCardsиContainers. - Контейнеры: поля
space(dashboard/selected),kind(board/stage/service/terminal),rules,policy,counts. Стадии «Выбранных» — контейнерыkind=stage/terminalкаталогаCardsDefaultContainers(planned…finished/rejected); служебные зоны —inbox/archive/trash. Карточка живёт в одном пространстве; «взять в работу» — перенос карточки вplanned, а не клон. - API: единый контракт
/api/cards+/api/containers; ручки/api/leads,/api/projects,/api/boards,/api/columnsудалены; SSEnew_cardвместоnew_lead. Единый префикс id —c_. Полная карта —docs/api/api-map.md. - Фронт: один слайс карточек (
src/frontend/src/store/cards.js) и единый канбан для дашборда и «Выбранных» (пространство определяется контейнером карточки).
Остаётся TODO (после этапа 7, Tasks 1–16):
- Живые проверки (⚠ Manual, нужен docker/креды): применение system-миграции
SystemSaaSи сквозная SaaS-curl-приёмка (оператор → тенант → инвайт → /api/join → лимиты/гейт → аудит → suspend → resume → IDOR-негативы); подъём compose.prod.yml и dev-smokesh scripts/dev-smoke.sh; mTLS-рукопожатие контейнеров; реальные Telegram/LLM-вызовы (с кредами); прогонscripts/backup.shи restore-тест (scripts/restore.sh). - Заделы (сознательно вне этапа 7; часть закрыта этапами 8–12): UI операторской админки и страницы активации
инвайта (сейчас API-only); OTel-метрики/Prometheus и дашборды метрик (закрыто этапом 12, пакет A — §7);
multi-instance rate-limit и бэкенд попыток входа (закрыто этапом 12 —
public.rate_limit_counters); мгновенный разлогин suspended-сессий (закрыто этапом 12); реклассификация «Неразобранного» на реальном ИИ (закрыто этапом 12 — reclassify с локальным фолбэком); purge-автоматика audit_log и auto-purge истории tenant_limits (закрыто этапом 12 —DataRetentionScheduler); экспорт/импорт ML-моделей; мультиаккаунтность Telegram на тенанта; саморегистрация/биллинг-провайдер/планы; k8s/Cloudflare-конфигурация. - Карта
/api—docs/api/api-map.md+ контрактыdocs/architecture/2026-09-10-unified-api-contract.mdиdocs/architecture/2026-09-10-operator-analytics-contract.md(актуальны на этап 12). - Пакетная миграция схем тенантов (сотни/тысячи) — реализована на этапе 12 (
POST /api/operator/maintenance/tenants/migrate, §13.10/§16; см. также §4/§7); с BL-SCALE-1000 (2026-09-11) обход шардирован страницами (ITenantRepository.ListPageAsync,DefaultPageSize=200) с параллелизмом внутри страницы и изоляцией сбоев. - Kafka — отложена.
12. Глоссарий
См. дизайн-док (§Приложение). Дополнительно:
- search_path — механизм Postgres выбора текущей схемы.
- outbox — таблица событий в той же транзакции, что и бизнес-изменение.
- карточка (card) — единая сущность всех дашбордов (ядро + модули); id с префиксом
c_. - контейнер (container) — колонка/стадия/зона единого реестра;
space+kind+rules/policy. - пространство (space) —
dashboardилиselected; карточка живёт ровно в одном.
13. Быстрый старт (dev; актуально для этапов 0–12 — финальное состояние)
Для этапов 0–7 ниже приведены исторические списки эндпоинтов (в т.ч.
/api/leads,/api/projects,/api/boards). С этапа 9 (2026-09-10) актуальны единые/api/cardsи/api/containers— см.docs/api/api-map.mdиdocs/architecture/2026-09-10-unified-api-contract.md.
Проверенный путь (2026-09-07, Windows + sh, .NET 10, Postgres 16 в Docker): системный контекст
(public), контекст тенанта (схема с settings + таблицами канбана, пайплайна и «Выбранных»), auth /api/auth, настройки
тенанта (Settings-модуль этапа 2), канбан этапа 3 (/api/boards, /api/leads, /api/events SSE,
демо /api/demo/*), пайплайн этапа 4 (вкладка «Обработка» /api/pipeline/*, демо-ingest, воркер 2 с,
FTS /api/search + /api/pipeline/rejected?q=, реальные /api/admin/tick и /api/admin/fts/rebuild),
«Выбранные» этапа 5 (вкладка Projects: /api/projects — стадии/напоминания/файлы/ссылки/история, файлы
через порт IFileStorage — Local data/attachments по умолчанию или MinIO deal-minio при конфигурации,
SSE reminder_due фоновым 30-с циклом),
провижининг схем и bootstrap дефолтного тенанта с admin при старте API. Логин/пароль по умолчанию —
admin/admin (env DEAL_BOOTSTRAP_LOGIN/DEAL_BOOTSTRAP_PASSWORD).
Разделы 1–6 ниже — «классический» host-путь этапов 1–5: core запускается с хоста на Local-заглушках
(код-дефолт Services:*:UseLocal=true), сервисы этапа 6 не нужны. Полный dev-стек этапа 6 (три сервиса +
core в docker, сквозной gRPC-режим) — §13.7.
1. Postgres
# хранилища для host-режима (core с хоста); весь стек (сервисы этапа 6 + core) — §13.7
docker compose -f deploy/compose.dev.yml up -d postgres minio
Полный стек поднимается той же командой без аргументов (... up -d --build): postgres + minio +
telegram/ai/ml-сервисы + core в сквозном gRPC-режиме (Services__*__UseLocal=false заданы в compose), см. §13.7.
Контейнер deal-postgres: наружный порт 5433, БД deal, пользователь deal
(пароль deal_dev_password). Тот же compose-файл поднимает deal-minio (MinIO для вложений этапа 5):
порты 9000 (S3 API) / 9001 (консоль), бакет deal-files создаётся лениво при первом upload.
Dev-режим по умолчанию работает БЕЗ MinIO — LocalFileStorage (каталог data/attachments под ContentRoot
Deal.Api); MinIO-режим включается секцией Storage:Minio или env-алиасами DEAL_MINIO_ENDPOINT/
DEAL_MINIO_ACCESS_KEY/DEAL_MINIO_SECRET_KEY/DEAL_MINIO_BUCKET/DEAL_MINIO_SECURE (см. §4e).
2. Системные миграции (public)
Из src/core:
dotnet ef database update --project Deal.Infrastructure --startup-project Deal.Api --context DealDbContext
Применяет InitialSystem — публичные таблицы tenants, users, sessions
(история — public.__EFMigrationsHistory). Строка подключения — ConnectionStrings:DealPostgres
(Deal.Api/appsettings.Development.json; перекрывается env ConnectionStrings__DealPostgres).
3. Запуск API
Из src/core:
dotnet run --project Deal.Api --urls http://localhost:5080
При старте TenantBootstrapService (идемпотентно) создаёт дефолтного тенанта
00000000-0000-0000-0000-000000000001 (имя Default) с его схемой
tenant_00000000000000000000000000000001 и таблицей settings (миграция InitialTenant), а также
пользователя admin — логин/пароль из env DEAL_BOOTSTRAP_LOGIN / DEAL_BOOTSTRAP_PASSWORD,
по умолчанию admin / admin. Схемы провижинируются для всех тенантов реестра; повторные старты
дублей не создают.
4. Проверка auth
curl -i -X POST http://localhost:5080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"login":"admin","password":"admin"}'
→ {"ok":true,"login":"admin"} (HTTP 200) и httpOnly-кука deal_session (SameSite=Lax, 30 дней;
срок — константа AuthService.SessionLifetimeDays, перекрывается Cookies__Days).
Прочие эндпоинты: GET /api/auth/me, POST /api/auth/logout, POST /api/auth/change-password;
health — GET /api/health → {"ok":true,"service":"deal"}.
4a. Шифрование секретов настроек (ключи AI/Telegram)
Секреты хранятся шифротекстом enc: + Base64(nonce‖ct‖tag), AES-256-GCM (nonce 12 Б, tag 16 Б).
Ключ шифрования — env DEAL_ENCRYPTION_KEY (32 байта в urlsafe-Base64); при отсутствии в dev
берётся/создаётся файл <ContentRoot>/data/encryption.key (путь переопределяется env
DEAL_ENCRYPTION_KEY_FILE) — при генерации лог-warning. Невалидный env-ключ — ошибка при старте.
Наружу секреты не отдаются: только маски keyMasked (первые 4 + «…» + последние 4, len≤8 — как есть)
и keySet.
Исторический раздел (этап 2). С этапа 12 ключей Telegram (
tgKeys/apiId/apiHash) в настройках тенанта нет — они задаются оператором глобально (таблицаpublic.global_settings,GET/PUT /api/operator/settings/telegram-keys; hash шифруется тем же AES-256-GCM).С 2026-09-14 там же живёт и конфигурация ИИ-провайдера (ключ
aiConfigвpublic.global_settings,GET/PUT /api/operator/settings/ai-config): провайдер, модель, baseUrl и API-ключ задаёт оператор, все ИИ-вызовы всех тенантов идут на эту конфигурацию; в настройках тенанта ключейaiProvider/aiConfigsбольше нет.
4b. Эндпоинты этапа 2 (настройки тенанта; сессия deal_session обязательна, иначе 401)
-
GET /api/settings— публичный снимок дерева настроек: дефолты модуля, перекрытые переопределениями изsettingsтенанта; включаетmyPromptsи колонки, но не настройки ИИ-провайдера (они операторские).PATCH /api/settings— частичное обновление (невалидное поле мягко пропускается, ответ — полный снимок). Побочные эффекты: приrateSource— фоновый refresh курсов. Внутренние ключи (ratesCache,mlDecisions,aiDecisions) в GET/PATCH не участвуют. -
GET /api/rates/POST /api/rates/refresh— курсы к RUB (base=RUB); источник поrateSource(mock— константа,cbr— ЦБ РФ, ≤4 запроса/сутки, интервал 6 ч;USDT=USD); кэш — внутренняя настройкаratesCache{rates, source, updatedAtMs}. -
GET /api/ml/status,POST /api/ml/reset|predict— ML-панель на детерминированной заглушкеLocalMlClient(этап 6 заменит на gRPC без правки эндпоинтов):ready:false, predict неготовой модели — «не уверен»,candidates→{items:[]},apply→ 404 (telegram-данных нет до этапа 6). -
POST /api/admin/check-message— тестер фильтров входящих:{stage1:{pass,reason}, stage2:{pass, reason, skipped}, passed}; этап-1 правила из настроек (длина/стоп-фразы/резюме/тип); ИИ-фильтр тестера на этапе 2 всегдаskipped:true.Актуально с 2026-09-11: тестер стал сухим прогоном по всему конвейеру (
PipelineWorkerService.DryRunAsync):{passed, wouldCreateCard, targetContainer, matchHits, parsed, stages[]}— стоп-правила → глобальные исключения → ML (спам/тип) → ИИ-фильтр/классификация → «без суммы»; без записи в систему. UI — вкладка настроек «Стоп-слова».
4c. Эндпоинты этапа 3 (канбан/дашборд; сессия deal_session обязательна, иначе 401)
На этапе 4
POST /api/admin/tickстал реальным (pipeline/pump/purge-отсева) иPOST /api/admin/fts/rebuild— реальным{ok, ready}(см. §4d); описание ниже — состояние этапа 3.На этапе 5
GET /api/projects— реальный список «Выбранных» (см. §4e); boot-заглушка/projectsснята, из boot-заглушек остался толькоGET /api/tg/status(telegram — этап 6).
- Доски:
GET/POST /api/boards(голый массив / создание),PATCH /api/boards/{id}(name/width/ collapsed/keywords/rules/suggested…),POST /api/boards/reorder,DELETE /api/boards/{id}(карточки → inbox). Правила колонки —{mode: all|any, direction[], keywords[], stack[], grade[], exclude[], budget}; совпавшие термины попадают вmatchHitsкарточки (1:1 с rules.py). - Карточки:
GET /api/leads?col=inbox|<boardId>|archive|trash(свежие сверху, полный §4.1),GET /api/leads/counts(плоская форма{new, learning, ml, ai}+ per-column{count, new}),GET /api/leads/{id},POST /leads/{id}/move|trash|restore,DELETE /api/leads/{id},POST /api/leads/clear-col(trash|archive),mark-col-seen|mark-all-seen,POST /leads/{id}/comments. Поиск:GET /api/search?q=(lower-LIKE по title/summary/contact/source_msg →{leads, messages:[]}). - Служебные:
POST /api/admin/tick→{storage:{archived,purgedArchive,purgedTrash,purgedRejected}, reminders:[], pipeline:{}, queue:0}(+SSE-тосты статистики);POST /api/admin/fts/rebuild— заглушка{ok:true, ready:true}(FTS-индекс — этап 4). - Boot-заглушки фронта:
GET /api/tg/status→ idle-форма §4.9 (telegram — этап 6; заглушкаGET /api/projects→{items:[]}снята на этапе 5 — реальный список см. §4e). - SSE:
GET /api/events— text/event-stream канала тенанта; событияnew_lead(полная карточка, после simulate) иtoast{text, icon}(демо-лид sparkles, автоархив/тик clock, ИИ-предложения sparkles); ping: pingкаждые 15 с. Публикуют только эндпоинты Api (модуль чист). - Демо-режим (флаг
DEAL_DEMO=1; Development включает и без env):POST /api/demo/simulate-lead(карточка из демо-пула 1:1 с прототипом → inbox + SSE new_lead/toast),POST /api/demo/age-lead(состаривание самой старой карточки досок + тик автоархива + SSE-toast). Без флага — 404 «Демо-режим отключён». - ИИ-предложения (эвристика этапа 3, реальный ИИ — этап 6):
POST /api/ai/suggest-columns(накопите ≥6 карточек в «Неразобранном» → доскиsuggested:trueс note «Эвристика (этап 3):…» и раскладкой карточек; повторный вызов — cooldown 20 минlastSuggestAt),POST /api/ai/suggest-keywords(частотные маркеры по текстам). - Конверсии (Ruling 7): курсы —
POST /api/rates/refresh(mock/ЦБ), кэшratesCacheв settings; пересчётConvFrom/ConvTo/ConvCurактивных карточек (не archive/trash/taken) выполняется синхронно по listener'ам: после refresh курсов и приPATCH /api/settings {targetCurrency,…}.
4d. Эндпоинты этапа 4 (pipeline/вкладка «Обработка»; сессия обязательна, иначе 401)
Таблицы этапа — миграция TenantPipeline в схеме тенанта (владелец — модуль Deal.Modules.Pipeline):
QueueItems (очередь, id p_…, статус new|filtered), RejectedItems (отсев, детерминированный id
r_<dialog>_<msgId> либо r_+hex; колонка SearchTsv — to_tsvector('russian', text) STORED + GIN) и
DedupEntries (SHA1-хэш нормализованного текста, PK; LeadId — мягкая ссылка на Cards, чистится при
жёстком удалении карточки). В той же миграции — FTS-колонка Cards.SearchTsv (Title+Summary+SourceMsg+
Contact) + GIN-индекс; tsvector-колонки авто-актуальны (перестроение не требуется).
- Приём сообщений (этап 4 — только демо; этап 6 — gRPC telegram-service):
POST /api/demo/ingest{text, dialogId?, channelName?, channelHandle?, channelHue?, msgId?, msgAt?}(флагDEAL_DEMO=1, иначе 404 «Демо-режим отключён») →{ok, id:p_…, queue:{new,ai,total}}; пустой текст — 400 «Текст сообщения пуст»; повторdialogId+msgIdуже в очереди —id:null(гвард Telethon-дублей); нет dialogId — no-op. - Разбор очереди: фоновый
PipelineWorkerSchedulerкаждые 2 с (per-tenant pump, общий гейт с ручным тиком) иPOST /api/admin/tick. Конвейер 1:1 с прототипом: «устарело» (msgAt старшеarchiveAfterDaysприautoArchive) → правила этапа-1 (IncomingRules: длина/стоп-фразы/резюме/тип) → дедуп по тексту → ML-слот (IMlClient, локальная модель не готова — «не уверен») → ИИ-слот (LocalAiClassifierдо этапа 6;aiEnabled=false— локальный разбор) → карточка/отсев. Счётчики решений pump — в ответе тика и KVmlDecisions/aiDecisions. - Карточка из сообщения (CardComposer, через публичный
IKanjStore.AddCardAsync): title, блок «О заявке» (summary), stack ≤12, бюджет (нормализованный + конверсия в целевую валюту при поступлении), контакты (квалификация, ≤6, primary), поля каналаch,sourceMsg/sourceDialogId/sourceMsgId; колонка — inbox либо доска поBoardAccepts+правилам; после успешной классификации карточка получаетisVacancy/isVacancyKnown; создание карточки связывает хэш вDedupEntriesс её id.
Актуально с 2026-09-11: единый контракт источника. Карточка, очередь и отсев работают с generic-типом
SourceItem(SourceRef+SourceContent). В таблицахCards/QueueItems/RejectedItemsвместо Telegram-колонок (ChannelName/SourceMsg/SourceMsgId…) хранятсяSourceKind/SourceExternalId/SourceOriginRef/SourceJson/ContentJson(карточка — ещёSourceTextи FTS по нему; очередь/отсев —SourceKeyдля дедупа). Вложение —DataRef(ссылка на общий Storage-сервис); контакты/ссылки — поляSourceContent. Telegram-поля остались только в тонком адаптере приёма telegram-сервиса. Tenant-миграции пересозданы с нуля (данных нет). Детали —docs/superpowers/specs/2026-09-11-source-contract-design.md.
- Отсев — источник решения
stop|ml|ai|stale|dup(подписи «правила/ML/ИИ/система») и этапlength|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup(подписи UI: «короткое сообщение», «стоп-фраза», «резюме соискателя», «нет суммы», «устарело», «повтор»…), причина ≤500, kw — сработавшая фраза. Возврат (force) повторно проводит сообщение мимо правил/устарелости/ИИ-фильтра и снимает у ML вес «спама» для spam-отсева. - Вкладка «Обработка» (фронт на поллинге; SSE
pipeline_statsне публикуем — Ruling 9):GET /api/pipeline/stats→{queue:{new,ai,total}, rejected};GET /api/pipeline/queue?limit=(≤500, дефолт 100) →{items, counts:{new,ai,total}, rejected};GET /api/pipeline/rejected?q=&offset=&limit=→{items,total,offset,limit};q— FTS-кандидаты (SearchTsv @@ plainto_tsquery('russian'), ts_rank) ∪ LIKE-дополнение поlower(text)/reason/kw/ch_name(страница из объединения, offset/limit ≤500). - Возврат и очистки:
POST /api/pipeline/rejected/{rejId}/return {reason}→{id, returned:true, returnedAt}; 404 «Запись не найдена»; 400 «Сообщение уже возвращено в обработку» / «Повтор: карточка с таким текстом уже есть в системе — возвращать нечего» (источник dup) / «В записи нет текста сообщения».DELETE /api/pipeline/rejected/{rejId}→{ok:true}(404 не шлём);POST /api/pipeline/rejected/clear→{ok, cleared}. Автоочистка отсева — 3 суток (RejectedAt), выполняется в тике правил хранения (ручной tick и фоновыйStorageTickScheduler30 с), при ненулевой очистке — SSE-тост «Отсев очищен: N записей (3 дн.)». POST /api/admin/tick(этап 4): ответ{storage:{archived, purgedArchive, purgedTrash, purgedRejected}, reminders:[], pipeline:{staged, rulesStored, mlStored, mlDrop, typeDrop, aiStored, aiDrop, aiFail, noBudget}, queue}; после pump — SSEnew_leadпо созданным карточкам и тосты статистики.POST /api/admin/fts/rebuild→{ok:true, ready:true}(идемпотентно:CREATE INDEX IF NOT EXISTS+ANALYZECards/RejectedItems).- Поиск карточек
GET /api/search?q=(q ≥ 2): один SQL —SearchTsv @@ plainto_tsquery('russian', q)ORlower(title/summary/source_msg/contact) LIKE '%q%', порядокts_rank DESC, ReceivedAt DESC, лимит 12; ответ{leads, messages:[]}— русская морфология (например, q=работа находит «работой»).
4e. Эндпоинты этапа 5 (Projects/«Выбранные»; сессия deal_session обязательна, иначе 401)
Исторический раздел (как было на этапе 5). С этапа 9 все перечисленные операции живут под
/api/cards*, модульDeal.Modules.Projectsи таблицаProjectCardsупразднены — актуальный контракт см. §3.5/§5 иdocs/api/api-map.md.
Таблица этапа — миграция TenantProjects в схеме тенанта (владелец — чистый модуль
Deal.Modules.Projects): ProjectCards — 1:1 с таблицей projects прототипа. Колонки (PascalCase):
Id (pr_…), Stage (каталог ProjectStages 1:1 с PIPELINE_STAGES: planned → reply → work → hold →
ready, терминальные finished/rejected), Local, LeadId (partial UNIQUE IX_ProjectCards_LeadId —
лид может быть взят в работу ровно один раз), Title, Summary, StackJson, BudgetFrom/BudgetTo/
BudgetCur, Contact, TzText, JSON-поля CommentsJson/LinksJson/FilesJson/HistoryJson
(история — только создание created/createdLocal и смены стадии, Ruling 7) и напоминание
ReminderAt (timestamptz)/ReminderFired; времена наружу — epoch-ms, список — UpdatedAt DESC.
- Взять в работу:
POST /api/projects/take {leadId}→ проектная карточкаlocal=false,stage=planned,leadId+title/summary/stack/budget/contactскопированы из лида, комментарий «Взял в работу из лида.», историяcreated. Лид помечаетсяcol='taken', is_new=falseчерез публичный порт Kanban (IKanjStore.MarkTakenAsync) — исчезает из/api/leadsи/api/search, не попадает в архив/корзину тика; повторныйtakeидемпотентен (возвращает ту же карточку), partial-UNIQUELeadIdстрахует гонки. - Карточки:
GET /api/projects(?stage=фильтр) →{items:[…]}(UpdatedAt DESC),POST /api/projects(ручное создание{title,…,stage?}; стадия — каталог, по умолчанию planned;local=true,createdLocal),GET/PATCH /api/projects/{cardId}(PATCH presence-aware:budget:null/stack:nullочищают поле; 404 «Карточка не найдена»),POST /api/projects/{cardId}/move {stage}(валидация каталогом: 400 «Неизвестная стадия»; смена стадии дописывает историю{id h_, at, stage}и сбрасывает напоминание),POST /api/projects/clear-rejected→{ok, cleared}(единственный hard-delete — стадия «Отклонено»). - Комментарии и ссылки:
POST /{cardId}/comments {text}(400 «Пустой комментарий»; ответ{comments}),POST /{cardId}/links {url,name?}(схема добавляется: example.com → https://example.com; name = url по умолчанию),DELETE /{cardId}/links/{linkId}. Значки-счётчики — из массивов карточки §4.3. - Напоминания «Отложено»:
POST/DELETE /{cardId}/reminder {at}(прошлое допустимо — «выстрелит» на ближайшей проверке; включённость — настройкаremindersEnabled, дефолт true; при выключенной set → 400 «Напоминания об отложенных выключены в настройках») иPOST /{cardId}/reminder/snooze(now + 24 ч). Срабатывание: фоновыйStorageTickSchedulerкаждые 30 с (и ручнойPOST /api/admin/tick→reminders:[{id,title,stage}]) вызываетProjectReminderService.CheckDueAsync— due-строкиstage='hold'помечаютсяReminderFired=trueи публикуются SSE-событиемreminder_due {id,title,stage}в канал тенанта (баннер фронта; публикации — только из Api, Ruling 8). Любой move с hold снимает напоминание (ReminderAt/ReminderFiredочищаются). - Файлы:
POST /{cardId}/files(multipart, полеfiles, 1–N) →{items:[{id pf_, name, size, kind, label, objectKey}]}; тип —FileKindDetectorпо MIME+расширению (document/«Документ»,image/«Изображение» и т.д.); объект кладётся через портIFileStorage(Contracts/Integrations):LocalFileStorage(дефолт, кореньdata/attachments, key → путьprojects/<cardId>/<ms>_<name>) илиMinioFileStorage(включается секциейStorage:Minio/envDEAL_MINIO_*; бакетdeal-filesсоздаётся лениво; compose - сервисdeal-minio:9000/:9001).GET /{cardId}/files/{fileId}/download—attachment(Content-Length/Type из дескриптора; локально MIME пуст →application/octet-stream, 1:1 прототип),DELETE /{cardId}/files/{fileId}→{ok:true}(мета + объект). - Сознательно НЕ реализованы (Ruling 9, api-map п.9/п.6):
GET /api/projects/reminders(список активных напоминаний — у фронта UI нет) иDELETE /api/projects/{id}(удаление проектной карточки отключено; hard-delete — только clear-rejected). Всего 16 эндпоинтов/api/projects*. - Демо:
DEAL_DEMO=1включает демо-эндпоинты (simulate/ingest) этапов 3–4; сам контур «Выбранных» работает без флага (сессии +admin/admin).
5. Проверка схем (psql)
docker exec deal-postgres psql -U deal -d deal -c '\dn'
docker exec deal-postgres psql -U deal -d deal -c '\dt public.*'
docker exec deal-postgres psql -U deal -d deal -c '\dt tenant_*.*'
Ожидается: схемы public и tenant_00000000000000000000000000000001; в public — tenants, users,
sessions, invites, operators, operator_sessions, tenant_limits, audit_log,
token_usage_events, global_settings, rate_limit_counters, __EFMigrationsHistory; в схеме тенанта —
settings, Cards, Containers, LeadComments, CardMoves, MlOutbox, QueueItems, RejectedItems,
DedupEntries, Dialogs, TgMessages, DiscTasks/DiscCandidates/DiscBlacklist/DiscLog
и __TenantMigrationsHistory.
Ключевые колонки Cards (PascalCase): Id, Col, IsNew, Title, Summary, StackJson, BudgetCur,
ConvCur, ReceivedAt, PrevCol, MatchHitsJson, ArchivedAt, SearchTsv (tsvector STORED);
RejectedItems — Stage/Reason/Kw/Source/Returned/SearchTsv; DedupEntries — Hash
(PK)/LeadId. Правила контейнеров — в Containers.RulesJson, состояние
колонок — в settings (ключ colState).
6. Тесты и сборка (из корня репозитория)
sh scripts/build.sh # сборка всех 5 решений (0 warnings / 0 errors)
sh scripts/test.sh # тесты всех сервисов + lint:i18n (core 1315, telegram 130, ai 52, ml 38, storage 9)
sh scripts/ci.sh # полный CI-прогон: build + test + скан уязвимостей + сборка фронта
Сквозные приёмки этапов — curl-сценарии на :5080 в .superpowers/sdd/deal-stage{4,5}-projects/
(task-N-curl-acceptance.sh/.log): этап 4 — финальный Task 13 PASS=74 FAIL=0; этап 5 — финальный Task 13
PASS=75 FAIL=0 (карточки ProjectCards, лид col=taken, файлы на диске data/attachments,
SSE reminder_due фоновым циклом БЕЗ ручного tick).
Каждая из четырёх sln собирается 0 warnings / 0 errors (TreatWarningsAsErrors): src/core/Deal.sln,
src/telegram-service/Deal.Telegram.sln, src/ml-service/Deal.Ml.sln, src/ai-service/Deal.Ai.sln.
Приёмки этапа 6 (.superpowers/sdd/deal-stage6-services/): in-proc gRPC-тесты (ингресс PushMessage→
карточка, флашер MlOutbox, ai-фильтр/классификация/инструменты) + curl-сценарии Task 14 (/api/tg:
PASS=20 FAIL=0) и Task 19 (/api/discovery: PASS=37 FAIL=0).
Финальный прогон этапа 7 (Task 16, docker выключен): core 1123/1123 PASS, telegram 114/114, ai 50/50,
ml 36/36 PASS; build 0/0 всех четырёх sln; docker compose -f deploy/compose.prod.yml config rc=0;
sh -n scripts/dev-smoke.sh/backup.sh/restore.sh/mtls-certs.sh rc=0. Живые приёмки (SaaS-curl-сценарий
этапа 7, подъём compose.dev/prod, бэкап/restore, mTLS, реальные сервисы) — ⚠ Manual, чек-лист —
.superpowers/sdd/deal-stage7-saas/task-16-report.md.
7. Этап 6 — автономные сервисы telegram/ai/ml + Discovery + каналы (полный dev-стек)
Реализация — src/telegram-service, src/ml-service, src/ai-service (отдельные sln/процессы, .NET 10,
общий код — только .proto через src/contracts/Deal.Proto.csproj, Task 1); core остаётся единственным
владельцем БД и бизнес-логики (сервисы не знают домен и не ходят в tenant-БД). Контракты, сервисы и
интеграция — план этапа 6 (.superpowers/sdd/deal-stage6-services/), Rulings 1–13.
Порты и процессы (deploy/compose.dev.yml)
| Контейнер | Порт | Назначение |
|---|---|---|
deal-postgres |
5433 | БД (host-порт; внутри — 5432) |
deal-minio |
9000/9001 | S3-API / консоль (файлы вложений; в Local-режиме необязателен) |
deal-core (Deal.Api) |
HTTP 5080, gRPC-ингресс 5082 | портал /api + приём PushSource/SyncDialogs/ReportStatus |
deal-telegram-service |
5101 | Telegram: сессии/QR/диалоги/мониторинг/backfill/discovery-операции |
deal-ai-service |
5102 | LLM-фасад: Filter/Classify/GenerateKeywords/EvaluateFit |
deal-ml-service |
5103 | инкрементальная модель per-tenant: predict/train/status/reset |
Порт каждого сервиса — env GRPC_PORT (контейнерный 5101/5102/5103), порт ингресса core — env
GRPC_INGRESS_PORT (5082). Health-проверки контейнеров — встроенный gRPC-health (grpc_health_probe
в образе, /bin/grpc_health_probe), Deal-RPC health не трогают.
gRPC-контракты и безопасность (Rulings 1/2/13)
src/contracts/{telegram,ai,ml}.proto— пакетыdeal.telegram.v1/deal.ai.v1/deal.ml.v1(csharp_namespaceDeal.Grpc.Telegram/Ai/Ml); общий проект кодогенерацииDeal.Proto(Grpc.Tools, client+server в одном проходе; каждый процесс собирает свою sln вместе с ним).- Каждый RPC несёт metadata
tenant-id+service-token; серверный интерцептор каждого процесса fail-closed сверяет токен с envDEAL_SERVICE_TOKEN(единый для всех процессов в compose; отказ —UNAUTHENTICATED;grpc.health.v1.Healthосвобождён). Принадлежность (сессия/модель тенанта) проверяется сервисом по своей модели — полю не доверяется. Ошибки домена —INVALID_ARGUMENT/NOT_FOUND/UNAVAILABLE/RESOURCE_EXHAUSTED(flood) с текстом 1:1. - Dev — gRPC plaintext без mTLS (Ruling 2); mTLS-сертификаты, их генерация и prod-compose — этап 7.
Флаги интеграций core (Ruling 6)
- Код-дефолт —
Services:{Ml,Ai,Telegram}:UseLocal=true(appsettings.json): Local-адаптеры (LocalMlClient,LocalAiClassifier/LocalAiTools,LocalTelegramGateway) — core работает без сервисов (host-путь §13.1–13.6, этапы 2–5). deploy/compose.dev.ymlзадаёт для coreServices__{Ml,Ai,Telegram}__UseLocal: "false"+ эндпоинтыhttp://ml-service:5103/http://ai-service:5102/http://telegram-service:5101— полный стек «по-настоящему». Выбор реализации — на старте (рантайм-переключения нет); фолбэки: недоступность ml/ai — локальные пути воркеров (ml predict — «не уверен», ai — локальный разбор), telegram — idle-форма эндпоинтов.
Env (compose.dev.yml; dev-дефолты ${VAR:-…}, перекрываются .env/экспортом)
- Общие:
DEAL_SERVICE_TOKEN(единый service-token core+сервисов),DEAL_ENCRYPTION_KEY(32 байта base64, AES-GCM секретов настроек core; fail-closed), creds БД/минио ниже. - core:
ConnectionStrings__DealPostgres(hostpostgres, порт 5432 внутри compose),Storage__Minio__*(или env-алиасыDEAL_MINIO_*),Services__*__{UseLocal,Endpoint},GRPC_INGRESS_PORT=5082,ASPNETCORE_URLS=http://0.0.0.0:5080. - telegram-service:
GRPC_PORT,DEAL_SERVICE_TOKEN,DEAL_TELEGRAM_SESSION_KEY(32 байта base64, обязателен — fail-closed: сессии только шифрованные AES-256-GCM, файлы/data/sessionsна volumedeal_tg_sessions),DEAL_TELEGRAM_SESSION_DIR=/data/sessions,SERVICES__CORE__INGRESS(http://core:5082в compose; для core с хоста —DEAL_CORE_INGRESS=http://host.docker.internal:5082). - ml-service:
GRPC_PORT,DEAL_ML_DATA_DIR=/data/ml(volumedeal_ml_data; SQLite-файлы моделей/data/ml/<tenantId>.sqlite). - ai-service:
GRPC_PORT(stateless — промпты/конфиг провайдера приходят в теле запроса, volume не нужен).
Полный стек и smoke-проверка
cd /c/telbase
docker compose -f deploy/compose.dev.yml up -d --build # весь стек (первый прогон собирает 4 образа)
sh scripts/dev-smoke.sh # сквозной smoke и авто-очистка (trap → down)
docker compose -f deploy/compose.dev.yml down # погасить стек вручную (volumes сохраняются)
scripts/dev-smoke.sh: подъём стека → health всех контейнеров → login admin/admin → GET /api/tg/status
(idle-форма через GrpcTelegramClient) → GET /api/containers?space=dashboard → POST /api/cards
(локальная карточка в planned) → POST /api/cards/{id}/trash
(сигнал spam → строка MlOutbox) → ожидание флашера MlOutboxFlushScheduler (TrainBatch в ml-service) →
GET /api/ml/status: reachable:true, stats.outbox:0, класс spam в модели. Скрипт ничего не оставляет
в фоне (trap EXIT → docker compose down, временные файлы удаляются).
Каналы-вкладка /api/tg (модуль Telegram; Rulings 3/7/8)
-
Таблицы схемы тенанта (миграция
TenantTelegram):Dialogs(каталог каналов: Name/Handle/Kind/Hue, Monitor, LastText/LastAt, Backfilled) иTgMessages(превью сообщений,LeadIdnullable) — владелец чистый модульDeal.Modules.Telegram(ITelegramStore+DialogsService, порт-гейтITelegramGateway16 команд). -
gRPC-ингресс core (
Deal.Api/Telegram/TelegramIngressService, :5082):PushMessage→PipelineIngestService.EnqueueAsync(тот же контракт, что demo-ingest) + превью вTgMessages;SyncDialogs→ синхронизация каталога/мониторинга;ReportStatus→ KVtgStatus/tgAccount+ SSEsystem_status/тосты переходов.Актуально с 2026-09-11: приём записей вынесен из
TelegramIngressServiceв genericDeal.Api/Sources/SourceIngressGrpcService(sources.proto→PushSource, proto → домен черезSourceProtoMapper, тенант —IngressTenantResolver, превью —TelegramSourceIngestObserver).TelegramIngressServiceобслуживает толькоSyncDialogs/ReportStatus. -
Эндпоинты 1:1 api-map §3.3 (14 шт.): статус (§4.9 — live-поля фазы,
accountиз KV,monitoredизcount(Dialogs WHERE Monitor),keysSet), start-phone/start-qr/send-code/send-password/logout, QR-image (SVG, Net.Codecrete.QrCodeGenerator; 404 «QR не активен — начните вход по QR»), dialogs/refresh/ monitor-all/backfill-all/{id}/monitor/{id}/backfill (сервер-only)/preview. Boot-заглушкаGET /api/tg/statusснята (Task 14). telegram-service реализует команды (сессии по тенантам 1:1, фазы idle|code|password|qr| ready, auto_resume+heartbeat, backfill с паузами 1.5–3 с/сообщение, discovery-операции).
Discovery (Rulings 9–11)
- Таблицы схемы тенанта (миграция
TenantDiscovery):DiscTasks/DiscCandidates/DiscBlacklist/DiscLog(+json-колонки marks/topics/keywords); владелец — чистый модульDeal.Modules.Discovery(сервисы задач/кандидатов/чёрного списка/лога,DiscoveryPlanGuard— план ≤discJoinLimit, бюджет активных задач). - Фоновый
DiscoveryWorkerScheduler(5 с, per-tenant, одно действие за тик): план достигнут → done; поиск следующего ключа через gateway (Search); оценкаnew-кандидата каскадом (info → выборка → язык/число сообщений → ML-спам (если mlEnabled) → ИИEvaluateFit(если aiEnabled) → эвристика по ключам; форумы — по темам); авто-вступлениеreviewпри autoJoin с паузами и квотами (50–70 с, лимит авто-вступлений/сутки поDiscLog, стоп-кранdiscFloodDay/discPaused), join_failures ≥3 → удаление задачи. Внешний анти-бан — владение core; внутренние паузы сервиса — telegram-service. - Эндпоинты 1:1 api-map §3.8 (13 шт.): tasks CRUD+start/pause+generate-keywords (мягкая ошибка
{keywords:[],error}HTTP 200), candidates по статусам, join/reject (ручные, вне квот), blacklist, log.
ML-модель ml-service (Ruling 4)
- Порт python
mlservice/model.py1:1: инкрементальный наивный Байес по терминам (OnlineNaiveBayes, tokenize/upsert/predict/adaptive margin/самооценка eval), НЕ ONNX/ML.NET. Пороги:MIN_TOTAL 20,MIN_WINNER 6,MIN_WINNER_SPAM 4,MIN_HITS 2,MARGIN 0.9; адаптивный отрыв 0.35/0.5/0.7 после 400/150/60 примеров; классыt:hire/t:order(MIN_TYPE_WINNER 4). - Хранилище — SQLite на тенанта (
/data/ml/<tenantId>.sqlite, таблицы classes/terms/eval_log, запись транзакциями); пулConcurrentDictionary<tenantId, TenantModel>с lazy-load и lock на модель. Веса сигналов обучения 1.0 (пользователь) / 0.4 (ИИ) / 0.6 (правила). - Core:
PushAsyncВСЕГДА пишет вMlOutbox; фоновыйMlOutboxFlushScheduler(10 с, только приUseLocal=false) выгружает батчами по 10 (≤100/цикл) через RPCTrainBatch, строки удаляются после успеха; недоступность сервиса — строки остаются.Reset= Reset RPC +ClearOutboxAsync. Кэш статуса 15 с →reachableв/api/ml/status.
AI-фасад ai-service (Ruling 5)
- Без БД: core передаёт в теле запроса заполненные промпты (подстановка
{domain}/{keywords}), конфиг провайдера (id/base/model/apiKey/api_style — расшифрованный изaiConfigs) и текст. Методы:Filter→ {pass,reason};Classify→ {ok,json} (json-строку маппит core вAiParsedLeadDto, строгий маппинг);GenerateKeywords→ {keywords};EvaluateFit→ {fit,reason} (Discovery). - Транспорт: OpenAI-совместимые
POST {base}/chat/completions(Bearer) и AnthropicPOST {base}/v1/messages(x-api-key); temperature 0.2, таймауты 90/60 с, retry max_retries=2 (паузы 0.8/2 с), извлечение JSON из markdown. Ошибки провайдера наружу —UNAVAILABLE(«ИИ (имя) не ответил корректно — повторите попытку через несколько секунд»); учёт токеновusage(оценка ≈chars/4 при отсутствии) → KVaiTokenUsage(лимиты/бюджеты — этап 7). Без ключа LLM сервис недоступен — воркер ядра падает в локальные пути (фолбэк по замыслу).
Ручные проверки этапа 6 (нужны креды)
- Telegram-вход: ключи приложения (
api_id/api_hash) задаёт оператор глобально (PUT /api/operator/settings/telegram-keys, hash шифруется) →POST /api/tg/start-qr→ QR-скан → фазаready(«Telegram подключён, сессия сохранена»), затем реальные диалоги/мониторинг/«Перечитать»/ discovery-поиск и вступления. В настройках тенанта ключей нет (решение владельца, вариант A). - LLM: оператор задаёт провайдера и модель в консоли (
PUT /api/operator/settings/ai-config, напр. DeepSeek или локальный OpenAI-совместимый) →POST /api/operator/settings/ai-config/check; реальная классификация/фильтр/генерация ключей приServices__Ai__UseLocal=false. - Сквозной smoke стека —
scripts/dev-smoke.sh(одна команда; Docker Desktop должен быть поднят).
8. Этап 7 — SaaS-контур (Tasks 1–14; бэкапы — §13.9; финальные доки — Task 16): оператор/инвайты/лимиты/аудит/rate-limit/mTLS/логи/compose-prod
Кратко (детали — планы docs/superpowers/plans/2026-09-05-deal-stage7-saas.md Rulings 1–11 и отчёты
.superpowers/sdd/deal-stage7-saas/task-*-report.md; api-map — раздел «Реализовано в Deal» (Task 16);
живые проверки — ⚠ Manual, чек-лист task-16-report.md):
- Оператор (
public.operators/operator_sessions, кукаdeal_operator_session, срок 12 ч): bootstrap из envDEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD(Development без env —operator/operator; Production без env — warning и пропуск). Ручки —/api/operator/auth/*(login/logout/me); отдельныйOperatorSessionMiddleware— тенантные ручки операторских сессий не видят и наоборот (401/403). - Инвайты/регистрация: оператор создаёт инвайт (код 16 симв., срок 72 ч, email-unique; список/отзыв —
/api/operator/invites), пользователь активирует публичной ручкойPOST /api/join{code, email, name?, password}— создание пользователя (Argon2id) и, для инвайта «на новый тенант», тенанта с провижинингом схемы. - Лимиты ИИ-бюджета (
public.tenant_limits; период месяц/день, ленивый reset): списание —TokenUsageRecorder(успешные RPC ai-service), гейт-декораторыBudgetedAiClassifier/BudgetedAiTools(исчерпание/suspended→ Local-фолбэк), SSE-тосты 80/100% (BudgetAlertScheduler, 60 с). Дефолт-бюджет нового тенанта — envDEAL_DEFAULT_AI_BUDGET(константа 10 000 000 токенов/месяц). - Операторские ручки
/api/operator/*: тенанты (список/создание/статус/impersonation), лимиты (просмотр/смена бюджета + usage), аудит (append-onlypublic.audit_log), health (core/БД/ml/ai/telegram). С этапа 10 у них есть UI — оператор-консоль и страница активации инвайта (см. §13.10). - Rate limiting (Ruling 5): секция
RateLimit,Enabled=falseв dev/тестах; PROD включает env из compose.prod: политики api/auth (600/10 в минуту на тенанта/IP), интерцептор gRPC-ингресса :5082 (600/мин/тенанта, health освобождён),LoginAttemptGuard(5 неудач/15 мин → 429). Ответ 429 —{detail}. - mTLS (Ruling 6): env
DEAL_MTLS_*(Enabled=falsedefault) — Kestrel внутренних gRPC-эндпоинтов (+ ингресс core) и исходящие каналы core/telegram-service. Сертификаты —scripts/mtls-certs.sh→deploy/certs/(PFX процессов, общийdeal-client.pfx+ PEMdeal-client.crt/.keyдля grpc_health_probe). Живое рукопожатие — ⚠ Manual. - Логи/наблюдаемость (Ruling 7; метрики — этап 12, пакет A): Serilog.AspNetCore во всех 4 процессах — консоль JSON
(CompactJsonFormatter; в Development — текст) + rolling-файл
data/logs/deal-<процесс>.json(30 дней; envDEAL_LOG_LEVEL/DEAL_LOGS_DIR). Access-логи: HTTP (HttpAccessLogMiddleware) и gRPC (RpcCallLoggingInterceptor; gRPC-health не логируется). Метрики — OTel → Prometheus:/metrics(HTTP/1.1 :9464) + прикладныеdeal.*(токены/вызовы AI/ML, аудит, глубины очередей, сессии) — см. §7. PROD-стек: docker-логи → Promtail → Loki (retention 7 сут.) → Grafana (127.0.0.1:3001, SSH-туннель), метрики → Prometheus (127.0.0.1:9090) → Grafana, трейсы OTel → otel-collector → Tempo, ресурсы cAdvisor/node-exporter → Prometheus; профильobservabilitycompose.prod. - compose.prod (Ruling 9):
deploy/compose.prod.yml— postgres/minio (без host-портов), core + telegram/ai/ml (mTLS env; healthcheck —grpc_health_probe, при mTLS — TLS-проба с PEM),caddy(80/443: статикаsrc/frontend/dist+reverse_proxy /api → core:5080, security-заголовки; домен/TLS/Cloudflare — шапкаdeploy/caddy/Caddyfile), профильobservability(otel-collector/tempo/loki/promtail/prometheus/cadvisor/node-exporter/grafana). Секреты — только из.env.prod(шаблонdeploy/.env.prod.example, без дефолтных паролей, fail-fast:?). Запуск:docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml up -d --build(+--profile observability); авто-проверка —... configrc=0. - Быстрый сценарий оператора (после подъёма): login оператора → создать тенанта → инвайт →
POST /api/join(или инвайт «на существующего тенанта») → вход тенанта и работа/api→ оператор: лимиты/usage/health/аудит, приостановка тенанта (вход 403, ИИ-гейт заморожен). Dev-прогон без docker-сервисов — как §1–3 (core с Postgres :5433; операторские ручки/лимиты/аудит живут в том же процессе, AI — Local-режим).
9. Бэкапы и восстановление (Task 15, Ruling 8) — scripts/backup.sh / restore.sh
Реализация ежедневных бэкапов и восстановления — scripts/backup.sh + scripts/restore.sh
(общие env-дефолты/хелперы — scripts/deal-backup-lib.sh). Планировщик — вне контейнера
(cron/systemd, примеры ниже): скрипты ничего не ставят. Реальный прогон и restore-тест —
⚠ Manual (нужен поднятый docker-стек; здесь — синтаксис sh -n и error-path-проверки).
Что входит в бэкап (4 источника данных Ruling 8):
- Postgres — БД
dealцеликом (схемыpublic+tenant_*):pg_dump -Fc(custom, сжатие) →$BACKUP_DIR/pg/backup-<TS>.dump. По умолчанию —docker exec deal-postgres(локальный socket, пароль не нужен); при заданномDEAL_PG_HOST— прямоеpg_dumpс хоста. - MinIO — бакет
deal-files(вложения карточек):mc mirror→$BACKUP_DIR/minio/backup-<TS>/(бэкап = выгрузка ИЗ MinIO в BACKUP_DIR). mc берётся с хоста, если есть в PATH; иначе — разовый контейнерminio/mcв docker-сети контейнера MinIO. Секреты передаются env-алиасомMC_HOST_deal(в конфиг mc не пишутся). Endpoint: docker-режим —http://minio:9000(алиас compose-сервиса, работает и в compose.dev, и в compose.prod; в prod-сети DNSdeal-minioНЕ существует — там нет container_name); host-режим (dev, порт 9000 опубликован) —http://localhost:9000. Нестандартная схема — envDEAL_MINIO_ENDPOINT. compose.prod порты MinIO не публикует — для prod не ставьте хостовый mc (он не достанет MinIO), docker-режим работает из коробки. При Local-хранилище (MinIO не поднят) —DEAL_MINIO_SKIP=1. - Файловые данные — tar каталогов
DEAL_TAR_DIRSвнутриDEAL_DATA_DIR→$BACKUP_DIR/data/backup-<TS>.tar.gz. Дефолт:attachments(вложения Local-фолбэка),telegram_sessions(сессии telegram-service; контейнерный путь —/data/sessions, шифрованы AES-GCM — архив без доп. шифрования, доступ только root),ml(SQLite-модели ml-service). Для docker-томов задайтеDEAL_TAR_VOLUMES(список имён, напр.deploy_deal_api_data deploy_deal_tg_sessions deploy_deal_ml_data;docker volume ls | grep deal_) — тар выполнит busybox-контейнер. - Retention — удаление снапшотов старше
RETENTION_DAYS(датаYYYYMMDDиз имени файла/каталога, дефолт 14). При ежедневном запуске хранится ~15 копий (эквивалентfind -mtime +14).
НЕ входит: сам BACKUP_DIR (не кладите его внутрь тарируемых каталогов), docker-образы и
compose-конфиги, логи (data/logs — собственная rolling-ротация 30 дней), БД LeadRadar/прочие.
Структура и запуск (из корня репозитория):
# ежедневный бэкап: консоль + $BACKUP_DIR/logs/backup-YYYYMM.log; rc=0 при успехе
bash scripts/backup.sh
# восстановление (сначала остановите сервисы, см. ниже): всё из последнего снапшота /
# из снапшота с конкретной меткой / только шаг:
bash scripts/restore.sh # all — pg + minio + data из последнего pg-снапшота
bash scripts/restore.sh 20260908-021500 # TS вида YYYYMMDD-HHMMSS (из имени файла backup-<TS>…)
bash scripts/restore.sh pg|minio|data [TS]
# структура: $BACKUP_DIR/{pg,minio,data}/backup-YYYYMMDD-HHMMSS{,.dump,/,…}, logs/
Скрипты используют bash-специфику (
set -o pipefail) — запускать именноbash …(или исполняемый файл./scripts/backup.sh), НЕsh …(на системах с dash/sh=bash-не-гарантированно).
BACKUP_DIR по умолчанию — <репозиторий>/data/backups (env BACKUP_DIR/DEAL_BACKUP_DIR).
Dev-дефолты env соответствуют deploy/compose.dev.yml (БД deal/user deal; MinIO
deal_minio/deal_minio_secret, бакет deal-files); прод-имена секретов читаются как fallback
(DEAL_MINIO_ACCESS_KEY ← MINIO_ROOT_USER, DEAL_MINIO_SECRET_KEY ← MINIO_ROOT_PASSWORD;
DEAL_PG_PASSWORD совпадает с compose.prod). Полная таблица env — шапка scripts/deal-backup-lib.sh.
Планировщик (вне контейнера; запуск от пользователя с доступом к docker):
# cron — ежедневно в 02:00 («0 2 * * *»):
0 2 * * * /opt/deal/scripts/backup.sh >> /opt/deal/data/backups/cron.log 2>&1
# prod-вариант: секреты из deploy/.env.prod читаются сами (MINIO_ROOT_*, DEAL_PG_PASSWORD),
# BACKUP_DIR вынести из data/. prod НЕ публикует порты MinIO → mc в docker-режиме, endpoint по
# умолчанию http://minio:9000 (алиас сервиса); DEAL_MINIO_ENDPOINT задавать не нужно:
# 0 2 * * * cd /opt/deal && BACKUP_DIR=/var/backups/deal \
# bash scripts/backup.sh >> /var/backups/deal/cron.log 2>&1
# dev: хостовый mc + опубликованный порт 9000 → http://localhost:9000; docker-режим — http://minio:9000.
#
# systemd: /etc/systemd/system/deal-backup.{service,timer}
# [Unit] Description=Deal daily backup
# [Service] Type=oneshot; ExecStart=/opt/deal/scripts/backup.sh
# [Timer] OnCalendar=*-*-* 02:00:00; Persistent=true
# [Install] WantedBy=timers.target → systemctl enable --now deal-backup.timer
Восстановление — порядок (сводка — техдок §9):
# 1) остановить core и сервисы (БД/тома не должны быть заняты):
docker compose -f deploy/compose.dev.yml stop core telegram-service ml-service # dev
# prod: docker compose --env-file deploy/.env.prod -f deploy/compose.prod.yml stop
# 2) восстановить данные (шаг 1 → 3: pg → minio → data при restore all):
bash scripts/restore.sh
# 3) поднять сервисы обратно:
docker compose -f deploy/compose.dev.yml start core telegram-service ml-service
Восстановление — overlay: pg-шаг пересоздаёт БД целиком (dropdb+createdb, pg_restore --exit-on-error — rc=1 при любой ошибке), а minio/data дописывают ПОВЕРХ текущих данных:
файл/объект, которого нет в снапшоте, останется. Строгий снимок бакета 1:1 — DEAL_MINIO_MIRROR_REMOVE=1
(mc mirror --remove); для data-каталогов/томов при необходимости очистите целевой каталог/том вручную
перед распаковкой.
Рекомендация Ruling 8: раз в месяц — тест восстановления на отдельном инстансе/томах
(поднять копию стека, restore.sh, curl-приёмка /api). Потеря данных при ежедневном бэкапе
допустима ≤ 24 ч (SLA тестового этапа). Требования: bash + GNU date (coreutils), docker;
секреты скрипты не логируют; параллельный запуск backup.sh не поддерживается.
10. Этап 10 — оператор-консоль, аналитика и аудит действий (Tasks 1–7)
Кратко (детали — план docs/superpowers/plans/2026-09-10-deal-stage10-operator-analytics.md, отчёты
.superpowers/sdd/deal-stage10-operator-analytics/task-*-report.md, контракт
docs/architecture/2026-09-10-operator-analytics-contract.md; api-map — §6; наблюдаемость — §7):
- Фронт: hash-роутер без зависимостей (
src/frontend/src/router.js;vue-routerне добавлялся). Три верхнеуровневых экрана:#/— основное приложение (как раньше),#/operator— консоль оператора (подразделы#/operator/<section>),#/join?code=…— активация инвайта. Разбор hash синхронный (первый рендер сразу на нужном экране). Операторская ссылка на активацию формируется функциейjoinLink(code)—origin+pathname#/join?code=<код>. - Оператор-консоль (
src/frontend/src/views/operator/*): вход оператора (отдельная ручка и кука, экран выводит подсказку dev-дефолтаoperator / operator); разделы «Тенанты» (список/создание/suspend/resume/ impersonate), «Приглашения» (создание/отзыв/копирование ссылки), «Лимиты ИИ» (сводка/правка), «Аудит» (фильтры/пагинация), «Аналитика» (обзор/токены/действия), «Состояние системы». Внешних chart-библиотек нет — визуализации на Tailwind-компонентах. - Impersonation:
POST /api/operator/tenants/{id}/impersonateвыпускает tenant-сессию целевого пользователя (обычный механизмAuthService) и ставит httpOnly-кукуdeal_sessionв том же ответе (Deal.Api/Http/SessionCookieWriter.cs) — оператор сразу попадает в тенант; завершение — обычныйPOST /api/auth/logout(аудитimpersonation_stopped). - Страница активации инвайта (
src/frontend/src/views/JoinView.vue,#/join?code=…): формаemail, имя пространства (необязательно), пароль (минимум 8 символов) →POST /api/join; кука не ставится — после успеха пользователь входит обычнымPOST /api/auth/login. Тексты причин отказа берутся с сервера как есть (код не найден/истёк/использован/отозван, email не совпал/занят, тенант не найден/приостановлен). - История расхода токенов (
public.token_usage_events, миграция20260910152246_AddTokenUsageEvents):Id(bigint identity),TenantId(uuid →public.tenants, Restrict),At(timestamptz),Provider,Model,Kind(ai|ml, text),PromptTokens/CompletionTokens/TotalTokens(bigint),DetailJson(text). Индексы(TenantId, At)и(At). Запись — единая точкаTokenUsageRecorderв момент списания (успешный RPC ai-service — провайдер/модель из конфигурации; локальный ML-вызов —kind=ml, оценка токенов ≈ chars/4). Агрегатpublic.tenant_limitsостаётся для гейта; история — для аналитики. - Операторская аналитика (read-only;
src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs):GET /api/operator/analytics/overview,/tokens(groupBy=day|tenant|provider|model, неизвестное — 400),/activity(лента аудита с фильтрами и пагинацией). Все ответы — camelCase, время ISO-8601 (from/toвключительно), без операторской сессии — 401 «Требуется вход оператора».GET /api/operator/auditрасширен фильтромactorIdиoffset(ответ{items, total}без изменений). Полная форма запросов/ответов — в контракте (ссылка выше). - Аудит действий (этап 10, T1): единая точка
AuditService/AuditAppender(append-onlypublic.audit_log; акторtenant/operator/system; секреты не пишутся). К SaaS-событиям этапа 7 добавлены:tenant_logout,operator_logout,invite_joined, действия карточек (card_created,card_moved,card_trashed,card_restored,card_deleted,card_comment_added), контейнеры (container_created,container_updated,container_deleted),settings_updated,channel_enabled,telegram_linked(таблица — в контракте;channel_createdзарезервирован, но не эмитится). - Наблюдаемость (Grafana provisioning + promtail-лейблы, дашборды
Deal-Auth/Errors/Rps/Logs; с этапа 12 — метрики OTel → Prometheus и дашбордDeal-Metrics-Overview; трейсы OTel → Collector → Tempo (Deal-Traces) и ресурсы cAdvisor/node-exporter (Deal-Resources), см. §7). - Как открыть (dev):
docker compose -f deploy/compose.dev.yml up -d --build(или core на:5080с Postgres:5433, AI в Local-режиме) → фронтcd src/frontend && npm run dev(:5173, прокси/api) → оператор:http://localhost:5173/#/operator, входoperator/operator(dev-дефолт; в Production — envDEAL_OPERATOR_*); активация:http://localhost:5173/#/join?code=<код>; основное приложение —http://localhost:5173/#/. Prod-сценарий — §13.8. - Ограничения: реальные Telegram/LLM-креды — ⚠ Manual (по решению владельца); access-лог с
BL-LOG-ACTOR содержит
actor/tenant(IP в строке нет) — полный аудит действий вpublic.audit_log.
14. Локализация интерфейса (i18n, этап 11)
- Назначение. Все пользовательские строки фронтенда вынесены из компонентов и логики в словари-ресурсы (единый источник текстов). Язык один — русский; переключатель языка и второй язык — в бэклоге (делаем, когда возникнет потребность).
- Модуль
src/frontend/src/i18n/:index.js— ядро:t(key, params)с подстановкой{name}, реактивныйlocale(по умолчаниюru),setLocale(code),registerLocale(code, dict),availableLocales(),useI18n(). Фолбэк: активный язык →ru→ сам ключ. Плагин Vue даёт шаблонам$t(...); в<script setup>tимпортируется.locales/ru.js— словарь сообщений (единственный источник текстов), сгруппирован по областям:common/ nav/ search/ cards/ drawer/ columns/ settings/ channels/ processing/ auth/ operator/ join/ errors/.locales/ru.data.js— языковой контент-каталог (валюты, AI-провайдеры, дефолтные промпты, шаблоны); реэкспортируется изsrc/data.js, поэтому потребители не меняются.errors.js—localizeError(err, fallbackKey): известные HTTP-статусы и сетевые сбои → ключиerrors/*; осмысленный{detail}бэка и всё неизвестное показываются как есть. Точка применения —errMsgвsrc/frontend/src/store/core.js.
- Правила. Технические id/ключи/логи и бренд «Дейл» не локализуются; для исключений — директива
i18n-ignoreв строке. Значение ключа равно отображаемой строке (1:1). - Проверка.
npm run lint:i18n(скриптsrc/frontend/scripts/i18n-lint.mjs) падает, если внеsrc/i18n/locales/**осталась кириллица в пользовательских строках (комментарии иi18n-ignoreигнорируются). Сборка —npm run build. - Механизм (если язык когда-нибудь понадобится):
- Создать словарь
src/frontend/src/i18n/locales/<code>.jsс теми же ключами (и, при необходимости, контент-каталог<code>.data.js). - Зарегистрировать словарь через
registerLocale('<code>', dict)при старте приложения. - Вызвать
setLocale('<code>'). Отсутствующие ключи берутся изru; компоненты менять не нужно. - Перевод форматирования дат/чисел (
Intl) и плюрализации — вместе с языком.
- Создать словарь
- Ограничение: константы, вычисленные один раз при загрузке модуля (контент-каталог, карты подсказок),
держат текст стартового языка; «горячее» переключение потребовало бы обернуть их в
computed.
15. Темы оформления (внешний вид, §8.12 ТЗ)
- Назначение. В настройках появился раздел «Внешний вид»: выбор темы — «Тёмная» (по умолчанию), «Светлая» и «Системная». Переключение мгновенное, без перезагрузки; выбор запоминается на устройстве.
- Устройство палитры. Все цвета — токены Tailwind v4 в
src/frontend/src/style.css(блок@theme:--color-ink/panel/raise/hover/edge/hi/mid/low/brand/brand-2/online/danger/warn). Утилиты (bg-ink,text-hi,border-edge, …) ссылаются на эти переменные, поэтому тема меняется переопределением значений токенов, без правок компонентов. - Тёмная тема — дефолт (значения в
@theme, вид 1:1, не менялся). Светлая включается атрибутомdata-theme="light"на<html>: блок:root[data-theme="light"]переопределяет те же токены, а также тени (--shadow-card/--shadow-pop), цвет скроллбара, линии фоновой сетки на экранах входа и подложки встроенного Markdown (.tgmd). Здесь же выставляетсяcolor-scheme: lightдля нативных контролов (select/date/time), в тёмной —color-scheme: darkнаhtml. - Полупрозрачные слои. Подсветки, разделители и hover свёрстаны как
bg-white/Nиborder-white/N. На светлом фоне белое «поверх» невидимо, поэтому в светлой теме токен--color-whiteнамеренно указывает на тёмный оттенок (#0b1220) — получаются мягкие серые подложки и границы. Литеральныеtext-white/bg-white(текст на бренд-градиенте, фон QR-кода, ползунок тумблера) остаются белыми — для них добавлены отдельные правила. Для текста поверх сплошной заливки введены токены--color-on-brandи--color-on-warn(тёмный на светлой заливке в тёмной теме и наоборот). - Где хранится выбор.
src/frontend/src/composables/theme.js: значенияdark | light | system, ключlocalStorage—leadradar_theme;setTheme()меняет атрибутdata-themeи сохраняет выбор, режимsystemслушаетprefers-color-scheme. Инлайн-скрипт вsrc/frontend/index.htmlприменяет сохранённую тему к<html>до первого рендера — без «мигания» тёмной темы. - UI. Раздел «Настройки → Внешний вид» — вкладка
appearance(src/frontend/src/components/settings/AppearanceTab.vue), добавлена в список вкладокSettingsView.vueрядом с «Уведомлениями». Все строки — в словаре (settings.vneshnij-vid,settings.tema-*вlocales/ru.js). - Проверка.
npm run buildиnpm run lint:i18n— зелёные; новых зависимостей нет.
16. Добивка по ТЗ (этап 12) — что добавлено
По итогам аудита docs/superpowers/reviews/2026-09-10-tz-compliance-audit.md закрыты частично/незакрытые пункты:
- ML-проверка на канале/сообщении (§8, E11).
POST /api/ml/candidatesиPOST /api/ml/apply— не заглушки: отдают реальные сообщения (очередь/отсев/карточки) с мнением ML и применяют ручную разметку (skip|spam|board:<id>) через существующие сервисы (обучение ML без дублей). Ядро —MlReviewService. - Глобальные исключения до ML/ИИ (§5.14). Настройки
excludeKeywords/Locations/Types,excludeBudgetFrom/To; применяются на стоп-этапе, отсев пишет причину (exclude_kw/location/type/budget). - Группы фильтров колонки (§6.3). В
rulesдобавленыlevels/locations/types/prices; движок правил иmatchHitsдополнены метками «Уровень/Локация/Тип/Цена». /api/operator/health(§10.2). Добавленыqueues:{pipeline,mlOutbox}иsessions:{active}(общийRuntimeDepthsCollector, без дублей SQL).- Подозрительная активность (§10.5).
SuspiciousActivityService+GET /api/operator/analytics/suspicious(всплеск неудачных входов по IP/логину, входы актора с множества IP, серии по тенанту, перебор разных логинов с одного IPdistinct_logins_per_ip; пороги — константы). Плюс real-time учётSuspiciousActivityReporter: метрикаdeal.security.suspicious{kind}и предупреждающий лог на 429 rate limiter (rate_limit) и блокировке входа (login_blocked). - «Открыть исходник» на карточке (§6.6) и темы оформления (§8.12, §15) — во фронтенде.
Итог: core-тесты 1275/1275; фронт npm run build + lint:i18n зелёные. Контракт API —
docs/architecture/2026-09-10-unified-api-contract.md.